diff --git a/package-lock.json b/package-lock.json index 339c4e03..689b6a62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "browser-extension", - "version": "1.2.8", + "version": "1.2.9", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index d31a95a0..17f2f270 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser-extension", - "version": "1.2.8", + "version": "1.2.9", "private": true, "dependencies": { "@mintlayer/entropy-generator": "^1.0.4", diff --git a/public/manifestDefault.json b/public/manifestDefault.json index 2e299086..3f2df829 100644 --- a/public/manifestDefault.json +++ b/public/manifestDefault.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Mojito - A Mintlayer Wallet", - "version": "1.2.8", + "version": "1.2.9", "short_name": "Mojito", "description": "Mojito is a non-custodial decentralized crypto wallet that lets you send and receive BTC and ML from any other address.", "homepage_url": "https://www.mintlayer.org/", diff --git a/public/manifestFirefox.json b/public/manifestFirefox.json index 8d44be4d..78996593 100644 --- a/public/manifestFirefox.json +++ b/public/manifestFirefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Mojito - A Mintlayer Wallet", - "version": "1.2.8", + "version": "1.2.9", "description": "Mojito is a non-custodial decentralized crypto wallet that lets you send and receive BTC and ML from any other address.", "homepage_url": "https://www.mintlayer.org/", "icons": { diff --git a/src/components/composed/Balance/Balance.css b/src/components/composed/Balance/Balance.css index 841f3c85..f5fa85e4 100644 --- a/src/components/composed/Balance/Balance.css +++ b/src/components/composed/Balance/Balance.css @@ -29,7 +29,7 @@ .balance-btc span { font-weight: 600; - font-size: 1.9rem; + font-size: 1.8rem; } .balance-usd { @@ -47,6 +47,7 @@ bottom: -25px; opacity: 0.2; white-space: nowrap; + cursor: pointer; } .balance-locked:hover { diff --git a/src/components/composed/Balance/Balance.js b/src/components/composed/Balance/Balance.js index 77a86d91..e4a39cd2 100644 --- a/src/components/composed/Balance/Balance.js +++ b/src/components/composed/Balance/Balance.js @@ -1,4 +1,5 @@ import React, { useContext } from 'react' +import { useNavigate, useParams } from 'react-router-dom' import { ReactComponent as BtcLogo } from '@Assets/images/btc-logo.svg' import { LogoRound } from '@BasicComponents' @@ -12,6 +13,8 @@ import TokenLogoRound from '../../basic/TokenLogoRound/TokenLogoRound' const Balance = ({ balance, balanceLocked, exchangeRate, walletType }) => { const { networkType } = useContext(SettingsContext) const { tokenBalances } = useContext(NetworkContext) + const { coinType } = useParams() + const navigate = useNavigate() // TODO Consider the correct format for 0,00 that might also be 0.00 const balanceInUSD = networkType === AppInfo.NETWORK_TYPES.TESTNET @@ -44,6 +47,10 @@ const Balance = ({ balance, balanceLocked, exchangeRate, walletType }) => { ) } + const onLockedClick = () => { + navigate('/wallet/' + coinType + '/locked-balance') + } + return (
{ {Format.fiatValue(balanceInUSD)} USD

{parseFloat(balanceLocked) > 0 ? ( -
+
Locked: {balanceLocked} {symbol()}
) : ( diff --git a/src/components/composed/LockedBalanceList/LockedBalanceList.css b/src/components/composed/LockedBalanceList/LockedBalanceList.css new file mode 100644 index 00000000..4c4f6d26 --- /dev/null +++ b/src/components/composed/LockedBalanceList/LockedBalanceList.css @@ -0,0 +1,26 @@ +.locked-table { + width: 100%; +} + +.locked-table-wrapper { + display: flex; + justify-content: center; + align-items: flex-start; + height: 465px; + overflow: auto; +} + +.locked-title { + text-align: left; + padding: 10px; + background: rgb(var(--color-green)); + width: 50%; + color: rgb(var(--color-white)); +} + +.locked-loading-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: 465px; +} diff --git a/src/components/composed/LockedBalanceList/LockedBalanceList.js b/src/components/composed/LockedBalanceList/LockedBalanceList.js new file mode 100644 index 00000000..c00b11ec --- /dev/null +++ b/src/components/composed/LockedBalanceList/LockedBalanceList.js @@ -0,0 +1,122 @@ +import React, { useContext, useState, useEffect, useCallback } from 'react' +import { NetworkContext } from '@Contexts' + +import { Loading } from '@ComposedComponents' +import { Mintlayer } from '@APIs' +import { addDays } from 'date-fns' +import LockedBalanceListItem from './LockedBalanceListItem' +import './LockedBalanceList.css' + +const LockedBalanceList = () => { + const { lockedUtxos, transactions, fetchingUtxos } = + useContext(NetworkContext) + const [loading, setLoading] = useState(false) + const [updatedUtxosList, setUpdatedUtxosList] = useState([]) + + const getBlockData = async (blockId) => { + try { + const blockData = await Mintlayer.getBlockDataByHash(blockId) + const parsedData = JSON.parse(blockData) + if (parsedData && parsedData.height) { + return parsedData.height + } + } catch (error) { + console.error('Failed to fetch block data:', error) + } + } + + const getUpdatedUtxosWithDate = useCallback( + async (utxos) => { + setLoading(true) + const updatedUtxos = await Promise.all( + utxos.map(async (utxo) => { + if (utxo.utxo.lock.type === 'ForBlockCount') { + const initialTransaction = transactions.find( + (tx) => tx.txid === utxo.outpoint.source_id, + ) + if (initialTransaction && !utxo.utxo.lock.content.unlockBlock) { + // Calculating the unlock date + const calculatedUnlockTimestamp = + addDays( + new Date(initialTransaction.date * 1000), + 10, + ).getTime() / 1000 + + const blockData = await getBlockData(initialTransaction.blockId) + + utxo.utxo.lock.content = { + lockedFor: utxo.utxo.lock.content, + timestamp: calculatedUnlockTimestamp, + unlockBlock: blockData + utxo.utxo.lock.content, + } + } + } + return utxo + }), + ) + setLoading(false) + return updatedUtxos + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [lockedUtxos, transactions], + ) + + useEffect(() => { + const fetchUpdatedUtxos = async () => { + const updatedUtxos = await getUpdatedUtxosWithDate(lockedUtxos) + const sortedUtxos = updatedUtxos.sort( + (a, b) => a.utxo.lock.content.timestamp - b.utxo.lock.content.timestamp, + ) + setUpdatedUtxosList(sortedUtxos) + } + + fetchUpdatedUtxos() + }, [lockedUtxos, transactions, getUpdatedUtxosWithDate]) + + return ( + <> +
+ {(fetchingUtxos || loading) ? ( +
+ +
+ ) : ( + + + + + + + + + {lockedUtxos && + updatedUtxosList.map((utxo, index) => ( + + ))} + +
+ Date + + Amount +
+ )} +
+ + ) +} + +export default LockedBalanceList diff --git a/src/components/composed/LockedBalanceList/LockedBalanceListItem.css b/src/components/composed/LockedBalanceList/LockedBalanceListItem.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/composed/LockedBalanceList/LockedBalanceListItem.js b/src/components/composed/LockedBalanceList/LockedBalanceListItem.js new file mode 100644 index 00000000..0d7dbe3c --- /dev/null +++ b/src/components/composed/LockedBalanceList/LockedBalanceListItem.js @@ -0,0 +1,34 @@ +import { useMemo } from 'react' +import { format } from 'date-fns' + +import './LockedBalanceListItem.css' + +const LockedBalanceListItem = ({ index, utxo }) => { + + const displayDate = useMemo(() => { + if (utxo.utxo.lock.type === 'ForBlockCount') { + return `~ ${format( + new Date(utxo.utxo.lock.content.timestamp * 1000), + 'dd/MM/yyyy HH:mm', + )} (Block height: ${utxo.utxo.lock.content.unlockBlock})` + } else if (utxo.utxo.lock.type !== 'ForBlockCount') { + return format( + new Date(utxo.utxo.lock.content.timestamp * 1000), + 'dd/MM/yyyy HH:mm', + ) + } + }, [utxo]) + + return ( + + {displayDate} + {utxo.utxo.value.amount.decimal} ML + + ) +} + +export default LockedBalanceListItem diff --git a/src/components/composed/index.js b/src/components/composed/index.js index 2e2144bc..d2224828 100644 --- a/src/components/composed/index.js +++ b/src/components/composed/index.js @@ -20,6 +20,7 @@ import CurrentStaking from './CurrentStaking/CurrentStaking' import HelpTooltip from './HelpTooltip/HelpTooltip' import RestoreSeedField from './RestoreSeedField/RestoreSeedField' import UpdateButton from './UpdateButton/UpdateButton' +import LockedBalanceList from './LockedBalanceList/LockedBalanceList' export { Balance, @@ -44,4 +45,5 @@ export { HelpTooltip, RestoreSeedField, UpdateButton, + LockedBalanceList, } diff --git a/src/contexts/NetworkProvider/NetworkProvider.js b/src/contexts/NetworkProvider/NetworkProvider.js index 8e60cafa..8a441cf9 100644 --- a/src/contexts/NetworkProvider/NetworkProvider.js +++ b/src/contexts/NetworkProvider/NetworkProvider.js @@ -1,9 +1,5 @@ import React, { createContext, useContext, useEffect, useState } from 'react' -import { - getAddressData, - getChainTip, - getTransactionData, -} from '../../services/API/Mintlayer/Mintlayer' + import { AccountContext, SettingsContext } from '@Contexts' import { AppInfo } from '@Constants' import { ML_ATOMS_PER_COIN } from '../../utils/Constants/AppInfo/AppInfo' @@ -33,6 +29,7 @@ const NetworkProvider = ({ value: propValue, children }) => { const [lockedBalance, setLockedBalance] = useState(0) const [unusedAddresses, setUnusedAddresses] = useState({}) const [utxos, setUtxos] = useState([]) + const [lockedUtxos, setLockedUtxos] = useState([]) const [transactions, setTransactions] = useState([]) const [feerate, setFeerate] = useState(0) @@ -84,12 +81,12 @@ const NetworkProvider = ({ value: propValue, children }) => { const addresses_data_receive = await Promise.all( currentMlAddresses.mlReceivingAddresses.map((address) => - getAddressData(address), + Mintlayer.getAddressData(address), ), ) const addresses_data_change = await Promise.all( currentMlAddresses.mlChangeAddresses.map((address) => - getAddressData(address), + Mintlayer.getAddressData(address), ), ) const addresses_data = [...addresses_data_receive, ...addresses_data_change] @@ -143,7 +140,9 @@ const NetworkProvider = ({ value: propValue, children }) => { setCurrentAccountId(accountID) // fetch transactions data - const transactions = transaction_ids.map((txid) => getTransactionData(txid)) + const transactions = transaction_ids.map((txid) => + Mintlayer.getTransactionData(txid), + ) const transactions_data = await Promise.all(transactions) const parsedTransactions = ML.getParsedTransactions( @@ -160,11 +159,19 @@ const NetworkProvider = ({ value: propValue, children }) => { const unconfirmedTransactions = LocalStorageService.getItem(unconfirmedTransactionString) || [] - const utxos = await Mintlayer.getWalletUtxos(addressList) - const parsedUtxos = utxos + const fetchedUtxos = await Mintlayer.getWalletUtxos(addressList) + const fetchedSpendableUtxos = + await Mintlayer.getWalletSpendableUtxos(addressList) + + const parsedUtxos = fetchedUtxos + .map((utxo) => JSON.parse(utxo)) + .filter((utxo) => utxo.length > 0) + + const parsedSpendableUtxos = fetchedSpendableUtxos .map((utxo) => JSON.parse(utxo)) .filter((utxo) => utxo.length > 0) - const available = parsedUtxos + + const available = parsedSpendableUtxos .flatMap((utxo) => [...utxo]) .filter((item) => item.utxo.value) .filter((item) => { @@ -189,7 +196,12 @@ const NetworkProvider = ({ value: propValue, children }) => { setCurrentNetworkType(networkType) const availableUtxos = available.map((item) => item) + const lockedUtxos = parsedUtxos + .flat() + .filter((obj) => obj.utxo.type === 'LockThenTransfer') + setUtxos(availableUtxos) + setLockedUtxos(lockedUtxos) setFetchingUtxos(false) @@ -305,7 +317,7 @@ const NetworkProvider = ({ value: propValue, children }) => { useEffect(() => { const getData = async () => { - const result = await getChainTip() + const result = await Mintlayer.getChainTip() const { block_height } = JSON.parse(result) setOnlineHeight(block_height) } @@ -321,6 +333,7 @@ const NetworkProvider = ({ value: propValue, children }) => { tokenBalances, // addresses, utxos, + lockedUtxos, transactions, currentHeight, onlineHeight, diff --git a/src/index.js b/src/index.js index 4e14d804..f2d0ab40 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ import { CreateDelegationPage, DelegationStakePage, DelegationWithdrawPage, + LockedBalancePage, } from '@Pages' import { @@ -232,6 +233,10 @@ const App = () => { path="/wallet/:coinType/staking/create-delegation" element={} /> + } + /> { className="footnote-version" data-testid="footnote-name" > - v1.2.8 + v1.2.9
diff --git a/src/pages/LockedBalance/LockedBalance.css b/src/pages/LockedBalance/LockedBalance.css new file mode 100644 index 00000000..07c522a8 --- /dev/null +++ b/src/pages/LockedBalance/LockedBalance.css @@ -0,0 +1,5 @@ +.animate-list-accounts { + overflow: hidden; + transform: translateX(-200%); + transition: transform 1s ease-in-out; +} diff --git a/src/pages/LockedBalance/LockedBalance.js b/src/pages/LockedBalance/LockedBalance.js new file mode 100644 index 00000000..f5b1bd73 --- /dev/null +++ b/src/pages/LockedBalance/LockedBalance.js @@ -0,0 +1,14 @@ +import { LockedBalanceList } from '@ComposedComponents' + +import './LockedBalance.css' + +const LockedBalancePage = () => { + + return ( + <> + + + ) +} + +export default LockedBalancePage diff --git a/src/pages/index.js b/src/pages/index.js index c23026a4..c17b8dd7 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -13,6 +13,7 @@ import ConnectionPage from './ConnectionPage/ConnectionPage' import CreateDelegationPage from './CreateDelegation/CreateDelegation' import DelegationStakePage from './DelegationStake/DelegationStake' import DelegationWithdrawPage from './DelegationWithdraw/DelegationWithdraw' +import LockedBalancePage from './LockedBalance/LockedBalance' export { CreateAccountPage, @@ -30,4 +31,5 @@ export { CreateDelegationPage, DelegationStakePage, DelegationWithdrawPage, + LockedBalancePage } diff --git a/src/services/API/Mintlayer/Mintlayer.js b/src/services/API/Mintlayer/Mintlayer.js index 21d800c4..f69b44c9 100644 --- a/src/services/API/Mintlayer/Mintlayer.js +++ b/src/services/API/Mintlayer/Mintlayer.js @@ -7,7 +7,8 @@ const prefix = '/api/v2' const MINTLAYER_ENDPOINTS = { GET_ADDRESS_DATA: '/address/:address', GET_TRANSACTION_DATA: '/transaction/:txid', - GET_ADDRESS_UTXO: '/address/:address/spendable-utxos', + GET_ADDRESS_UTXO: '/address/:address/all-utxos', + GET_ADDRESS_SPENDABLE_UTXO: '/address/:address/spendable-utxos', POST_TRANSACTION: '/transaction', GET_FEES_ESTIMATES: '/feerate', GET_ADDRESS_DELEGATIONS: '/address/:address/delegations', @@ -211,6 +212,18 @@ const getWalletUtxos = (addresses) => { return Promise.all(utxosPromises) } +const getAddressSpendableUtxo = (address) => + tryServers( + MINTLAYER_ENDPOINTS.GET_ADDRESS_SPENDABLE_UTXO.replace(':address', address), + ) + +const getWalletSpendableUtxos = (addresses) => { + const utxosPromises = addresses.map((address) => + getAddressSpendableUtxo(address), + ) + return Promise.all(utxosPromises) +} + const getTokensData = async (tokens) => { const tokensData = {} tokens.forEach((token) => { @@ -252,6 +265,10 @@ const getBlockDataByHeight = (height) => { }) } +const getBlockDataByHash = (hash) => { + return tryServers(MINTLAYER_ENDPOINTS.GET_BLOCK_DATA.replace(':hash', hash)) +} + const getWalletDelegations = (addresses) => { const delegationsPromises = addresses.map((address) => getAddressDelegations(address), @@ -304,6 +321,8 @@ export { requestMintlayer, getAddressUtxo, getWalletUtxos, + getAddressSpendableUtxo, + getWalletSpendableUtxos, getAddressDelegations, getWalletDelegations, getDelegationDetails, @@ -311,6 +330,7 @@ export { broadcastTransaction, getFeesEstimates, getBlocksData, + getBlockDataByHash, getTokensData, getPoolsData, MINTLAYER_ENDPOINTS, diff --git a/src/utils/Helpers/ML/ML.js b/src/utils/Helpers/ML/ML.js index cc3f4e73..fb7067a8 100644 --- a/src/utils/Helpers/ML/ML.js +++ b/src/utils/Helpers/ML/ML.js @@ -221,8 +221,10 @@ const getParsedTransactions = (transactions, addresses) => { const txid = transaction.txid const fee = transaction.fee.decimal const isConfirmed = confirmations > 0 + const blockId = transaction.block_id return { + blockId, direction, destAddress, value: value || 0,