diff --git a/src/components/basic/Input/Input.css b/src/components/basic/Input/Input.css index 00f3c350..ea596976 100644 --- a/src/components/basic/Input/Input.css +++ b/src/components/basic/Input/Input.css @@ -16,6 +16,7 @@ .input::placeholder { color: rgb(var(--color-dark-gray)); + opacity: 0.2; font-size: 1.5rem; } diff --git a/src/components/composed/Balance/Balance.test.js b/src/components/composed/Balance/Balance.test.js index 1caaf646..29b8f185 100644 --- a/src/components/composed/Balance/Balance.test.js +++ b/src/components/composed/Balance/Balance.test.js @@ -25,7 +25,7 @@ test('Render account balance with ML', () => { expect(balanceParagraphs[0].textContent).toBe(BALANCE_SAMPLE + ' ML') expect(balanceParagraphs[1].textContent).toBe( - BALANCE_SAMPLE * EXCHANGE_RATE_SAMPLE + ',00 USD', + BALANCE_SAMPLE * EXCHANGE_RATE_SAMPLE + '.00 USD', ) }) @@ -49,7 +49,7 @@ test('Render account balance with BTC', () => { expect(balanceParagraphs[0].textContent).toBe(BALANCE_SAMPLE + ' BTC') expect(balanceParagraphs[1].textContent).toBe( - BALANCE_SAMPLE * EXCHANGE_RATE_SAMPLE + ',00 USD', + BALANCE_SAMPLE * EXCHANGE_RATE_SAMPLE + '.00 USD', ) }) diff --git a/src/components/composed/CryptoFiatField/CryptoFiatField.js b/src/components/composed/CryptoFiatField/CryptoFiatField.js index 4d68faaa..0586ed10 100644 --- a/src/components/composed/CryptoFiatField/CryptoFiatField.js +++ b/src/components/composed/CryptoFiatField/CryptoFiatField.js @@ -39,7 +39,7 @@ const CryptoFiatField = ({ const [value, setValue] = useState(inputValue) const [validity, setValidity] = useState(parentValidity) const amountErrorMessage = 'Amount set is bigger than this wallet balance.' - const amountFormatErrorMessage = 'Amount format is invalid. Use 0,00 instead.' + const amountFormatErrorMessage = 'Amount format is invalid. Use 0.00 instead.' const zeroErrorMessage = 'Amount must be greater than 0.' const isDelegationMode = transactionMode === AppInfo.ML_TRANSACTION_MODES.DELEGATION && @@ -77,7 +77,7 @@ const CryptoFiatField = ({ // Consider the correct format for 0,00 that might also be 0.00 const displayedBottomValue = networkType === AppInfo.NETWORK_TYPES.TESTNET - ? `≈ 0,00 ${fiatName}` + ? `≈ 0.00 ${fiatName}` : formattedBottomValue const calculateFiatValue = (value) => { @@ -141,12 +141,12 @@ const CryptoFiatField = ({ setAmountValidity(true) setValidity('valid') } - setValue(value || 0) - updateValue(value || 0) + setValue(value || '') + updateValue(value || '') const validity = AppInfo.amountRegex.test(value) - if (!validity) { + if (parsedValue > 0 && !validity) { setValidity('invalid') setAmountValidity(false) setErrorMessage(amountFormatErrorMessage) diff --git a/src/components/composed/CryptoFiatField/CryptoFiatField.test.js b/src/components/composed/CryptoFiatField/CryptoFiatField.test.js index c7a855c3..ff540ade 100644 --- a/src/components/composed/CryptoFiatField/CryptoFiatField.test.js +++ b/src/components/composed/CryptoFiatField/CryptoFiatField.test.js @@ -60,7 +60,7 @@ test('Render TextField component', () => { }) expect(input).toHaveValue(maxValueInToken.toString()) - expect(bottomNote).toHaveTextContent('≈ 10054453,50 USD') + expect(bottomNote).toHaveTextContent('≈ 10054453.50 USD') // expect(switchButton).toBeInTheDocument() // expect(arrowIcons).toHaveLength(2) @@ -102,7 +102,7 @@ test('Render TextField component fdf', async () => { const maxValueInCrypto = maxValueInToken - totalFeeCrypto fireEvent.click(actionButton) - expect(cryptoInput).toHaveValue(maxValueInCrypto.toString().replace('.', ',')) + expect(cryptoInput).toHaveValue(maxValueInCrypto.toString()) // fireEvent.click(switchButton) // const fiatInput = screen.getByTestId('input') @@ -156,7 +156,7 @@ test('Render TextField when networkType is testnet', () => { }) expect(input).toHaveValue(maxValueInToken.toString()) - expect(bottomNote).toHaveTextContent('≈ 0,00 USD') + expect(bottomNote).toHaveTextContent('≈ 0.00 USD') // expect(switchButton).toBeInTheDocument() diff --git a/src/components/composed/CurrentStaking/CurrentStaking.js b/src/components/composed/CurrentStaking/CurrentStaking.js index 9a4de933..8b66ce2e 100644 --- a/src/components/composed/CurrentStaking/CurrentStaking.js +++ b/src/components/composed/CurrentStaking/CurrentStaking.js @@ -12,6 +12,7 @@ import { TransactionContext, SettingsContext, AccountContext } from '@Contexts' import './CurrentStaking.css' import Timer from '../../basic/Timer/Timer' +import { useEffectOnce } from '../../../hooks/etc/useEffectOnce' const CurrentStaking = ({ addressList }) => { const { networkType } = useContext(SettingsContext) @@ -39,6 +40,10 @@ const CurrentStaking = ({ addressList }) => { getTransactions, } = useMlWalletInfo(addressList) + useEffectOnce(() => { + getDelegations() + }) + const onNextButtonClick = () => { setDelegationStep(2) } diff --git a/src/components/containers/SendTransaction/SendTransaction.js b/src/components/containers/SendTransaction/SendTransaction.js index 39a6384a..5803b32b 100644 --- a/src/components/containers/SendTransaction/SendTransaction.js +++ b/src/components/containers/SendTransaction/SendTransaction.js @@ -29,6 +29,7 @@ const SendTransaction = ({ confirmTransaction, goBackToWallet, preEnterAddress, + setAdjustedFee, }) => { const { walletType, balanceLoading } = useContext(AccountContext) const { feeLoading, transactionMode, currentDelegationInfo } = @@ -72,7 +73,9 @@ const SendTransaction = ({ } const openConfirmation = async () => { + if (!isFormValid) return setPopupState(true) + setTxErrorMessage('') onSendTransaction && onSendTransaction({ to: addressTo, @@ -106,6 +109,19 @@ const SendTransaction = ({ setPassValidity(false) setPass('') setAllowClosing(true) + } else if (e.message.includes('minimum fee')) { + // need to adjust fee + setAskPassword(false) + setPassPristinity(false) + setPassValidity(false) + setPass('') + setFee(e.message.split('minimum fee ')[1]) // Override fee with minimum fee + setTotalFeeCryptoParent(e.message.split('minimum fee ')[1]) + setAdjustedFee(e.message.split('minimum fee ')[1]) + setTxErrorMessage('Transaction fee adjusted') + console.error(e) + setAllowClosing(true) + setPopupState(true) } else { // handle other errors setAskPassword(false) @@ -336,7 +352,7 @@ const SendTransaction = ({ fiatName={fiatName} totalFeeFiat={totalFeeFiat} totalFeeCrypto={totalFeeCrypto} - // txErrorMessage={txErrorMessage} // TODO move update on confirmation stage + txErrorMessage={txErrorMessage} // TODO move update on confirmation stage fee={fee} onConfirm={handleConfirm} onCancel={handleCancel} diff --git a/src/components/containers/Wallet/Delegation.test.js b/src/components/containers/Wallet/Delegation.test.js index d3cd68b7..e8938985 100644 --- a/src/components/containers/Wallet/Delegation.test.js +++ b/src/components/containers/Wallet/Delegation.test.js @@ -41,7 +41,7 @@ describe('Delegation', () => { ) expect(screen.getByTestId('delegation-date')).toHaveTextContent(date) expect(screen.getByTestId('delegation-amount')).toHaveTextContent( - 'Amount: 0,001', + 'Amount: 0.001', ) }) diff --git a/src/hooks/UseWalletInfo/useBtcWalletInfo.test.js b/src/hooks/UseWalletInfo/useBtcWalletInfo.test.js index 09aad964..57203df8 100644 --- a/src/hooks/UseWalletInfo/useBtcWalletInfo.test.js +++ b/src/hooks/UseWalletInfo/useBtcWalletInfo.test.js @@ -76,7 +76,7 @@ test('UseBtcWalletInfo hook', async () => { transactionsList = result.current.btcTransactionsList }) - expect(balance).toBe('0,02881771') + expect(balance).toBe('0.02881771') // TODO: +1 because of the message transaction. This is a temporary solution expect(transactionsList.length).toBe(rawTransactions.length + 1) }) diff --git a/src/hooks/UseWalletInfo/useMlWalletInfo.js b/src/hooks/UseWalletInfo/useMlWalletInfo.js index 717207fb..399eaf01 100644 --- a/src/hooks/UseWalletInfo/useMlWalletInfo.js +++ b/src/hooks/UseWalletInfo/useMlWalletInfo.js @@ -93,11 +93,22 @@ const useMlWalletInfo = (addresses) => { const delegation_details = await Mintlayer.getDelegationDetails( delegations.map((delegation) => delegation.delegation_id), ) + const blocks_data = await Mintlayer.getBlocksData( + delegation_details.map( + (delegation) => delegation.creation_block_height, + ), + ) const mergedDelegations = delegations.map((delegation, index) => { return { ...delegation, - creation_time: delegation_details[index].creation_time.timestamp, + balance: delegation.balance.atoms, + creation_block_height: + delegation_details[index].creation_block_height, + creation_time: blocks_data.find( + ({ height }) => + height === delegation_details[index].creation_block_height, + ).header.timestamp.timestamp, } }) @@ -119,9 +130,9 @@ const useMlWalletInfo = (addresses) => { if (effectCalled.current) return effectCalled.current = true - getTransactions() - getDelegations() - getBalance() + // getTransactions() + // getDelegations() + // getBalance() }, [getBalance, getTransactions, getDelegations]) return { @@ -134,6 +145,7 @@ const useMlWalletInfo = (addresses) => { mlDelegationsBalance, getDelegations, getTransactions, + getBalance, } } diff --git a/src/hooks/etc/useEffectOnce.js b/src/hooks/etc/useEffectOnce.js new file mode 100644 index 00000000..136db9aa --- /dev/null +++ b/src/hooks/etc/useEffectOnce.js @@ -0,0 +1,10 @@ +import { useEffect, useRef } from 'react' +export function useEffectOnce(effect) { + const called = useRef(false) + useEffect(() => { + if (!called.current) { + called.current = true + effect() + } + }, [effect]) +} diff --git a/src/index.js b/src/index.js index f58cd1af..0e082ad7 100644 --- a/src/index.js +++ b/src/index.js @@ -127,7 +127,6 @@ const App = () => { 'This script should only be loaded in a browser extension.' ) { // not extension env - console.log('not extension env') return } // other error throw further diff --git a/src/pages/Dashboard/Dashboard.js b/src/pages/Dashboard/Dashboard.js index b749e10e..475fcb5e 100644 --- a/src/pages/Dashboard/Dashboard.js +++ b/src/pages/Dashboard/Dashboard.js @@ -18,6 +18,7 @@ import useOneDayAgoHist from 'src/hooks/UseOneDayAgoHist/useOneDayAgoHist' import { useNavigate } from 'react-router-dom' import { BTC } from '@Helpers' import { AppInfo } from '@Constants' +import { useEffectOnce } from 'src/hooks/etc/useEffectOnce' const DashboardPage = () => { const { addresses, accountName, setWalletType, accountID } = @@ -37,7 +38,7 @@ const DashboardPage = () => { const [connectedWalletType, setConnectedWalletType] = useState('') const { btcBalance } = useBtcWalletInfo(currentBtcAddress) - const { mlBalance } = useMlWalletInfo(currentMlAddresses) + const { mlBalance, getBalance } = useMlWalletInfo(currentMlAddresses) const { exchangeRate: btcExchangeRate } = useExchangeRates('btc', 'usd') const { exchangeRate: mlExchangeRate } = useExchangeRates('ml', 'usd') const { yesterdayExchangeRate: btcYesterdayExchangeRate } = @@ -172,6 +173,10 @@ const DashboardPage = () => { getCurrentAccount(accountID).then((account) => setAccount(account)) }, [accountID]) + useEffectOnce(() => { + getBalance() + }, []) + return ( <>
diff --git a/src/pages/SendTransaction/SendTransaction.js b/src/pages/SendTransaction/SendTransaction.js index 07dfdf7b..10f4f259 100644 --- a/src/pages/SendTransaction/SendTransaction.js +++ b/src/pages/SendTransaction/SendTransaction.js @@ -21,6 +21,7 @@ import { ML } from '@Cryptos' import { Mintlayer } from '@APIs' import './SendTransaction.css' +import { useEffectOnce } from '../../hooks/etc/useEffectOnce' const SendTransactionPage = () => { const { addresses, accountID, walletType } = useContext(AccountContext) @@ -36,6 +37,7 @@ const SendTransactionPage = () => { : addresses.mlTestnetAddresses const [totalFeeFiat, setTotalFeeFiat] = useState(0) const [totalFeeCrypto, setTotalFeeCrypto] = useState(0) + const [adjustedFee, setAdjustedFee] = useState(0) const navigate = useNavigate() const tokenName = walletType.name === 'Mintlayer' ? 'ML' : 'BTC' const fiatName = 'USD' @@ -48,7 +50,11 @@ const SendTransactionPage = () => { const { exchangeRate } = useExchangeRates(tokenName, fiatName) const { btcBalance } = useBtcWalletInfo(currentBtcAddress) - const { mlBalance } = useMlWalletInfo(currentMlAddresses) + const { mlBalance, getBalance } = useMlWalletInfo(currentMlAddresses) + + useEffectOnce(() => { + getBalance() + }) const maxValueToken = walletType.name === 'Mintlayer' ? mlBalance : btcBalance @@ -90,26 +96,30 @@ const SendTransactionPage = () => { const calculateMlTotalFee = async (transactionInfo) => { setFeeLoading(true) const address = transactionInfo.to - const amountToSend = MLHelpers.getAmountInAtoms( - transactionInfo.amount, - ).toString() + const amountToSend = MLHelpers.getAmountInAtoms(transactionInfo.amount) const unusedChangeAddress = await ML.getUnusedAddress(changeAddress) const utxos = await Mintlayer.getWalletUtxos(mlAddressList) const parsedUtxos = utxos .map((utxo) => JSON.parse(utxo)) .filter((utxo) => utxo.length > 0) - const fee = await MLTransaction.calculateFee( - parsedUtxos, - address, - unusedChangeAddress, - amountToSend, - networkType, - ) - const feeInCoins = MLHelpers.getAmountInCoins(fee) - setTotalFeeFiat(Format.fiatValue(feeInCoins * exchangeRate)) - setTotalFeeCrypto(feeInCoins) - setFeeLoading(false) - return feeInCoins + try { + const fee = await MLTransaction.calculateFee({ + utxosTotal: parsedUtxos, + address: address, + changeAddress: unusedChangeAddress, + amountToUse: amountToSend, + network: networkType, + }) + const feeInCoins = MLHelpers.getAmountInCoins(Number(fee)) + setTotalFeeFiat(Format.fiatValue(feeInCoins * exchangeRate)) + setTotalFeeCrypto(feeInCoins) + setFeeLoading(false) + return feeInCoins + } catch (e) { + console.error('Error calculating fee:', e) + goBackToWallet() + setFeeLoading(false) + } } const createTransaction = async (transactionInfo) => { @@ -151,7 +161,7 @@ const SendTransactionPage = () => { const confirmMlTransaction = async (password) => { const amountToSend = MLHelpers.getAmountInAtoms( transactionInformation.amount, - ).toString() + ) const { mlPrivKeys } = await Account.unlockAccount(accountID, password) const privKey = networkType === 'mainnet' @@ -173,14 +183,17 @@ const SendTransactionPage = () => { const parsedUtxos = utxos .map((utxo) => JSON.parse(utxo)) .filter((utxo) => utxo.length > 0) - const result = await MLTransaction.sendTransaction( - parsedUtxos, - keysList, - transactionInformation.to, - unusedChageAddress, - amountToSend, - networkType, - ) + const result = await MLTransaction.sendTransaction({ + utxosTotal: parsedUtxos, + keysList: keysList, + address: transactionInformation.to, + changeAddress: unusedChageAddress, + amountToUse: amountToSend, + network: networkType, + ...(adjustedFee && { + adjustedFee: MLHelpers.getAmountInAtoms(adjustedFee), + }), + }) return result } @@ -195,6 +208,7 @@ const SendTransactionPage = () => { totalFeeFiat={totalFeeFiat} totalFeeCrypto={totalFeeCrypto} setTotalFeeCrypto={setTotalFeeCrypto} + setAdjustedFee={setAdjustedFee} transactionData={transactionData} exchangeRate={exchangeRate} maxValueInToken={maxValueToken} diff --git a/src/pages/Staking/Staking.js b/src/pages/Staking/Staking.js index acfbcc8b..868ed725 100644 --- a/src/pages/Staking/Staking.js +++ b/src/pages/Staking/Staking.js @@ -14,6 +14,7 @@ import { ML } from '@Cryptos' import { Mintlayer } from '@APIs' import './Staking.css' +import { useEffectOnce } from '../../hooks/etc/useEffectOnce' const StakingPage = () => { const { state } = useLocation() @@ -50,7 +51,7 @@ const StakingPage = () => { const [transactionInformation, setTransactionInformation] = useState(null) const { exchangeRate } = useExchangeRates(tokenName, fiatName) - const { mlBalance } = useMlWalletInfo(currentMlAddresses) + const { mlBalance, getBalance } = useMlWalletInfo(currentMlAddresses) const delegationBalance = Format.BTCValue( MLHelpers.getAmountInCoins(currentDelegationInfo.balance), ) @@ -63,6 +64,10 @@ const StakingPage = () => { navigate('/wallet') } + useEffectOnce(()=>{ + getBalance() + }) + useEffect(() => { if (state && state.action === 'createDelegate') { setDelegationStep(2) @@ -96,7 +101,7 @@ const StakingPage = () => { const address = transactionInfo.to const amountToSend = MLHelpers.getAmountInAtoms( transactionInfo.amount, - ).toString() + ) const unusedChangeAddress = await ML.getUnusedAddress(changeAddresses) const unusedReceivingAddress = await ML.getUnusedAddress(receivingAddresses) const utxos = await Mintlayer.getWalletUtxos(mlAddressList) @@ -105,15 +110,13 @@ const StakingPage = () => { .filter((utxo) => utxo.length > 0) const fee = transactionMode === AppInfo.ML_TRANSACTION_MODES.STAKING - ? await MLTransaction.calculateFee( - parsedUtxos, - undefined, - unusedChangeAddress, - amountToSend, - networkType, - undefined, - address, - ) + ? await MLTransaction.calculateFee({ + utxosTotal: parsedUtxos, + changeAddress: unusedChangeAddress, + amountToUse: amountToSend, + network: networkType, + delegationId: address, + }) : transactionMode === AppInfo.ML_TRANSACTION_MODES.WITHDRAW ? await MLTransaction.calculateSpenDelegFee( address, @@ -121,15 +124,15 @@ const StakingPage = () => { networkType, currentDelegationInfo, ) - : await MLTransaction.calculateFee( - parsedUtxos, - unusedReceivingAddress, - unusedChangeAddress, - amountToSend, - networkType, - address, - ) - const feeInCoins = MLHelpers.getAmountInCoins(fee) + : await MLTransaction.calculateFee({ + utxosTotal: parsedUtxos, + address: unusedReceivingAddress, + changeAddress: unusedChangeAddress, + amountToUse: BigInt(0), + network: networkType, + poolId: address, + }) + const feeInCoins = MLHelpers.getAmountInCoins(Number(fee)) setTotalFeeFiat(Format.fiatValue(feeInCoins * exchangeRate)) setTotalFeeCrypto(feeInCoins) setFeeLoading(false) @@ -144,7 +147,7 @@ const StakingPage = () => { const confirmMlTransaction = async (password) => { const amountToSend = MLHelpers.getAmountInAtoms( transactionInformation.amount, - ).toString() + ) const { mlPrivKeys } = await Account.unlockAccount(accountID, password) const privKey = networkType === 'mainnet' @@ -170,16 +173,14 @@ const StakingPage = () => { const result = transactionMode === AppInfo.ML_TRANSACTION_MODES.STAKING - ? await MLTransaction.sendTransaction( - parsedUtxos, - keysList, - undefined, - unusedChageAddress, - amountToSend, - networkType, - undefined, - transactionInformation.to, - ) + ? await MLTransaction.sendTransaction({ + utxosTotal: parsedUtxos, + keysList: keysList, + changeAddress: unusedChageAddress, + amountToUse: amountToSend, + network: networkType, + delegationId: transactionInformation.to, + }) : transactionMode === AppInfo.ML_TRANSACTION_MODES.WITHDRAW ? await MLTransaction.spendFromDelegation( keysList, @@ -188,17 +189,16 @@ const StakingPage = () => { networkType, currentDelegationInfo, ) - : await MLTransaction.sendTransaction( - parsedUtxos, - keysList, - unusedReceivingAddress, - unusedChageAddress, - '0', - networkType, - transactionInformation.to, - undefined, - transactionMode, - ) + : await MLTransaction.sendTransaction({ + utxosTotal: parsedUtxos, + keysList: keysList, + address: unusedReceivingAddress, + changeAddress: unusedChageAddress, + amountToUse: BigInt('0'), + network: networkType, + poolId: transactionInformation.to, + transactionMode: transactionMode, + }) return result } diff --git a/src/pages/Wallet/Wallet.js b/src/pages/Wallet/Wallet.js index fed8e860..1096c9e5 100644 --- a/src/pages/Wallet/Wallet.js +++ b/src/pages/Wallet/Wallet.js @@ -12,6 +12,7 @@ import { AppInfo } from '@Constants' import { LocalStorageService } from '@Storage' import './Wallet.css' +import { useEffectOnce } from '../../hooks/etc/useEffectOnce' const WalletPage = () => { const navigate = useNavigate() @@ -29,8 +30,13 @@ const WalletPage = () => { : addresses.mlTestnetAddresses const [openShowAddress, setOpenShowAddress] = useState(false) const { btcTransactionsList, btcBalance } = useBtcWalletInfo(btcAddress) - const { mlTransactionsList, mlBalance, mlBalanceLocked } = - useMlWalletInfo(currentMlAddresses) + const { + mlTransactionsList, + mlBalance, + mlBalanceLocked, + getTransactions, + getBalance, + } = useMlWalletInfo(currentMlAddresses) const { exchangeRate: btcExchangeRate } = useExchangeRates('btc', 'usd') const { exchangeRate: mlExchangeRate } = useExchangeRates('ml', 'usd') @@ -64,6 +70,11 @@ const WalletPage = () => { LocalStorageService.getItem(unconfirmedTransactionString) && walletType.name === 'Mintlayer' + useEffectOnce(() => { + getTransactions() + getBalance() + }) + return (
diff --git a/src/services/API/Mintlayer/Mintlayer.js b/src/services/API/Mintlayer/Mintlayer.js index 5e12834c..162b8bf0 100644 --- a/src/services/API/Mintlayer/Mintlayer.js +++ b/src/services/API/Mintlayer/Mintlayer.js @@ -2,17 +2,19 @@ import { EnvVars } from '@Constants' import { LocalStorageService } from '@Storage' import { AppInfo } from '@Constants' -const prefix = '/api/v1' +const prefix = '/api/v2' const MINTLAYER_ENDPOINTS = { GET_ADDRESS_DATA: '/address/:address', GET_TRANSACTION_DATA: '/transaction/:txid', - GET_ADDRESS_UTXO: '/address/:address/available-utxos', + GET_ADDRESS_UTXO: '/address/:address/spendable-utxos', POST_TRANSACTION: '/transaction', GET_FEES_ESTIMATES: '/feerate', GET_ADDRESS_DELEGATIONS: '/address/:address/delegations', GET_DELEGATION: '/delegation/:delegation', GET_CHAIN_TIP: '/chain/tip', + GET_BLOCK_HASH: '/chain/:height', + GET_BLOCK_DATA: '/block/:hash', } const requestMintlayer = async (url, body = null, request = fetch) => { @@ -28,6 +30,20 @@ const requestMintlayer = async (url, body = null, request = fetch) => { ) } + // handle RPC error + if ( + error.error.includes( + 'Mempool error: Transaction does not pay sufficient fees to be relayed', + ) + ) { + const errorMessage = error.error + .split('Mempool error: ')[1] + .split(')')[0] + .replace('(tx_fee:', '. estimated fee') + .replace('min_relay_fee:', 'minimum fee') + throw new Error(errorMessage) + } + // handle RPC error if (error.error.includes('Mempool error:')) { const errorMessage = error.error @@ -83,10 +99,10 @@ const getAddressBalance = async (address) => { const response = await getAddressData(address) const data = JSON.parse(response) const balance = { - balanceInAtoms: data.coin_balance, + balanceInAtoms: data.coin_balance.atoms, } const balanceLocked = { - balanceInAtoms: data.locked_coin_balance || 0, + balanceInAtoms: data.locked_coin_balance.atoms || 0, } return { balance, balanceLocked } } catch (error) { @@ -192,6 +208,18 @@ const getDelegation = (delegation) => MINTLAYER_ENDPOINTS.GET_DELEGATION.replace(':delegation', delegation), ) +const getBlockDataByHeight = (height) => { + return tryServers( + MINTLAYER_ENDPOINTS.GET_BLOCK_HASH.replace(':height', height), + ) + .then(JSON.parse) + .then((response) => { + return tryServers( + MINTLAYER_ENDPOINTS.GET_BLOCK_DATA.replace(':hash', response), + ) + }) +} + const getWalletDelegations = (addresses) => { const delegationsPromises = addresses.map((address) => getAddressDelegations(address), @@ -208,6 +236,12 @@ const getDelegationDetails = (delegations) => { results.flatMap(JSON.parse), ) } +const getBlocksData = (heights) => { + const heightsPromises = heights.map((height) => getBlockDataByHeight(height)) + return Promise.all(heightsPromises).then((results) => + results.flatMap(JSON.parse), + ) +} const getChainTip = async () => { return tryServers(MINTLAYER_ENDPOINTS.GET_CHAIN_TIP) @@ -237,5 +271,6 @@ export { getChainTip, broadcastTransaction, getFeesEstimates, + getBlocksData, MINTLAYER_ENDPOINTS, } diff --git a/src/services/Crypto/Mintlayer/Mintlayer.js b/src/services/Crypto/Mintlayer/Mintlayer.js index fbac8687..45359211 100644 --- a/src/services/Crypto/Mintlayer/Mintlayer.js +++ b/src/services/Crypto/Mintlayer/Mintlayer.js @@ -14,7 +14,6 @@ import init, { estimate_transaction_size, encode_lock_until_time, encode_output_lock_then_transfer, - encode_lock_until_height, encode_lock_for_block_count, encode_output_create_delegation, encode_output_delegate_staking, @@ -173,6 +172,7 @@ export const getOutputs = async ({ if (type === 'LockThenTransfer' && !lock) { throw new Error('LockThenTransfer requires a lock') } + const amountInstace = Amount.from_atoms(amount) const networkIndex = NETWORKS[networkType] @@ -185,7 +185,7 @@ export const getOutputs = async ({ lockEncoded = encode_lock_until_time(BigInt(lock.UntilTime.timestamp)) } if (lock.ForBlockCount) { - lockEncoded = encode_lock_until_height(BigInt(lock.ForBlockCount)) + lockEncoded = encode_lock_for_block_count(BigInt(lock.ForBlockCount)) } return encode_output_lock_then_transfer( amountInstace, diff --git a/src/services/Database/IndexedDB/IndexedDB.js b/src/services/Database/IndexedDB/IndexedDB.js index 3d861764..6af94f9e 100644 --- a/src/services/Database/IndexedDB/IndexedDB.js +++ b/src/services/Database/IndexedDB/IndexedDB.js @@ -68,7 +68,6 @@ const saveAccounts = async (accounts, onError, DB = IDB) => { for (const account of accounts) { await update(oldAccounts, account) } - console.log('Accounts saved successfully') db.close() } catch (error) { diff --git a/src/utils/Constants/AppInfo/AppInfo.js b/src/utils/Constants/AppInfo/AppInfo.js index 2cdbc046..1601cdef 100644 --- a/src/utils/Constants/AppInfo/AppInfo.js +++ b/src/utils/Constants/AppInfo/AppInfo.js @@ -6,9 +6,9 @@ const appAccounts = async () => { return accounts } -const decimalSeparator = ',' -const thousandsSeparator = '.' -const amountRegex = /^\d+(,\d+)?$/ +const decimalSeparator = '.' +const thousandsSeparator = ' ' +const amountRegex = /^\d+(.\d+)?$/ const minEntropyLength = 192 const DEFAULT_WALLETS_TO_CREATE = ['btc'] const ML_ATOMS_PER_COIN = 100000000000 diff --git a/src/utils/Helpers/ML/ML.js b/src/utils/Helpers/ML/ML.js index 8d19a1ef..c3e0b2b1 100644 --- a/src/utils/Helpers/ML/ML.js +++ b/src/utils/Helpers/ML/ML.js @@ -7,7 +7,7 @@ const getAmountInCoins = (amointInAtoms) => { } const getAmountInAtoms = (amountInCoins) => { - return Math.round(amountInCoins * AppInfo.ML_ATOMS_PER_COIN) + return BigInt(Math.round(amountInCoins * AppInfo.ML_ATOMS_PER_COIN)) } const getParsedTransactions = (transactions, addresses) => { @@ -90,38 +90,38 @@ const getParsedTransactions = (transactions, addresses) => { const totalValue = transaction.outputs.reduce((acc, output) => { if (!addresses.includes(output.destination)) { if (output.type === 'Transfer') { - return acc + output.value.amount + return acc + output.value.amount.decimal } if (output.type === 'LockThenTransfer') { - return acc + Number(output.value.amount) + return acc + Number(output.value.amount.decimal) } if (output.type === 'CreateStakePool') { type = 'CreateStakePool' destAddress = output.pool_id - return acc + Number(output.data.amount) + return acc + Number(output.data.amount.decimal) } if (output.type === 'DelegateStaking') { type = 'DelegateStaking' destAddress = output.delegation_id - return acc + Number(output.amount) + return acc + Number(output.amount.decimal) } if (output.type === 'CreateDelegationId') { type = 'CreateDelegationId' destAddress = output.pool_id sameWalletTransaction = false - return acc + Number(output.amount) + return acc + Number(output.amount.decimal) } } if (addresses.includes(output.destination)) { if (output.type === 'CreateStakePool') { type = 'CreateStakePool' destAddress = output.pool_id - return acc + Number(output.data.amount) + return acc + Number(output.data.amount.decimal) } if (output.type === 'DelegateStaking') { type = 'DelegateStaking' destAddress = output.delegation_id - return acc + Number(output.amount) + return acc + Number(output.amount.decimal) } if (output.type === 'CreateDelegationId') { type = 'CreateDelegationId' @@ -132,7 +132,7 @@ const getParsedTransactions = (transactions, addresses) => { } return acc }, 0) - value = getAmountInCoins(totalValue, AppInfo.ML_ATOMS_PER_COIN) + value = totalValue } if (withInputUTXO && direction === 'in' && transaction.outputs.length > 0) { @@ -140,15 +140,15 @@ const getParsedTransactions = (transactions, addresses) => { const totalValue = transaction.outputs.reduce((acc, output) => { if (addresses.includes(output.destination)) { if (output.type === 'Transfer') { - return acc + output.value.amount + return acc + output.value.amount.decimal } if (output.type === 'LockThenTransfer') { - return acc + Number(output.value.amount) + return acc + Number(output.value.amount.decimal) } } return acc }, 0) - value = getAmountInCoins(totalValue, AppInfo.ML_ATOMS_PER_COIN) + value = totalValue } if ( @@ -163,31 +163,28 @@ const getParsedTransactions = (transactions, addresses) => { const totalValue = transaction.outputs.reduce((acc, output) => { if (addresses.includes(output.destination)) { if (output.type === 'Transfer') { - return acc + output.value.amount + return acc + output.value.amount.decimal } if (output.type === 'LockThenTransfer') { if ( - transaction.inputs[0].input?.Account?.account - ?.DelegationBalance[0] + transaction.inputs[0].input?.account_type === 'DelegationBalance' ) { type = 'Delegate Withdrawal' - destAddress = - transaction.inputs[0].input?.Account?.account - ?.DelegationBalance[0] + destAddress = transaction.inputs[0].input?.delegation_id } - return acc + Number(output.value.amount) + return acc + Number(output.value.amount.decimal) } } return acc }, 0) - value = getAmountInCoins(totalValue, AppInfo.ML_ATOMS_PER_COIN) + value = totalValue } const confirmations = transaction.confirmations const date = transaction.timestamp const txid = transaction.txid - const fee = transaction.fee + const fee = transaction.fee.decimal const isConfirmed = confirmations > 0 return { diff --git a/src/utils/Helpers/ML/MLTransaction.js b/src/utils/Helpers/ML/MLTransaction.js index edede9ea..919bf46a 100644 --- a/src/utils/Helpers/ML/MLTransaction.js +++ b/src/utils/Helpers/ML/MLTransaction.js @@ -6,7 +6,10 @@ import { ML as MLHelpers } from '@Helpers' import { AppInfo } from '@Constants' const getUtxoBalance = (utxo) => { - return utxo.reduce((sum, item) => sum + Number(item.utxo.value.amount), 0) + return utxo.reduce( + (sum, item) => sum + BigInt(item.utxo.value.amount.atoms), + BigInt(0), + ) } const getUtxoAvailable = (utxo) => { @@ -23,7 +26,7 @@ const getUtxoAvailable = (utxo) => { const getUtxoTransaction = (utxo) => { return utxo.map((item) => ({ - transaction: item.outpoint.id.Transaction, + transaction: item.outpoint.source_id, index: item.outpoint.index, })) } @@ -56,19 +59,40 @@ const getTxInput = async (outpointSourceId) => { ) } -const getTransactionUtxos = (utxos, amountToUse, fee = 0) => { - let balance = 0 +/** + * Get utxos to spend + * NOTE: This function require optimization to get UTXOs with the lowest amounts first or 50% lowest and 50% highest, see: https://arxiv.org/pdf/2311.01113.pdf + * At this point there is a risk of not having enough UTXOs to spend because first picked UTXOs is equal to the amount to spend without fee + * In that case backend will return error with proper fee amount wich is parsed and passed as override fee value. + * Need to add some "backup" additional UTXO is AMOUNT is equal of UTXOs amount so that server error less likely to happen but I'm leaving it just to be sure + * @param utxos + * @param amountToUse + * @param fee + * @returns {*[]} + */ +const getTransactionUtxos = (utxos, amountToUse, fee = BigInt(0)) => { + let balance = BigInt(0) const utxosToSpend = [] + let lastIndex = 0 for (let i = 0; i < utxos.length; i++) { + lastIndex = i const utxoBalance = getUtxoBalance(utxos[i]) - if (balance < Number(amountToUse) + fee) { + if (balance < BigInt(amountToUse) + fee) { balance += utxoBalance utxosToSpend.push(utxos[i]) } else { break } } + + if (balance === BigInt(amountToUse)) { + // pick up extra UTXO + if (utxos[lastIndex + 1]) { + utxosToSpend.push(utxos[lastIndex + 1]) + } + } + return utxosToSpend } @@ -130,7 +154,7 @@ const getOptUtxos = async (utxos, network) => { const opt_utxos = await Promise.all( utxos.map((item) => { return ML.getOutputs({ - amount: item.utxo.value.amount, + amount: item.utxo.value.amount.atoms, address: item.utxo.destination, networkType: network, type: item.utxo.type, @@ -188,9 +212,11 @@ const totalUtxosAmount = (utxosToSpend) => { return utxosToSpend .flatMap((utxo) => [...utxo]) .reduce((acc, utxo) => { - const amount = utxo.utxo.value ? Number(utxo.utxo.value.amount) : 0 + const amount = utxo?.utxo?.value?.amount + ? BigInt(utxo.utxo.value.amount.atoms) + : 0 return acc + amount - }, 0) + }, BigInt(0)) } const getUtxoAddress = (utxosToSpend) => { @@ -199,7 +225,7 @@ const getUtxoAddress = (utxosToSpend) => { .map((utxo) => utxo.utxo.destination) } -const calculateFee = async ( +const calculateFee = async ({ utxosTotal, address, changeAddress, @@ -207,11 +233,11 @@ const calculateFee = async ( network, poolId, delegationId, -) => { - const amountToUseFinale = Number(amountToUse) <= 0 ? 1 : amountToUse +}) => { + const amountToUseFinale = amountToUse <= 0 ? BigInt(1) : amountToUse const utxos = getUtxoAvailable(utxosTotal) const totalAmount = !poolId ? totalUtxosAmount(utxos) : 0 - if (totalAmount < Number(amountToUse) && !poolId) { + if (totalAmount < BigInt(amountToUse) && !poolId) { throw new Error('Insufficient funds') } const requireUtxo = getTransactionUtxos(utxos, amountToUseFinale) @@ -229,7 +255,7 @@ const calculateFee = async ( delegationId, ) const changeAmount = ( - totalUtxosAmount(requireUtxo) - Number(amountToUseFinale) + totalUtxosAmount(requireUtxo) - amountToUseFinale ).toString() const txChangeOutput = await getTxOutput(changeAmount, changeAddress, network) const outputs = [...txOutput, ...txChangeOutput] @@ -244,7 +270,7 @@ const calculateFee = async ( const feeEstimates = JSON.parse(feeEstimatesResponse) const fee = Math.ceil((Number(feeEstimates) / 1000) * size) - return fee + return BigInt(fee) } const calculateSpenDelegFee = async (address, amount, network, delegation) => { @@ -277,7 +303,7 @@ const calculateSpenDelegFee = async (address, amount, network, delegation) => { return fee } -const sendTransaction = async ( +const sendTransaction = async ({ utxosTotal, keysList, address, @@ -287,25 +313,29 @@ const sendTransaction = async ( poolId, delegationId, transactionMode, -) => { + adjustedFee, +}) => { const utxos = getUtxoAvailable(utxosTotal) const totalAmount = totalUtxosAmount(utxos) - const fee = await calculateFee( - utxos, - address, - changeAddress, - amountToUse, - network, - poolId, - delegationId, - ) - if (fee > AppInfo.MAX_ML_FEE) { + const fee = + adjustedFee || + (await calculateFee({ + utxosTotal: utxos, + address, + changeAddress, + amountToUse, + network, + poolId, + delegationId, + })) + + if (fee > BigInt(AppInfo.MAX_ML_FEE)) { throw new Error('Fee is too high, please try again later.') } let amount = amountToUse - if (totalAmount < Number(amountToUse) + fee) { + if (totalAmount < amountToUse + fee) { amount = totalAmount - fee } @@ -322,11 +352,8 @@ const sendTransaction = async ( poolId, delegationId, ) - const changeAmount = ( - totalUtxosAmount(requireUtxo) - - Number(amount) - - fee - ).toString() + + const changeAmount = (totalUtxosAmount(requireUtxo) - amount - fee).toString() const txChangeOutput = await getTxOutput(changeAmount, changeAddress, network) const outputs = [...txOutput, ...txChangeOutput] const optUtxos = await getOptUtxos(requireUtxo.flat(), network) @@ -364,11 +391,11 @@ const sendTransaction = async ( direction: 'out', type: 'Unconfirmed', destAddress: address || delegationId, - value: MLHelpers.getAmountInCoins(amount), + value: MLHelpers.getAmountInCoins(Number(amount)), confirmations: 0, date: '', txid: JSON.parse(result).tx_id, - fee: fee, + fee: fee.toString(), isConfirmed: false, mode: transactionMode, poolId: poolId, @@ -448,7 +475,7 @@ const spendFromDelegation = async ( direction: 'out', type: 'Unconfirmed', destAddress: address, - value: MLHelpers.getAmountInCoins(amount), + value: MLHelpers.getAmountInCoins(Number(amount)), confirmations: 0, date: '', txid: JSON.parse(result).tx_id, diff --git a/src/utils/Helpers/ML/MLTransaction.test.js b/src/utils/Helpers/ML/MLTransaction.test.js index 8b1fe23f..5ebd133c 100644 --- a/src/utils/Helpers/ML/MLTransaction.test.js +++ b/src/utils/Helpers/ML/MLTransaction.test.js @@ -8,51 +8,60 @@ import { const UTXSOS_MOCK = [ { outpoint: { - id: { - Transaction: - '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', - }, - index: 1, + source_id: + '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', + source_type: 'Transaction', + input_type: 'UTXO', + index: 0, }, utxo: { destination: 'tmt1qylgafccyyy26zrtqk8gjvcwzut26taruuyzmcr6', type: 'Transfer', value: { - amount: '200', + amount: { + atoms: '200', + decimals: '0.000000002', + }, type: 'Coin', }, }, }, { outpoint: { - id: { - Transaction: - '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', - }, + source_id: + '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', + source_type: 'Transaction', + input_type: 'UTXO', index: 1, }, utxo: { destination: 'tmt1qylgafccyyy26zrtqk8gjvcwzut26taruuyzmcr6', type: 'Transfer', value: { - amount: '200', + amount: { + atoms: '200', + decimals: '0.000000002', + }, type: 'Coin', }, }, }, { outpoint: { - id: { - Transaction: - '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', - }, - index: 1, + source_id: + '0cd5de5319de96f7c967a24a51224f4eab7882e6dbe336ab6837b15e1b68dace', + source_type: 'Transaction', + input_type: 'UTXO', + index: 2, }, utxo: { destination: 'tmt1qylgafccyyy26zrtqk8gjvcwzut26taruuyzmcr6', type: 'Transfer', value: { - amount: '200', + amount: { + atoms: '200', + decimals: '0.000000002', + }, type: 'Coin', }, }, diff --git a/src/utils/Helpers/Number/Format.js b/src/utils/Helpers/Number/Format.js index 6110378c..945c215a 100644 --- a/src/utils/Helpers/Number/Format.js +++ b/src/utils/Helpers/Number/Format.js @@ -5,14 +5,6 @@ import { getDecimalNumber } from './Number' const getNumber = (value) => typeof value === 'number' ? value : NumbersHelper.floatStringToNumber(value) -//TODO fix the value -// const BTCValue = (value) => -// getNumber(value) -// .toFixed(8) -// .replace(/\.0+$/, '') -// .replace(/(\.0{0,}[1-9]+)(0+)$/, '$1') -// .replace('.', AppInfo.decimalSeparator) - const BTCValue = (value) => { let str = getNumber(value).toString() const decimalIndex = str.indexOf('.')