From 49a3d57d16810df328a01fdcf12c51f409fbdc34 Mon Sep 17 00:00:00 2001 From: zzggo Date: Mon, 3 Feb 2025 18:31:15 +1100 Subject: [PATCH 01/43] fixed: type check for transaction functions --- src/background/controller/wallet.ts | 15 +++-- src/ui/views/InnerRoute.tsx | 1 + src/ui/views/Send/SendAmount.tsx | 2 +- ...firmation.tsx => EvmToEvmConfirmation.tsx} | 8 +-- ...irmation.tsx => FlowToEVMConfirmation.tsx} | 18 ++--- src/ui/views/Send/SendEth/index.tsx | 9 +-- src/ui/views/Send/TransferConfirmation.tsx | 65 +++++++++++-------- src/ui/views/Send/index.tsx | 2 +- 8 files changed, 63 insertions(+), 57 deletions(-) rename src/ui/views/Send/SendEth/{EvmConfirmation.tsx => EvmToEvmConfirmation.tsx} (97%) rename src/ui/views/Send/SendEth/{ToEthConfirmation.tsx => FlowToEVMConfirmation.tsx} (95%) diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index b6840d3e..d0f83e70 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -8,6 +8,7 @@ import { ethErrors } from 'eth-rpc-errors'; import * as ethUtil from 'ethereumjs-util'; import { getApp } from 'firebase/app'; import { getAuth } from 'firebase/auth/web-extension'; +import type { TokenInfo } from 'flow-native-token-registry'; import { encode } from 'rlp'; import web3, { TransactionError } from 'web3'; @@ -1885,11 +1886,11 @@ export class WalletController extends BaseController { transferFTToEvmV2 = async ( vaultIdentifier: string, - amount = '1.0', - recipient + amount = '0.0', + recipient: string ): Promise => { await this.getNetwork(); - const formattedAmount = parseFloat(amount).toFixed(8); + const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); const script = await getScripts('bridge', 'bridgeTokensToEvmAddressV2'); @@ -1912,9 +1913,9 @@ export class WalletController extends BaseController { transferFTFromEvm = async ( flowidentifier: string, - amount = '1.0', + amount: string, receiver: string, - tokenResult + tokenResult: TokenInfo ): Promise => { await this.getNetwork(); const amountStr = amount.toString(); @@ -1951,9 +1952,9 @@ export class WalletController extends BaseController { return txID; }; - withdrawFlowEvm = async (amount = '1.0', address: string): Promise => { + withdrawFlowEvm = async (amount = '0.0', address: string): Promise => { await this.getNetwork(); - const formattedAmount = parseFloat(amount).toFixed(8); + const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); const script = await getScripts('evm', 'withdrawCoa'); const txID = await userWalletService.sendTransaction(script, [ diff --git a/src/ui/views/InnerRoute.tsx b/src/ui/views/InnerRoute.tsx index e678c997..89168e2d 100644 --- a/src/ui/views/InnerRoute.tsx +++ b/src/ui/views/InnerRoute.tsx @@ -9,6 +9,7 @@ import { useWallet } from 'ui/utils'; import Deposit from '../views/Deposit'; import Enable from '../views/Enable'; import Send from '../views/Send'; +//Transaction TODO: this is not used anymore, should be removed import Swap from '../views/Swap'; import Dashboard from './Dashboard'; diff --git a/src/ui/views/Send/SendAmount.tsx b/src/ui/views/Send/SendAmount.tsx index b31a3b04..9871eff8 100644 --- a/src/ui/views/Send/SendAmount.tsx +++ b/src/ui/views/Send/SendAmount.tsx @@ -52,7 +52,7 @@ const SendAmount = () => { const [coinList, setCoinList] = useState([]); const [isConfirmationOpen, setConfirmationOpen] = useState(false); const [exceed, setExceed] = useState(false); - const [amount, setAmount] = useState(undefined); + const [amount, setAmount] = useState('0'); const [secondAmount, setSecondAmount] = useState('0.0'); const [validated, setValidated] = useState(null); const [userInfo, setUser] = useState(USER_CONTACT); diff --git a/src/ui/views/Send/SendEth/EvmConfirmation.tsx b/src/ui/views/Send/SendEth/EvmToEvmConfirmation.tsx similarity index 97% rename from src/ui/views/Send/SendEth/EvmConfirmation.tsx rename to src/ui/views/Send/SendEth/EvmToEvmConfirmation.tsx index d6f5765e..972369f9 100644 --- a/src/ui/views/Send/SendEth/EvmConfirmation.tsx +++ b/src/ui/views/Send/SendEth/EvmToEvmConfirmation.tsx @@ -14,7 +14,7 @@ import IconNext from 'ui/FRWAssets/svg/next.svg'; import { LLSpinner, LLProfile, FRWProfile } from 'ui/FRWComponent'; import { useWallet } from 'ui/utils'; -interface ToEthConfirmationProps { +interface EvmConfirmationProps { isConfirmationOpen: boolean; data: any; handleCloseIconClicked: () => void; @@ -22,7 +22,7 @@ interface ToEthConfirmationProps { handleAddBtnClicked: () => void; } -const ToEthConfirmation = (props: ToEthConfirmationProps) => { +const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { const usewallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); @@ -84,8 +84,8 @@ const ToEthConfirmation = (props: ToEthConfirmationProps) => { }, []); const transferToken = useCallback(async () => { - const amount = props.data.amount * 1e18; const network = await usewallet.getNetwork(); + //Transaction TODO: tokeninfo getting directly from api using tokenSymbol, need to add filter on contractName and address const tokenResult = await usewallet.openapi.getTokenInfo(props.data.tokenSymbol, network); const amountStr = props.data.amount.toString(); @@ -375,4 +375,4 @@ const ToEthConfirmation = (props: ToEthConfirmationProps) => { ); }; -export default ToEthConfirmation; +export default EvmToEvmConfirmation; diff --git a/src/ui/views/Send/SendEth/ToEthConfirmation.tsx b/src/ui/views/Send/SendEth/FlowToEVMConfirmation.tsx similarity index 95% rename from src/ui/views/Send/SendEth/ToEthConfirmation.tsx rename to src/ui/views/Send/SendEth/FlowToEVMConfirmation.tsx index 6c0198bb..4df2f5fd 100644 --- a/src/ui/views/Send/SendEth/ToEthConfirmation.tsx +++ b/src/ui/views/Send/SendEth/FlowToEVMConfirmation.tsx @@ -1,6 +1,7 @@ import CloseIcon from '@mui/icons-material/Close'; import InfoIcon from '@mui/icons-material/Info'; import { Box, Typography, Drawer, Stack, Grid, CardMedia, IconButton, Button } from '@mui/material'; +import BN from 'bignumber.js'; import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; @@ -20,7 +21,7 @@ interface ToEthConfirmationProps { handleAddBtnClicked: () => void; } -const ToEthConfirmation = (props: ToEthConfirmationProps) => { +const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { const wallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); @@ -79,7 +80,7 @@ const ToEthConfirmation = (props: ToEthConfirmationProps) => { }, []); const transferFlow = useCallback(async () => { - const amount = parseFloat(props.data.amount).toFixed(8); + const amount = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); wallet .transferFlowEvm(props.data.contact.address, amount) @@ -106,17 +107,10 @@ const ToEthConfirmation = (props: ToEthConfirmationProps) => { }, [history, props, wallet]); const transferFt = useCallback(async () => { - const amount = props.data.amount * 1e18; setSending(true); - // TB: I don't know why this is needed - const encodedData = props.data.erc20Contract.methods - .transfer(props.data.contact.address, amount) - .encodeABI(); + const tokenResult = await wallet.openapi.getTokenInfo(props.data.tokenSymbol); - // Note that gas is not used in this function - const gas = '1312d00'; - const value = parseFloat(props.data.amount).toFixed(8); - const data = encodedData; + const value = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); const address = tokenResult!.address.startsWith('0x') ? tokenResult!.address.slice(2) @@ -389,4 +383,4 @@ const ToEthConfirmation = (props: ToEthConfirmationProps) => { ); }; -export default ToEthConfirmation; +export default FlowToEVMConfirmation; diff --git a/src/ui/views/Send/SendEth/index.tsx b/src/ui/views/Send/SendEth/index.tsx index 011e1b33..6f97c239 100644 --- a/src/ui/views/Send/SendEth/index.tsx +++ b/src/ui/views/Send/SendEth/index.tsx @@ -17,8 +17,9 @@ import { useWallet } from 'ui/utils'; import CancelIcon from '../../../../components/iconfont/IconClose'; import TransferAmount from '../TransferAmount'; -import EvmConfirmation from './EvmConfirmation'; -import ToEthConfirmation from './ToEthConfirmation'; +import EvmToEvmConfirmation from './EvmToEvmConfirmation'; +import FlowToEVMConfirmation from './FlowToEVMConfirmation'; + interface ContactState { contact: Contact; } @@ -304,7 +305,7 @@ const SendEth = () => { {childType === 'evm' ? ( - { }} /> ) : ( - { '#41CC5D', '#41CC5D', ]; + const startCount = useCallback(() => { let count = 0; let intervalId; @@ -82,17 +84,35 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { setOccupied(false); }, []); - const transferToken = async () => { - // TODO: Replace it with real data - if (props.data.childType === 'evm') { - withDrawEvm(); - return; - } else if (props.data.childType) { - sendFromChild(); - return; + const runTransaction = async () => { + setSending(true); + try { + if (props.data.childType === 'evm') { + await handleEvmTransfer(); + } else if (props.data.childType) { + await tokenFromChild(); + } else { + await transferTokenOnCadence(); + } + } catch { + setFailed(true); + } finally { + setSending(false); } + }; + + const handleEvmTransfer = async () => { + if (props.data.tokenSymbol.toLowerCase() === 'flow') { + await flowFromEvm(); + } else { + await otherFTFromEvm(); + } + }; + + const transferTokenOnCadence = async () => { setSending(true); - const amount = parseFloat(props.data.amount).toFixed(8); + const amount = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); + wallet .transferInboxTokens(props.data.tokenSymbol, props.data.contact.address, amount) .then(async (txID) => { @@ -117,8 +137,9 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { }); }; - const sendFromChild = async () => { - const amount = parseFloat(props.data.amount).toFixed(8); + const tokenFromChild = async () => { + const amount = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); + wallet .sendFTfromChild( props.data.userContact.address, @@ -149,21 +170,9 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { }); }; - const withDrawEvm = async () => { - console.log('transferToken ->', props.data); - setSending(true); - if (props.data.tokenSymbol.toLowerCase() === 'flow') { - transferFlow(); - } else { - transferFt(); - } - }; - - const transferFlow = async () => { - const amount = parseFloat(props.data.amount).toFixed(8); - // const txID = await wallet.transferTokens(props.data.tokenSymbol, props.data.contact.address, amount); + const flowFromEvm = async () => { wallet - .withdrawFlowEvm(amount, props.data.contact.address) + .withdrawFlowEvm(props.data.amount, props.data.contact.address) .then(async (txID) => { await wallet.setRecent(props.data.contact); wallet.listenTransaction( @@ -185,7 +194,7 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { }); }; - const transferFt = async () => { + const otherFTFromEvm = async () => { const tokenResult = await wallet.openapi.getEvmTokenInfo(props.data.tokenSymbol); console.log('tokenResult ', tokenResult, props.data.amount); @@ -194,7 +203,7 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { tokenResult!['flowIdentifier'], props.data.amount, props.data.contact.address, - tokenResult + tokenResult! ) .then(async (txID) => { await wallet.setRecent(props.data.contact); @@ -396,7 +405,7 @@ const TransferConfirmation = (props: TransferConfirmationProps) => { /> - {childType === 'evm' ? ( + {transactionState.fromNetwork === 'Evm' ? ( setConfirmationOpen(false)} @@ -333,12 +284,13 @@ const SendEth = () => { setConfirmationOpen(false)} diff --git a/src/ui/views/Send/[toAddress]/index.tsx b/src/ui/views/Send/[toAddress]/index.tsx index 2c6eac80..dd33751d 100644 --- a/src/ui/views/Send/[toAddress]/index.tsx +++ b/src/ui/views/Send/[toAddress]/index.tsx @@ -14,7 +14,7 @@ import SendToEvm from '../SendToEVM'; export const SendTo = () => { // Remove or use only in development - // console.log('SendTo'); + console.log('SendTo'); const wallet = useWallet(); const { mainAddress, currentWallet, userInfo } = useProfileStore(); @@ -34,6 +34,7 @@ export const SendTo = () => { username: '', avatar: '', }, + toNetwork: isValidEthereumAddress(toAddress) ? 'Evm' : 'Cadence', }); const handleTokenChange = useCallback( @@ -74,6 +75,20 @@ export const SendTo = () => { }, }, }); + // Setup the to address + dispatch({ + type: 'setToAddress', + payload: { + address: toAddress as WalletAddress, + contact: { + id: 0, + address: toAddress as WalletAddress, + contact_name: '', + username: '', + avatar: '', + }, + }, + }); // Set the token to the default token handleTokenChange(INITIAL_TRANSACTION_STATE.selectedToken.symbol); } @@ -84,29 +99,42 @@ export const SendTo = () => { userInfo?.username, userInfo?.avatar, handleTokenChange, + toAddress, ]); - const handleAmountChange = (amount: string) => { - dispatch({ - type: 'setAmount', - payload: amount, - }); - }; + const handleAmountChange = useCallback( + (amount: string) => { + dispatch({ + type: 'setAmount', + payload: amount, + }); + }, + [dispatch] + ); - const handleSwitchFiatOrCoin = () => { + const handleSwitchFiatOrCoin = useCallback(() => { dispatch({ type: 'switchFiatOrCoin', }); - }; + }, [dispatch]); - const handleMaxClick = () => { + const handleMaxClick = useCallback(() => { dispatch({ type: 'setAmountToMax', }); - }; - if (isValidEthereumAddress(transactionState.toAddress)) { - //return ; - } else if (isValidFlowAddress(transactionState.toAddress)) { + }, [dispatch]); + + if (isValidEthereumAddress(toAddress)) { + return ( + + ); + } else if (isValidFlowAddress(toAddress)) { return ( { const handleTransactionRedirect = (contact: Contact) => { const isEvmAddress = isValidEthereumAddress(contact.address); - const pathname = isEvmAddress - ? '/dashboard/wallet/sendeth' - : `/dashboard/wallet/send/${contact.address}`; + const pathname = `/dashboard/wallet/send/${contact.address}`; // Set transaction destination network and address useTransactionStore.getState().setToNetwork(isEvmAddress ? 'Evm' : 'Cadence'); From 25c6c4e323361ad064e502671c3a072d0dcebd3f Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:01:21 +1100 Subject: [PATCH 17/43] Corrected address lookup redirect --- src/ui/views/Send/index.tsx | 62 +++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/ui/views/Send/index.tsx b/src/ui/views/Send/index.tsx index 7a7c5a7a..cf828197 100644 --- a/src/ui/views/Send/index.tsx +++ b/src/ui/views/Send/index.tsx @@ -25,7 +25,13 @@ import { useHistory } from 'react-router-dom'; import SwipeableViews from 'react-swipeable-views'; import { type Contact } from '@/shared/types/network-types'; -import { withPrefix, isValidEthereumAddress } from '@/shared/utils/address'; +import { type WalletAddress } from '@/shared/types/wallet-types'; +import { + withPrefix, + isValidEthereumAddress, + isValidAddress, + isValidFlowAddress, +} from '@/shared/utils/address'; import { useContactHook } from '@/ui/hooks/useContactHook'; import { useContactStore } from '@/ui/stores/contactStore'; import { useTransactionStore } from '@/ui/stores/transactionStore'; @@ -217,31 +223,39 @@ const Send = () => { return false; }); - const checkAddress = keyword.trim(); - if (checkAddress) { - const contact = findContact(checkAddress); - if (contact) { - handleTransactionRedirect(contact); - } + const trimmedSearchTerm = keyword.trim(); + // Check if they've entered a valid address + if (isValidAddress(trimmedSearchTerm)) { + // Yep, they've entered a valid address + const address = trimmedSearchTerm as WalletAddress; + handleTransactionRedirect(address); } setFilteredContacts(filtered); setHasNoFilteredContacts(isEmpty(filtered)); console.log('recentContacts', filtered); }; - const handleTransactionRedirect = (contact: Contact) => { - const isEvmAddress = isValidEthereumAddress(contact.address); - const pathname = `/dashboard/wallet/send/${contact.address}`; + const handleTransactionRedirect = useCallback( + (address: string) => { + if (isValidAddress(address)) { + const pathname = `/dashboard/wallet/send/${address}`; - // Set transaction destination network and address - useTransactionStore.getState().setToNetwork(isEvmAddress ? 'Evm' : 'Cadence'); - useTransactionStore.getState().setToAddress(contact.address); - - history.push({ - pathname, - state: { contact }, - }); - }; + history.push({ + pathname, + }); + } else { + console.error('Invalid address', address); + } + }, + [history] + ); + // Handle the click of a contact + const handleContactClick = useCallback( + (contact: Contact) => { + handleTransactionRedirect(contact.address); + }, + [handleTransactionRedirect] + ); useEffect(() => { fetchAddressBook(); @@ -351,21 +365,21 @@ const Send = () => { @@ -407,7 +421,7 @@ const Send = () => { )} @@ -433,7 +447,7 @@ const Send = () => { )} From 5ec6f641e7bb27340c1063734c9df3cb987afa17 Mon Sep 17 00:00:00 2001 From: zzggo Date: Wed, 12 Feb 2025 18:08:44 +1100 Subject: [PATCH 18/43] fixed: change to useContact, fixed the address check and rerendering issue in frontend address book --- src/ui/hooks/useContactHook.ts | 16 ++++++------ .../views/Send/AddressLists/AccountsList.tsx | 25 ++++++------------- src/ui/views/Send/index.tsx | 24 +++++++++++------- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/ui/hooks/useContactHook.ts b/src/ui/hooks/useContactHook.ts index 42344fe7..c2794bc6 100644 --- a/src/ui/hooks/useContactHook.ts +++ b/src/ui/hooks/useContactHook.ts @@ -136,16 +136,14 @@ export function useContactHook() { } }, [walletList, childAccounts, mainAddress, evmAddress, evmWallet, contactStore]); - const findContact = useCallback( + const useContact = useCallback( (address: string): Contact | null => { - const { recentContacts, accountList, evmAccounts, childAccounts, filteredContacts } = - contactStore; return ( - recentContacts.find((c) => c.address === address) || - accountList.find((c) => c.address === address) || - evmAccounts.find((c) => c.address === address) || - childAccounts.find((c) => c.address === address) || - filteredContacts.find((c) => c.address === address) || + contactStore.recentContacts.find((c) => c.address === address) || + contactStore.accountList.find((c) => c.address === address) || + contactStore.evmAccounts.find((c) => c.address === address) || + contactStore.childAccounts.find((c) => c.address === address) || + contactStore.filteredContacts.find((c) => c.address === address) || null ); }, @@ -159,6 +157,6 @@ export function useContactHook() { updateFromContact, fetchAddressBook, setupAccounts, - findContact, + useContact, }; } diff --git a/src/ui/views/Send/AddressLists/AccountsList.tsx b/src/ui/views/Send/AddressLists/AccountsList.tsx index 111ff2e2..18cee493 100644 --- a/src/ui/views/Send/AddressLists/AccountsList.tsx +++ b/src/ui/views/Send/AddressLists/AccountsList.tsx @@ -1,35 +1,24 @@ import { List, ListSubheader, ButtonBase, Box } from '@mui/material'; import { groupBy, isEmpty } from 'lodash'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import { withPrefix, isValidEthereumAddress } from '@/shared/utils/address'; import { LLContactCard, LLContactEth, FWContactCard } from '@/ui/FRWComponent'; import { useContactHook } from '@/ui/hooks/useContactHook'; import { useContactStore } from '@/ui/stores/contactStore'; -import { useProfileStore } from '@/ui/stores/profileStore'; -import { useWallet } from 'ui/utils'; - -type ChildAccount = { - [key: string]: { - name: string; - description: string; - thumbnail: { - url: string; - }; - }; -}; const AccountsList = ({ filteredContacts, isLoading, handleClick, isSend = true }) => { const { accountList, evmAccounts, childAccounts } = useContactStore(); const { setupAccounts } = useContactHook(); - const [, setGrouped] = useState([]); + const mounted = useRef(false); useEffect(() => { - const group = groupBy(filteredContacts, (contact) => contact.contact_name[0]); - setGrouped(group); - setupAccounts(); - }, [filteredContacts, setupAccounts]); + if (!mounted.current) { + mounted.current = true; + setupAccounts(); + } + }, [setupAccounts]); return ( diff --git a/src/ui/views/Send/index.tsx b/src/ui/views/Send/index.tsx index 7a7c5a7a..7d9b31e4 100644 --- a/src/ui/views/Send/index.tsx +++ b/src/ui/views/Send/index.tsx @@ -1,3 +1,4 @@ +import { ChangeHistory } from '@mui/icons-material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded'; import SearchIcon from '@mui/icons-material/Search'; @@ -20,12 +21,12 @@ import { import { useTheme, styled, StyledEngineProvider } from '@mui/material/styles'; import { makeStyles } from '@mui/styles'; import { isEmpty } from 'lodash'; -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import SwipeableViews from 'react-swipeable-views'; import { type Contact } from '@/shared/types/network-types'; -import { withPrefix, isValidEthereumAddress } from '@/shared/utils/address'; +import { withPrefix, isValidEthereumAddress, isValidFlowAddress } from '@/shared/utils/address'; import { useContactHook } from '@/ui/hooks/useContactHook'; import { useContactStore } from '@/ui/stores/contactStore'; import { useTransactionStore } from '@/ui/stores/transactionStore'; @@ -132,7 +133,7 @@ const Send = () => { setSearchContacts, setHasNoFilteredContacts, } = useContactStore(); - const { findContact, fetchAddressBook } = useContactHook(); + const { fetchAddressBook } = useContactHook(); const [tabValue, setTabValue] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -140,6 +141,8 @@ const Send = () => { const [searchKey, setSearchKey] = useState(''); const [searched, setSearched] = useState(false); + const mounted = useRef(false); + const checkContain = (searchResult: Contact) => { if (sortedContacts.some((e) => e.contact_name === searchResult.username)) { return true; @@ -218,11 +221,11 @@ const Send = () => { }); const checkAddress = keyword.trim(); - if (checkAddress) { - const contact = findContact(checkAddress); - if (contact) { - handleTransactionRedirect(contact); - } + if (isValidFlowAddress(checkAddress) || isValidEthereumAddress(checkAddress)) { + const checkedAdressContact = searchResult; + checkedAdressContact.address = checkAddress; + + handleTransactionRedirect(checkedAdressContact); } setFilteredContacts(filtered); setHasNoFilteredContacts(isEmpty(filtered)); @@ -244,7 +247,10 @@ const Send = () => { }; useEffect(() => { - fetchAddressBook(); + if (!mounted.current) { + mounted.current = true; + fetchAddressBook(); + } }, [fetchAddressBook]); return ( From d93e6d4e39e7ca61f71a656172a20593f9d4d16b Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:23:21 +1100 Subject: [PATCH 19/43] EVM confirm in progress --- e2e/sendTransaction.test.ts | 4 +- e2e/sendTransactionFromFlow.test.ts | 3 +- src/ui/hooks/useWeb3.ts | 16 ++ .../Send/SendToEVM/EvmToEvmConfirmation.tsx | 178 +++++++++++------- src/ui/views/Send/SendToEVM/index.tsx | 11 +- 5 files changed, 133 insertions(+), 79 deletions(-) create mode 100644 src/ui/hooks/useWeb3.ts diff --git a/e2e/sendTransaction.test.ts b/e2e/sendTransaction.test.ts index c1c17f4c..e3f0b508 100644 --- a/e2e/sendTransaction.test.ts +++ b/e2e/sendTransaction.test.ts @@ -129,7 +129,7 @@ test('send BETA token COA to EOA', async ({ page }) => { successtext: 'success', }); }); -//Move FTs from COA to FLOW +/* //Move FTs from COA to FLOW test('move Flow COA to FLOW', async ({ page }) => { // Move FLOW token from COA to FLOW await moveTokenCOA({ @@ -164,7 +164,7 @@ test('move USDC token COA to FLOW homepage', async ({ page }) => { tokenname: 'Bridged USDC (Celer)', amount: '0.000123', }); -}); +}); */ //Send NFT from COA to COA //Send NFT from COA to FLOW //Send NFT from COA to EOA diff --git a/e2e/sendTransactionFromFlow.test.ts b/e2e/sendTransactionFromFlow.test.ts index ca414f30..49a0a451 100644 --- a/e2e/sendTransactionFromFlow.test.ts +++ b/e2e/sendTransactionFromFlow.test.ts @@ -124,7 +124,7 @@ test('send BETA flow to EOA', async ({ page }) => { ingoreFlowCharge: true, }); }); -//Move FTs from Flow to COA +/* //Move FTs from Flow to COA test('move Flow Flow to COA', async ({ page }) => { // Move FLOW token from FLOW to COA await moveTokenFlow({ @@ -160,3 +160,4 @@ test('move USDC token Flow to COA homepage', async ({ page }) => { ingoreFlowCharge: true, }); }); + */ diff --git a/src/ui/hooks/useWeb3.ts b/src/ui/hooks/useWeb3.ts new file mode 100644 index 00000000..c9ff12e7 --- /dev/null +++ b/src/ui/hooks/useWeb3.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react'; + +import { useNetworkStore } from '../stores/networkStore'; +import { useProviderStore } from '../stores/providerStore'; + +export const useWeb3 = () => { + const { currentNetwork: network } = useNetworkStore(); + + const providerStore = useProviderStore(); + + useEffect(() => { + providerStore.setWeb3Instance(network); + }, [network, providerStore]); + + return providerStore.web3Instance; +}; diff --git a/src/ui/views/Send/SendToEVM/EvmToEvmConfirmation.tsx b/src/ui/views/Send/SendToEVM/EvmToEvmConfirmation.tsx index 759d6ef2..dc9b819b 100644 --- a/src/ui/views/Send/SendToEVM/EvmToEvmConfirmation.tsx +++ b/src/ui/views/Send/SendToEVM/EvmToEvmConfirmation.tsx @@ -5,25 +5,34 @@ import BN from 'bignumber.js'; import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { type TransactionState } from '@/shared/types/transaction-types'; import { ensureEvmAddressPrefix } from '@/shared/utils/address'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import StorageExceededAlert from '@/ui/FRWComponent/StorageExceededAlert'; import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; +import { useWeb3 } from '@/ui/hooks/useWeb3'; import { useStorageCheck } from '@/ui/utils/useStorageCheck'; +import erc20ABI from 'background/utils/erc20.abi.json'; import IconNext from 'ui/FRWAssets/svg/next.svg'; import { LLSpinner, LLProfile, FRWProfile } from 'ui/FRWComponent'; import { useWallet } from 'ui/utils'; interface EvmConfirmationProps { + transactionState: TransactionState; isConfirmationOpen: boolean; - data: any; handleCloseIconClicked: () => void; handleCancelBtnClicked: () => void; handleAddBtnClicked: () => void; } -const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { - const usewallet = useWallet(); +const EvmToEvmConfirmation = ({ + transactionState, + isConfirmationOpen, + handleCloseIconClicked, + handleCancelBtnClicked, + handleAddBtnClicked, +}: EvmConfirmationProps) => { + const wallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); const [failed, setFailed] = useState(false); @@ -34,11 +43,25 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { const [tid, setTid] = useState(''); const [count, setCount] = useState(0); - const transferAmount = props?.data?.amount ? parseFloat(props.data.amount) : undefined; + const [erc20Contract, setErc20Contract] = useState(null); + + const web3Instance = useWeb3(); + + useEffect(() => { + if (isConfirmationOpen && web3Instance) { + const contractInstance = new web3Instance.eth.Contract( + erc20ABI, + transactionState.selectedToken.address + ); + setErc20Contract(contractInstance); + } + }, [web3Instance, transactionState.selectedToken.address, isConfirmationOpen]); + + const transferAmount = transactionState.amount ? parseFloat(transactionState.amount) : undefined; const { sufficient: isSufficient, sufficientAfterAction } = useStorageCheck({ transferAmount, - coin: props.data?.coinInfo?.coin, + coin: transactionState.coinInfo?.coin, // the transfer is within the EVM network, the flag should be false movingBetweenEVMAndFlow: false, }); @@ -59,7 +82,7 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { const startCount = useCallback(() => { let count = 0; let intervalId; - if (props.data.contact.address) { + if (transactionState.toAddress) { intervalId = setInterval(function () { count++; if (count === 7) { @@ -67,67 +90,71 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { } setCount(count); }, 500); - } else if (!props.data.contact.address) { + } else if (!transactionState.toAddress) { clearInterval(intervalId); } - }, [props?.data?.contact?.address]); + }, [transactionState.toAddress]); const getPending = useCallback(async () => { - const pending = await usewallet.getPendingTx(); + const pending = await wallet.getPendingTx(); if (pending.length > 0) { setOccupied(true); } - }, [usewallet]); + }, [wallet]); const updateOccupied = useCallback(() => { setOccupied(false); }, []); const transferTokensOnEvm = useCallback(async () => { - const network = await usewallet.getNetwork(); - //Transaction TODO: tokeninfo getting directly from api using tokenSymbol, need to add filter on contractName and address - const tokenResult = await usewallet.openapi.getTokenInfo(props.data.tokenSymbol, network); - - const amountStr = props.data.amount.toString(); + // the amount is always stored as a string in the transaction state + const amountStr: string = transactionState.amount; + // TODO: check if the amount is a valid number + // Create an integer string based on the required token decimals const amountBN = new BN(amountStr.replace('.', '')); + const decimalsCount = amountStr.split('.')[1]?.length || 0; - const scaleFactor = new BN(10).pow(tokenResult!.decimals - decimalsCount); + const decimalDifference = transactionState.selectedToken.decimals - decimalsCount; + if (decimalDifference < 0) { + throw new Error('Too many decimal places have been provided'); + } + const scaleFactor = new BN(10).pow(decimalDifference); const integerAmount = amountBN.multipliedBy(scaleFactor); const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); + setSending(true); let address, gas, value, data; - const encodedData = props.data.erc20Contract.methods - .transfer(ensureEvmAddressPrefix(props.data.contact.address), integerAmountStr) - .encodeABI(); - if (props.data.coinInfo.unit.toLowerCase() === 'flow') { - address = props.data.contact.address; + if (transactionState.coinInfo.unit.toLowerCase() === 'flow') { + address = transactionState.toAddress; gas = '1'; - value = BigInt(Math.round(props.data.amount * 1e18)).toString(16); + // const amountBN = new BN(transactionState.amount).multipliedBy(new BN(10).pow(18)); + // the amount is always stored as a string in the transaction state + value = integerAmount.toString(16); data = '0x'; } else { - const tokenInfo = await usewallet.openapi.getEvmTokenInfo( - props.data.coinInfo.unit.toLowerCase() - ); + const encodedData = erc20Contract.methods + .transfer(ensureEvmAddressPrefix(transactionState.toAddress), integerAmountStr) + .encodeABI(); gas = '1312d00'; - address = ensureEvmAddressPrefix(tokenInfo!.address); + address = ensureEvmAddressPrefix(transactionState.selectedToken.address); value = '0x0'; // Zero value as hex data = encodedData.startsWith('0x') ? encodedData : `0x${encodedData}`; } try { - const txId = await usewallet.sendEvmTransaction(address, gas, value, data); - await usewallet.setRecent(props.data.contact); - usewallet.listenTransaction( + const txId = await wallet.sendEvmTransaction(address, gas, value, data); + await wallet.setRecent(transactionState.toContact); + wallet.listenTransaction( txId, true, - `${props.data.amount} ${props.data.coinInfo.coin} Sent`, - `You have sent ${props.data.amount} ${props.data.tokenSymbol} to ${props.data.contact.contact_name}. \nClick to view this transaction.`, - props.data.coinInfo.icon + `${transactionState.amount} ${transactionState.coinInfo.coin} Sent`, + `You have sent ${transactionState.amount} ${transactionState.selectedToken.symbol} to ${transactionState.toContact?.contact_name}. \nClick to view this transaction.`, + transactionState.coinInfo.icon ); - props.handleCloseIconClicked(); - await usewallet.setDashIndex(0); + handleCloseIconClicked(); + await wallet.setDashIndex(0); setSending(false); setTid(txId); history.push(`/dashboard?activity=1&txId=${txId}`); @@ -137,24 +164,38 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { setFailed(true); setErrorMessage(err.message); } - }, [history, props, usewallet]); + }, [ + transactionState.amount, + transactionState.selectedToken.decimals, + transactionState.selectedToken.address, + transactionState.selectedToken.symbol, + transactionState.coinInfo.unit, + transactionState.coinInfo.coin, + transactionState.coinInfo.icon, + transactionState.toAddress, + transactionState.toContact, + erc20Contract?.methods, + wallet, + handleCloseIconClicked, + history, + ]); const transferTokens = useCallback(async () => { try { setSending(true); - switch (props.data.currentTxState) { + switch (transactionState.currentTxState) { case 'FlowFromEvmToEvm': case 'FTFromEvmToEvm': await transferTokensOnEvm(); break; default: - throw new Error(`Unsupported transaction state: ${props.data.currentTxState}`); + throw new Error(`Unsupported transaction state: ${transactionState.currentTxState}`); } } catch (error) { console.error('Transaction failed:', error); setFailed(true); } - }, [transferTokensOnEvm, props.data.currentTxState]); + }, [transferTokensOnEvm, transactionState.currentTxState]); const transactionDoneHandler = useCallback( (request) => { @@ -183,10 +224,10 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { }, [getPending, startCount, transactionDoneHandler]); useEffect(() => { - if (props.data.coinInfo.unit) { + if (transactionState.coinInfo.unit) { setOccupied(false); } - }, [props.data]); + }, [transactionState]); const renderContent = () => ( { )} - + @@ -239,7 +280,7 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', py: '16px' }} > { ))} - + { }} > - + - {props.data.coinInfo.coin} + {transactionState.coinInfo.coin} - {props.data.amount} {props.data.coinInfo.unit} + {transactionState.amount} {transactionState.coinInfo.unit} @@ -300,7 +344,7 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { color="info" sx={{ fontSize: '14px', fontWeight: 'semi-bold', textAlign: 'end' }} > - $ {props.data.secondAmount} + $ {transactionState.fiatAmount} @@ -332,7 +376,7 @@ const EvmToEvmConfirmation = (props: EvmConfirmationProps) => { /> - - ); + + + + $ {transactionState.fiatAmount} + + + - return ( - isConfirmationOpen && ( - <> - + + + {/* */} + + + {chrome.i18n.getMessage('Your_address_is_currently_processing_another_transaction')} + + + + + + + + setErrorCode(null)} /> + ); }; diff --git a/src/ui/views/Send/SendToEVM/FlowToEVMConfirmation.tsx b/src/ui/views/Send/SendToEVM/FlowToEVMConfirmation.tsx index 5390f7cd..6701512e 100644 --- a/src/ui/views/Send/SendToEVM/FlowToEVMConfirmation.tsx +++ b/src/ui/views/Send/SendToEVM/FlowToEVMConfirmation.tsx @@ -5,24 +5,30 @@ import BN from 'bignumber.js'; import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { type TransactionState } from '@/shared/types/transaction-types'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import StorageExceededAlert from '@/ui/FRWComponent/StorageExceededAlert'; import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; -import { useTransactionStore } from '@/ui/stores/transactionStore'; import { useStorageCheck } from '@/ui/utils/useStorageCheck'; import IconNext from 'ui/FRWAssets/svg/next.svg'; -import { LLSpinner, LLProfile, FRWProfile, FRWTargetProfile } from 'ui/FRWComponent'; -import { useWallet } from 'ui/utils'; +import { LLSpinner, LLProfile, FRWTargetProfile } from 'ui/FRWComponent'; +import { stripFinalAmount, useWallet } from 'ui/utils'; interface ToEthConfirmationProps { isConfirmationOpen: boolean; - data: any; + transactionState: TransactionState; handleCloseIconClicked: () => void; handleCancelBtnClicked: () => void; handleAddBtnClicked: () => void; } -const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { +const FlowToEVMConfirmation = ({ + transactionState, + isConfirmationOpen, + handleCloseIconClicked, + handleCancelBtnClicked, + handleAddBtnClicked, +}: ToEthConfirmationProps) => { const wallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); @@ -35,7 +41,7 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { const [count, setCount] = useState(0); const { sufficient: isSufficient, sufficientAfterAction } = useStorageCheck({ transferAmount: 0, - coin: props.data?.coinInfo?.coin, + coin: transactionState?.coinInfo?.coin, // the transfer is within the EVM network, the flag should be false movingBetweenEVMAndFlow: false, }); @@ -56,7 +62,7 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { const startCount = useCallback(() => { let count = 0; let intervalId; - if (props.data.contact.address) { + if (transactionState.toAddress) { intervalId = setInterval(function () { count++; if (count === 7) { @@ -64,10 +70,10 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { } setCount(count); }, 500); - } else if (!props.data.contact.address) { + } else if (!transactionState.toAddress) { clearInterval(intervalId); } - }, [props?.data?.contact?.address]); + }, [transactionState.toAddress]); const getPending = useCallback(async () => { const pending = await wallet.getPendingTx(); @@ -81,20 +87,22 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { }, []); const transferFlowFromCadenceToEvm = useCallback(async () => { - const amount = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); - + const amount = new BN(transactionState.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); + if (stripFinalAmount(amount, 8) !== stripFinalAmount(transactionState.amount, 8)) { + throw new Error('Amount entered does not match required precision'); + } wallet - .transferFlowEvm(props.data.contact.address, amount) + .transferFlowEvm(transactionState.toAddress, amount) .then(async (txId) => { - await wallet.setRecent(props.data.contact); + await wallet.setRecent(transactionState.toContact); wallet.listenTransaction( txId, true, - `${props.data.amount} ${props.data.coinInfo.coin} Sent`, - `You have sent ${props.data.amount} ${props.data.tokenSymbol} to ${props.data.contact.contact_name}. \nClick to view this transaction.`, - props.data.coinInfo.icon + `${transactionState.amount} ${transactionState.coinInfo.coin} Sent`, + `You have sent ${transactionState.amount} ${transactionState.selectedToken?.symbol} to ${transactionState.toContact?.contact_name}. \nClick to view this transaction.`, + transactionState.coinInfo.icon ); - props.handleCloseIconClicked(); + handleCloseIconClicked(); await wallet.setDashIndex(0); setSending(false); setTid(txId); @@ -105,33 +113,35 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { setFailed(true); }); // Depending on history is probably not great - }, [history, props, wallet]); + }, [history, transactionState, wallet, handleCloseIconClicked]); const transferFTFromCadenceToEvm = useCallback(async () => { setSending(true); - const value = new BN(props.data.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); - - const address = props.data.selectedToken!.address.startsWith('0x') - ? props.data.selectedToken!.address.slice(2) - : props.data.selectedToken!.address; + const amount = new BN(transactionState.amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); + if (stripFinalAmount(amount, 8) !== stripFinalAmount(transactionState.amount, 8)) { + throw new Error('Amount entered does not match required precision'); + } + const address = transactionState.selectedToken!.address.startsWith('0x') + ? transactionState.selectedToken!.address.slice(2) + : transactionState.selectedToken!.address; wallet .transferFTToEvmV2( - `A.${address}.${props.data.selectedToken!.contractName}.Vault`, - value, - props.data.contact.address + `A.${address}.${transactionState.selectedToken!.contractName}.Vault`, + amount, + transactionState.toAddress ) .then(async (txId) => { - await wallet.setRecent(props.data.contact); + await wallet.setRecent(transactionState.toContact); wallet.listenTransaction( txId, true, - `${props.data.amount} ${props.data.coinInfo.coin} Sent`, - `You have sent ${props.data.amount} ${props.data.tokenSymbol} to ${props.data.contact.contact_name}. \nClick to view this transaction.`, - props.data.coinInfo.icon + `${transactionState.amount} ${transactionState.coinInfo.coin} Sent`, + `You have sent ${transactionState.amount} ${transactionState.selectedToken?.symbol} to ${transactionState.toContact?.contact_name}. \nClick to view this transaction.`, + transactionState.coinInfo.icon ); - props.handleCloseIconClicked(); + handleCloseIconClicked(); await wallet.setDashIndex(0); setSending(false); setTid(txId); @@ -143,12 +153,22 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { setFailed(true); }); // Depending on history is probably not great - }, [history, props, wallet]); + }, [ + handleCloseIconClicked, + history, + transactionState.amount, + transactionState.coinInfo.coin, + transactionState.coinInfo.icon, + transactionState.selectedToken, + transactionState.toAddress, + transactionState.toContact, + wallet, + ]); const transferTokens = useCallback(async () => { try { setSending(true); - switch (props.data.currentTxState) { + switch (transactionState.currentTxState) { case 'FlowFromCadenceToEvm': await transferFlowFromCadenceToEvm(); break; @@ -156,13 +176,13 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { await transferFTFromCadenceToEvm(); break; default: - throw new Error(`Unsupported transaction state: ${props.data.currentTxState}`); + throw new Error(`Unsupported transaction state: ${transactionState.currentTxState}`); } } catch (error) { console.error('Transaction failed:', error); setFailed(true); } - }, [transferFlowFromCadenceToEvm, transferFTFromCadenceToEvm, props.data.currentTxState]); + }, [transferFlowFromCadenceToEvm, transferFTFromCadenceToEvm, transactionState.currentTxState]); const transactionDoneHandler = useCallback( (request) => { @@ -232,7 +252,7 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { )} - + @@ -240,10 +260,10 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { - {props.data.childType && props.data.childType !== 'evm' ? ( - + {transactionState.fromNetwork === 'Evm' ? ( + ) : ( - + )} { ))} - + { }} > - + {transactionState.coinInfo.icon && ( + + )} - {props.data.coinInfo.coin} + {transactionState.coinInfo.coin} - {props.data.amount} {props.data.coinInfo.unit} + {transactionState.amount} {transactionState.coinInfo.unit} @@ -301,7 +326,7 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { color="info" sx={{ fontSize: '14px', fontWeight: 'semi-bold', textAlign: 'end' }} > - $ {props.data.secondAmount} + $ {transactionState.fiatAmount} @@ -375,7 +400,7 @@ const FlowToEVMConfirmation = (props: ToEthConfirmationProps) => { <> void; handleMaxClick: () => void; }) => { - console.log('SendEth '); const history = useHistory(); const wallet = useWallet(); const { currentNetwork: network } = useNetworkStore(); const [isConfirmationOpen, setConfirmationOpen] = useState(false); const [validated, setValidated] = useState(null); - // TODO: move this to some store - const web3Instance = useMemo(() => { - const provider = new Web3.providers.HttpProvider(EVM_ENDPOINT[network]); - return new Web3(provider); - }, [network]); - - const [erc20Contract, setErc20Contract] = useState(null); - - const updateContractInfo = useCallback( - async (address: string, symbol: string) => { - // Update the contract instance - let contractAddress = '0x7cd84a6b988859202cbb3e92830fff28813b9341'; - if (symbol.toLowerCase() !== 'flow') { - contractAddress = address; - } - const contractInstance = new web3Instance.eth.Contract(erc20ABI, contractAddress); - - setErc20Contract(contractInstance); - }, - [web3Instance] - ); - const checkAddress = useCallback(async () => { //wallet controller api try { @@ -103,18 +80,8 @@ const SendEth = ({ }, [transactionState.toAddress]); useEffect(() => { - console.log('SendEth useEffect '); - updateContractInfo( - transactionState.selectedToken.address, - transactionState.selectedToken.symbol - ); checkAddress(); - }, [ - updateContractInfo, - checkAddress, - transactionState.selectedToken.address, - transactionState.selectedToken.symbol, - ]); + }, [checkAddress]); return (
@@ -131,7 +98,7 @@ const SendEth = ({ /> */} - {validated ? ( + {validated !== null && validated ? ( <> ) : ( setConfirmationOpen(false)} handleCancelBtnClicked={() => setConfirmationOpen(false)} handleAddBtnClicked={() => { diff --git a/src/ui/views/Send/TransferAmount.tsx b/src/ui/views/Send/TransferAmount.tsx index ad71f7fb..b02dad14 100644 --- a/src/ui/views/Send/TransferAmount.tsx +++ b/src/ui/views/Send/TransferAmount.tsx @@ -110,7 +110,6 @@ const useStyles = makeStyles(() => ({ height: '25px', }, })); - const TransferAmount = ({ transactionState, handleAmountChange, @@ -132,17 +131,11 @@ const TransferAmount = ({ (option) => { const selectCoin = coinStore.coins.find((coin) => coin.unit === option); if (selectCoin) { - // Debounce only the state updates, not the render - const updateStates = debounce(() => { - handleTokenChange(option); - }, 300); - - updateStates(); return ; } return null; }, - [coinStore, handleTokenChange] + [coinStore] ); return ( @@ -215,9 +208,10 @@ const TransferAmount = ({ - {coinList.map((coin) => ( - - - - - {coin.coin} - - ))} - - - - - - } - /> - - - - {chrome.i18n.getMessage('Balance')} - {coinInfo.balance} - - - - - - - {chrome.i18n.getMessage('Insufficient_balance') + - (coinInfo.unit.toLowerCase() === 'flow' - ? chrome.i18n.getMessage('on_Flow_the_balance_cant_less_than_0001_FLOW') - : '')} - - - - - - ); -}; - -export default MoveToken; diff --git a/src/ui/views/EvmMove/Components/TransferFrom.tsx b/src/ui/views/EvmMove/Components/TransferFrom.tsx deleted file mode 100644 index d33b1a47..00000000 --- a/src/ui/views/EvmMove/Components/TransferFrom.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Box, Typography, CardMedia } from '@mui/material'; -import { StyledEngineProvider } from '@mui/material/styles'; -import React, { useEffect, useState, useCallback } from 'react'; - -import { useProfileStore } from '@/ui/stores/profileStore'; - -const tempEmoji = { - emoji: '🥥', - name: 'Coconut', - bgcolor: '#FFE4C4', -}; - -const TransferFrom = ({ wallet, userInfo, isChild = false }) => { - const { parentWallet } = useProfileStore(); - const [emoji, setEmoji] = useState(tempEmoji); - - const getEmoji = useCallback(async () => { - const emojiObject = { - ...tempEmoji, - emoji: parentWallet.icon, - name: parentWallet.name, - bgcolor: parentWallet.color, - type: 'parent', - }; - setEmoji(emojiObject); - }, [parentWallet, setEmoji]); - - useEffect(() => { - getEmoji(); - }, [getEmoji]); - - return ( - - - - - - {isChild ? ( - - ) : ( - {emoji.emoji} - )} - - - - {isChild ? userInfo.contact_name : emoji.name} - - - {userInfo.address} - - - - - - - ); -}; - -export default TransferFrom; diff --git a/src/ui/views/EvmMove/Components/TransferTo.tsx b/src/ui/views/EvmMove/Components/TransferTo.tsx deleted file mode 100644 index 6e5fc24f..00000000 --- a/src/ui/views/EvmMove/Components/TransferTo.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Box, Typography, CardMedia } from '@mui/material'; -import { StyledEngineProvider } from '@mui/material/styles'; -import React, { useState, useEffect, useCallback } from 'react'; - -import { useProfileStore } from '@/ui/stores/profileStore'; -import { formatAddress } from 'ui/utils'; - -const tempEmoji = { - emoji: '🥥', - name: 'Coconut', - bgcolor: '#FFE4C4', -}; - -const TransferTo = ({ wallet, userInfo }) => { - const { evmWallet } = useProfileStore(); - const [emoji, setEmoji] = useState(tempEmoji); - - const getEmoji = useCallback(async () => { - if (!emoji['type']) { - console.log('getEvmWallet ', evmWallet); - const emojiObject = tempEmoji; - emojiObject.emoji = evmWallet.icon; - emojiObject.name = evmWallet.name; - emojiObject.bgcolor = evmWallet.color; - emojiObject['type'] = 'evm'; - setEmoji(emojiObject); - } - }, [emoji, evmWallet]); - - useEffect(() => { - getEmoji(); - }, [getEmoji]); - - return ( - - - - - - {emoji.emoji} - - - - {emoji.name} - - EVM - - - - - {formatAddress(wallet)} - - - - - - - ); -}; - -export default TransferTo; diff --git a/src/ui/views/EvmMove/MoveFromChild/index.tsx b/src/ui/views/EvmMove/MoveFromChild/index.tsx deleted file mode 100644 index ae21dcb5..00000000 --- a/src/ui/views/EvmMove/MoveFromChild/index.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import CloseIcon from '@mui/icons-material/Close'; -import { Box, Button, Typography, Drawer, IconButton, Grid } from '@mui/material'; -import React, { useState, useEffect, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; - -import type { Contact } from '@/shared/types/network-types'; -import { isValidEthereumAddress, withPrefix } from '@/shared/utils/address'; -import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; -import { useProfileStore } from '@/ui/stores/profileStore'; -import { useStorageCheck } from '@/ui/utils/useStorageCheck'; -import type { CoinItem } from 'background/service/coinList'; -import { LLSpinner } from 'ui/FRWComponent'; -import { useWallet } from 'ui/utils'; - -import IconSwitch from '../../../../components/iconfont/IconSwitch'; -import theme from '../../../style/LLTheme'; -import MoveToken from '../Components/MoveToken'; -import TransferFrom from '../Components/TransferFrom'; -import TransferTo from '../Components/TransferTo'; - -interface TransferConfirmationProps { - isConfirmationOpen: boolean; - data: any; - handleCloseIconClicked: () => void; - handleCancelBtnClicked: () => void; - handleAddBtnClicked: () => void; -} -const USER_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const CHILD_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const MoveFromChild = (props: TransferConfirmationProps) => { - enum ENV { - Mainnet = 'mainnet', - Testnet = 'testnet', - } - enum Error { - Exceed = 'Insufficient balance', - Fail = 'Cannot find swap pair', - } - - // declare enum Strategy { - // GitHub = 'GitHub', - // Static = 'Static', - // CDN = 'CDN' - // } - - const empty: CoinItem = { - coin: '', - unit: '', - balance: 0, - price: 0, - change24h: 0, - total: 0, - icon: '', - }; - - const usewallet = useWallet(); - const history = useHistory(); - const { childAccounts } = useProfileStore(); - const [userWallet, setWallet] = useState(null); - const [currentCoin, setCurrentCoin] = useState('flow'); - const [coinList, setCoinList] = useState([]); - // const [exceed, setExceed] = useState(false); - const [amount, setAmount] = useState(''); - // const [validated, setValidated] = useState(null); - const [userInfo, setUser] = useState(USER_CONTACT); - const [childUserInfo, setChildUser] = useState(CHILD_CONTACT); - const [network, setNetwork] = useState('mainnet'); - const [childAddress, setChildAddress] = useState(''); - const [coinInfo, setCoinInfo] = useState(empty); - const [isLoading, setLoading] = useState(false); - const [errorType, setErrorType] = useState(null); - const [exceed, setExceed] = useState(false); - - const { sufficient: isSufficient, sufficientAfterAction } = useStorageCheck({ - transferAmount: Number(amount) || 0, - coin: currentCoin, - // Rendering this component means we are moving from a FLOW child account - // We are moving to userInfo.address. Check if it's an EVM address - movingBetweenEVMAndFlow: isValidEthereumAddress(userInfo.address), - }); - - const isLowStorage = isSufficient !== undefined && !isSufficient; // isSufficient is undefined when the storage check is not yet completed - const isLowStorageAfterAction = sufficientAfterAction !== undefined && !sufficientAfterAction; - - const setUserWallet = useCallback(async () => { - // const walletList = await storage.get('userWallet'); - setLoading(true); - const token = await usewallet.getCurrentCoin(); - - // This is the main wallet address - const wallet = await usewallet.getMainWallet(); - const network = await usewallet.getNetwork(); - setNetwork(network); - setCurrentCoin(token); - // userWallet - await setWallet(wallet); - const coinList = await usewallet.getCoinList(); - setCoinList(coinList); - const currentAddress = await usewallet.getCurrentAddress(); - setChildAddress(currentAddress!); - const coinInfo = coinList.find((coin) => coin.unit.toLowerCase() === token.toLowerCase()); - setCoinInfo(coinInfo!); - - const info = await usewallet.getUserInfo(false); - - const walletAddress = withPrefix(wallet) || ''; - setUser({ - ...USER_CONTACT, - address: walletAddress, - avatar: info.avatar, - contact_name: info.username, - }); - - const cwallet = childAccounts[currentAddress!]; - - setChildUser({ - ...CHILD_CONTACT, - address: withPrefix(currentAddress!) || '', - avatar: cwallet.thumbnail.url, - contact_name: cwallet.name, - }); - setLoading(false); - return; - }, [usewallet, childAccounts]); - - const moveToken = useCallback(async () => { - setLoading(true); - const tokenResult = await usewallet.openapi.getTokenInfo(currentCoin, network); - usewallet - .moveFTfromChild(childUserInfo!.address, 'flowTokenProvider', amount!, tokenResult!.name) - .then(async (txId) => { - usewallet.listenTransaction( - txId, - true, - 'Transfer complete', - `Your have moved ${amount} ${tokenResult!.name} to your address ${userWallet}. \nClick to view this transaction.` - ); - await usewallet.setDashIndex(0); - history.push(`/dashboard?activity=1&txId=${txId}`); - setLoading(false); - props.handleCloseIconClicked(); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }, [currentCoin, network, usewallet, childUserInfo, amount, userWallet, history, props]); - - const handleMove = async () => { - moveToken(); - }; - - const handleCoinInfo = useCallback(async () => { - if (coinList.length > 0) { - const coinInfo = coinList.find( - (coin) => coin.unit.toLowerCase() === currentCoin.toLowerCase() - ); - setCoinInfo(coinInfo!); - } - }, [coinList, currentCoin]); - - useEffect(() => { - setUserWallet(); - }, [setUserWallet]); - - useEffect(() => { - handleCoinInfo(); - }, [currentCoin, handleCoinInfo]); - - return ( - - - - - - - {chrome.i18n.getMessage('move_tokens')} - - - - - - - - - {childAddress && ( - - )} - - {isLoading ? ( - - - - ) : ( - - - - )} - - {userWallet && } - - - - - {coinInfo.unit && ( - - )} - - - - - - - - ); -}; - -export default MoveFromChild; diff --git a/src/ui/views/EvmMove/MoveFromEvm/index.tsx b/src/ui/views/EvmMove/MoveFromEvm/index.tsx deleted file mode 100644 index e5595ccc..00000000 --- a/src/ui/views/EvmMove/MoveFromEvm/index.tsx +++ /dev/null @@ -1,344 +0,0 @@ -import CloseIcon from '@mui/icons-material/Close'; -import { Box, Button, Typography, Drawer, IconButton, Grid } from '@mui/material'; -import React, { useState, useEffect, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; - -import type { Contact } from '@/shared/types/network-types'; -import { isValidEthereumAddress, withPrefix } from '@/shared/utils/address'; -import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; -import { useTransactionHook } from '@/ui/hooks/useTransactionHook'; -import { useCoinStore } from '@/ui/stores/coinStore'; -import { useProfileStore } from '@/ui/stores/profileStore'; -import { useTransactionStore } from '@/ui/stores/transactionStore'; -import { useStorageCheck } from '@/ui/utils/useStorageCheck'; -import type { CoinItem } from 'background/service/coinList'; -import { LLSpinner } from 'ui/FRWComponent'; -import { useWallet } from 'ui/utils'; - -import IconSwitch from '../../../../components/iconfont/IconSwitch'; -import theme from '../../../style/LLTheme'; -import MoveToken from '../Components/MoveToken'; -import TransferFrom from '../Components/TransferFrom'; -import TransferTo from '../Components/TransferTo'; - -interface TransferConfirmationProps { - isConfirmationOpen: boolean; - data: any; - handleCloseIconClicked: () => void; - handleCancelBtnClicked: () => void; - handleAddBtnClicked: () => void; -} -const USER_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const EVM_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const EMPTY_COIN: CoinItem = { - coin: '', - unit: '', - balance: 0, - price: 0, - change24h: 0, - total: 0, - icon: '', -}; - -const MoveFromEvm = (props: TransferConfirmationProps) => { - const usewallet = useWallet(); - const history = useHistory(); - - const { evmWallet, mainAddress, parentWallet, userInfo } = useProfileStore(); - const { coins: coinList } = useCoinStore(); - const { selectedToken, setFromNetwork, setToNetwork, setTokenType } = useTransactionStore(); - const { fetchAndSetToken } = useTransactionHook(); - - const [currentCoin, setCurrentCoin] = useState('flow'); - // const [exceed, setExceed] = useState(false); - const [amount, setAmount] = useState(''); - // const [validated, setValidated] = useState(null); - const [flowUserInfo, setFlowUser] = useState(USER_CONTACT); - const [evmUserInfo, setEvmUser] = useState(EVM_CONTACT); - const [evmAddress, setEvmAddress] = useState(''); - const [coinInfo, setCoinInfo] = useState(EMPTY_COIN); - const [isLoading, setLoading] = useState(false); - const [exceed, setExceed] = useState(false); - - const { sufficient: isSufficient, sufficientAfterAction } = useStorageCheck({ - transferAmount: Number(amount) || 0, - coin: currentCoin, - // Rendering this component means we are moving from an EVM account - // If we are not moving to an EVM account, we are moving to a FLOW account - movingBetweenEVMAndFlow: !isValidEthereumAddress(flowUserInfo.address), - }); - - const isLowStorage = isSufficient !== undefined && !isSufficient; // isSufficient is undefined when the storage check is not yet completed - const isLowStorageAfterAction = sufficientAfterAction !== undefined && !sufficientAfterAction; - - const setUserWallet = useCallback(async () => { - setCurrentCoin(selectedToken.symbol); - setEvmAddress(evmWallet.address); - const tokenResult = selectedToken; - if (selectedToken?.symbol.toLowerCase() !== 'flow') { - setTokenType('FT'); - } else { - setTokenType('Flow'); - } - setFromNetwork(evmWallet.address); - setToNetwork(parentWallet.address); - const coinInfo = coinList.find( - (coin) => coin && coin.unit.toLowerCase() === tokenResult!.symbol.toLowerCase() - ); - setCoinInfo(coinInfo!); - - const userContact = { - ...USER_CONTACT, - address: withPrefix(parentWallet.address) || '', - avatar: userInfo!.avatar, - contact_name: userInfo!.username, - }; - setFlowUser(userContact); - - const evmContact = { - ...EVM_CONTACT, - address: withPrefix(evmWallet.address) || '', - avatar: evmWallet.icon, - contact_name: evmWallet.name, - }; - setEvmUser(evmContact); - - setLoading(false); - return; - }, [ - evmWallet, - userInfo, - coinList, - parentWallet, - selectedToken, - setEvmAddress, - setCurrentCoin, - setFromNetwork, - setToNetwork, - setTokenType, - ]); - - const moveToken = async () => { - setLoading(true); - usewallet - .withdrawFlowEvm(amount, flowUserInfo.address) - .then(async (txId) => { - usewallet.listenTransaction( - txId, - true, - 'Transfer from EVM complete', - `Your have moved ${amount} Flow to your address ${parentWallet.address}. \nClick to view this transaction.` - ); - await usewallet.setDashIndex(0); - history.push(`/dashboard?activity=1&txId=${txId}`); - setLoading(false); - props.handleCloseIconClicked(); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const bridgeToken = async () => { - setLoading(true); - const tokenResult = selectedToken; - - let flowId = tokenResult!['flowIdentifier']; - - if (!flowId) { - const address = tokenResult!.address.startsWith('0x') - ? tokenResult!.address.slice(2) - : tokenResult!.address; - flowId = `A.${address}.${tokenResult!.contractName}.Vault`; - } - - usewallet - .bridgeToFlow(flowId, amount, tokenResult) - .then(async (txId) => { - usewallet.listenTransaction( - txId, - true, - 'Transfer from EVM complete', - `Your have moved ${amount} ${flowId.split('.')[2]} to your address ${parentWallet.address}. \nClick to view this transaction.` - ); - await usewallet.setDashIndex(0); - history.push(`/dashboard?activity=1&txId=${txId}`); - setLoading(false); - props.handleCloseIconClicked(); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const handleCoinInfo = useCallback(async () => { - if (coinList.length > 0) { - const coinInfo = coinList.find( - (coin) => coin.unit.toLowerCase() === currentCoin.toLowerCase() - ); - setCoinInfo(coinInfo!); - } - }, [coinList, currentCoin]); - - const handleMove = async () => { - if (currentCoin.toLowerCase() === 'flow') { - moveToken(); - } else { - bridgeToken(); - } - }; - - useEffect(() => { - setLoading(true); - if (userInfo && coinList.length > 0) { - setUserWallet(); - } - }, [userInfo, coinList, setUserWallet]); - - useEffect(() => { - handleCoinInfo(); - }, [currentCoin, handleCoinInfo]); - - return ( - - - - - - - {chrome.i18n.getMessage('move_tokens')} - - - - - - - - - {parentWallet.address && } - - {isLoading ? ( - - - - ) : ( - - - - )} - - {evmAddress && } - - - - - {coinInfo && coinInfo.unit && ( - - )} - - - - - - - ); -}; - -export default MoveFromEvm; diff --git a/src/ui/views/EvmMove/MoveFromFlow/index.tsx b/src/ui/views/EvmMove/MoveFromFlow/index.tsx deleted file mode 100644 index 2afd1712..00000000 --- a/src/ui/views/EvmMove/MoveFromFlow/index.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import CloseIcon from '@mui/icons-material/Close'; -import { Box, Button, Typography, Drawer, IconButton, Grid } from '@mui/material'; -import React, { useState, useEffect, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; - -import type { Contact } from '@/shared/types/network-types'; -import { withPrefix, isValidEthereumAddress } from '@/shared/utils/address'; -import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; -import { useTransactionHook } from '@/ui/hooks/useTransactionHook'; -import { useCoinStore } from '@/ui/stores/coinStore'; -import { useProfileStore } from '@/ui/stores/profileStore'; -import { useTransactionStore } from '@/ui/stores/transactionStore'; -import { useStorageCheck } from '@/ui/utils/useStorageCheck'; -import type { CoinItem } from 'background/service/coinList'; -import { LLSpinner } from 'ui/FRWComponent'; -import { useWallet } from 'ui/utils'; - -import IconSwitch from '../../../../components/iconfont/IconSwitch'; -import theme from '../../../style/LLTheme'; -import MoveToken from '../Components/MoveToken'; -import TransferFrom from '../Components/TransferFrom'; -import TransferTo from '../Components/TransferTo'; - -interface TransferConfirmationProps { - isConfirmationOpen: boolean; - data: any; - handleCloseIconClicked: () => void; - handleCancelBtnClicked: () => void; - handleAddBtnClicked: () => void; -} -const USER_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const EVM_CONTACT = { - address: '', - id: 0, - contact_name: '', - avatar: '', - domain: { - domain_type: 999, - value: '', - }, -} as unknown as Contact; - -const EMPTY_COIN: CoinItem = { - coin: '', - unit: '', - balance: 0, - price: 0, - change24h: 0, - total: 0, - icon: '', -}; - -const MoveFromFlow = (props: TransferConfirmationProps) => { - const usewallet = useWallet(); - const history = useHistory(); - - const { evmWallet, mainAddress, currentWallet, userInfo } = useProfileStore(); - const { coins: coinList } = useCoinStore(); - const { selectedToken, setFromNetwork, setToNetwork, setTokenType } = useTransactionStore(); - const { fetchAndSetToken } = useTransactionHook(); - - const [amount, setAmount] = useState(''); - const [flowUserInfo, setSender] = useState(USER_CONTACT); - const [evmUserInfo, setEvmUser] = useState(EVM_CONTACT); - const [coinInfo, setCoinInfo] = useState(EMPTY_COIN); - const [isLoading, setLoading] = useState(false); - const [errorType, setErrorType] = useState(null); - const [exceed, setExceed] = useState(false); - - const { sufficient: isSufficient, sufficientAfterAction } = useStorageCheck({ - transferAmount: Number(amount) || 0, - coin: selectedToken.symbol, - // We are moving from a Flow account to an EVM account - movingBetweenEVMAndFlow: true, - }); - - const isLowStorage = isSufficient !== undefined && !isSufficient; // isSufficient is undefined when the storage check is not yet completed - const isLowStorageAfterAction = sufficientAfterAction !== undefined && !sufficientAfterAction; - - const setUserWallet = useCallback(async () => { - const token = selectedToken.symbol; - if (selectedToken?.symbol.toLowerCase() !== 'flow') { - setTokenType('FT'); - } else { - setTokenType('Flow'); - } - setFromNetwork(currentWallet.address); - setToNetwork(evmWallet.address); - const coinInfo = coinList.find((coin) => coin.unit.toLowerCase() === token.toLowerCase()); - setCoinInfo(coinInfo!); - - const userContact = { - ...USER_CONTACT, - address: withPrefix(mainAddress) || '', - avatar: userInfo!.avatar, - contact_name: userInfo!.username, - }; - setSender(userContact); - - const evmContact = { - ...EVM_CONTACT, - address: withPrefix(evmWallet.address) || '', - avatar: evmWallet.icon, - contact_name: evmWallet.name, - }; - setEvmUser(evmContact); - setLoading(false); - - return; - }, [ - evmWallet, - userInfo, - coinList, - mainAddress, - currentWallet, - selectedToken, - setFromNetwork, - setToNetwork, - setTokenType, - ]); - - const moveToken = async () => { - setLoading(true); - usewallet - .fundFlowEvm(amount) - .then(async (txId) => { - usewallet.listenTransaction( - txId, - true, - 'Transfer to EVM complete', - `Your have moved ${amount} Flow to your EVM address ${evmWallet.address}. \nClick to view this transaction.` - ); - await usewallet.setDashIndex(0); - history.push(`/dashboard?activity=1&txId=${txId}`); - setLoading(false); - props.handleCloseIconClicked(); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const bridgeToken = async () => { - setLoading(true); - const address = selectedToken!.address.startsWith('0x') - ? selectedToken!.address.slice(2) - : selectedToken!.address; - - let flowId = `A.${address}.${selectedToken!.contractName}.Vault`; - - if (isValidEthereumAddress(address)) { - flowId = selectedToken!['flowIdentifier']; - } - usewallet - .bridgeToEvm(flowId, amount) - .then(async (txId) => { - usewallet.listenTransaction( - txId, - true, - 'Transfer to EVM complete', - `Your have moved ${amount} ${selectedToken!.contractName} to your EVM address ${evmWallet.address}. \nClick to view this transaction.` - ); - await usewallet.setDashIndex(0); - history.push(`/dashboard?activity=1&txId=${txId}`); - setLoading(false); - props.handleCloseIconClicked(); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const handleMove = async () => { - if (selectedToken.symbol.toLowerCase() === 'flow') { - moveToken(); - } else { - bridgeToken(); - } - }; - - const handleCoinInfo = useCallback(async () => { - if (coinList.length > 0) { - const coinInfo = coinList.find( - (coin) => coin.unit.toLowerCase() === selectedToken.symbol.toLowerCase() - ); - setCoinInfo(coinInfo!); - } - }, [coinList, selectedToken]); - - useEffect(() => { - setLoading(true); - if (userInfo && coinList.length > 0) { - setUserWallet(); - } - }, [userInfo, coinList, setUserWallet]); - - useEffect(() => { - handleCoinInfo(); - }, [selectedToken, handleCoinInfo]); - - return ( - - - - - - - {chrome.i18n.getMessage('move_tokens')} - - - - - - - - - {currentWallet && } - - {isLoading ? ( - - - - ) : ( - - - - )} - - {evmWallet.address && } - - - - - {coinInfo.unit && ( - - )} - - - - - - - ); -}; - -export default MoveFromFlow; diff --git a/src/ui/views/MoveBoard/index.tsx b/src/ui/views/MoveBoard/index.tsx index 51918fe7..0cfce8eb 100644 --- a/src/ui/views/MoveBoard/index.tsx +++ b/src/ui/views/MoveBoard/index.tsx @@ -11,9 +11,6 @@ import moveftbg from 'ui/FRWAssets/svg/moveftbg.svg'; import movenftbg from 'ui/FRWAssets/svg/movenftbg.svg'; import { useWallet } from 'ui/utils'; -import MoveFromEvm from '../EvmMove/MoveFromEvm'; -import MoveFromFlow from '../EvmMove/MoveFromFlow'; - import MoveEvm from './MoveEvm'; import MoveFromChild from './MoveFromChild'; import MoveToChild from './MoveToChild'; @@ -27,8 +24,8 @@ interface MoveBoardProps { const MoveBoard = (props: MoveBoardProps) => { const usewallet = useWallet(); + const history = useHistory(); const [showSelectNft, setSelectBoard] = useState(false); - const [moveFtOpen, setMoveFt] = useState(false); const [childType, setChildType] = useState(null); const [network, setNetwork] = useState(''); const [alertOpen, setAlertOpen] = useState(false); @@ -42,11 +39,6 @@ const MoveBoard = (props: MoveBoardProps) => { setChildType(result); }, [usewallet]); - const closeFullPage = () => { - setMoveFt(false); - props.handleCancelBtnClicked(); - }; - useEffect(() => { requestChildType(); }, [requestChildType]); @@ -90,34 +82,6 @@ const MoveBoard = (props: MoveBoardProps) => { ); }; - const renderMoveFT = () => { - if (childType === 'evm') { - return ( - closeFullPage()} - handleCancelBtnClicked={() => setMoveFt(false)} - handleAddBtnClicked={() => { - setMoveFt(false); - }} - /> - ); - } else { - return ( - closeFullPage()} - handleCancelBtnClicked={() => setMoveFt(false)} - handleAddBtnClicked={() => { - setMoveFt(false); - }} - /> - ); - } - }; - return ( { }, }} onClick={() => { - if (childType && childType !== 'evm') { - setAlertOpen(true); - } else { - setMoveFt(true); - } + history.push('/dashboard/token/flow/send'); }} > { setAlertOpen(false)} /> )} {showSelectNft && renderMoveComponent()} - - {moveFtOpen && renderMoveFT()} ); }; diff --git a/src/ui/views/TokenDetail/TokenInfoCard.tsx b/src/ui/views/TokenDetail/TokenInfoCard.tsx index e4b10734..6b92bda6 100644 --- a/src/ui/views/TokenDetail/TokenInfoCard.tsx +++ b/src/ui/views/TokenDetail/TokenInfoCard.tsx @@ -5,7 +5,6 @@ import { useHistory } from 'react-router-dom'; import { isValidEthereumAddress } from '@/shared/utils/address'; import { LLPrimaryButton } from '@/ui/FRWComponent'; -import { useTransactionStore } from '@/ui/stores/transactionStore'; import iconMove from 'ui/FRWAssets/svg/moveIcon.svg'; import { useWallet } from 'ui/utils'; import { addDotSeparators } from 'ui/utils/number'; @@ -16,26 +15,12 @@ import { TokenPrice } from './TokenValue'; // import tips from 'ui/FRWAssets/svg/tips.svg'; -const TokenInfoCard = ({ - price, - token, - setAccessible, - accessible, - setMoveOpen, - tokenInfo, - network, - childType, - childAccount, - setAlertOpen, -}) => { +const TokenInfoCard = ({ price, token, setAccessible, accessible, tokenInfo, childType }) => { const wallet = useWallet(); const history = useHistory(); const isMounted = useRef(true); - const { setSelectedToken } = useTransactionStore(); const [balance, setBalance] = useState(0); - const [active, setIsActive] = useState(true); const [data, setData] = useState(undefined); - const [evmEnabled, setEvmEnabled] = useState(false); const [canMoveChild, setCanMoveChild] = useState(true); @@ -53,31 +38,15 @@ const TokenInfoCard = ({ }, [balance, tokenInfo.custom, wallet]); const toSend = () => { - console.log('tokenInfo ', tokenInfo); - setSelectedToken(tokenInfo); history.push(`/dashboard/token/${tokenInfo.symbol}/send`); }; - const moveToken = () => { - if (childType && childType !== 'evm') { - setAlertOpen(true); - } else if (data) { - console.log('data setCurrentCoin ', data); - setSelectedToken(data); - setMoveOpen(true); - } - }; - const getActive = useCallback(async () => { - const evmEnabled = await wallet.getEvmEnabled(); - setEvmEnabled(evmEnabled); const isChild = await wallet.getActiveWallet(); const timerId = setTimeout(async () => { if (!isMounted.current) return; // Early exit if component is not mounted setData(tokenInfo!); - console.log('tokenInfo ', tokenInfo); - setIsActive(true); setAccessible(true); if (isChild === 'evm') { const coins = await wallet.getCoinList(); @@ -189,7 +158,7 @@ const TokenInfoCard = ({ {canMoveChild && ( - moveToken()}> + toSend()}> { const classes = useStyles(); const usewallet = useWallet(); const history = useHistory(); - const { childAccounts, currentWallet } = useProfileStore(); + const { currentWallet } = useProfileStore(); const [price, setPrice] = useState(0); const [accessible, setAccessible] = useState(true); const token = useParams<{ id: string }>().id.toLowerCase(); const [network, setNetwork] = useState('mainnet'); const [walletName, setCurrentWallet] = useState({ name: '' }); - const [moveOpen, setMoveOpen] = useState(false); const [tokenInfo, setTokenInfo] = useState(undefined); const [providers, setProviders] = useState([]); - const [childAccount, setChildAccount] = useState({}); const [childType, setChildType] = useState(null); - const [alertOpen, setAlertOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const handleMenuToggle = () => { @@ -151,49 +143,11 @@ const TokenDetail = () => { const requestChildType = useCallback(async () => { const result = await usewallet.getActiveWallet(); - setChildAccount(childAccounts); setChildType(result); - }, [usewallet, childAccounts]); + }, [usewallet]); - const renderMoveComponent = () => { - if (childType === 'evm') { - return ( - setMoveOpen(false)} - handleCancelBtnClicked={() => setMoveOpen(false)} - handleAddBtnClicked={() => { - setMoveOpen(false); - }} - /> - ); - } else if (childType) { - // We are moving from a FLOW child account - return ( - setMoveOpen(false)} - handleCancelBtnClicked={() => setMoveOpen(false)} - handleAddBtnClicked={() => { - setMoveOpen(false); - }} - /> - ); - } else { - return ( - setMoveOpen(false)} - handleCancelBtnClicked={() => setMoveOpen(false)} - handleAddBtnClicked={() => { - setMoveOpen(false); - }} - /> - ); - } + const handleMoveOpen = () => { + history.push(`/dashboard/token/${token}/send`); }; useEffect(() => { @@ -236,12 +190,8 @@ const TokenDetail = () => { token={token} setAccessible={setAccessible} accessible={accessible} - setMoveOpen={setMoveOpen} tokenInfo={tokenInfo} - network={network} childType={childType} - childAccount={childAccount} - setAlertOpen={setAlertOpen} /> )} {token === 'flow' && } @@ -249,13 +199,7 @@ const TokenDetail = () => { {providers?.length > 0 && ( )} - {moveOpen && renderMoveComponent()} - {network === 'mainnet' && ( - setAlertOpen(false)} - /> - )} + {token === 'flow' && }
From b8dc3750af2842710d7a32473d1e3519090096ed Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:17:55 +1100 Subject: [PATCH 32/43] =?UTF-8?q?Moved=20all=20transaction=20logic=20to=20?= =?UTF-8?q?the=20background=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/background/controller/wallet.ts | 229 +++++++++++-------- src/ui/reducers/transaction-reducer.ts | 2 +- src/ui/views/SendTo/TransferConfirmation.tsx | 152 +----------- 3 files changed, 136 insertions(+), 247 deletions(-) diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index e9b1d65a..c6b32c9c 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -11,7 +11,7 @@ import { getApp } from 'firebase/app'; import { getAuth } from 'firebase/auth/web-extension'; import type { TokenInfo } from 'flow-native-token-registry'; import { encode } from 'rlp'; -import web3, { TransactionError } from 'web3'; +import web3, { TransactionError, Web3 } from 'web3'; import { findAddressWithNetwork, @@ -27,10 +27,10 @@ import { import eventBus from '@/eventBus'; import { type FeatureFlagKey, type FeatureFlags } from '@/shared/types/feature-types'; import { type TrackingEvents } from '@/shared/types/tracking-types'; -import { type ActiveChildType, type LoggedInAccount } from '@/shared/types/wallet-types'; +import { type TransactionState } from '@/shared/types/transaction-types'; +import { type LoggedInAccount } from '@/shared/types/wallet-types'; import { ensureEvmAddressPrefix, isValidEthereumAddress, withPrefix } from '@/shared/utils/address'; -import { getHashAlgo, getSignAlgo } from '@/shared/utils/algo'; -import { retryOperation } from '@/shared/utils/retryOperation'; +import { getSignAlgo } from '@/shared/utils/algo'; import { keyringService, preferenceService, @@ -60,7 +60,7 @@ import emoji from 'background/utils/emoji.json'; import fetchConfig from 'background/utils/remoteConfig'; import { notification, storage } from 'background/webapi'; import { openIndexPage } from 'background/webapi/tab'; -import { INTERNAL_REQUEST_ORIGIN, EVENTS, KEYRING_TYPE } from 'consts'; +import { INTERNAL_REQUEST_ORIGIN, EVENTS, KEYRING_TYPE, EVM_ENDPOINT } from 'consts'; import type { BlockchainResponse, @@ -76,6 +76,7 @@ import type { PreferenceAccount } from '../service/preference'; import { type EvaluateStorageResult, StorageEvaluator } from '../service/storage-evaluator'; import type { UserInfoStore } from '../service/user'; import defaultConfig from '../utils/defaultConfig.json'; +import erc20ABI from '../utils/erc20.abi.json'; import { getLoggedInAccount } from '../utils/getLoggedInAccount'; import BaseController from './base'; @@ -775,27 +776,6 @@ export class WalletController extends BaseController { const keyring = await keyringService.getKeyringForAccount(from, type); const res = await keyringService.signTransaction(keyring, data, options); - /* - cadence_transaction_signed: { - cadence: string; // SHA256 Hashed Cadence that was signed. - tx_id: string; // String of the transaction ID. - authorizers: string[]; // Comma separated list of authorizer account address in the transaction - proposer: string; // Address of the transactions proposer. - payer: string; // Payer of the transaction. - success: boolean; // Boolean of if the transaction was sent successful or not. true/false - }; - evm_transaction_signed: { - success: boolean; // Boolean of if the transaction was sent successful or not. true/false - flow_address: string; // Address of the account that signed the transaction - evm_address: string; // EVM Address of the account that signed the transaction - tx_id: string; // transaction id - }; - mixpanelTrack.track('transaction_signed', { - address: from, - type, - ...res, - }); - */ return res; }; @@ -880,16 +860,7 @@ export class WalletController extends BaseController { updateAlianName = (address: string, name: string) => preferenceService.updateAlianName(address, name); getAllAlianName = () => preferenceService.getAllAlianName(); - // getInitAlianNameStatus = () => preferenceService.getInitAlianNameStatus(); - // updateInitAlianNameStatus = () => - // preferenceService.changeInitAlianNameStatus(); - // getLastTimeGasSelection = (chainId) => { - // return preferenceService.getLastTimeGasSelection(chainId); - // }; - // updateLastTimeGasSelection = (chainId: string, gas: ChainGas) => { - // return preferenceService.updateLastTimeGasSelection(chainId, gas); - // }; getIsFirstOpen = () => { return preferenceService.getIsFirstOpen(); }; @@ -899,14 +870,6 @@ export class WalletController extends BaseController { listChainAssets = async (address: string) => { return await openapiService.getCoinList(address); }; - // getAddedToken = (address: string) => { - // return preferenceService.getAddedToken(address); - // }; - // updateAddedToken = (address: string, tokenList: []) => { - // return preferenceService.updateAddedToken(address, tokenList); - // }; - - // lilico new service // userinfo getUserInfo = async (forceRefresh: boolean) => { @@ -1002,17 +965,7 @@ export class WalletController extends BaseController { checkAccessibleNft = async (childAccount) => { try { - // const res = await openapiService.checkChildAccount(address); - // const nfts = await openapiService.queryAccessible( - // '0x84221fe0294044d7', - // '0x16c41a2b76dee69b' - // ); const nfts = await openapiService.checkChildAccountNFT(childAccount); - // openapiService.checkChildAccountNFT(address).then((res) => { - // console.log(res) - // }).catch((err) => { - // console.log(err) - // }) return nfts; } catch (error) { @@ -1025,13 +978,7 @@ export class WalletController extends BaseController { const network = await this.getNetwork(); const address = await userWalletService.getMainWallet(network); - // const res = await openapiService.checkChildAccount(address); const result = await openapiService.queryAccessibleFt(address, childAccount); - // openapiService.checkChildAccountNFT(address).then((res) => { - // console.log(res) - // }).catch((err) => { - // console.log(err) - // }) return result; }; @@ -1348,12 +1295,6 @@ export class WalletController extends BaseController { try { await this.fetchBalance({ signal }); - // const allTokens = await openapiService.getAllTokenInfo(); - // const enabledSymbols = tokenList.map((token) => token.symbol); - // const disableSymbols = allTokens.map((token) => token.symbol).filter((symbol) => !enabledSymbols.includes(symbol)); - // console.log('disableSymbols are these ', disableSymbols, enabledSymbols, coins) - // disableSymbols.forEach((coin) => coinListService.removeCoin(coin, network)); - const coinListResult = coinListService.listCoins(network); return coinListResult; } catch (err) { @@ -1843,6 +1784,132 @@ export class WalletController extends BaseController { }); }; + // Master send token function that takes a transaction state from the front end and returns the transaction ID + transferTokens = async (transactionState: TransactionState): Promise => { + const transferTokensOnCadence = async () => { + return this.transferCadenceTokens( + transactionState.selectedToken.symbol, + transactionState.toAddress, + transactionState.amount + ); + }; + + const transferTokensFromChildToCadence = async () => { + return this.sendFTfromChild( + transactionState.fromAddress, + transactionState.toAddress, + 'flowTokenProvider', + transactionState.amount, + transactionState.selectedToken.symbol + ); + }; + + const transferFlowFromEvmToCadence = async () => { + console.log('transferFlowFromEvmToCadence'); + return this.withdrawFlowEvm(transactionState.amount, transactionState.toAddress); + }; + + const transferFTFromEvmToCadence = async () => { + return this.transferFTFromEvm( + transactionState.selectedToken['flowIdentifier'], + transactionState.amount, + transactionState.toAddress, + transactionState.selectedToken + ); + }; + + // Returns the transaction ID + const transferTokensOnEvm = async () => { + // the amount is always stored as a string in the transaction state + const amountStr: string = transactionState.amount; + // TODO: check if the amount is a valid number + // Create an integer string based on the required token decimals + const amountBN = new BN(amountStr.replace('.', '')); + + const decimalsCount = amountStr.split('.')[1]?.length || 0; + const decimalDifference = transactionState.selectedToken.decimals - decimalsCount; + if (decimalDifference < 0) { + throw new Error('Too many decimal places have been provided'); + } + const scaleFactor = new BN(10).pow(decimalDifference); + const integerAmount = amountBN.multipliedBy(scaleFactor); + const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); + + let address, gas, value, data; + + if (transactionState.selectedToken.symbol.toLowerCase() === 'flow') { + address = transactionState.toAddress; + gas = '1'; + // const amountBN = new BN(transactionState.amount).multipliedBy(new BN(10).pow(18)); + // the amount is always stored as a string in the transaction state + value = integerAmount.toString(16); + data = '0x'; + } else { + // Get the current network + const network = await this.getNetwork(); + // Get the Web3 provider + const provider = new Web3.providers.HttpProvider(EVM_ENDPOINT[network]); + // Get the web3 instance + const web3Instance = new Web3(provider); + // Get the erc20 contract + const erc20Contract = new web3Instance.eth.Contract( + erc20ABI, + transactionState.selectedToken.address + ); + // Encode the data + const encodedData = erc20Contract.methods + .transfer(ensureEvmAddressPrefix(transactionState.toAddress), integerAmountStr) + .encodeABI(); + gas = '1312d00'; + address = ensureEvmAddressPrefix(transactionState.selectedToken.address); + value = '0x0'; // Zero value as hex + data = encodedData.startsWith('0x') ? encodedData : `0x${encodedData}`; + } + + // Send the transaction + return this.sendEvmTransaction(address, gas, value, data); + }; + + const transferFlowFromCadenceToEvm = async () => { + return this.transferFlowEvm(transactionState.toAddress, transactionState.amount); + }; + + const transferFTFromCadenceToEvm = async () => { + const address = transactionState.selectedToken!.address.startsWith('0x') + ? transactionState.selectedToken!.address.slice(2) + : transactionState.selectedToken!.address; + + return this.transferFTToEvmV2( + `A.${address}.${transactionState.selectedToken!.contractName}.Vault`, + transactionState.amount, + transactionState.toAddress + ); + }; + + // Switch on the current transaction state + switch (transactionState.currentTxState) { + case 'FTFromEvmToCadence': + return await transferFTFromEvmToCadence(); + case 'FlowFromEvmToCadence': + return await transferFlowFromEvmToCadence(); + case 'FTFromChildToCadence': + case 'FlowFromChildToCadence': + return await transferTokensFromChildToCadence(); + case 'FTFromCadenceToCadence': + case 'FlowFromCadenceToCadence': + return await transferTokensOnCadence(); + case 'FlowFromEvmToEvm': + case 'FTFromEvmToEvm': + return await transferTokensOnEvm(); + case 'FlowFromCadenceToEvm': + return await transferFlowFromCadenceToEvm(); + case 'FTFromCadenceToEvm': + return await transferFTFromCadenceToEvm(); + default: + throw new Error(`Unsupported transaction state: ${transactionState.currentTxState}`); + } + }; + transferFlowEvm = async ( recipientEVMAddressHex: string, amount = '1.0', @@ -2358,37 +2425,7 @@ export class WalletController extends BaseController { }; // TODO: Replace with generic token - transferTokens = async (symbol: string, address: string, amount: string): Promise => { - const token = await openapiService.getTokenInfo(symbol); - if (!token) { - throw new Error(`Invaild token name - ${symbol}`); - } - await this.getNetwork(); - const script = await getScripts('ft', 'transferTokensV3'); - - const txID = await userWalletService.sendTransaction( - script - .replaceAll('', token.contractName) - .replaceAll('', token.path.balance) - .replaceAll('', token.path.receiver) - .replaceAll('', token.path.vault) - .replaceAll('', token.address), - [fcl.arg(amount, t.UFix64), fcl.arg(address, t.Address)] - ); - - mixpanelTrack.track('ft_transfer', { - from_address: (await this.getCurrentAddress()) || '', - to_address: address, - amount: parseFloat(amount), - ft_identifier: token.contractName, - type: 'flow', - }); - - return txID; - }; - - // TODO: Replace with generic token - transferInboxTokens = async ( + transferCadenceTokens = async ( symbol: string, address: string, amount: string diff --git a/src/ui/reducers/transaction-reducer.ts b/src/ui/reducers/transaction-reducer.ts index ed881a1f..8e425947 100644 --- a/src/ui/reducers/transaction-reducer.ts +++ b/src/ui/reducers/transaction-reducer.ts @@ -17,7 +17,7 @@ export const INITIAL_TRANSACTION_STATE: TransactionState = { currentTxState: '', rootAddress: '', fromAddress: '', - tokenType: 'FT', + tokenType: 'Flow', fromNetwork: 'Evm', toNetwork: 'Evm', toAddress: '', diff --git a/src/ui/views/SendTo/TransferConfirmation.tsx b/src/ui/views/SendTo/TransferConfirmation.tsx index 1f0b3933..052e0e94 100644 --- a/src/ui/views/SendTo/TransferConfirmation.tsx +++ b/src/ui/views/SendTo/TransferConfirmation.tsx @@ -1,20 +1,15 @@ import CloseIcon from '@mui/icons-material/Close'; import InfoIcon from '@mui/icons-material/Info'; import { Box, Typography, Drawer, Stack, Grid, CardMedia, IconButton, Button } from '@mui/material'; -import BN from 'bignumber.js'; import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { type Contract } from 'web3'; import { type TransactionState } from '@/shared/types/transaction-types'; -import { ensureEvmAddressPrefix, isValidEthereumAddress } from '@/shared/utils/address'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import StorageExceededAlert from '@/ui/FRWComponent/StorageExceededAlert'; import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; import { useContactHook } from '@/ui/hooks/useContactHook'; -import { useWeb3 } from '@/ui/hooks/useWeb3'; import { useStorageCheck } from '@/ui/utils/useStorageCheck'; -import erc20ABI from 'background/utils/erc20.abi.json'; import IconNext from 'ui/FRWAssets/svg/next.svg'; import { LLSpinner } from 'ui/FRWComponent'; import { Profile } from 'ui/FRWComponent/Send/Profile'; @@ -47,23 +42,6 @@ const TransferConfirmation = ({ const [tid, setTid] = useState(''); const [count, setCount] = useState(0); - const web3Instance = useWeb3(); - const [erc20Contract, setErc20Contract] = useState | null>(null); - - useEffect(() => { - if ( - isConfirmationOpen && - web3Instance && - isValidEthereumAddress(transactionState.selectedToken?.address) - ) { - const contractInstance = new web3Instance.eth.Contract( - erc20ABI, - transactionState.selectedToken.address - ); - setErc20Contract(contractInstance); - } - }, [web3Instance, transactionState.selectedToken.address, isConfirmationOpen]); - const transferAmount = transactionState.amount ? parseFloat(transactionState.amount) : undefined; // Check if the transfer is between EVM and Flow networks @@ -117,129 +95,14 @@ const TransferConfirmation = ({ setOccupied(false); }, []); - const transferTokensOnCadence = useCallback(async () => { - return wallet.transferInboxTokens( - transactionState.selectedToken.symbol, - transactionState.toAddress, - transactionState.amount - ); - }, [transactionState, wallet]); - - const transferTokensFromChildToCadence = useCallback(async () => { - return wallet.sendFTfromChild( - transactionState.fromAddress, - transactionState.toAddress, - 'flowTokenProvider', - transactionState.amount, - transactionState.selectedToken.symbol - ); - }, [transactionState, wallet]); - - const transferFlowFromEvmToCadence = useCallback(async () => { - return wallet.withdrawFlowEvm(transactionState.amount, transactionState.toAddress); - }, [wallet, transactionState]); - - const transferFTFromEvmToCadence = useCallback(async () => { - return wallet.transferFTFromEvm( - transactionState.selectedToken['flowIdentifier'], - transactionState.amount, - transactionState.toAddress, - transactionState.selectedToken - ); - }, [wallet, transactionState]); - - const transferTokensOnEvm = useCallback(async () => { - // the amount is always stored as a string in the transaction state - const amountStr: string = transactionState.amount; - // TODO: check if the amount is a valid number - // Create an integer string based on the required token decimals - const amountBN = new BN(amountStr.replace('.', '')); - - const decimalsCount = amountStr.split('.')[1]?.length || 0; - const decimalDifference = transactionState.selectedToken.decimals - decimalsCount; - if (decimalDifference < 0) { - throw new Error('Too many decimal places have been provided'); - } - const scaleFactor = new BN(10).pow(decimalDifference); - const integerAmount = amountBN.multipliedBy(scaleFactor); - const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); - - let address, gas, value, data; - - if (transactionState.selectedToken.symbol.toLowerCase() === 'flow') { - address = transactionState.toAddress; - gas = '1'; - // const amountBN = new BN(transactionState.amount).multipliedBy(new BN(10).pow(18)); - // the amount is always stored as a string in the transaction state - value = integerAmount.toString(16); - data = '0x'; - } else { - const encodedData = erc20Contract!.methods - .transfer(ensureEvmAddressPrefix(transactionState.toAddress), integerAmountStr) - .encodeABI(); - gas = '1312d00'; - address = ensureEvmAddressPrefix(transactionState.selectedToken.address); - value = '0x0'; // Zero value as hex - data = encodedData.startsWith('0x') ? encodedData : `0x${encodedData}`; - } - - // Send the transaction - return wallet.sendEvmTransaction(address, gas, value, data); - }, [transactionState, erc20Contract, wallet]); - - const transferFlowFromCadenceToEvm = useCallback(async () => { - return wallet.transferFlowEvm(transactionState.toAddress, transactionState.amount); - }, [transactionState, wallet]); - - const transferFTFromCadenceToEvm = useCallback(async () => { - const address = transactionState.selectedToken!.address.startsWith('0x') - ? transactionState.selectedToken!.address.slice(2) - : transactionState.selectedToken!.address; - - return wallet.transferFTToEvmV2( - `A.${address}.${transactionState.selectedToken!.contractName}.Vault`, - transactionState.amount, - transactionState.toAddress - ); - }, [transactionState, wallet]); - const transferTokens = useCallback(async () => { try { // Set the sending state to true setSending(true); // Initialize the transaction ID - let txId: string; + const txId: string = await wallet.transferTokens(transactionState); - // Switch on the current transaction state - switch (transactionState.currentTxState) { - case 'FTFromEvmToCadence': - txId = await transferFTFromEvmToCadence(); - break; - case 'FlowFromEvmToCadence': - txId = await transferFlowFromEvmToCadence(); - break; - case 'FTFromChildToCadence': - case 'FlowFromChildToCadence': - txId = await transferTokensFromChildToCadence(); - break; - case 'FTFromCadenceToCadence': - case 'FlowFromCadenceToCadence': - txId = await transferTokensOnCadence(); - break; - case 'FlowFromEvmToEvm': - case 'FTFromEvmToEvm': - txId = await transferTokensOnEvm(); - break; - case 'FlowFromCadenceToEvm': - txId = await transferFlowFromCadenceToEvm(); - break; - case 'FTFromCadenceToEvm': - txId = await transferFTFromCadenceToEvm(); - break; - default: - throw new Error(`Unsupported transaction state: ${transactionState.currentTxState}`); - } // Set the transaction ID so we can show that we're processing setTid(txId); @@ -267,18 +130,7 @@ const TransferConfirmation = ({ // Set the sending state to false regardless of whether the transaction was successful or not setSending(false); } - }, [ - transactionState, - wallet, - history, - transferFTFromEvmToCadence, - transferFlowFromEvmToCadence, - transferTokensFromChildToCadence, - transferTokensOnCadence, - transferTokensOnEvm, - transferFlowFromCadenceToEvm, - transferFTFromCadenceToEvm, - ]); + }, [transactionState, wallet, history]); const transactionDoneHandler = useCallback( (request) => { From 02b5ca55de69fdfeeae38edf113260211f3cc045 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:31:23 +1100 Subject: [PATCH 33/43] Added tests for transaction reducer --- .../__tests__/transaction-reducer.test.ts | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 src/ui/reducers/__tests__/transaction-reducer.test.ts diff --git a/src/ui/reducers/__tests__/transaction-reducer.test.ts b/src/ui/reducers/__tests__/transaction-reducer.test.ts new file mode 100644 index 00000000..bb5ee986 --- /dev/null +++ b/src/ui/reducers/__tests__/transaction-reducer.test.ts @@ -0,0 +1,343 @@ +import { type TokenInfo } from 'flow-native-token-registry'; +import { describe, expect, it } from 'vitest'; + +import { type CoinItem } from '@/shared/types/wallet-types'; + +import { + INITIAL_TRANSACTION_STATE, + getTransactionStateString, + transactionReducer, +} from '../transaction-reducer'; + +describe('Transaction Reducer', () => { + describe('Initial State', () => { + it('should have the correct initial state', () => { + expect(INITIAL_TRANSACTION_STATE).toMatchObject({ + currentTxState: '', + rootAddress: '', + fromAddress: '', + tokenType: 'Flow', + fromNetwork: 'Evm', + toNetwork: 'Evm', + toAddress: '', + amount: '0.0', + fiatAmount: '0.0', + fiatCurrency: 'USD', + fiatOrCoin: 'coin', + balanceExceeded: false, + }); + }); + }); + + describe('getTransactionStateString', () => { + it('should return empty string when required fields are missing', () => { + const state = { ...INITIAL_TRANSACTION_STATE, tokenType: '' }; + expect(getTransactionStateString(state)).toBe(''); + }); + + it('should return correct transaction state string', () => { + const state = { + ...INITIAL_TRANSACTION_STATE, + tokenType: 'Flow', + fromNetwork: 'Evm', + toNetwork: 'Cadence', + }; + expect(getTransactionStateString(state)).toBe('FlowFromEvmToCadence'); + }); + }); + + describe('Action Handlers', () => { + describe('initTransactionState', () => { + it('should initialize transaction state with EVM address', () => { + const action = { + type: 'initTransactionState' as const, + payload: { + rootAddress: '0x123', + fromAddress: '0x1234567890123456789012345678901234567890', + }, + }; + + const newState = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(newState.rootAddress).toBe('0x123'); + expect(newState.fromAddress).toBe('0x1234567890123456789012345678901234567890'); + expect(newState.fromNetwork).toBe('Evm'); + }); + + it('should initialize transaction state with Cadence address', () => { + const rootAddress = '0x123abc'; + const action = { + type: 'initTransactionState' as const, + payload: { + rootAddress, + fromAddress: rootAddress, + }, + }; + + const newState = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(newState.fromNetwork).toBe('Cadence'); + }); + }); + + describe('setSelectedToken', () => { + const mockTokenInfo: TokenInfo = { + name: 'Test Token', + address: '0x123', + contractName: 'TestToken', + path: { + balance: '/public/testBalance', + receiver: '/public/testReceiver', + vault: '/storage/testVault', + }, + logoURI: 'test.svg', + decimals: 8, + symbol: 'TEST', + }; + + const mockCoinInfo: CoinItem = { + coin: 'test', + unit: 'TEST', + balance: 100, + price: 1, + change24h: 0, + total: 100, + icon: 'test.svg', + }; + + it('should set token info and update token type for non-Flow token', () => { + const action = { + type: 'setSelectedToken' as const, + payload: { + tokenInfo: mockTokenInfo, + coinInfo: mockCoinInfo, + }, + }; + + const newState = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(newState.selectedToken).toEqual(mockTokenInfo); + expect(newState.tokenType).toBe('FT'); + expect(newState.coinInfo).toEqual(mockCoinInfo); + }); + + it('should adjust amount decimals when switching to token with different decimals', () => { + // First set an amount with the initial Flow token (8 decimals) + const stateWithAmount = transactionReducer(INITIAL_TRANSACTION_STATE, { + type: 'setAmount', + payload: '123.456789012345', + }); + + // Then switch to a token with 6 decimals + const token6Decimals: TokenInfo = { + ...mockTokenInfo, + decimals: 6, + }; + + const newState = transactionReducer(stateWithAmount, { + type: 'setSelectedToken', + payload: { + tokenInfo: token6Decimals, + coinInfo: mockCoinInfo, + }, + }); + + // Should truncate to 6 decimals + expect(newState.amount).toBe('123.456789'); + }); + + it('should handle switching to token with more decimals', () => { + // First set state with a 2 decimal token + const token2Decimals: TokenInfo = { + ...mockTokenInfo, + decimals: 2, + }; + + const stateWith2Decimals = transactionReducer(INITIAL_TRANSACTION_STATE, { + type: 'setSelectedToken', + payload: { + tokenInfo: token2Decimals, + coinInfo: mockCoinInfo, + }, + }); + + // Set an amount + const stateWithAmount = transactionReducer(stateWith2Decimals, { + type: 'setAmount', + payload: '123.456', + }); + + // Should be truncated to 2 decimals + expect(stateWithAmount.amount).toBe('123.45'); + + // Switch to 8 decimal token + const token8Decimals: TokenInfo = { + ...mockTokenInfo, + decimals: 8, + }; + + const finalState = transactionReducer(stateWithAmount, { + type: 'setSelectedToken', + payload: { + tokenInfo: token8Decimals, + coinInfo: mockCoinInfo, + }, + }); + + // Should maintain 2 decimals since that was the input + expect(finalState.amount).toBe('123.45'); + }); + }); + + describe('setToAddress', () => { + it('should set to address and determine correct network for EVM address', () => { + const action = { + type: 'setToAddress' as const, + payload: { + address: '0x1234567890123456789012345678901234567890', + }, + }; + + const newState = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(newState.toAddress).toBe('0x1234567890123456789012345678901234567890'); + expect(newState.toNetwork).toBe('Evm'); + }); + + it('should set to address and determine correct network for Cadence address', () => { + const action = { + type: 'setToAddress' as const, + payload: { + address: '0x1234.5678', + }, + }; + + const newState = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(newState.toAddress).toBe('0x1234.5678'); + expect(newState.toNetwork).toBe('Cadence'); + }); + }); + + describe('setAmount', () => { + const stateWithBalance = { + ...INITIAL_TRANSACTION_STATE, + coinInfo: { + ...INITIAL_TRANSACTION_STATE.coinInfo, + balance: 100, + price: 2, + }, + }; + + it('should handle coin amount input', () => { + const action = { + type: 'setAmount' as const, + payload: '50', + }; + + const newState = transactionReducer(stateWithBalance, action); + expect(newState.amount).toBe('50'); + expect(newState.fiatAmount).toBe('100.000'); + expect(newState.balanceExceeded).toBe(false); + }); + + it('should detect balance exceeded', () => { + const action = { + type: 'setAmount' as const, + payload: '150', + }; + + const newState = transactionReducer(stateWithBalance, action); + expect(newState.balanceExceeded).toBe(true); + }); + + it('should truncate decimals based on token decimals', () => { + // Create state with a 4 decimal token + const token4Decimals: TokenInfo = { + ...INITIAL_TRANSACTION_STATE.selectedToken, + decimals: 4, + }; + + const stateWith4Decimals = { + ...stateWithBalance, + selectedToken: token4Decimals, + }; + + const action = { + type: 'setAmount' as const, + payload: '123.456789', + }; + + const newState = transactionReducer(stateWith4Decimals, action); + expect(newState.amount).toBe('123.4567'); + }); + + it('should handle amounts with no decimals', () => { + const action = { + type: 'setAmount' as const, + payload: '100', + }; + + const newState = transactionReducer(stateWithBalance, action); + expect(newState.amount).toBe('100'); + }); + + it('should preserve trailing zeros for precision', () => { + const action = { + type: 'setAmount' as const, + payload: '100.100000', + }; + + const newState = transactionReducer(stateWithBalance, action); + // Should preserve trailing zeros for precision in crypto transactions + expect(newState.amount).toBe('100.100000'); + }); + }); + + describe('setAmountToMax', () => { + const stateWithBalance = { + ...INITIAL_TRANSACTION_STATE, + coinInfo: { + ...INITIAL_TRANSACTION_STATE.coinInfo, + balance: 100, + price: 2, + }, + }; + + it('should set maximum amount in coin mode', () => { + const action = { + type: 'setAmountToMax' as const, + }; + + const newState = transactionReducer(stateWithBalance, action); + expect(newState.amount).toBe('100'); + expect(newState.fiatAmount).toBe('200.000'); + }); + + it('should set maximum amount in fiat mode', () => { + const action = { + type: 'setAmountToMax' as const, + }; + + const stateInFiat = { + ...stateWithBalance, + fiatOrCoin: 'fiat' as const, + }; + + const newState = transactionReducer(stateInFiat, action); + expect(newState.amount).toBe('100'); + expect(newState.fiatAmount).toBe('200.000'); + expect(newState.fiatOrCoin).toBe('fiat'); + }); + }); + + describe('switchFiatOrCoin', () => { + it('should switch between fiat and coin modes', () => { + const action = { + type: 'switchFiatOrCoin' as const, + }; + + const state1 = transactionReducer(INITIAL_TRANSACTION_STATE, action); + expect(state1.fiatOrCoin).toBe('fiat'); + + const state2 = transactionReducer(state1, action); + expect(state2.fiatOrCoin).toBe('coin'); + }); + }); + }); +}); From 982327a6c6522776ea02646a5eeca74deed44276 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:50:21 +1100 Subject: [PATCH 34/43] Corrected some of the tests --- .../__tests__/transaction-reducer.test.ts | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/ui/reducers/__tests__/transaction-reducer.test.ts b/src/ui/reducers/__tests__/transaction-reducer.test.ts index bb5ee986..44cee0c9 100644 --- a/src/ui/reducers/__tests__/transaction-reducer.test.ts +++ b/src/ui/reducers/__tests__/transaction-reducer.test.ts @@ -1,7 +1,13 @@ import { type TokenInfo } from 'flow-native-token-registry'; import { describe, expect, it } from 'vitest'; -import { type CoinItem } from '@/shared/types/wallet-types'; +import { type Contact } from '@/shared/types/network-types'; +import { + type NetworkType, + type TokenType, + type TransactionStateString, +} from '@/shared/types/transaction-types'; +import { type CoinItem, type WalletAddress } from '@/shared/types/wallet-types'; import { INITIAL_TRANSACTION_STATE, @@ -16,9 +22,9 @@ describe('Transaction Reducer', () => { currentTxState: '', rootAddress: '', fromAddress: '', - tokenType: 'Flow', - fromNetwork: 'Evm', - toNetwork: 'Evm', + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Evm' as NetworkType, toAddress: '', amount: '0.0', fiatAmount: '0.0', @@ -31,16 +37,16 @@ describe('Transaction Reducer', () => { describe('getTransactionStateString', () => { it('should return empty string when required fields are missing', () => { - const state = { ...INITIAL_TRANSACTION_STATE, tokenType: '' }; + const state = { ...INITIAL_TRANSACTION_STATE, tokenType: '' as TokenType }; expect(getTransactionStateString(state)).toBe(''); }); it('should return correct transaction state string', () => { const state = { ...INITIAL_TRANSACTION_STATE, - tokenType: 'Flow', - fromNetwork: 'Evm', - toNetwork: 'Cadence', + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, }; expect(getTransactionStateString(state)).toBe('FlowFromEvmToCadence'); }); @@ -52,8 +58,8 @@ describe('Transaction Reducer', () => { const action = { type: 'initTransactionState' as const, payload: { - rootAddress: '0x123', - fromAddress: '0x1234567890123456789012345678901234567890', + rootAddress: '0x123' as WalletAddress, + fromAddress: '0x1234567890123456789012345678901234567890' as WalletAddress, }, }; @@ -64,7 +70,7 @@ describe('Transaction Reducer', () => { }); it('should initialize transaction state with Cadence address', () => { - const rootAddress = '0x123abc'; + const rootAddress = '0x123abc' as WalletAddress; const action = { type: 'initTransactionState' as const, payload: { @@ -191,7 +197,7 @@ describe('Transaction Reducer', () => { const action = { type: 'setToAddress' as const, payload: { - address: '0x1234567890123456789012345678901234567890', + address: '0x1234567890123456789012345678901234567890' as WalletAddress, }, }; @@ -204,7 +210,7 @@ describe('Transaction Reducer', () => { const action = { type: 'setToAddress' as const, payload: { - address: '0x1234.5678', + address: '0x1234.5678' as WalletAddress, }, }; @@ -222,6 +228,10 @@ describe('Transaction Reducer', () => { balance: 100, price: 2, }, + selectedToken: { + ...INITIAL_TRANSACTION_STATE.selectedToken, + decimals: 8, + }, }; it('should handle coin amount input', () => { @@ -256,6 +266,9 @@ describe('Transaction Reducer', () => { const stateWith4Decimals = { ...stateWithBalance, selectedToken: token4Decimals, + tokenType: 'FT' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, }; const action = { @@ -273,7 +286,15 @@ describe('Transaction Reducer', () => { payload: '100', }; - const newState = transactionReducer(stateWithBalance, action); + const newState = transactionReducer( + { + ...stateWithBalance, + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, + }, + action + ); expect(newState.amount).toBe('100'); }); @@ -283,7 +304,15 @@ describe('Transaction Reducer', () => { payload: '100.100000', }; - const newState = transactionReducer(stateWithBalance, action); + const newState = transactionReducer( + { + ...stateWithBalance, + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, + }, + action + ); // Should preserve trailing zeros for precision in crypto transactions expect(newState.amount).toBe('100.100000'); }); @@ -297,6 +326,9 @@ describe('Transaction Reducer', () => { balance: 100, price: 2, }, + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, }; it('should set maximum amount in coin mode', () => { @@ -332,7 +364,15 @@ describe('Transaction Reducer', () => { type: 'switchFiatOrCoin' as const, }; - const state1 = transactionReducer(INITIAL_TRANSACTION_STATE, action); + const state1 = transactionReducer( + { + ...INITIAL_TRANSACTION_STATE, + tokenType: 'Flow' as TokenType, + fromNetwork: 'Evm' as NetworkType, + toNetwork: 'Cadence' as NetworkType, + }, + action + ); expect(state1.fiatOrCoin).toBe('fiat'); const state2 = transactionReducer(state1, action); From 0e243c4daf145c5e3bd14c4d207a6c060d496687 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:28:09 +1100 Subject: [PATCH 35/43] Merge branch 'dev' into 436-feature-complete-type-checks-on-token-transactions --- src/background/service/openapi.ts | 5 +++-- src/ui/views/SendTo/SendToCadenceOrEvm.tsx | 13 +++++++------ src/ui/views/SendTo/TransferAmount.tsx | 17 +++++++++-------- src/ui/views/SendTo/TransferConfirmation.tsx | 9 +++++++-- src/ui/views/TokenDetail/TokenInfoCard.tsx | 4 ++-- src/ui/views/TokenDetail/TokenValue.tsx | 17 ++++++++++++++--- src/ui/views/Wallet/Coinlist.tsx | 6 +++--- src/ui/views/Wallet/TransferList.tsx | 4 ++-- 8 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/background/service/openapi.ts b/src/background/service/openapi.ts index 4f82643a..65a72da0 100644 --- a/src/background/service/openapi.ts +++ b/src/background/service/openapi.ts @@ -1483,14 +1483,15 @@ class OpenApiService { WEB_NEXT_URL ); - return data.tokens; + return this.addFlowTokenIfMissing(data.tokens); }; addFlowTokenIfMissing = (tokens) => { const hasFlowToken = tokens.some((token) => token.symbol.toLowerCase() === 'flow'); if (!hasFlowToken) { - tokens.push(defaultFlowToken); + return [defaultFlowToken, ...tokens]; } + return tokens; }; mergeCustomTokens = (tokens, customTokens) => { diff --git a/src/ui/views/SendTo/SendToCadenceOrEvm.tsx b/src/ui/views/SendTo/SendToCadenceOrEvm.tsx index 0056f5f9..417704c7 100644 --- a/src/ui/views/SendTo/SendToCadenceOrEvm.tsx +++ b/src/ui/views/SendTo/SendToCadenceOrEvm.tsx @@ -13,6 +13,7 @@ import { useNetworkStore } from '@/ui/stores/networkStore'; import { useWallet } from 'ui/utils'; import CancelIcon from '../../../components/iconfont/IconClose'; +import { TokenValue } from '../TokenDetail/TokenValue'; import TransferAmount from './TransferAmount'; import TransferConfirmation from './TransferConfirmation'; @@ -150,12 +151,12 @@ const SendToCadenceOrEvm = ({ fontSize: '15px', }} > - {(Math.round(transactionState.coinInfo.balance * 100) / 100).toFixed(2) + - ' ' + - transactionState.coinInfo.unit.toUpperCase() + - ' ≈ ' + - '$ ' + - transactionState.coinInfo.total} + + {' ≈ '} + diff --git a/src/ui/views/SendTo/TransferAmount.tsx b/src/ui/views/SendTo/TransferAmount.tsx index 95571235..a84c303e 100644 --- a/src/ui/views/SendTo/TransferAmount.tsx +++ b/src/ui/views/SendTo/TransferAmount.tsx @@ -1,4 +1,3 @@ -import AttachMoneyRoundedIcon from '@mui/icons-material/AttachMoneyRounded'; import { Box, Typography, @@ -20,6 +19,7 @@ import React, { useCallback } from 'react'; import { type TransactionState } from '@/shared/types/transaction-types'; import SlideRelative from '@/ui/FRWComponent/SlideRelative'; import { useCoinStore } from '@/ui/stores/coinStore'; +import { TokenValue } from '@/ui/views/TokenDetail/TokenValue'; import CancelIcon from '../../../components/iconfont/IconClose'; import IconSwitch from '../../../components/iconfont/IconSwitch'; @@ -263,14 +263,15 @@ const TransferAmount = ({ > ≈ {transactionState.fiatOrCoin === 'fiat' ? ( - + <> + {' '} + + ) : ( - - )} - {transactionState.fiatOrCoin === 'fiat' ? ( - {amount} - ) : ( - {fiatAmount} + )} diff --git a/src/ui/views/SendTo/TransferConfirmation.tsx b/src/ui/views/SendTo/TransferConfirmation.tsx index 052e0e94..57cfce7d 100644 --- a/src/ui/views/SendTo/TransferConfirmation.tsx +++ b/src/ui/views/SendTo/TransferConfirmation.tsx @@ -15,6 +15,8 @@ import { LLSpinner } from 'ui/FRWComponent'; import { Profile } from 'ui/FRWComponent/Send/Profile'; import { useWallet } from 'ui/utils'; +import { TokenValue } from '../TokenDetail/TokenValue'; + interface TransferConfirmationProps { transactionState: TransactionState; isConfirmationOpen: boolean; @@ -284,7 +286,10 @@ const TransferConfirmation = ({ variant="body1" sx={{ fontSize: '18px', fontWeight: '400', textAlign: 'end' }} > - {transactionState.amount} {transactionState.coinInfo.unit} + @@ -293,7 +298,7 @@ const TransferConfirmation = ({ color="info" sx={{ fontSize: '14px', fontWeight: 'semi-bold', textAlign: 'end' }} > - $ {transactionState.fiatAmount} + diff --git a/src/ui/views/TokenDetail/TokenInfoCard.tsx b/src/ui/views/TokenDetail/TokenInfoCard.tsx index 6b92bda6..8cb435a3 100644 --- a/src/ui/views/TokenDetail/TokenInfoCard.tsx +++ b/src/ui/views/TokenDetail/TokenInfoCard.tsx @@ -11,7 +11,7 @@ import { addDotSeparators } from 'ui/utils/number'; import IconChevronRight from '../../../components/iconfont/IconChevronRight'; -import { TokenPrice } from './TokenValue'; +import { TokenValue } from './TokenValue'; // import tips from 'ui/FRWAssets/svg/tips.svg'; @@ -196,7 +196,7 @@ const TokenInfoCard = ({ price, token, setAccessible, accessible, tokenInfo, chi - = ({ +export const TokenValue: React.FC = ({ value, className = '', - prefix = '$', + prefix = '', postFix = '', }) => { if (value === 0 || value === null || value === undefined) { return {''}; } + const numberWithCommas = (x: string) => { + // Check if the number is between 1000 and 999999 + const num = parseFloat(x); + if (num >= 1000 && num <= 999999) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + return x; + }; + // convert value to number if it's a string const valueNumber = typeof value === 'string' ? parseFloat(value) : value; @@ -29,7 +38,9 @@ export const TokenPrice: React.FC = ({ return ( {prefix} - {leadingPart} + + {numberWithCommas(leadingPart)} + {zeroPart !== null && ( { // const wallet = useWallet(); @@ -110,7 +110,7 @@ const CoinList = ({ data, ableFt, isActive, childType, coinLoading }) => { }} > {props.change === null ? '-' : ''} - + {props.change !== 0 && ( { secondaryAction={ } + secondary={} unit={coin.unit} change={parseFloat(coin.change24h.toFixed(2))} /> diff --git a/src/ui/views/Wallet/TransferList.tsx b/src/ui/views/Wallet/TransferList.tsx index 0a8c867e..20bdd48b 100644 --- a/src/ui/views/Wallet/TransferList.tsx +++ b/src/ui/views/Wallet/TransferList.tsx @@ -24,7 +24,7 @@ import { useTransferListStore } from '@/ui/stores/transferListStore'; import activity from 'ui/FRWAssets/svg/activity.svg'; import { useWallet } from 'ui/utils'; -import { TokenPrice } from '../TokenDetail/TokenValue'; +import { TokenValue } from '../TokenDetail/TokenValue'; dayjs.extend(relativeTime); @@ -87,7 +87,7 @@ const TransferList = () => { }} > {props.type === 1 ? ( - From f4197fa55320f76f01a4a49d6221ea394a4f444e Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:39:52 +1100 Subject: [PATCH 36/43] Removed code that altered values in wallet. Moved some code to number around integer strings and added tests. --- src/background/controller/wallet.ts | 166 ++++------------ src/shared/types/tracking-types.ts | 4 +- src/shared/utils/__tests__/number.test.ts | 218 +++++++++++++++++++++ src/{ui => shared}/utils/number.ts | 76 +++---- src/ui/reducers/transaction-reducer.ts | 4 +- src/ui/utils/__tests__/number.test.ts | 132 ------------- src/ui/utils/index.ts | 2 +- src/ui/views/TokenDetail/TokenInfoCard.tsx | 2 +- src/ui/views/Wallet/Coinlist.tsx | 2 +- src/ui/views/Wallet/index.tsx | 2 +- 10 files changed, 298 insertions(+), 310 deletions(-) create mode 100644 src/shared/utils/__tests__/number.test.ts rename src/{ui => shared}/utils/number.ts (66%) delete mode 100644 src/ui/utils/__tests__/number.test.ts diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 2eb35a50..70d71f4c 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -31,6 +31,7 @@ import { type TransactionState } from '@/shared/types/transaction-types'; import { type LoggedInAccount } from '@/shared/types/wallet-types'; import { ensureEvmAddressPrefix, isValidEthereumAddress, withPrefix } from '@/shared/utils/address'; import { getSignAlgo } from '@/shared/utils/algo'; +import { convertToIntegerAmount, validateAmount } from '@/shared/utils/number'; import { keyringService, preferenceService, @@ -55,7 +56,7 @@ import { import i18n from 'background/service/i18n'; import { type DisplayedKeryring, KEYRING_CLASS } from 'background/service/keyring'; import type { CacheState } from 'background/service/pageStateCache'; -import { findKeyAndInfo, getScripts } from 'background/utils'; +import { getScripts } from 'background/utils'; import emoji from 'background/utils/emoji.json'; import fetchConfig from 'background/utils/remoteConfig'; import { notification, storage } from 'background/webapi'; @@ -1195,7 +1196,8 @@ export class WalletController extends BaseController { coin: token.name, unit: token.symbol.toLowerCase(), icon: token['logoURI'] || '', - balance: parseFloat(parseFloat(allBalanceMap[tokenId]).toFixed(8)), + // Keep the balance as a string to avoid precision loss + balance: allBalanceMap[tokenId], price: allPrice[index] === null ? 0 : new BN(allPrice[index].price.last).toNumber(), change24h: allPrice[index] === null || !allPrice[index].price || !allPrice[index].price.change @@ -1268,7 +1270,8 @@ export class WalletController extends BaseController { coin: token.name, unit: token.symbol.toLowerCase(), icon: token['logoURI'] || '', - balance: parseFloat(parseFloat(allBalanceMap[tokenId]).toFixed(8)), + // Keep the balance as a string to avoid precision loss + balance: allBalanceMap[tokenId], price: allPrice[index] === null ? 0 : new BN(allPrice[index].price.last).toNumber(), change24h: allPrice[index] === null || !allPrice[index].price || !allPrice[index].price.change @@ -1775,7 +1778,6 @@ export class WalletController extends BaseController { }; const transferFlowFromEvmToCadence = async () => { - console.log('transferFlowFromEvmToCadence'); return this.withdrawFlowEvm(transactionState.amount, transactionState.toAddress); }; @@ -1856,6 +1858,11 @@ export class WalletController extends BaseController { ); }; + // Validate the amount. Just to be sure! + if (!validateAmount(transactionState.amount, transactionState.selectedToken.decimals)) { + throw new Error('Invalid amount or decimal places'); + } + // Switch on the current transaction state switch (transactionState.currentTxState) { case 'FTFromEvmToCadence': @@ -1885,9 +1892,6 @@ export class WalletController extends BaseController { amount = '1.0', gasLimit = 30000000 ): Promise => { - await this.getNetwork(); - const formattedAmount = parseFloat(amount).toFixed(8); - const script = await getScripts('evm', 'transferFlowToEvmAddress'); if (recipientEVMAddressHex.startsWith('0x')) { recipientEVMAddressHex = recipientEVMAddressHex.substring(2); @@ -1895,14 +1899,14 @@ export class WalletController extends BaseController { const txID = await userWalletService.sendTransaction(script, [ fcl.arg(recipientEVMAddressHex, t.String), - fcl.arg(formattedAmount, t.UFix64), + fcl.arg(amount, t.UFix64), fcl.arg(gasLimit, t.UInt64), ]); mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: recipientEVMAddressHex, - amount: parseFloat(formattedAmount), + amount: amount, ft_identifier: 'FLOW', type: 'evm', }); @@ -1917,9 +1921,6 @@ export class WalletController extends BaseController { contractEVMAddress: string, data ): Promise => { - await this.getNetwork(); - const formattedAmount = parseFloat(amount).toFixed(8); - const script = await getScripts('bridge', 'bridgeTokensToEvmAddress'); if (contractEVMAddress.startsWith('0x')) { contractEVMAddress = contractEVMAddress.substring(2); @@ -1932,7 +1933,7 @@ export class WalletController extends BaseController { const txID = await userWalletService.sendTransaction(script, [ fcl.arg(tokenContractAddress, t.Address), fcl.arg(tokenContractName, t.String), - fcl.arg(formattedAmount, t.UFix64), + fcl.arg(amount, t.UFix64), fcl.arg(contractEVMAddress, t.String), fcl.arg(regularArray, t.Array(t.UInt8)), fcl.arg(gasLimit, t.UInt64), @@ -1940,7 +1941,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: tokenContractAddress, - amount: parseFloat(formattedAmount), + amount: amount, ft_identifier: tokenContractName, type: 'evm', }); @@ -1952,21 +1953,18 @@ export class WalletController extends BaseController { amount = '0.0', recipient: string ): Promise => { - await this.getNetwork(); - const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); - const script = await getScripts('bridge', 'bridgeTokensToEvmAddressV2'); const txID = await userWalletService.sendTransaction(script, [ fcl.arg(vaultIdentifier, t.String), - fcl.arg(formattedAmount, t.UFix64), + fcl.arg(amount, t.UFix64), fcl.arg(recipient, t.String), ]); mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: recipient, - amount: parseFloat(formattedAmount), + amount: amount, ft_identifier: vaultIdentifier, type: 'evm', }); @@ -1980,23 +1978,14 @@ export class WalletController extends BaseController { receiver: string, tokenResult: TokenInfo ): Promise => { - await this.getNetwork(); - const amountStr = amount.toString(); - - const amountBN = new BN(amountStr); - const decimals = tokenResult.decimals ?? 18; if (decimals < 0 || decimals > 77) { // 77 is BN.js max safe decimals throw new Error('Invalid decimals'); } - const scaleFactor = new BN(10).pow(new BN(decimals)); - // Multiply amountBN by scaleFactor - const integerAmount = amountBN.multipliedBy(scaleFactor); - const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); + const integerAmountStr = convertToIntegerAmount(amount, decimals); - console.log('integerAmountStr amount ', integerAmountStr, amount); const script = await getScripts('bridge', 'bridgeTokensFromEvmToFlowV3'); const txID = await userWalletService.sendTransaction(script, [ fcl.arg(flowidentifier, t.String), @@ -2007,7 +1996,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: receiver, - amount: parseFloat(integerAmountStr), + amount: amount, ft_identifier: flowidentifier, type: 'evm', }); @@ -2016,11 +2005,10 @@ export class WalletController extends BaseController { }; withdrawFlowEvm = async (amount = '0.0', address: string): Promise => { - const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); const script = await getScripts('evm', 'withdrawCoa'); const txID = await userWalletService.sendTransaction(script, [ - fcl.arg(formattedAmount, t.UFix64), + fcl.arg(amount, t.UFix64), fcl.arg(address, t.Address), ]); @@ -2028,10 +2016,9 @@ export class WalletController extends BaseController { }; fundFlowEvm = async (amount = '1.0'): Promise => { - const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); const script = await getScripts('evm', 'fundCoa'); - return await userWalletService.sendTransaction(script, [fcl.arg(formattedAmount, t.UFix64)]); + return await userWalletService.sendTransaction(script, [fcl.arg(amount, t.UFix64)]); }; coaLink = async (): Promise => { @@ -2066,19 +2053,17 @@ export class WalletController extends BaseController { }; bridgeToEvm = async (flowIdentifier, amount = '1.0'): Promise => { - const formattedAmount = new BN(amount).decimalPlaces(8, BN.ROUND_DOWN).toString(); - const script = await getScripts('bridge', 'bridgeTokensToEvmV2'); const txID = await userWalletService.sendTransaction(script, [ fcl.arg(flowIdentifier, t.String), - fcl.arg(formattedAmount, t.UFix64), + fcl.arg(amount, t.UFix64), ]); mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: await this.getRawEvmAddressWithPrefix(), - amount: parseFloat(formattedAmount), + amount: amount, ft_identifier: flowIdentifier, type: 'evm', }); @@ -2087,19 +2072,12 @@ export class WalletController extends BaseController { }; bridgeToFlow = async (flowIdentifier, amount = '1.0', tokenResult): Promise => { - const amountStr = amount.toString(); - - const amountBN = new BN(amountStr); const decimals = tokenResult.decimals ?? 18; if (decimals < 0 || decimals > 77) { // 77 is BN.js max safe decimals throw new Error('Invalid decimals'); } - const scaleFactor = new BN(10).pow(new BN(decimals)); - - // Multiply amountBN by scaleFactor - const integerAmount = amountBN.multipliedBy(scaleFactor); - const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); + const integerAmountStr = convertToIntegerAmount(amount, decimals); const script = await getScripts('bridge', 'bridgeTokensFromEvmV2'); const txID = await userWalletService.sendTransaction(script, [ @@ -2110,7 +2088,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: await this.getRawEvmAddressWithPrefix(), to_address: (await this.getCurrentAddress()) || '', - amount: parseFloat(integerAmountStr), + amount: amount, ft_identifier: flowIdentifier, type: 'flow', }); @@ -2212,7 +2190,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: await this.getRawEvmAddressWithPrefix(), to_address: to, - amount: Number(transactionValue), + amount: value, ft_identifier: 'FLOW', type: 'evm', }); @@ -2261,7 +2239,7 @@ export class WalletController extends BaseController { // Convert hex to BigInt directly to avoid potential number overflow const transactionValue = value === '0x' ? BigInt(0) : BigInt(value); - const result = await userWalletService.sendTransaction(script, [ + await userWalletService.sendTransaction(script, [ fcl.arg(to, t.String), fcl.arg(transactionValue.toString(), t.UInt256), fcl.arg(regularArray, t.Array(t.UInt8)), @@ -2273,7 +2251,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: evmAddress, to_address: to, - amount: parseFloat(transactionValue.toString()), + amount: transactionValue.toString(), ft_identifier: 'FLOW', type: 'evm', }); @@ -2406,6 +2384,11 @@ export class WalletController extends BaseController { if (!token) { throw new Error(`Invaild token name - ${symbol}`); } + // Validate the amount just to be safe + if (!validateAmount(amount, token.decimals)) { + throw new Error(`Invalid amount - ${amount}`); + } + await this.getNetwork(); const txID = await userWalletService.sendTransaction( @@ -2421,7 +2404,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: address, - amount: parseFloat(amount), + amount: amount, ft_identifier: token.contractName, type: 'flow', }); @@ -2570,7 +2553,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: (await this.getCurrentAddress()) || '', to_address: childAddress, - amount: parseFloat(amount), + amount: amount, ft_identifier: token.contractName, type: 'flow', }); @@ -2588,6 +2571,10 @@ export class WalletController extends BaseController { if (!token) { throw new Error(`Invaild token name - ${symbol}`); } + // Validate the amount just to be safe + if (!validateAmount(amount, token.decimals)) { + throw new Error(`Invalid amount - ${amount}`); + } const script = await getScripts('hybridCustody', 'sendChildFT'); @@ -2608,7 +2595,7 @@ export class WalletController extends BaseController { mixpanelTrack.track('ft_transfer', { from_address: childAddress, to_address: receiver, - amount: parseFloat(amount), + amount: amount, ft_identifier: token.contractName, type: 'flow', }); @@ -2927,66 +2914,6 @@ export class WalletController extends BaseController { return txID; }; - bridgeChildFTToEvm = async ( - childAddr: string, - identifier: string, - amount: number, - token - ): Promise => { - const script = await getScripts('hybridCustody', 'bridgeChildFTToEvm'); - - const txID = await userWalletService.sendTransaction( - script - .replaceAll('', token.contract_name) - .replaceAll('', token.address) - .replaceAll('', token.path.storage_path) - .replaceAll('', token.path.public_type) - .replaceAll('', token.path.public_path), - [fcl.arg(identifier, t.String), fcl.arg(childAddr, t.Address), fcl.arg(amount, t.UFix64)] - ); - mixpanelTrack.track('ft_transfer', { - from_address: childAddr, - to_address: (await this.getCurrentAddress()) || '', - ft_identifier: identifier, - type: 'evm', - amount: amount, - }); - return txID; - }; - - bridgeChildFTFromEvm = async ( - childAddr: string, - vaultIdentifier: string, - ids: Array, - token, - amount: number - ): Promise => { - const script = await getScripts('hybridCustody', 'bridgeChildFTFromEvm'); - - const txID = await userWalletService.sendTransaction( - script - .replaceAll('', token.contract_name) - .replaceAll('', token.address) - .replaceAll('', token.path.storage_path) - .replaceAll('', token.path.public_type) - .replaceAll('', token.path.public_path), - [ - fcl.arg(vaultIdentifier, t.String), - fcl.arg(childAddr, t.Address), - fcl.arg(ids, t.Array(t.UInt256)), - fcl.arg(amount, t.UFix64), - ] - ); - mixpanelTrack.track('ft_transfer', { - from_address: childAddr, - to_address: (await this.getCurrentAddress()) || '', - ft_identifier: vaultIdentifier, - type: 'evm', - amount: amount, - }); - return txID; - }; - bridgeNftToEvmAddress = async ( nftContractAddress: string, nftContractName: string, @@ -3654,21 +3581,8 @@ export class WalletController extends BaseController { }; getNftCatalog = async () => { - const catStorage = await storage.get('catalogData'); - - const now = new Date(); - const exp = 1000 * 60 * 60 * 1 + now.getTime(); - if (catStorage && catStorage['expiry'] && now.getTime() <= catStorage['expiry']) { - return catStorage['data']; - } - const data = (await openapiService.nftCatalog()) ?? []; - // TODO: check if data is empty - const catalogData = { - data: data, - expiry: exp, - }; return data; }; diff --git a/src/shared/types/tracking-types.ts b/src/shared/types/tracking-types.ts index f0f1f512..a66bc8e8 100644 --- a/src/shared/types/tracking-types.ts +++ b/src/shared/types/tracking-types.ts @@ -30,7 +30,7 @@ export type TrackingEvents = { delegation_created: { address: string; // Address of the account that delegated node_id: string; // ID of the node that was delegated to - amount: number; // Amount of FLOW. e.g. 200.12 + amount: string; // Amount as a string }; on_ramp_clicked: { source: OnRampSourceType; // The on ramp platform the user choose e.g. moonpay or coinbase @@ -74,7 +74,7 @@ export type TrackingEvents = { from_address: string; // Address of the account that transferred the FT to_address: string; // Address of the account that received the FT type: string; // Type of FT sent (e.g., "FLOW", "USDf") - amount: number; // The amount of FT + amount: string; // The amount of FT as a fixed string ft_identifier: string; // The identifier of fungible token }; nft_transfer: { diff --git a/src/shared/utils/__tests__/number.test.ts b/src/shared/utils/__tests__/number.test.ts new file mode 100644 index 00000000..110779e4 --- /dev/null +++ b/src/shared/utils/__tests__/number.test.ts @@ -0,0 +1,218 @@ +import { describe, it, expect } from 'vitest'; + +import { + formatLargeNumber, + addDotSeparators, + stripEnteredAmount, + stripFinalAmount, + validateAmount, + convertToIntegerAmount, +} from '../number'; + +describe('formatLargeNumber', () => { + it('should format numbers into human readable format', () => { + expect(formatLargeNumber(1500000)).toBe('1.500M'); + expect(formatLargeNumber(2500000000)).toBe('2.500B'); + expect(formatLargeNumber(1500000000000)).toBe('1.500T'); + }); + + it('should handle numbers less than 1M', () => { + expect(formatLargeNumber(999999)).toBe('999999'); + }); + + it('should handle string numbers with $ prefix', () => { + expect(formatLargeNumber('$1500000')).toBe('1.500M'); + }); +}); + +describe('addDotSeparators', () => { + it('should format numbers with proper separators', () => { + expect(addDotSeparators('1234567.89')).toBe('1,234,567.89'); + }); + + it('should handle trailing zeros', () => { + expect(addDotSeparators('1234.50000000')).toBe('1,234.5'); + }); + + it('should preserve at least some decimal places', () => { + expect(addDotSeparators('1234.00000000')).toBe('1,234.000'); + }); +}); + +describe('stripEnteredAmount', () => { + it('should handle real-time input correctly', () => { + // Basic cases + expect(stripEnteredAmount('123', 2)).toBe('123'); + expect(stripEnteredAmount('123.', 2)).toBe('123.'); + expect(stripEnteredAmount('123.4', 2)).toBe('123.4'); + + // Multiple decimal points + expect(stripEnteredAmount('12..34.56', 2)).toBe('12.'); + expect(stripEnteredAmount('12.34.56', 2)).toBe('12.34'); + + // Leading zeros + expect(stripEnteredAmount('000123', 2)).toBe('123'); + expect(stripEnteredAmount('0', 2)).toBe('0'); + expect(stripEnteredAmount('00.123', 2)).toBe('0.12'); + + // Decimal cases + expect(stripEnteredAmount('.123', 2)).toBe('0.12'); + expect(stripEnteredAmount('.', 2)).toBe('0.'); + expect(stripEnteredAmount('', 2)).toBe(''); + }); +}); + +describe('stripFinalAmount', () => { + it('should handle empty and invalid inputs', () => { + expect(stripFinalAmount('', 2)).toBe('0'); + expect(stripFinalAmount(' ', 2)).toBe('0'); + expect(stripFinalAmount('.', 2)).toBe('0'); + expect(stripFinalAmount('..', 2)).toBe('0'); + }); + + it('should format final amounts correctly', () => { + // Basic cases + expect(stripFinalAmount('123', 2)).toBe('123'); + expect(stripFinalAmount('123.', 2)).toBe('123'); + expect(stripFinalAmount('123.4', 2)).toBe('123.4'); + expect(stripFinalAmount('123.40', 2)).toBe('123.4'); + // Removes extra zeros even when truncating + expect(stripFinalAmount('123.401', 2)).toBe('123.4'); + expect(stripFinalAmount('123.471', 2)).toBe('123.47'); + // Truncates doesn't round up + expect(stripFinalAmount('123.479', 2)).toBe('123.47'); + + // Multiple decimal points + expect(stripFinalAmount('12..34.56', 2)).toBe('12'); + expect(stripFinalAmount('12.34.56', 2)).toBe('12.34'); + + // Leading zeros + expect(stripFinalAmount('000123', 2)).toBe('123'); + expect(stripFinalAmount('0', 2)).toBe('0'); + expect(stripFinalAmount('00.123', 2)).toBe('0.12'); + + // Decimal cases + expect(stripFinalAmount('.123', 2)).toBe('0.12'); + expect(stripFinalAmount('.', 2)).toBe('0'); + expect(stripFinalAmount('', 2)).toBe('0'); + // Handle getting rid of trailing zeros + expect(stripFinalAmount('123.000', 2)).toBe('123'); + }); +}); + +describe('validateAmount', () => { + describe('with exact=false (default)', () => { + it('should validate whole numbers', () => { + expect(validateAmount('123', 2)).toBe(true); + expect(validateAmount('0', 2)).toBe(true); + expect(validateAmount('1000000', 2)).toBe(true); + }); + + it('should validate numbers with decimals up to maxDecimals', () => { + expect(validateAmount('123.45', 2)).toBe(true); + expect(validateAmount('123.4', 2)).toBe(true); + expect(validateAmount('0.45', 2)).toBe(true); + }); + + it('should reject numbers with more decimals than maxDecimals', () => { + expect(validateAmount('123.456', 2)).toBe(false); + expect(validateAmount('0.123', 2)).toBe(false); + }); + + it('should reject invalid number formats', () => { + expect(validateAmount('123..45', 2)).toBe(false); + expect(validateAmount('123.', 2)).toBe(false); + expect(validateAmount('.123', 2)).toBe(false); + expect(validateAmount('abc', 2)).toBe(false); + expect(validateAmount('12.34.56', 2)).toBe(false); + expect(validateAmount('-123.45', 2)).toBe(false); + expect(validateAmount('+123.45', 2)).toBe(false); + }); + }); + + describe('with exact=true', () => { + it('should validate numbers with exactly maxDecimals decimal places', () => { + expect(validateAmount('123.45', 2, true)).toBe(true); + expect(validateAmount('0.45', 2, true)).toBe(true); + }); + + it('should reject numbers with fewer decimals than maxDecimals', () => { + expect(validateAmount('123.4', 2, true)).toBe(false); + expect(validateAmount('123', 2, true)).toBe(false); + }); + + it('should reject numbers with more decimals than maxDecimals', () => { + expect(validateAmount('123.456', 2, true)).toBe(false); + }); + + it('should reject invalid number formats', () => { + expect(validateAmount('123..45', 2, true)).toBe(false); + expect(validateAmount('123.', 2, true)).toBe(false); + expect(validateAmount('.123', 2, true)).toBe(false); + expect(validateAmount('abc', 2, true)).toBe(false); + expect(validateAmount('12.34.56', 2, true)).toBe(false); + expect(validateAmount('-123.45', 2, true)).toBe(false); + expect(validateAmount('+123.45', 2, true)).toBe(false); + }); + }); + + describe('edge cases', () => { + it('should handle zero maxDecimals', () => { + expect(validateAmount('123', 0)).toBe(true); + expect(validateAmount('123.0', 0)).toBe(false); + expect(validateAmount('123.', 0)).toBe(false); + }); + + it('should handle large maxDecimals', () => { + expect(validateAmount('123.123456789', 9)).toBe(true); + expect(validateAmount('123.1234567890', 9)).toBe(false); + }); + + it('should handle leading zeros', () => { + expect(validateAmount('00123.45', 2)).toBe(false); + expect(validateAmount('0123.45', 2)).toBe(false); + }); + + it('should handle trailing zeros', () => { + expect(validateAmount('123.450', 2)).toBe(false); + expect(validateAmount('123.4500', 2)).toBe(false); + }); + }); +}); + +describe('convertToIntegerAmount', () => { + it('should convert decimal amounts to integer amounts with correct padding', () => { + expect(convertToIntegerAmount('123.45', 4)).toBe('1234500'); + expect(convertToIntegerAmount('0.1', 8)).toBe('10000000'); + expect(convertToIntegerAmount('1000.5', 6)).toBe('1000500000'); + }); + + it('should handle whole numbers', () => { + expect(convertToIntegerAmount('123', 2)).toBe('12300'); + expect(convertToIntegerAmount('1000', 4)).toBe('10000000'); + expect(convertToIntegerAmount('0', 6)).toBe('0'); + }); + + it('should handle amounts with exact decimal places', () => { + expect(convertToIntegerAmount('123.4567', 4)).toBe('1234567'); + expect(convertToIntegerAmount('0.12345678', 8)).toBe('12345678'); + }); + + it('should throw error for invalid amounts', () => { + expect(() => convertToIntegerAmount('123.456', 2)).toThrow('Invalid amount or decimal places'); + expect(() => convertToIntegerAmount('abc', 2)).toThrow('Invalid amount or decimal places'); + expect(() => convertToIntegerAmount('123..45', 2)).toThrow('Invalid amount or decimal places'); + expect(() => convertToIntegerAmount('-123.45', 2)).toThrow('Invalid amount or decimal places'); + }); + + it('should handle zero amounts with different decimal places', () => { + expect(convertToIntegerAmount('0.0', 4)).toBe('0'); + expect(convertToIntegerAmount('0.00', 4)).toBe('0'); + expect(convertToIntegerAmount('0', 8)).toBe('0'); + }); + + it('should handle amounts with trailing zeros', () => { + expect(convertToIntegerAmount('123.4000', 6)).toBe('123400000'); + expect(convertToIntegerAmount('100.00', 4)).toBe('1000000'); + }); +}); diff --git a/src/ui/utils/number.ts b/src/shared/utils/number.ts similarity index 66% rename from src/ui/utils/number.ts rename to src/shared/utils/number.ts index 4edfb5a6..fd81957a 100644 --- a/src/ui/utils/number.ts +++ b/src/shared/utils/number.ts @@ -1,42 +1,10 @@ -import BigNumber from 'bignumber.js'; - -import { DecimalMappingValues } from '@/shared/types/transaction-types'; - -export const splitNumberByStep = ( - num: number | string, - step = 3, - symbol = ',', - forceInt = false -) => { - let [int, float] = (num + '').split('.'); - const reg = new RegExp(`(\\d)(?=(\\d{${step}})+(?!\\d))`, 'g'); - - int = int.replace(reg, `$1${symbol}`); - if (Number(num) > 1000000 || forceInt) { - // hide the after-point part if number is more than 1000000 - float = ''; - } - if (float) { - return `${int}.${float}`; - } - return int; -}; - -export const formatTokenAmount = (amount: number | string, decimals = 4) => { - if (!amount) return '0'; - const bn = new BigNumber(amount); - const str = bn.toFixed(); - const split = str.split('.'); - if (!split[1] || split[1].length < decimals) { - return splitNumberByStep(bn.toFixed()); - } - return splitNumberByStep(bn.toFixed(decimals)); -}; +// TODO: remove this function. It's called from CoinList and Wallet export const formatLargeNumber = (num) => { if (typeof num === 'string' && num.startsWith('$')) { num = num.slice(1); } + if (num >= 1e12) { return (num / 1e12).toFixed(3) + 'T'; // Trillions } else if (num >= 1e9) { @@ -48,6 +16,7 @@ export const formatLargeNumber = (num) => { } }; +// TODO: remove this function. It's called from TokenInfoCard.tsx export const addDotSeparators = (num) => { // replace with http://numeraljs.com/ if more requirements const [integerPart, decimalPart] = parseFloat(num).toFixed(8).split('.'); @@ -61,16 +30,6 @@ export const addDotSeparators = (num) => { return `${newIntegerPart}.${formattedDecimal}`; }; -export const checkDecimals = (value: string, maxDecimals: number) => { - const decimals = value.includes('.') ? value.split('.')[1]?.length || 0 : 0; - return decimals <= maxDecimals; -}; - -export const getMaxDecimals = (currentTxState: string | undefined) => { - if (!currentTxState) return 8; - return DecimalMappingValues[currentTxState as keyof typeof DecimalMappingValues]; -}; - export const stripEnteredAmount = (value: string, maxDecimals: number) => { // Remove minus signs and non-digit/non-decimal characters const cleanValue = value.replace(/[^\d.]/g, ''); @@ -116,3 +75,32 @@ export const stripFinalAmount = (value: string, maxDecimals: number) => { return strippedValue; }; + +// Validate that a string is a valid transaction amount with the given max decimal places +// If exact is true, the amount must have exactly maxDecimals decimal places +export const validateAmount = (amount: string, maxDecimals: number, exact = false): boolean => { + const regex = + maxDecimals === 0 + ? `^(0|[1-9]\\d*)$` + : exact + ? `^(0|[1-9]\\d*)\\.\\d{${maxDecimals}}$` + : `^(0|[1-9]\\d*)(\\.\\d{1,${maxDecimals}})?$`; + return new RegExp(regex).test(amount); +}; + +export const convertToIntegerAmount = (amount: string, decimals: number): string => { + // Check if the amount is valid + if (!validateAmount(amount, decimals)) { + throw new Error('Invalid amount or decimal places'); + } + + // Create an integer string based on the required token decimals + const parts = amount.split('.'); + const hasDecimal = parts.length > 1; + const integerPart = parts[0]; + const decimalPart = hasDecimal ? parts[1] : ''; + // Pad the decimal part with zeros + const paddedDecimal = decimalPart.padEnd(decimals, '0'); + // Remove leading zeros + return `${integerPart}${paddedDecimal}`.replace(/^0+/, '') || '0'; +}; diff --git a/src/ui/reducers/transaction-reducer.ts b/src/ui/reducers/transaction-reducer.ts index 8e425947..d929ab6e 100644 --- a/src/ui/reducers/transaction-reducer.ts +++ b/src/ui/reducers/transaction-reducer.ts @@ -11,7 +11,7 @@ import type { import { type CoinItem, type WalletAddress } from '@/shared/types/wallet-types'; import { isValidEthereumAddress } from '@/shared/utils/address'; -import { getMaxDecimals, stripEnteredAmount, stripFinalAmount } from '../utils/number'; +import { stripEnteredAmount, stripFinalAmount } from '../../shared/utils/number'; export const INITIAL_TRANSACTION_STATE: TransactionState = { currentTxState: '', @@ -212,7 +212,7 @@ export const transactionReducer = ( amountInCoin = '0.0'; } else { amountInCoin = calculatedAmountInCoin.toFixed( - getMaxDecimals(state.currentTxState!), + state.selectedToken.decimals, BN.ROUND_DOWN ); } diff --git a/src/ui/utils/__tests__/number.test.ts b/src/ui/utils/__tests__/number.test.ts deleted file mode 100644 index 1779107a..00000000 --- a/src/ui/utils/__tests__/number.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { - splitNumberByStep, - formatTokenAmount, - formatLargeNumber, - addDotSeparators, - stripEnteredAmount, - stripFinalAmount, -} from '../number'; - -describe('splitNumberByStep', () => { - it('should format numbers with default parameters', () => { - expect(splitNumberByStep('1234567')).toBe('1,234,567'); - expect(splitNumberByStep('1234.89')).toBe('1,234.89'); - }); - - it('should handle custom step and symbol', () => { - expect(splitNumberByStep('1234567', 2, '_')).toBe('1_23_45_67'); - }); - - it('should handle forceInt parameter', () => { - expect(splitNumberByStep('1234567.89', 3, ',', true)).toBe('1,234,567'); - }); -}); - -describe('formatTokenAmount', () => { - it('should format token amounts with default decimals', () => { - expect(formatTokenAmount('1234.5678')).toBe('1,234.5678'); - expect(formatTokenAmount('1234.56789')).toBe('1,234.5679'); - }); - - it('should handle custom decimals', () => { - expect(formatTokenAmount('1234.5678', 2)).toBe('1,234.57'); - }); - - it('should handle zero and empty values', () => { - expect(formatTokenAmount(0)).toBe('0'); - expect(formatTokenAmount('')).toBe('0'); - }); -}); - -describe('formatLargeNumber', () => { - it('should format numbers into human readable format', () => { - expect(formatLargeNumber(1500000)).toBe('1.500M'); - expect(formatLargeNumber(2500000000)).toBe('2.500B'); - expect(formatLargeNumber(1500000000000)).toBe('1.500T'); - }); - - it('should handle numbers less than 1M', () => { - expect(formatLargeNumber(999999)).toBe('999999'); - }); - - it('should handle string numbers with $ prefix', () => { - expect(formatLargeNumber('$1500000')).toBe('1.500M'); - }); -}); - -describe('addDotSeparators', () => { - it('should format numbers with proper separators', () => { - expect(addDotSeparators('1234567.89')).toBe('1,234,567.89'); - }); - - it('should handle trailing zeros', () => { - expect(addDotSeparators('1234.50000000')).toBe('1,234.5'); - }); - - it('should preserve at least some decimal places', () => { - expect(addDotSeparators('1234.00000000')).toBe('1,234.000'); - }); -}); - -describe('stripEnteredAmount', () => { - it('should handle real-time input correctly', () => { - // Basic cases - expect(stripEnteredAmount('123', 2)).toBe('123'); - expect(stripEnteredAmount('123.', 2)).toBe('123.'); - expect(stripEnteredAmount('123.4', 2)).toBe('123.4'); - - // Multiple decimal points - expect(stripEnteredAmount('12..34.56', 2)).toBe('12.'); - expect(stripEnteredAmount('12.34.56', 2)).toBe('12.34'); - - // Leading zeros - expect(stripEnteredAmount('000123', 2)).toBe('123'); - expect(stripEnteredAmount('0', 2)).toBe('0'); - expect(stripEnteredAmount('00.123', 2)).toBe('0.12'); - - // Decimal cases - expect(stripEnteredAmount('.123', 2)).toBe('0.12'); - expect(stripEnteredAmount('.', 2)).toBe('0.'); - expect(stripEnteredAmount('', 2)).toBe(''); - }); -}); - -describe('stripFinalAmount', () => { - it('should handle empty and invalid inputs', () => { - expect(stripFinalAmount('', 2)).toBe('0'); - expect(stripFinalAmount(' ', 2)).toBe('0'); - expect(stripFinalAmount('.', 2)).toBe('0'); - expect(stripFinalAmount('..', 2)).toBe('0'); - }); - - it('should format final amounts correctly', () => { - // Basic cases - expect(stripFinalAmount('123', 2)).toBe('123'); - expect(stripFinalAmount('123.', 2)).toBe('123'); - expect(stripFinalAmount('123.4', 2)).toBe('123.4'); - expect(stripFinalAmount('123.40', 2)).toBe('123.4'); - // Removes extra zeros even when truncating - expect(stripFinalAmount('123.401', 2)).toBe('123.4'); - expect(stripFinalAmount('123.471', 2)).toBe('123.47'); - // Truncates doesn't round up - expect(stripFinalAmount('123.479', 2)).toBe('123.47'); - - // Multiple decimal points - expect(stripFinalAmount('12..34.56', 2)).toBe('12'); - expect(stripFinalAmount('12.34.56', 2)).toBe('12.34'); - - // Leading zeros - expect(stripFinalAmount('000123', 2)).toBe('123'); - expect(stripFinalAmount('0', 2)).toBe('0'); - expect(stripFinalAmount('00.123', 2)).toBe('0.12'); - - // Decimal cases - expect(stripFinalAmount('.123', 2)).toBe('0.12'); - expect(stripFinalAmount('.', 2)).toBe('0'); - expect(stripFinalAmount('', 2)).toBe('0'); - // Handle getting rid of trailing zeros - expect(stripFinalAmount('123.000', 2)).toBe('123'); - }); -}); diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 9d09d01a..36a8024f 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -11,7 +11,7 @@ export * from './webapi'; export * from './time'; -export * from './number'; +export * from '../../shared/utils/number'; const UI_TYPE = { Tab: 'index', diff --git a/src/ui/views/TokenDetail/TokenInfoCard.tsx b/src/ui/views/TokenDetail/TokenInfoCard.tsx index 8cb435a3..9f923c20 100644 --- a/src/ui/views/TokenDetail/TokenInfoCard.tsx +++ b/src/ui/views/TokenDetail/TokenInfoCard.tsx @@ -4,10 +4,10 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { isValidEthereumAddress } from '@/shared/utils/address'; +import { addDotSeparators } from '@/shared/utils/number'; import { LLPrimaryButton } from '@/ui/FRWComponent'; import iconMove from 'ui/FRWAssets/svg/moveIcon.svg'; import { useWallet } from 'ui/utils'; -import { addDotSeparators } from 'ui/utils/number'; import IconChevronRight from '../../../components/iconfont/IconChevronRight'; diff --git a/src/ui/views/Wallet/Coinlist.tsx b/src/ui/views/Wallet/Coinlist.tsx index 0fe8106a..9f28a22d 100644 --- a/src/ui/views/Wallet/Coinlist.tsx +++ b/src/ui/views/Wallet/Coinlist.tsx @@ -13,7 +13,7 @@ import { Box } from '@mui/system'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { formatLargeNumber } from 'ui/utils/number'; +import { formatLargeNumber } from '@/shared/utils/number'; import IconCreate from '../../../components/iconfont/IconCreate'; import { TokenValue } from '../TokenDetail/TokenValue'; diff --git a/src/ui/views/Wallet/index.tsx b/src/ui/views/Wallet/index.tsx index 76bc1628..4f662a07 100644 --- a/src/ui/views/Wallet/index.tsx +++ b/src/ui/views/Wallet/index.tsx @@ -9,6 +9,7 @@ import SwipeableViews from 'react-swipeable-views'; import { IconNfts } from '@/components/iconfont'; import eventBus from '@/eventBus'; import { type ActiveChildType } from '@/shared/types/wallet-types'; +import { formatLargeNumber } from '@/shared/utils/number'; import buyIcon from '@/ui/FRWAssets/svg/buyIcon.svg'; import iconMove from '@/ui/FRWAssets/svg/homeMove.svg'; import receiveIcon from '@/ui/FRWAssets/svg/receiveIcon.svg'; @@ -20,7 +21,6 @@ import { useInitHook } from '@/ui/hooks'; import { useCoinStore } from '@/ui/stores/coinStore'; import { useProfileStore } from '@/ui/stores/profileStore'; import { useWallet } from '@/ui/utils'; -import { formatLargeNumber } from '@/ui/utils/number'; import { withPrefix } from '../../../shared/utils/address'; import theme from '../../style/LLTheme'; From e5ab5be7cb12080d5d9eac1ad51732928265c1ed Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:02:49 +1100 Subject: [PATCH 37/43] Removed unused code and providers --- src/shared/types/transaction-types.ts | 31 ------- src/ui/hooks/useTransactionHook.ts | 26 ------ src/ui/hooks/useWeb3.ts | 17 ---- src/ui/stores/providerStore.ts | 17 ---- src/ui/stores/transactionStore.ts | 124 -------------------------- 5 files changed, 215 deletions(-) delete mode 100644 src/ui/hooks/useTransactionHook.ts delete mode 100644 src/ui/hooks/useWeb3.ts delete mode 100644 src/ui/stores/providerStore.ts delete mode 100644 src/ui/stores/transactionStore.ts diff --git a/src/shared/types/transaction-types.ts b/src/shared/types/transaction-types.ts index 4b196e30..6f6627bb 100644 --- a/src/shared/types/transaction-types.ts +++ b/src/shared/types/transaction-types.ts @@ -57,34 +57,3 @@ export type TransactionState = { // The transaction if of the transaction txId?: string; }; - -// Type for the mapping -export type DecimalMapping = { - [K in TransactionStateString]: number; -}; - -export const DecimalMappingValues: DecimalMapping = { - // To Evm - FTFromEvmToEvm: 16, - FTFromCadenceToEvm: 8, - FTFromChildToEvm: 8, - FlowFromEvmToEvm: 16, - FlowFromCadenceToEvm: 8, - FlowFromChildToEvm: 8, - - // To Cadence - FTFromEvmToCadence: 18, - FTFromCadenceToCadence: 8, - FTFromChildToCadence: 8, - FlowFromEvmToCadence: 8, - FlowFromCadenceToCadence: 8, - FlowFromChildToCadence: 8, - - // To Child unused for now - FTFromEvmToChild: 18, - FTFromCadenceToChild: 18, - FTFromChildToChild: 18, - FlowFromEvmToChild: 18, - FlowFromCadenceToChild: 18, - FlowFromChildToChild: 18, -} as const; diff --git a/src/ui/hooks/useTransactionHook.ts b/src/ui/hooks/useTransactionHook.ts deleted file mode 100644 index 9bf822ff..00000000 --- a/src/ui/hooks/useTransactionHook.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useCallback } from 'react'; - -import { useTransactionStore } from '@/ui/stores/transactionStore'; -import { useWallet } from '@/ui/utils/WalletContext'; - -// Temporary fix before getting the coinlist tokenlist upgrade in the background. -export const useTransactionHook = () => { - const usewallet = useWallet(); - const { setSelectedToken } = useTransactionStore(); - - const fetchAndSetToken = useCallback( - async (address: string) => { - try { - const tokenInfo = await usewallet.openapi.getTokenInfo(address); - if (tokenInfo) { - setSelectedToken(tokenInfo); - } - } catch (error) { - console.error('Error fetching token info:', error); - } - }, - [setSelectedToken, usewallet] - ); - - return { fetchAndSetToken }; -}; diff --git a/src/ui/hooks/useWeb3.ts b/src/ui/hooks/useWeb3.ts deleted file mode 100644 index 98a978d4..00000000 --- a/src/ui/hooks/useWeb3.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo } from 'react'; -import Web3 from 'web3'; - -import { EVM_ENDPOINT } from '@/constant'; - -import { useNetworkStore } from '../stores/networkStore'; - -export const useWeb3 = () => { - const { currentNetwork } = useNetworkStore(); - const network = currentNetwork || 'mainnet'; - const web3instance = useMemo(() => { - const provider = new Web3.providers.HttpProvider(EVM_ENDPOINT[network]); - return new Web3(provider); - }, [network]); - - return web3instance; -}; diff --git a/src/ui/stores/providerStore.ts b/src/ui/stores/providerStore.ts deleted file mode 100644 index c4767b49..00000000 --- a/src/ui/stores/providerStore.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Web3 from 'web3'; -import { create } from 'zustand'; - -import { EVM_ENDPOINT } from 'consts'; - -interface ProviderStore { - web3Instance: Web3 | null; - setWeb3Instance: (network: string) => void; -} - -export const useProviderStore = create((set) => ({ - web3Instance: null, - setWeb3Instance: (network) => { - const provider = new Web3.providers.HttpProvider(EVM_ENDPOINT[network]); - set({ web3Instance: new Web3(provider) }); - }, -})); diff --git a/src/ui/stores/transactionStore.ts b/src/ui/stores/transactionStore.ts deleted file mode 100644 index e5837d83..00000000 --- a/src/ui/stores/transactionStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { TokenInfo } from 'flow-native-token-registry'; -import { debounce } from 'lodash'; -import { create } from 'zustand'; -import { subscribeWithSelector } from 'zustand/middleware'; - -import type { - NetworkType, - TokenType, - TransactionStateString, -} from '@/shared/types/transaction-types'; -import { isValidEthereumAddress } from '@/shared/utils/address'; -import { useProfileStore } from '@/ui/stores/profileStore'; - -interface TransactionStore { - currentTxState: TransactionStateString | null; - tokenType: TokenType | null; - fromNetwork: NetworkType | null; - toNetwork: NetworkType | null; - toAddress: string; - selectedToken: TokenInfo; - setTokenType: (type: TokenType) => void; - setFromNetwork: (address: string) => void; - setToNetwork: (address: string) => void; - setToAddress: (address: string) => void; - setSelectedToken: (input: TokenInfo) => Promise; - createTransactionState: () => void; -} - -export const useTransactionStore = create()( - subscribeWithSelector((set, get) => ({ - currentTxState: null, - tokenType: null, - fromNetwork: null, - toNetwork: null, - toAddress: '', - selectedToken: { - name: 'Flow', - address: '0x4445e7ad11568276', - contractName: 'FlowToken', - path: { - balance: '/public/flowTokenBalance', - receiver: '/public/flowTokenReceiver', - vault: '/storage/flowTokenVault', - }, - logoURI: - 'https://cdn.jsdelivr.net/gh/FlowFans/flow-token-list@main/token-registry/A.1654653399040a61.FlowToken/logo.svg', - decimals: 8, - symbol: 'flow', - }, - setSelectedToken: async (input: TokenInfo) => { - console.log('Selected Token:', input); - set({ selectedToken: input }); - }, - setTokenType: (type) => set({ tokenType: type }), - setFromNetwork: (address) => { - const mainAddress = useProfileStore.getState().mainAddress; - let networkType: NetworkType = 'Child'; - - if (isValidEthereumAddress(address)) { - networkType = 'Evm'; - } else if (address === mainAddress) { - networkType = 'Cadence'; - } - console.log('setFromNetwork networkType', networkType); - - set({ fromNetwork: networkType }); - }, - setToNetwork: (address) => { - const mainAddress = useProfileStore.getState().mainAddress; - let networkType: NetworkType = 'Child'; - - if (isValidEthereumAddress(address)) { - networkType = 'Evm'; - } else if (address === mainAddress) { - networkType = 'Cadence'; - } - console.log('networkType', networkType, address, mainAddress); - set({ toNetwork: networkType }); - }, - setToAddress: (address) => { - set({ toAddress: address }); - get().setToNetwork(address); - }, - createTransactionState: () => { - const { tokenType, fromNetwork, toNetwork, currentTxState } = get(); - if (!tokenType || !fromNetwork || !toNetwork) return; - - const newTxState: TransactionStateString = `${tokenType}From${fromNetwork}To${toNetwork}`; - if (currentTxState !== newTxState) { - console.log('Creating new transaction state:', newTxState); - set({ currentTxState: newTxState }); - } - }, - })) -); - -// Subscription with equality check -useTransactionStore.subscribe( - (state) => ({ - tokenType: state.tokenType, - fromNetwork: state.fromNetwork, - toNetwork: state.toNetwork, - }), - (newState, prevState) => { - // Skip if nothing changed - if (!hasStateChanged(newState, prevState)) { - console.log('No state changes, skipping update'); - return; - } - - if (newState.tokenType && newState.fromNetwork && newState.toNetwork) { - console.log('State changed, creating new transaction state'); - useTransactionStore.getState().createTransactionState(); - } - }, - { - equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b), // Deep equality check - } -); - -// Helper function -const hasStateChanged = (newState: any, prevState: any) => { - return Object.keys(newState).some((key) => newState[key] !== prevState[key]); -}; From 3e7aaae90769fb355c4b8e3f29665e81fbeccbb3 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:05:33 +1100 Subject: [PATCH 38/43] Removed temp code from webpack --- webpack.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/webpack.config.ts b/webpack.config.ts index 8f4755d5..ff47c68d 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -30,9 +30,6 @@ const configs: Record<'dev' | 'pro' | 'none', webpack.Configuration> = { ], resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], - extensionAlias: { - '.js': ['.tsx', '.ts', '.js'], - }, fallback: { buffer: require.resolve('buffer'), url: require.resolve('url/'), From fc033ad49aa113996552b70e1825ed357980c6ad Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:40:28 +1100 Subject: [PATCH 39/43] Returns empty string if empty string passed in --- src/ui/views/TokenDetail/TokenValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/TokenDetail/TokenValue.tsx b/src/ui/views/TokenDetail/TokenValue.tsx index 0681983b..16f7300a 100644 --- a/src/ui/views/TokenDetail/TokenValue.tsx +++ b/src/ui/views/TokenDetail/TokenValue.tsx @@ -16,7 +16,7 @@ export const TokenValue: React.FC = ({ prefix = '', postFix = '', }) => { - if (value === 0 || value === null || value === undefined) { + if (value === 0 || value === null || value === undefined || value === '') { return {''}; } From d05537a40b00f13930ffa5fcb7efc15e4aa537c6 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:53:16 +1100 Subject: [PATCH 40/43] Added metamask EOA address --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa014c27..44b14163 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,8 @@ jobs: TEST_SEED_PHRASE_SENDER="${{ secrets.TEST_SEED_PHRASE_SENDER }}" TEST_SENDER_ADDR="${{ secrets.TEST_SENDER_ADDR }}" TEST_RECEIVER_ADDR="${{ secrets.TEST_RECEIVER_ADDR }}" + TEST_RECEIVER_EVM_ADDR="${{ secrets.TEST_RECEIVER_EVM_ADDR }}" + TEST_RECEIVER_METAMASK_EVM_ADDR="${{ secrets.TEST_RECEIVER_METAMASK_EVM_ADDR }}" EOF - name: Run tests @@ -138,6 +140,7 @@ jobs: TEST_RECEIVER_ADDR="${{ secrets.TEST_RECEIVER_ADDR }}" TEST_SENDER_EVM_ADDR="${{ secrets.TEST_SENDER_EVM_ADDR }}" TEST_RECEIVER_EVM_ADDR="${{ secrets.TEST_RECEIVER_EVM_ADDR }}" + TEST_RECEIVER_METAMASK_EVM_ADDR="${{ secrets.TEST_RECEIVER_METAMASK_EVM_ADDR }}" EOF - name: Install pnpm uses: pnpm/action-setup@v4 From 36f8b0357e15a1c82f0b51c4d76a9452de8f4603 Mon Sep 17 00:00:00 2001 From: zzggo Date: Fri, 14 Feb 2025 15:17:05 +1100 Subject: [PATCH 41/43] fixed: return the contact card with icons first --- src/ui/hooks/useContactHook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/hooks/useContactHook.ts b/src/ui/hooks/useContactHook.ts index 0dd4eae6..927df07a 100644 --- a/src/ui/hooks/useContactHook.ts +++ b/src/ui/hooks/useContactHook.ts @@ -141,11 +141,11 @@ export function useContactHook() { const useContact = useCallback( (address: string): Contact | null => { return ( - contactStore.recentContacts.find((c) => c.address === address) || contactStore.accountList.find((c) => c.address === address) || contactStore.evmAccounts.find((c) => c.address === address) || contactStore.childAccounts.find((c) => c.address === address) || contactStore.filteredContacts.find((c) => c.address === address) || + contactStore.recentContacts.find((c) => c.address === address) || null ); }, From f4e0bf4f5b39aad683f969b925ff8c983c331143 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:19:23 +1100 Subject: [PATCH 42/43] Corrected evm transaction coversion to integer, and coin selector --- src/background/controller/wallet.ts | 22 +++++----------------- src/ui/views/SendTo/TransferAmount.tsx | 4 +++- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 70d71f4c..99c8eaf3 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -1792,29 +1792,17 @@ export class WalletController extends BaseController { // Returns the transaction ID const transferTokensOnEvm = async () => { - // the amount is always stored as a string in the transaction state - const amountStr: string = transactionState.amount; - // TODO: check if the amount is a valid number - // Create an integer string based on the required token decimals - const amountBN = new BN(amountStr.replace('.', '')); - - const decimalsCount = amountStr.split('.')[1]?.length || 0; - const decimalDifference = transactionState.selectedToken.decimals - decimalsCount; - if (decimalDifference < 0) { - throw new Error('Too many decimal places have been provided'); - } - const scaleFactor = new BN(10).pow(decimalDifference); - const integerAmount = amountBN.multipliedBy(scaleFactor); - const integerAmountStr = integerAmount.integerValue(BN.ROUND_DOWN).toFixed(); + const integerAmountStr = convertToIntegerAmount( + transactionState.amount, + transactionState.selectedToken.decimals + ); let address, gas, value, data; if (transactionState.selectedToken.symbol.toLowerCase() === 'flow') { address = transactionState.toAddress; gas = '1'; - // const amountBN = new BN(transactionState.amount).multipliedBy(new BN(10).pow(18)); - // the amount is always stored as a string in the transaction state - value = integerAmount.toString(16); + value = new BN(integerAmountStr).toString(16); data = '0x'; } else { // Get the current network diff --git a/src/ui/views/SendTo/TransferAmount.tsx b/src/ui/views/SendTo/TransferAmount.tsx index a84c303e..ae3e488d 100644 --- a/src/ui/views/SendTo/TransferAmount.tsx +++ b/src/ui/views/SendTo/TransferAmount.tsx @@ -123,7 +123,9 @@ const TransferAmount = ({ const renderValue = useCallback( (option) => { - const selectCoin = coinStore.coins.find((coin) => coin.unit === option); + const selectCoin = coinStore.coins.find( + (coin) => coin.unit.toLowerCase() === option.toLowerCase() + ); if (selectCoin) { return ; } From f95f71ae7e440ec64f13dc5e3c40654c52174270 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:16:42 +1100 Subject: [PATCH 43/43] Corrected flow token coa transaction. Checks transaction amount in e2e tests --- e2e/sendTransaction.test.ts | 5 +++++ e2e/sendTransactionFromFlow.test.ts | 6 ++++++ e2e/utils/helper.ts | 18 +++++++++--------- src/background/controller/wallet.ts | 16 +++++++++++----- src/ui/views/TokenDetail/TokenValue.tsx | 2 +- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/e2e/sendTransaction.test.ts b/e2e/sendTransaction.test.ts index e3f0b508..32a9f376 100644 --- a/e2e/sendTransaction.test.ts +++ b/e2e/sendTransaction.test.ts @@ -73,6 +73,7 @@ test('send Flow COA to COA', async ({ page }) => { tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_EVM_ADDR!, successtext: 'success', + amount: '0.12345678', // 8 decimal places }); }); @@ -95,6 +96,7 @@ test('send Flow COA to FLOW', async ({ page }) => { tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_ADDR!, successtext: 'success', + amount: '0.00123456', // 8 decimal places }); }); @@ -105,6 +107,7 @@ test('send USDC token COA to FLOW', async ({ page }) => { tokenname: 'Bridged USDC (Celer) $', receiver: process.env.TEST_RECEIVER_ADDR!, successtext: 'success', + amount: '0.002468', // 6 decimal places }); }); @@ -117,6 +120,7 @@ test('send Flow COA to EOA', async ({ page }) => { tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_METAMASK_EVM_ADDR!, successtext: 'success', + amount: '0.00123456', // 8 decimal places }); }); @@ -127,6 +131,7 @@ test('send BETA token COA to EOA', async ({ page }) => { tokenname: 'BETA $', receiver: process.env.TEST_RECEIVER_METAMASK_EVM_ADDR!, successtext: 'success', + amount: '0.001234567890123456', // 8 decimal places }); }); /* //Move FTs from COA to FLOW diff --git a/e2e/sendTransactionFromFlow.test.ts b/e2e/sendTransactionFromFlow.test.ts index 49a0a451..590cf2e0 100644 --- a/e2e/sendTransactionFromFlow.test.ts +++ b/e2e/sendTransactionFromFlow.test.ts @@ -74,6 +74,7 @@ test('send FLOW flow to flow', async ({ page }) => { page, tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_ADDR!, + amount: '0.00123456', }); }); @@ -83,6 +84,7 @@ test('send stFlow flow to flow', async ({ page }) => { page, tokenname: 'Liquid Staked Flow $', receiver: process.env.TEST_RECEIVER_ADDR!, + amount: '0.00123456', }); }); @@ -93,6 +95,7 @@ test('send FLOW flow to COA', async ({ page }) => { page, tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_EVM_ADDR!, + amount: '0.00123456', }); }); //Send USDC from Flow to Flow @@ -102,6 +105,7 @@ test('send USDC flow to COA', async ({ page }) => { tokenname: 'USDC.e (Flow) $', receiver: process.env.TEST_RECEIVER_EVM_ADDR!, ingoreFlowCharge: true, + amount: '0.00123456', }); }); @@ -112,6 +116,7 @@ test('send FLOW flow to EOA', async ({ page }) => { page, tokenname: /^FLOW \$/i, receiver: process.env.TEST_RECEIVER_METAMASK_EVM_ADDR!, + amount: '0.00123456', }); }); @@ -122,6 +127,7 @@ test('send BETA flow to EOA', async ({ page }) => { tokenname: 'BETA $', receiver: process.env.TEST_RECEIVER_METAMASK_EVM_ADDR!, ingoreFlowCharge: true, + amount: '0.00123456', }); }); /* //Move FTs from Flow to COA diff --git a/e2e/utils/helper.ts b/e2e/utils/helper.ts index d04e239e..116da013 100644 --- a/e2e/utils/helper.ts +++ b/e2e/utils/helper.ts @@ -452,9 +452,9 @@ export const waitForTransaction = async ({ const progressBar = page.getByRole('progressbar'); await expect(progressBar).toBeVisible(); // Get the pending item with the cadence txId that was put in the url and status is pending - const pendingItem = page - .getByTestId(new RegExp(`^.*${txId}.*${ingoreFlowCharge ? '(? { - const integerAmountStr = convertToIntegerAmount( - transactionState.amount, - transactionState.selectedToken.decimals - ); - let address, gas, value, data; if (transactionState.selectedToken.symbol.toLowerCase() === 'flow') { address = transactionState.toAddress; gas = '1'; + // the amount is always stored as a string in the transaction state + const integerAmountStr = convertToIntegerAmount( + transactionState.amount, + // Flow needs 18 digits always for EVM + 18 + ); value = new BN(integerAmountStr).toString(16); data = '0x'; } else { + const integerAmountStr = convertToIntegerAmount( + transactionState.amount, + transactionState.selectedToken.decimals + ); + // Get the current network const network = await this.getNetwork(); // Get the Web3 provider diff --git a/src/ui/views/TokenDetail/TokenValue.tsx b/src/ui/views/TokenDetail/TokenValue.tsx index 16f7300a..92d4ea6a 100644 --- a/src/ui/views/TokenDetail/TokenValue.tsx +++ b/src/ui/views/TokenDetail/TokenValue.tsx @@ -36,7 +36,7 @@ export const TokenValue: React.FC = ({ const { leadingPart, zeroPart, endingPart } = formattedPrice; return ( - + {prefix} {numberWithCommas(leadingPart)}