From 0f8d3d273ec0ed4aac2dee8dba1f79d9e8e2b216 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Wed, 19 Jun 2024 13:14:12 +0300 Subject: [PATCH 01/21] token metadata batch fetching fix --- .../app/components/swap/SelectAssetDialog.js | 9 ++++++--- .../app/containers/swap/asset-swap/CreateSwapOrder.js | 6 ++++-- .../app/containers/swap/asset-swap/SwapPage.js | 5 ++++- .../asset-swap/edit-buy-amount/SelectBuyTokenFromList.js | 6 +++--- .../edit-sell-amount/SelectSellTokenFromList.js | 6 +++--- .../swap/context/swap-form/SwapFormProvider.js | 2 +- .../app/stores/toplevel/TokenInfoStore.js | 8 ++++++++ 7 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js index 5154a897c2..fa1ab44637 100644 --- a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js +++ b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js @@ -28,7 +28,7 @@ type Props = {| onAssetSelected: any => void, onClose: void => void, defaultTokenInfo: RemoteTokenInfo, - getTokenInfo: string => Promise, + getTokenInfoBatch: Array => { [string]: Promise }, |}; export default function SelectAssetDialog({ @@ -37,7 +37,7 @@ export default function SelectAssetDialog({ onAssetSelected, onClose, defaultTokenInfo, - getTokenInfo, + getTokenInfoBatch, }: Props): React$Node { const [searchTerm, setSearchTerm] = useState(''); @@ -55,6 +55,9 @@ export default function SelectAssetDialog({ .includes(searchTerm.toLowerCase()); }) || []; + const metadataPromiseMap: { [string]: Promise } = + getTokenInfoBatch(filteredAssets.map(a => a.id)) + return ( metadataPromiseMap[id]} /> ); })} diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js index e40ad94472..f675e981fc 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js @@ -24,6 +24,7 @@ type Props = {| swapStore: SwapStore, defaultTokenInfo: RemoteTokenInfo, getTokenInfo: string => Promise, + getTokenInfoBatch: Array => { [string]: Promise }, priceImpactState: ?PriceImpact, |}; @@ -33,6 +34,7 @@ export const CreateSwapOrder = ({ swapStore, defaultTokenInfo, getTokenInfo, + getTokenInfoBatch, priceImpactState, }: Props): React$Node => { const [openedDialog, setOpenedDialog] = useState(''); @@ -119,7 +121,7 @@ export const CreateSwapOrder = ({ sellTokenInfoChanged(val); }} defaultTokenInfo={defaultTokenInfo} - getTokenInfo={getTokenInfo} + getTokenInfoBatch={getTokenInfoBatch} /> )} {openedDialog === 'to' && ( @@ -131,7 +133,7 @@ export const CreateSwapOrder = ({ buyTokenInfoChanged(val); }} defaultTokenInfo={defaultTokenInfo} - getTokenInfo={getTokenInfo} + getTokenInfoBatch={getTokenInfoBatch} /> )} {openedDialog === 'slippage' && ( diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js index cecf998770..5249a67914 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js @@ -88,8 +88,10 @@ function SwapPage(props: StoresAndActionsProps): Node { const defaultTokenInfo = props.stores.tokenInfoStore.getDefaultTokenInfoSummary( network.NetworkId ); + const getTokenInfoBatch: Array => { [string]: Promise } = ids => + props.stores.tokenInfoStore.fetchMissingAndGetLocalOrRemoteMetadata(network, ids); const getTokenInfo: string => Promise = id => - props.stores.tokenInfoStore.getLocalOrRemoteMetadata(network, id); + getTokenInfoBatch([id])[id].then(res => res ?? {}); const disclaimerFlag = props.stores.substores.ada.swapStore.swapDisclaimerAcceptanceFlag; @@ -323,6 +325,7 @@ function SwapPage(props: StoresAndActionsProps): Node { onSetNewSlippage={onSetNewSlippage} defaultTokenInfo={defaultTokenInfo} getTokenInfo={getTokenInfo} + getTokenInfoBatch={getTokenInfoBatch} priceImpactState={priceImpactState} /> )} diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js index aad847d842..0efba0d0b7 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js @@ -12,10 +12,10 @@ type Props = {| onClose(): void, onTokenInfoChanged: * => void, defaultTokenInfo: RemoteTokenInfo, - getTokenInfo: string => Promise, + getTokenInfoBatch: Array => { [string]: Promise }, |}; -export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChanged, defaultTokenInfo, getTokenInfo }: Props): Node { +export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChanged, defaultTokenInfo, getTokenInfoBatch }: Props): Node { const { sellQuantity: { isTouched: isSellTouched }, buyQuantity: { isTouched: isBuyTouched }, @@ -67,7 +67,7 @@ export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChan onAssetSelected={handleAssetSelected} onClose={onClose} defaultTokenInfo={defaultTokenInfo} - getTokenInfo={getTokenInfo} + getTokenInfoBatch={getTokenInfoBatch} /> ); } diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-sell-amount/SelectSellTokenFromList.js b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-sell-amount/SelectSellTokenFromList.js index 2a6cc0fc00..770000ae23 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-sell-amount/SelectSellTokenFromList.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-sell-amount/SelectSellTokenFromList.js @@ -12,10 +12,10 @@ type Props = {| onClose(): void, onTokenInfoChanged: * => void, defaultTokenInfo: RemoteTokenInfo, - getTokenInfo: string => Promise, + getTokenInfoBatch: Array => { [string]: Promise }, |}; -export default function SelectSellTokenFromList({ store, onClose, onTokenInfoChanged, defaultTokenInfo, getTokenInfo }: Props): Node { +export default function SelectSellTokenFromList({ store, onClose, onTokenInfoChanged, defaultTokenInfo, getTokenInfoBatch }: Props): Node { const { onlyVerifiedTokens } = useSwapTokensOnlyVerified(); const assets = store.assets; const walletVerifiedAssets = useMemo(() => { @@ -62,7 +62,7 @@ export default function SelectSellTokenFromList({ store, onClose, onTokenInfoCha onAssetSelected={handleAssetSelected} onClose={onClose} defaultTokenInfo={defaultTokenInfo} - getTokenInfo={getTokenInfo} + getTokenInfoBatch={getTokenInfoBatch} /> ); } diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 7055241bfa..c65e3854c7 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -159,7 +159,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { if (sellTokenId != null && buyTokenId != null && sellTokenId !== buyTokenId) { console.log('fetching pools for pair: ', sellTokenId, buyTokenId); pools.list.byPair({ tokenA: sellTokenId, tokenB: buyTokenId }) - .then(pools => poolPairsChanged(pools)) + .then(poolsArray => poolPairsChanged(poolsArray)) .catch(err => console.error(`Failed to fetch pools for pair: ${sellTokenId}/${buyTokenId}`, err)); } }, [sellTokenId, buyTokenId]); diff --git a/packages/yoroi-extension/app/stores/toplevel/TokenInfoStore.js b/packages/yoroi-extension/app/stores/toplevel/TokenInfoStore.js index d560054b3e..8e1cb7f7f3 100644 --- a/packages/yoroi-extension/app/stores/toplevel/TokenInfoStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/TokenInfoStore.js @@ -84,6 +84,14 @@ export default class TokenInfoStore< return { name: undefined, ticker: undefined, decimals: undefined, logo: undefined }; } + fetchMissingAndGetLocalOrRemoteMetadata(network: $ReadOnly, tokenIds: Array): { [string]: Promise } { + const fetchPromise = this.fetchMissingTokenInfo(network.NetworkId, tokenIds); + return tokenIds.reduce((res, id) => { + res[id] = fetchPromise.then(() => this.getLocalOrRemoteMetadata(network, id)); + return res; + }, {}); + } + fetchMissingTokenInfo: (networkId: number, tokenIds: Array) => Promise = async ( networkId, tokenIds From 68deef03451177cda3c86cdd035a656a0d2c1a89 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:25:58 +0100 Subject: [PATCH 02/21] New translations en-us.json (Hungarian) --- packages/yoroi-extension/app/i18n/locales/hu-HU.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/yoroi-extension/app/i18n/locales/hu-HU.json b/packages/yoroi-extension/app/i18n/locales/hu-HU.json index d64a32da61..2ccb0ce494 100644 --- a/packages/yoroi-extension/app/i18n/locales/hu-HU.json +++ b/packages/yoroi-extension/app/i18n/locales/hu-HU.json @@ -50,8 +50,11 @@ "buysell.dialog.currentBalance": "Current balance: {amount} ADA", "buysell.dialog.disclaimer": "Disclaimer", "buysell.dialog.disclaimerText": "Yoroi Wallet utilizes third-party web3 on-and-off ramp solutions for direct Fiat-ADA exchanges. By clicking \"Proceed,\" you acknowledge that you will be redirected to our partner's website, where you may need to accept their terms and conditions. Please note, the third party web3 solution may have limitations based on your location and financial institution.", + "buysell.dialog.error.dialog.title": "url generation", "buysell.dialog.error.minimum": "Minimum {amount} ADA required", "buysell.dialog.error.not.enough": "Not enough balance", + "buysell.dialog.fail.text": "This service is currently unavailable. Please try again later.", + "buysell.dialog.longloading.text": "We are redirecting you outside Yoroi. Please wait.", "buysell.dialog.proceed": "PROCEED", "buysell.dialog.provider": "Provider", "buysell.dialog.providerFee": "Provider fee", @@ -949,6 +952,8 @@ "wallet.transaction.type.stakeKeyRegistered": "Staking key registered", "wallet.transaction.withdraw": "Withdraw", "wallet.transaction.withdrawalsLabel": "Withdrawals", + "wallet.transactions.success.button.exchange": "Go to the exchange page", + "wallet.transactions.success.sell": "Transaction has been submitted", "wallet.transfer.deregister.deregister": "Deregister", "wallet.transfer.deregister.keep": "Keep registered", "wallet.transfer.deregister.line1": "When withdrawing rewards, you also have the option to deregister the staking key", From 91017f1f7ed518e71dc6f3ac7fb4b38619a1b446 Mon Sep 17 00:00:00 2001 From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:25:59 +0100 Subject: [PATCH 03/21] New translations en-us.json (Vietnamese) --- packages/yoroi-extension/app/i18n/locales/vi-VN.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/yoroi-extension/app/i18n/locales/vi-VN.json b/packages/yoroi-extension/app/i18n/locales/vi-VN.json index 97176e5b74..c7e0fdb300 100644 --- a/packages/yoroi-extension/app/i18n/locales/vi-VN.json +++ b/packages/yoroi-extension/app/i18n/locales/vi-VN.json @@ -50,8 +50,11 @@ "buysell.dialog.currentBalance": "Current balance: {amount} ADA", "buysell.dialog.disclaimer": "Disclaimer", "buysell.dialog.disclaimerText": "Yoroi Wallet utilizes third-party web3 on-and-off ramp solutions for direct Fiat-ADA exchanges. By clicking \"Proceed,\" you acknowledge that you will be redirected to our partner's website, where you may need to accept their terms and conditions. Please note, the third party web3 solution may have limitations based on your location and financial institution.", + "buysell.dialog.error.dialog.title": "url generation", "buysell.dialog.error.minimum": "Minimum {amount} ADA required", "buysell.dialog.error.not.enough": "Not enough balance", + "buysell.dialog.fail.text": "This service is currently unavailable. Please try again later.", + "buysell.dialog.longloading.text": "We are redirecting you outside Yoroi. Please wait.", "buysell.dialog.proceed": "PROCEED", "buysell.dialog.provider": "Provider", "buysell.dialog.providerFee": "Provider fee", @@ -949,6 +952,8 @@ "wallet.transaction.type.stakeKeyRegistered": "Đăng ký khóa đặt cược", "wallet.transaction.withdraw": "Rút", "wallet.transaction.withdrawalsLabel": "Rút", + "wallet.transactions.success.button.exchange": "Go to the exchange page", + "wallet.transactions.success.sell": "Transaction has been submitted", "wallet.transfer.deregister.deregister": "Hủy đăng ký", "wallet.transfer.deregister.keep": "Giữ đăng ký", "wallet.transfer.deregister.line1": "Khi rút phần thưởng, bạn cũng có tùy chọn hủy đăng ký đặt cược", From 814bc1dba69651dbabe2f3059347f2e05e1a5d78 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Thu, 20 Jun 2024 18:41:09 +0300 Subject: [PATCH 04/21] removed state-updating logic from component render. Moved it to be an effect --- .../swap/asset-swap/CreateSwapOrder.js | 19 ++----------------- .../context/swap-form/SwapFormProvider.js | 18 +++++++++++------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js index f675e981fc..a7e1af3996 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js @@ -38,32 +38,17 @@ export const CreateSwapOrder = ({ priceImpactState, }: Props): React$Node => { const [openedDialog, setOpenedDialog] = useState(''); - const [prevSelectedPoolId, setPrevSelectedPoolId] = useState(undefined); const { orderData: { type: orderType, - selectedPoolCalculation, }, - // unsignedTxChanged, sellTokenInfoChanged, buyTokenInfoChanged, } = useSwap(); const { onChangeLimitPrice } = useSwapForm(); - const resetLimitPrice = () => { - onChangeLimitPrice(''); - }; - - if (orderType === 'market') { - const selectedPoolId = selectedPoolCalculation?.pool.poolId; - if (selectedPoolId !== prevSelectedPoolId) { - setPrevSelectedPoolId(selectedPoolId); - resetLimitPrice(); - } - } - return ( <> setOpenedDialog('')} onTokenInfoChanged={val => { - resetLimitPrice(); + onChangeLimitPrice(); sellTokenInfoChanged(val); }} defaultTokenInfo={defaultTokenInfo} @@ -129,7 +114,7 @@ export const CreateSwapOrder = ({ store={swapStore} onClose={() => setOpenedDialog('')} onTokenInfoChanged={val => { - resetLimitPrice(); + onChangeLimitPrice(); buyTokenInfoChanged(val); }} defaultTokenInfo={defaultTokenInfo} diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 716491085a..03f57e2ee7 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -10,6 +10,7 @@ import { Quantities } from '../../../../utils/quantities'; import SwapStore from '../../../../stores/ada/SwapStore'; import { defaultSwapFormState } from './DefaultSwapFormState'; import { PRICE_PRECISION } from '../../../../components/swap/common'; +import { runInAction } from 'mobx'; // const PRECISION = 14; type Props = {| @@ -208,10 +209,6 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { actions.buyInputValueChanged(input); }; - const limitPriceUpdateHandler = ({ input }) => { - actions.limitPriceInputValueChanged(input); - }; - const onChangeSellQuantity = useCallback( baseSwapFieldChangeHandler(swapFormState.sellTokenInfo, sellUpdateHandler), [sellQuantityChanged, actions, clearErrors] @@ -223,7 +220,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { ); const onChangeLimitPrice = useCallback( - text => { + (text = '') => { const [formattedPrice, price] = Quantities.parseFromText( text, orderData.tokens.priceDenomination, @@ -238,6 +235,13 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { [actions, clearErrors, orderData.tokens.priceDenomination, limitPriceChanged, numberLocale] ); + // on selected best pool changes + useEffect(() => { + if (orderData.type === 'market') { + onChangeLimitPrice(); + } + }, [orderData.selectedPoolCalculation?.pool.poolId]); + const sellFocusState = StateWrap(useState(false)); const buyFocusState = StateWrap(useState(false)); const limitPriceFocusState = StateWrap(useState(false)); @@ -266,7 +270,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { PRICE_PRECISION ); - limitPriceUpdateHandler({ input: formatted }); + actions.limitPriceInputValueChanged(formatted); } else if (orderData.type === 'market') { const formatted = Quantities.format( orderData.selectedPoolCalculation?.prices.market ?? Quantities.zero, @@ -274,7 +278,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { PRICE_PRECISION ); - limitPriceUpdateHandler({ input: formatted }); + actions.limitPriceInputValueChanged(formatted); } }, [ orderData.tokens.priceDenomination, From 460d46b291b64b985eddf75c14ae154514b4d188 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Thu, 20 Jun 2024 18:56:25 +0300 Subject: [PATCH 05/21] lint fix --- .../app/containers/swap/context/swap-form/SwapFormProvider.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 03f57e2ee7..73541f3dbb 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -10,7 +10,6 @@ import { Quantities } from '../../../../utils/quantities'; import SwapStore from '../../../../stores/ada/SwapStore'; import { defaultSwapFormState } from './DefaultSwapFormState'; import { PRICE_PRECISION } from '../../../../components/swap/common'; -import { runInAction } from 'mobx'; // const PRECISION = 14; type Props = {| From 897b60530ec253cb7fc21069de611b9481a808f3 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Fri, 21 Jun 2024 14:05:49 +0300 Subject: [PATCH 06/21] removed corsproxy usage in swap --- .../yoroi-extension/app/components/swap/SelectAssetDialog.js | 5 ++--- packages/yoroi-extension/app/components/swap/SwapInput.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js index fa1ab44637..cdd531fc4f 100644 --- a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js +++ b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js @@ -12,7 +12,6 @@ import adaTokenImage from '../../assets/images/ada.inline.svg'; import defaultTokenImage from '../../assets/images/revamp/asset-default.inline.svg'; import Dialog from '../widgets/Dialog'; import Table from '../common/table/Table'; -import { urlResolveForIpfsAndCorsproxy } from '../../coreUtils'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; import { PriceImpactColored, PriceImpactIcon } from './PriceImpact'; import { InfoTooltip } from '../widgets/InfoTooltip'; @@ -190,7 +189,7 @@ export const AssetAndAmountRow = ({ const isFrom = type === 'from'; - const { name = null, image = '', fingerprint: address, id, amount: assetAmount, ticker } = asset; + const { name = null, fingerprint: address, id, amount: assetAmount, ticker } = asset; const priceNotChanged = Number(priceChange100.replace('-', '').replace('%', '')) === 0; const priceIncreased = priceChange100 && priceChange100.charAt(0) !== '-'; const priceChange24h = priceChange100.replace('-', '') || '0%'; @@ -211,7 +210,7 @@ export const AssetAndAmountRow = ({ const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage - : remoteTokenLogo ?? urlResolveForIpfsAndCorsproxy(image) ?? defaultTokenImage; + : remoteTokenLogo ?? defaultTokenImage; const amount = displayAmount ?? assetAmount; diff --git a/packages/yoroi-extension/app/components/swap/SwapInput.js b/packages/yoroi-extension/app/components/swap/SwapInput.js index 8c946a5708..ee6552c62c 100644 --- a/packages/yoroi-extension/app/components/swap/SwapInput.js +++ b/packages/yoroi-extension/app/components/swap/SwapInput.js @@ -5,7 +5,6 @@ import { Box, Typography } from '@mui/material'; import { ReactComponent as ChevronDownIcon } from '../../assets/images/revamp/icons/chevron-down.inline.svg'; import adaTokenImage from '../../assets/images/ada.inline.svg'; import defaultTokenImage from '../../assets/images/revamp/token-default.inline.svg'; -import { urlResolveForIpfsAndCorsproxy } from '../../coreUtils'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; import type { State } from '../../containers/swap/context/swap-form/types'; import { useEffect, useState } from 'react'; @@ -38,7 +37,7 @@ export default function SwapInput({ focusState, }: Props): Node { const [remoteTokenLogo, setRemoteTokenLogo] = useState(null); - const { id, amount: quantity = undefined, image, ticker } = tokenInfo || {}; + const { id, amount: quantity = undefined, ticker } = tokenInfo || {}; const handleChange = e => { if (!disabled && value !== quantity) { @@ -66,7 +65,7 @@ export default function SwapInput({ const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage - : remoteTokenLogo ?? urlResolveForIpfsAndCorsproxy(image) ?? defaultTokenImage; + : remoteTokenLogo ?? defaultTokenImage; return ( From 7b68494d76a17244f4e83e032debd3d04c01f9fd Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Fri, 21 Jun 2024 14:18:01 +0300 Subject: [PATCH 07/21] fixed logger --- packages/yoroi-extension/app/api/common/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yoroi-extension/app/api/common/index.js b/packages/yoroi-extension/app/api/common/index.js index 632966c652..5d5e7b363d 100644 --- a/packages/yoroi-extension/app/api/common/index.js +++ b/packages/yoroi-extension/app/api/common/index.js @@ -241,7 +241,7 @@ export default class CommonApi { try { const newAddress = await request.popFunc(); - Logger.info(`${nameof(CommonApi)}::${nameof(this.createAddress)} success: ` + stringifyData(newAddress)); + Logger.debug(`${nameof(CommonApi)}::${nameof(this.createAddress)} success: ` + stringifyData(newAddress)); return newAddress; } catch (error) { Logger.error(`${nameof(CommonApi)}::${nameof(this.createAddress)} error: ` + stringifyError(error)); From b23948454b90b619acdda5004a37b270ecce1a7d Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Fri, 21 Jun 2024 15:47:24 +0300 Subject: [PATCH 08/21] swap form provider fixes --- .../context/swap-form/SwapFormProvider.js | 87 +++++++++---------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 73541f3dbb..fe3c5275ce 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -35,6 +35,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { const { quantity: sellQuantity, tokenId: sellTokenId } = orderData.amounts.sell; const { quantity: buyQuantity, tokenId: buyTokenId } = orderData.amounts.buy; + const { priceDenomination } = orderData.tokens; const swapFormReducer = (state: SwapFormState, action: SwapFormAction) => { const draft = { ...state }; @@ -171,13 +172,14 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { const baseSwapFieldChangeHandler = ( tokenInfo: any, handler: ({| input: string, quantity: string |}) => void - ) => (text: string) => { + ) => (text: string = '') => { if (tokenInfo.tokenId === '') { // empty input return; } const decimals = tokenInfo.decimals ?? 0; - const [input, quantity] = Quantities.parseFromText(text, decimals, numberLocale); + const precision = tokenInfo.precision ?? decimals; + const [input, quantity] = Quantities.parseFromText(text, decimals, numberLocale, precision); clearErrors(); handler({ quantity, input: text === '' ? '' : input }); }; @@ -193,7 +195,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { const [, availableQuantity] = Quantities.parseFromText( sellAvailableAmount, decimals, - numberLocale + numberLocale, ); if (Quantities.isGreaterThan(quantity, availableQuantity)) { actions.sellAmountErrorChanged('Not enough balance'); @@ -208,6 +210,13 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { actions.buyInputValueChanged(input); }; + const limitUpdateHandler = ({ input, quantity }) => { + if (quantity !== orderData.limitPrice) { + limitPriceChanged(quantity); + } + actions.limitPriceInputValueChanged(input); + }; + const onChangeSellQuantity = useCallback( baseSwapFieldChangeHandler(swapFormState.sellTokenInfo, sellUpdateHandler), [sellQuantityChanged, actions, clearErrors] @@ -219,33 +228,21 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { ); const onChangeLimitPrice = useCallback( - (text = '') => { - const [formattedPrice, price] = Quantities.parseFromText( - text, - orderData.tokens.priceDenomination, - numberLocale, - PRICE_PRECISION - ); - actions.limitPriceInputValueChanged(formattedPrice); - limitPriceChanged(price); - - clearErrors(); - }, - [actions, clearErrors, orderData.tokens.priceDenomination, limitPriceChanged, numberLocale] + baseSwapFieldChangeHandler( + { tokenId: 'priceDenomination', decimals: priceDenomination, precision: PRICE_PRECISION }, + limitUpdateHandler, + ), + [limitPriceChanged, actions, clearErrors, priceDenomination] ); - // on selected best pool changes - useEffect(() => { - if (orderData.type === 'market') { - onChangeLimitPrice(); - } - }, [orderData.selectedPoolCalculation?.pool.poolId]); - const sellFocusState = StateWrap(useState(false)); const buyFocusState = StateWrap(useState(false)); const limitPriceFocusState = StateWrap(useState(false)); - const updateSellInput = useCallback(() => { + /** + * On sell quantity changes + */ + useEffect(() => { if (swapFormState.sellQuantity.isTouched && !sellFocusState.value) { const decimals = swapFormState.sellTokenInfo.decimals ?? 0; const formatted = Quantities.format(sellQuantity, decimals); @@ -253,7 +250,10 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { } }, [sellQuantity, swapFormState.sellTokenInfo.decimals, swapFormState.sellQuantity.isTouched]); - const updateBuyInput = useCallback(() => { + /** + * On buy quantity changes + */ + useEffect(() => { if (swapFormState.buyQuantity.isTouched && !buyFocusState.value) { const decimals = swapFormState.buyTokenInfo.decimals ?? 0; const formatted = Quantities.format(buyQuantity, decimals); @@ -261,35 +261,26 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { } }, [buyQuantity, swapFormState.buyTokenInfo.decimals, swapFormState.buyQuantity.isTouched]); - const updateLimitPrice = useCallback(() => { - if (orderData.type === 'limit' && !limitPriceFocusState.value) { - const formatted = Quantities.format( - orderData.limitPrice ?? Quantities.zero, - orderData.tokens.priceDenomination, - PRICE_PRECISION - ); - - actions.limitPriceInputValueChanged(formatted); - } else if (orderData.type === 'market') { - const formatted = Quantities.format( - orderData.selectedPoolCalculation?.prices.market ?? Quantities.zero, - orderData.tokens.priceDenomination, - PRICE_PRECISION - ); - - actions.limitPriceInputValueChanged(formatted); - } + /** + * Limit price updater + */ + useEffect(() => { + const isLimit = orderData.type === 'limit'; + if (isLimit && limitPriceFocusState.value) return; + const quantity = (isLimit ? orderData.limitPrice : orderData.selectedPoolCalculation?.prices.market) ?? Quantities.zero; + actions.limitPriceInputValueChanged(Quantities.format( + quantity, + priceDenomination, + PRICE_PRECISION + )); }, [ - orderData.tokens.priceDenomination, + priceDenomination, orderData.limitPrice, orderData.selectedPoolCalculation?.prices.market, + orderData.selectedPoolCalculation?.pool.poolId, orderData.type, ]); - useEffect(updateSellInput, [updateSellInput]); - useEffect(updateBuyInput, [updateBuyInput]); - useEffect(updateLimitPrice, [updateLimitPrice]); - const allActions = { ...actions, sellFocusState, From 1024de3b28fe885403e6391d7c2851b470bc60ad Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Fri, 21 Jun 2024 15:57:43 +0300 Subject: [PATCH 09/21] swap form provider fixes --- .../containers/swap/context/swap-form/SwapFormProvider.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index fe3c5275ce..2d09f15de1 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -268,11 +268,8 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { const isLimit = orderData.type === 'limit'; if (isLimit && limitPriceFocusState.value) return; const quantity = (isLimit ? orderData.limitPrice : orderData.selectedPoolCalculation?.prices.market) ?? Quantities.zero; - actions.limitPriceInputValueChanged(Quantities.format( - quantity, - priceDenomination, - PRICE_PRECISION - )); + const formatted = Quantities.format(quantity, priceDenomination, PRICE_PRECISION); + limitUpdateHandler({ input: formatted, quantity: orderData.limitPrice }); }, [ priceDenomination, orderData.limitPrice, From b4c2d1f7e8aeaa931a14d69177abac11e81d0f77 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Fri, 21 Jun 2024 19:22:31 +0300 Subject: [PATCH 10/21] trying to come up with a better hook for orders --- .../app/containers/swap/hooks.js | 68 +++++++++++++++++++ .../app/containers/swap/orders/OrdersPage.js | 13 ++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/hooks.js b/packages/yoroi-extension/app/containers/swap/hooks.js index 2a6c4d8df6..eec179f22d 100644 --- a/packages/yoroi-extension/app/containers/swap/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/hooks.js @@ -8,6 +8,7 @@ import { import { Quantities } from '../../utils/quantities'; import { useSwapForm } from './context/swap-form'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; +import { useQuery } from 'react-query'; export function useSwapFeeDisplay( defaultTokenInfo: RemoteTokenInfo @@ -141,3 +142,70 @@ export function useRichCompletedOrders(): any { return []; } } + +export function useRichOrders(): {| openOrders: Array, completedOrders: Array |} { + const {order, tokens, stakingKey} = useSwap() + + const verifiedTokensMapQuery = useQuery({ + suspense: true, + queryKey: ['useSwapTokensOnlyVerified'], + queryFn: () => tokens.list.onlyVerified() + .then(tokens => tokens.reduce((map, t) => ({ ...map, [t.id]: t }), {})) + .catch(e => { + console.error('Failed to load verified tokens!', e); + throw e; + }), + }); + + const openOrdersQuery = useQuery({ + suspense: true, + queryKey: ['useSwapOrdersByStatusOpen', stakingKey], + queryFn: () => order.list.byStatusOpen().catch(e => { + console.error('Failed to load open orders!', e); + throw e; + }), + }); + + const completedOrdersQuery = useQuery({ + suspense: true, + queryKey: ['useSwapOrdersByStatusCompleted', stakingKey], + queryFn: () => order.list.byStatusCompleted().catch(e => { + console.error('Failed to load completed orders!', e); + throw e; + }), + }); + + let openOrders = []; + let completedOrders = []; + const tokensMap = verifiedTokensMapQuery.data; + if (tokensMap && openOrdersQuery.data) { + console.log('recalc open orders'); + openOrders = openOrdersQuery.data.map(o => { + const fromToken = tokensMap[o.from.tokenId]; + const toToken = tokensMap[o.to.tokenId]; + return { + utxo: o.utxo, + from: { quantity: o.from.quantity, token: fromToken }, + to: { quantity: o.to.quantity, token: toToken }, + batcherFee: o.batcherFee, + valueAttached: o.valueAttached, + deposit: o.deposit, + provider: o.provider, + sender: o.sender, + }; + }); + } + if (tokensMap && completedOrdersQuery.data) { + console.log('recalc completed orders'); + completedOrders = completedOrdersQuery.data.map(o => { + const fromToken = tokensMap[o.from.tokenId]; + const toToken = tokensMap[o.to.tokenId]; + return { + txHash: o.txHash, + from: { quantity: o.from.quantity, token: fromToken }, + to: { quantity: o.to.quantity, token: toToken }, + }; + }); + } + return { openOrders, completedOrders }; +} diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js index 388a1a96d2..5377f4530d 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js +++ b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js @@ -6,7 +6,7 @@ import Table from '../../../components/common/table/Table'; import CancelSwapOrderDialog from '../../../components/swap/CancelOrderDialog'; import AssetPair from '../../../components/common/assets/AssetPair'; import Tabs from '../../../components/common/tabs/Tabs'; -import { useRichCompletedOrders, useRichOpenOrders } from '../hooks'; +import { useRichCompletedOrders, useRichOpenOrders, useRichOrders } from '../hooks'; import type { StoresAndActionsProps } from '../../../types/injectedProps.types'; import { SwapPoolLabel } from '../../../components/swap/SwapPoolComponents'; import ExplorableHashContainer from '../../widgets/ExplorableHashContainer'; @@ -195,7 +195,7 @@ function mapCompletedOrder(order: any, defaultTokenInfo: RemoteTokenInfo): Mappe export default function SwapOrdersPage(props: StoresAndActionsProps): Node { const { - order: { cancel: swapCancelOrder }, + order: orderApi, } = useSwap(); const [showCompletedOrders, setShowCompletedOrders] = useState(false); @@ -218,8 +218,11 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { props.stores.explorers.selectedExplorer.get(network.NetworkId) ?? fail('No explorer for wallet network'); - const openOrders = useRichOpenOrders().map(o => mapOpenOrder(o, defaultTokenInfo)); - const completedOrders = useRichCompletedOrders().map(o => mapCompletedOrder(o, defaultTokenInfo)); + // const openOrders = useRichOpenOrders().map(o => mapOpenOrder(o, defaultTokenInfo)); + // const completedOrders = useRichCompletedOrders().map(o => mapCompletedOrder(o, defaultTokenInfo)); + let { openOrders, completedOrders } = useRichOrders(); + openOrders = openOrders.map(o => mapOpenOrder(o, defaultTokenInfo)); + completedOrders = completedOrders.map(o => mapCompletedOrder(o, defaultTokenInfo)); const txHashes = [...openOrders, ...completedOrders].map(o => o.txId); noop(props.stores.substores.ada.swapStore.fetchTransactionTimestamps({ wallet, txHashes })); @@ -270,7 +273,7 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { throw new Error('Cannot cancel a completed order (sender == null)'); } try { - const cancelTxCbor = await swapCancelOrder({ + const cancelTxCbor = await orderApi.cancel({ address: addressBech32ToHex(sender), utxos: { order: order.utxo, From b91dbc879dafe80b71ccff7d4eb9ef3ebf0565e4 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Sat, 22 Jun 2024 17:01:12 +0300 Subject: [PATCH 11/21] refactored queries for swap orders --- .../app/components/swap/CancelOrderDialog.js | 2 +- .../app/containers/swap/hooks.js | 140 --------------- .../app/containers/swap/orders/OrdersPage.js | 127 +------------- .../app/containers/swap/orders/hooks.js | 159 ++++++++++++++++++ .../app/containers/swap/orders/util.js | 51 ++++++ .../app/stores/ada/SwapStore.js | 1 + 6 files changed, 218 insertions(+), 262 deletions(-) create mode 100644 packages/yoroi-extension/app/containers/swap/orders/hooks.js create mode 100644 packages/yoroi-extension/app/containers/swap/orders/util.js diff --git a/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js b/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js index 61c3f629f6..233e2aab34 100644 --- a/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js +++ b/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js @@ -6,7 +6,6 @@ import TextField from '../common/TextField'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; import LoadingSpinner from '../widgets/LoadingSpinner'; import { useState } from 'react'; -import type { FormattedTokenValue } from '../../containers/swap/orders/OrdersPage'; import { WrongPassphraseError } from '../../api/ada/lib/cardanoCrypto/cryptoErrors'; import { stringifyError } from '../../utils/logging'; import { InfoTooltip } from '../widgets/InfoTooltip'; @@ -16,6 +15,7 @@ import type { TokenLookupKey } from '../../api/common/lib/MultiToken'; import type { TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import { SelectedExplorer } from '../../domain/SelectedExplorer'; import type LocalizableError from '../../i18n/LocalizableError'; +import type { FormattedTokenValue } from '../../containers/swap/orders/util'; type Props = {| order: any, diff --git a/packages/yoroi-extension/app/containers/swap/hooks.js b/packages/yoroi-extension/app/containers/swap/hooks.js index eec179f22d..cf059361fc 100644 --- a/packages/yoroi-extension/app/containers/swap/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/hooks.js @@ -1,14 +1,10 @@ //@flow import { useSwap, - useSwapOrdersByStatusCompleted, - useSwapOrdersByStatusOpen, - useSwapTokensOnlyVerified, } from '@yoroi/swap'; import { Quantities } from '../../utils/quantities'; import { useSwapForm } from './context/swap-form'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; -import { useQuery } from 'react-query'; export function useSwapFeeDisplay( defaultTokenInfo: RemoteTokenInfo @@ -73,139 +69,3 @@ export function useSwapFeeDisplay( formattedFee, }; } - -export function useRichOpenOrders(): any { - let openOrders = []; - try { - openOrders = useSwapOrdersByStatusOpen(); - } catch (e) { - console.warn('useRichCompletedOrders.useSwapOrdersByStatusOpen', e); - } - let onlyVerifiedTokens = []; - try { - const res = useSwapTokensOnlyVerified(); - onlyVerifiedTokens = res.onlyVerifiedTokens; - } catch (e) { - console.warn('useRichCompletedOrders.useSwapTokensOnlyVerified', e); - } - if ((openOrders?.length || 0) === 0 || (onlyVerifiedTokens?.length || 0) === 0) return []; - try { - const tokensMap = onlyVerifiedTokens.reduce((map, t) => ({ ...map, [t.id]: t }), {}); - return openOrders.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; - return { - utxo: o.utxo, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, - batcherFee: o.batcherFee, - valueAttached: o.valueAttached, - deposit: o.deposit, - provider: o.provider, - sender: o.sender, - }; - }); - } catch (e) { - console.warn('useRichOpenOrders', e); - return []; - } -} - -export function useRichCompletedOrders(): any { - let completedOrders = []; - try { - completedOrders = useSwapOrdersByStatusCompleted(); - } catch (e) { - console.warn('useRichCompletedOrders.useSwapOrdersByStatusCompleted', e); - } - let onlyVerifiedTokens = []; - try { - const res = useSwapTokensOnlyVerified(); - onlyVerifiedTokens = res.onlyVerifiedTokens; - } catch (e) { - console.warn('useRichCompletedOrders.useSwapTokensOnlyVerified', e); - } - if ((completedOrders?.length || 0) === 0 || (onlyVerifiedTokens?.length || 0) === 0) return []; - try { - const tokensMap = onlyVerifiedTokens.reduce((map, t) => ({ ...map, [t.id]: t }), {}); - return completedOrders.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; - return { - txHash: o.txHash, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, - }; - }); - } catch (e) { - console.warn('useRichCompletedOrders', e); - return []; - } -} - -export function useRichOrders(): {| openOrders: Array, completedOrders: Array |} { - const {order, tokens, stakingKey} = useSwap() - - const verifiedTokensMapQuery = useQuery({ - suspense: true, - queryKey: ['useSwapTokensOnlyVerified'], - queryFn: () => tokens.list.onlyVerified() - .then(tokens => tokens.reduce((map, t) => ({ ...map, [t.id]: t }), {})) - .catch(e => { - console.error('Failed to load verified tokens!', e); - throw e; - }), - }); - - const openOrdersQuery = useQuery({ - suspense: true, - queryKey: ['useSwapOrdersByStatusOpen', stakingKey], - queryFn: () => order.list.byStatusOpen().catch(e => { - console.error('Failed to load open orders!', e); - throw e; - }), - }); - - const completedOrdersQuery = useQuery({ - suspense: true, - queryKey: ['useSwapOrdersByStatusCompleted', stakingKey], - queryFn: () => order.list.byStatusCompleted().catch(e => { - console.error('Failed to load completed orders!', e); - throw e; - }), - }); - - let openOrders = []; - let completedOrders = []; - const tokensMap = verifiedTokensMapQuery.data; - if (tokensMap && openOrdersQuery.data) { - console.log('recalc open orders'); - openOrders = openOrdersQuery.data.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; - return { - utxo: o.utxo, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, - batcherFee: o.batcherFee, - valueAttached: o.valueAttached, - deposit: o.deposit, - provider: o.provider, - sender: o.sender, - }; - }); - } - if (tokensMap && completedOrdersQuery.data) { - console.log('recalc completed orders'); - completedOrders = completedOrdersQuery.data.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; - return { - txHash: o.txHash, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, - }; - }); - } - return { openOrders, completedOrders }; -} diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js index 5377f4530d..3dfdae5897 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js +++ b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js @@ -6,26 +6,25 @@ import Table from '../../../components/common/table/Table'; import CancelSwapOrderDialog from '../../../components/swap/CancelOrderDialog'; import AssetPair from '../../../components/common/assets/AssetPair'; import Tabs from '../../../components/common/tabs/Tabs'; -import { useRichCompletedOrders, useRichOpenOrders, useRichOrders } from '../hooks'; +import type { MappedOrder } from './hooks'; +import { useRichOrders } from './hooks'; import type { StoresAndActionsProps } from '../../../types/injectedProps.types'; import { SwapPoolLabel } from '../../../components/swap/SwapPoolComponents'; import ExplorableHashContainer from '../../widgets/ExplorableHashContainer'; import { truncateAddressShort } from '../../../utils/formatters'; import { Quantities } from '../../../utils/quantities'; -import { PRICE_PRECISION } from '../../../components/swap/common'; import { fail, forceNonNull, maybe, noop } from '../../../coreUtils'; import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; import { useSwap } from '@yoroi/swap'; import { addressBech32ToHex } from '../../../api/ada/lib/cardanoCrypto/utils'; -import { - getTransactionFeeFromCbor, - getTransactionTotalOutputFromCbor, -} from '../../../api/ada/transactions/utils'; +import { getTransactionFeeFromCbor, getTransactionTotalOutputFromCbor, } from '../../../api/ada/transactions/utils'; import { SelectedExplorer } from '../../../domain/SelectedExplorer'; import type { CardanoConnectorSignRequest } from '../../../connector/types'; import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; import moment from 'moment'; import { signTransactionHex } from '../../../api/ada/transactions/signTransactionHex'; +import { createFormattedTokenValues } from './util'; +import type { FormattedTokenValue } from './util'; type ColumnContext = {| completedOrders: boolean, @@ -83,116 +82,6 @@ const orderColumns: Array = [ }, ]; -export type FormattedTokenValue = {| - value: string, - formattedValue: string, - ticker: string, -|}; - -function createFormattedTokenValues({ - entries, - order, - defaultTokenInfo, -}: {| - entries: Array<{| id: string, amount: string |}>, - order: any, - defaultTokenInfo: RemoteTokenInfo, -|}): Array { - const tokenAmountMap = entries.reduce( - (map, v) => ({ ...map, [v.id]: Quantities.sum([map[v.id] ?? '0', v.amount]) }), - {} - ); - const ptDecimals = forceNonNull(defaultTokenInfo.decimals); - // $FlowIgnore[prop-missing] - const defaultTokenValue = tokenAmountMap[''] ?? tokenAmountMap['.'] ?? '0'; - const formattedTokenValues = [ - { - value: defaultTokenValue, - formattedValue: Quantities.format(defaultTokenValue, ptDecimals, ptDecimals), - ticker: defaultTokenInfo.ticker ?? '-', - }, - ]; - [order.from.token, order.to.token].forEach(t => { - if (t.id !== '' && t.id !== '.') { - maybe(tokenAmountMap[t.id], v => { - const formattedValue = Quantities.format(v, t.decimals, t.decimals); - formattedTokenValues.push({ - value: v, - formattedValue, - ticker: t.ticker ?? '-', - }); - }); - } - }); - return formattedTokenValues; -} - -function mapOrderAssets( - order: any, - defaultTokenInfo: RemoteTokenInfo -): {| - price: string, - amount: string, - totalValues: ?Array, - from: any, - to: any, -|} { - const price = Quantities.quotient(order.from.quantity, order.to.quantity); - const fromDecimals = order.from.token?.decimals ?? 0; - const toDecimals = order.to.token?.decimals ?? 0; - const priceDenomination = fromDecimals - toDecimals; - const formattedPrice = Quantities.format(price, priceDenomination, PRICE_PRECISION); - const formattedToQuantity = Quantities.format( - order.to.quantity, - toDecimals, - toDecimals - ); - const formattedAttachedValues = maybe(order.valueAttached, val => - createFormattedTokenValues({ - entries: val.map(({ token: id, amount }) => ({ id, amount })), - order, - defaultTokenInfo, - }) - ); - return { - price: formattedPrice, - amount: formattedToQuantity, - totalValues: formattedAttachedValues, - from: order.from, - to: order.to, - }; -} - -type MappedOrder = {| - txId: string, - utxo?: string, - sender?: string, - provider?: string, - price: string, - amount: string, - totalValues: ?Array, - from: any, - to: any, -|}; - -function mapOpenOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { - const txId = order.utxo.split('#')[0]; - return { - txId, - utxo: order.utxo, - sender: order.sender, - provider: order.provider, - ...mapOrderAssets(order, defaultTokenInfo), - }; -} - -function mapCompletedOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { - return { - txId: order.txHash, - ...mapOrderAssets(order, defaultTokenInfo), - }; -} - export default function SwapOrdersPage(props: StoresAndActionsProps): Node { const { order: orderApi, @@ -218,11 +107,7 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { props.stores.explorers.selectedExplorer.get(network.NetworkId) ?? fail('No explorer for wallet network'); - // const openOrders = useRichOpenOrders().map(o => mapOpenOrder(o, defaultTokenInfo)); - // const completedOrders = useRichCompletedOrders().map(o => mapCompletedOrder(o, defaultTokenInfo)); - let { openOrders, completedOrders } = useRichOrders(); - openOrders = openOrders.map(o => mapOpenOrder(o, defaultTokenInfo)); - completedOrders = completedOrders.map(o => mapCompletedOrder(o, defaultTokenInfo)); + let { openOrders, completedOrders } = useRichOrders(defaultTokenInfo); const txHashes = [...openOrders, ...completedOrders].map(o => o.txId); noop(props.stores.substores.ada.swapStore.fetchTransactionTimestamps({ wallet, txHashes })); diff --git a/packages/yoroi-extension/app/containers/swap/orders/hooks.js b/packages/yoroi-extension/app/containers/swap/orders/hooks.js new file mode 100644 index 0000000000..713c52f9ea --- /dev/null +++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js @@ -0,0 +1,159 @@ +//@flow +import { useSwap, } from '@yoroi/swap'; +import { useQuery } from 'react-query'; +import { useEffect, useState } from 'react'; +import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; +import { Quantities } from '../../../utils/quantities'; +import { PRICE_PRECISION } from '../../../components/swap/common'; +import { maybe } from '../../../coreUtils'; +import type { FormattedTokenValue } from './util'; +import { createFormattedTokenValues } from './util'; + +function mapOrderAssets( + order: any, + defaultTokenInfo: RemoteTokenInfo +): {| + price: string, + amount: string, + totalValues: ?Array, + from: any, + to: any, +|} { + const price = Quantities.quotient(order.from.quantity, order.to.quantity); + const fromDecimals = order.from.token?.decimals ?? 0; + const toDecimals = order.to.token?.decimals ?? 0; + const priceDenomination = fromDecimals - toDecimals; + const formattedPrice = Quantities.format(price, priceDenomination, PRICE_PRECISION); + const formattedToQuantity = Quantities.format( + order.to.quantity, + toDecimals, + toDecimals + ); + const formattedAttachedValues = maybe(order.valueAttached, val => + createFormattedTokenValues({ + entries: val.map(({ + token: id, + amount + }) => ({ + id, + amount + })), + order, + defaultTokenInfo, + }) + ); + return { + price: formattedPrice, + amount: formattedToQuantity, + totalValues: formattedAttachedValues, + from: order.from, + to: order.to, + }; +} + +export type MappedOrder = {| + txId: string, + utxo?: string, + sender?: string, + provider?: string, + price: string, + amount: string, + totalValues: ?Array, + from: any, + to: any, +|}; + +function mapOpenOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { + const txId = order.utxo.split('#')[0]; + return { + txId, + utxo: order.utxo, + sender: order.sender, + provider: order.provider, + ...mapOrderAssets(order, defaultTokenInfo), + }; +} + +function mapCompletedOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { + return { + txId: order.txHash, + ...mapOrderAssets(order, defaultTokenInfo), + }; +} + +export function useRichOrders( + defaultTokenInfo: RemoteTokenInfo +): {| openOrders: Array, completedOrders: Array |} { + const {order, tokens, stakingKey} = useSwap() + + const { data: tokensMap } = useQuery({ + suspense: true, + queryKey: ['useSwapTokensOnlyVerified'], + queryFn: () => tokens.list.onlyVerified() + .then(tokensArray => tokensArray.reduce((map, t) => ({ ...map, [t.id]: t }), {})) + .catch(e => { + console.error('Failed to load verified tokens!', e); + throw e; + }), + }); + + const { data: openOrdersData } = useQuery({ + suspense: true, + queryKey: ['useSwapOrdersByStatusOpen', stakingKey], + queryFn: () => order.list.byStatusOpen().catch(e => { + console.error('Failed to load open orders!', e); + throw e; + }), + }); + + const { data: completedOrdersData } = useQuery({ + suspense: true, + queryKey: ['useSwapOrdersByStatusCompleted', stakingKey], + queryFn: () => order.list.byStatusCompleted().catch(e => { + console.error('Failed to load completed orders!', e); + throw e; + }), + }); + + const [openOrders, setOpenOrders] = useState>([]); + const [completedOrders, setCompletedOrders] = useState>([]); + + useEffect(() => { + if (tokensMap && openOrdersData) { + setOpenOrders( + openOrdersData.map(o => { + const fromToken = tokensMap[o.from.tokenId]; + const toToken = tokensMap[o.to.tokenId]; + return { + utxo: o.utxo, + from: { quantity: o.from.quantity, token: fromToken }, + to: { quantity: o.to.quantity, token: toToken }, + batcherFee: o.batcherFee, + valueAttached: o.valueAttached, + deposit: o.deposit, + provider: o.provider, + sender: o.sender, + }; + }).map(o => mapOpenOrder(o, defaultTokenInfo)) + ); + } + }, [tokensMap, openOrdersData]); + + useEffect(() => { + if (tokensMap && completedOrdersData) { + setCompletedOrders( + completedOrdersData.map(o => { + const fromToken = tokensMap[o.from.tokenId]; + const toToken = tokensMap[o.to.tokenId]; + return { + txHash: o.txHash, + from: { quantity: o.from.quantity, token: fromToken }, + to: { quantity: o.to.quantity, token: toToken }, + }; + }).map(o => mapCompletedOrder(o, defaultTokenInfo)) + ); + } + }, [tokensMap, completedOrdersData]) + + return { openOrders, completedOrders }; +} diff --git a/packages/yoroi-extension/app/containers/swap/orders/util.js b/packages/yoroi-extension/app/containers/swap/orders/util.js new file mode 100644 index 0000000000..6c9467f391 --- /dev/null +++ b/packages/yoroi-extension/app/containers/swap/orders/util.js @@ -0,0 +1,51 @@ +// @flow +import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; +import { Quantities } from '../../../utils/quantities'; +import { forceNonNull, maybe } from '../../../coreUtils'; + +export type FormattedTokenValue = {| + value: string, + formattedValue: string, + ticker: string, +|}; + +export function createFormattedTokenValues({ + entries, + order, + defaultTokenInfo, +}: {| + entries: Array<{| id: string, amount: string |}>, + order: any, + defaultTokenInfo: RemoteTokenInfo, +|}): Array { + const tokenAmountMap = entries.reduce( + (map, v) => ({ + ...map, + [v.id]: Quantities.sum([map[v.id] ?? '0', v.amount]) + }), + {} + ); + const ptDecimals = forceNonNull(defaultTokenInfo.decimals); + // $FlowIgnore[prop-missing] + const defaultTokenValue = tokenAmountMap[''] ?? tokenAmountMap['.'] ?? '0'; + const formattedTokenValues = [ + { + value: defaultTokenValue, + formattedValue: Quantities.format(defaultTokenValue, ptDecimals, ptDecimals), + ticker: defaultTokenInfo.ticker ?? '-', + }, + ]; + [order.from.token, order.to.token].forEach(t => { + if (t.id !== '' && t.id !== '.') { + maybe(tokenAmountMap[t.id], v => { + const formattedValue = Quantities.format(v, t.decimals, t.decimals); + formattedTokenValues.push({ + value: v, + formattedValue, + ticker: t.ticker ?? '-', + }); + }); + } + }); + return formattedTokenValues; +} \ No newline at end of file diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js index d538ac620d..a266779076 100644 --- a/packages/yoroi-extension/app/stores/ada/SwapStore.js +++ b/packages/yoroi-extension/app/stores/ada/SwapStore.js @@ -233,6 +233,7 @@ export default class SwapStore extends Store { wallet, txHashes, }) => { + console.log('fetchTransactionTimestamps'); const existingSet = new Set(Object.keys(this.transactionTimestamps)); const filteredTxHashes = txHashes.filter(x => !existingSet.has(x.toLowerCase())); if (filteredTxHashes.length === 0) { From 5ade6e05fa4c0ff24e6e983f64d9cd1645d4c0a5 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Sat, 22 Jun 2024 21:24:27 +0300 Subject: [PATCH 12/21] moved swap orders timestamp fetching along with orders themselves; refactored swap order history mapping functions --- .../app/containers/swap/orders/OrdersPage.js | 29 ++--- .../app/containers/swap/orders/hooks.js | 117 ++++++++++-------- .../app/containers/swap/orders/util.js | 10 +- .../app/stores/ada/SwapStore.js | 21 ++-- 4 files changed, 93 insertions(+), 84 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js index 3dfdae5897..7b5db97f39 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js +++ b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js @@ -13,7 +13,7 @@ import { SwapPoolLabel } from '../../../components/swap/SwapPoolComponents'; import ExplorableHashContainer from '../../widgets/ExplorableHashContainer'; import { truncateAddressShort } from '../../../utils/formatters'; import { Quantities } from '../../../utils/quantities'; -import { fail, forceNonNull, maybe, noop } from '../../../coreUtils'; +import { fail, forceNonNull, maybe } from '../../../coreUtils'; import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; import { useSwap } from '@yoroi/swap'; import { addressBech32ToHex } from '../../../api/ada/lib/cardanoCrypto/utils'; @@ -96,31 +96,31 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { isSubmitting?: boolean, |}>(null); - const wallet = props.stores.wallets.selectedOrFail; + const { wallets, tokenInfoStore, explorers, substores: { ada: { swapStore } } } = props.stores; + + const wallet = wallets.selectedOrFail; const network = wallet.getParent().getNetworkInfo(); const walletVariant = wallet.getParent().getWalletVariant(); - const defaultTokenInfo = props.stores.tokenInfoStore.getDefaultTokenInfoSummary( + const defaultTokenInfo = tokenInfoStore.getDefaultTokenInfoSummary( network.NetworkId ); const selectedExplorer = - props.stores.explorers.selectedExplorer.get(network.NetworkId) ?? + explorers.selectedExplorer.get(network.NetworkId) ?? fail('No explorer for wallet network'); - let { openOrders, completedOrders } = useRichOrders(defaultTokenInfo); - - const txHashes = [...openOrders, ...completedOrders].map(o => o.txId); - noop(props.stores.substores.ada.swapStore.fetchTransactionTimestamps({ wallet, txHashes })); + const fetchTransactionTimestamps = txHashes => swapStore.fetchTransactionTimestamps({ wallet, txHashes }); + let { openOrders, completedOrders, transactionTimestamps } = useRichOrders(defaultTokenInfo, fetchTransactionTimestamps); const txHashToRenderedTimestamp: string => string = txHash => { - const date = props.stores.substores.ada.swapStore.transactionTimestamps[txHash]; + const date = transactionTimestamps[txHash]; return date == null ? '-' : moment(date).format('MMM D, YYYY H:mm'); }; const handleCancelRequest = async order => { setCancellationState({ order, tx: null }); try { - let utxoHex = await props.stores.substores.ada.swapStore.getCollateralUtxoHexForCancel({ + let utxoHex = await swapStore.getCollateralUtxoHexForCancel({ wallet, }); let collateralReorgTxHex: ?string = null; @@ -130,7 +130,7 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { unsignedTxHex, txData, collateralUtxoHex, - } = await props.stores.substores.ada.swapStore.createCollateralReorgForCancel({ wallet }); + } = await swapStore.createCollateralReorgForCancel({ wallet }); collateralReorgTxHex = unsignedTxHex; collateralReorgTxData = txData; utxoHex = collateralUtxoHex; @@ -174,7 +174,8 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { id: e.identifier, amount: e.amount.toString(), })), - order, + from: order.from, + to: order.to, defaultTokenInfo, }); const formattedFeeValue = Quantities.format( @@ -246,7 +247,7 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { signedCollateralReorgTx != null ? [signedCollateralReorgTx, signedCancelTx] : [signedCancelTx]; - await props.stores.substores.ada.swapStore.executeTransactionHexes({ + await swapStore.executeTransactionHexes({ wallet, signedTransactionHexes, }); @@ -328,7 +329,7 @@ export default function SwapOrdersPage(props: StoresAndActionsProps): Node { onCancelOrder={handleCancelConfirm} onDialogClose={() => setCancellationState(null)} defaultTokenInfo={defaultTokenInfo} - getTokenInfo={genLookupOrFail(props.stores.tokenInfoStore.tokenInfo)} + getTokenInfo={genLookupOrFail(tokenInfoStore.tokenInfo)} selectedExplorer={selectedExplorer} submissionError={null} walletType={walletVariant} diff --git a/packages/yoroi-extension/app/containers/swap/orders/hooks.js b/packages/yoroi-extension/app/containers/swap/orders/hooks.js index 713c52f9ea..a38b437532 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js @@ -6,11 +6,13 @@ import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; import { Quantities } from '../../../utils/quantities'; import { PRICE_PRECISION } from '../../../components/swap/common'; import { maybe } from '../../../coreUtils'; -import type { FormattedTokenValue } from './util'; +import type { FormattedTokenValue, OrderAsset } from './util'; import { createFormattedTokenValues } from './util'; function mapOrderAssets( - order: any, + from: OrderAsset, + to: OrderAsset, + valueAttached: ?any, defaultTokenInfo: RemoteTokenInfo ): {| price: string, @@ -19,26 +21,21 @@ function mapOrderAssets( from: any, to: any, |} { - const price = Quantities.quotient(order.from.quantity, order.to.quantity); - const fromDecimals = order.from.token?.decimals ?? 0; - const toDecimals = order.to.token?.decimals ?? 0; + const price = Quantities.quotient(from.quantity, from.quantity); + const fromDecimals = from.token?.decimals ?? 0; + const toDecimals = to.token?.decimals ?? 0; const priceDenomination = fromDecimals - toDecimals; const formattedPrice = Quantities.format(price, priceDenomination, PRICE_PRECISION); const formattedToQuantity = Quantities.format( - order.to.quantity, + to.quantity, toDecimals, toDecimals ); - const formattedAttachedValues = maybe(order.valueAttached, val => + const formattedAttachedValues = maybe(valueAttached, val => createFormattedTokenValues({ - entries: val.map(({ - token: id, - amount - }) => ({ - id, - amount - })), - order, + entries: val.map(({ token: id, amount }) => ({ id, amount })), + from, + to, defaultTokenInfo, }) ); @@ -46,8 +43,8 @@ function mapOrderAssets( price: formattedPrice, amount: formattedToQuantity, totalValues: formattedAttachedValues, - from: order.from, - to: order.to, + from, + to, }; } @@ -63,29 +60,19 @@ export type MappedOrder = {| to: any, |}; -function mapOpenOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { - const txId = order.utxo.split('#')[0]; - return { - txId, - utxo: order.utxo, - sender: order.sender, - provider: order.provider, - ...mapOrderAssets(order, defaultTokenInfo), - }; -} - -function mapCompletedOrder(order: any, defaultTokenInfo: RemoteTokenInfo): MappedOrder { - return { - txId: order.txHash, - ...mapOrderAssets(order, defaultTokenInfo), - }; -} - export function useRichOrders( - defaultTokenInfo: RemoteTokenInfo -): {| openOrders: Array, completedOrders: Array |} { + defaultTokenInfo: RemoteTokenInfo, + fetchTransactionTimestamps: (Array) => Promise<{ [string]: Date }>, +): {| + openOrders: Array, + completedOrders: Array, + transactionTimestamps: { [string]: Date }, +|} { const {order, tokens, stakingKey} = useSwap() + /** + * Fetch verified tokens list converted to map + */ const { data: tokensMap } = useQuery({ suspense: true, queryKey: ['useSwapTokensOnlyVerified'], @@ -97,8 +84,10 @@ export function useRichOrders( }), }); + /** + * Fetch open orders + */ const { data: openOrdersData } = useQuery({ - suspense: true, queryKey: ['useSwapOrdersByStatusOpen', stakingKey], queryFn: () => order.list.byStatusOpen().catch(e => { console.error('Failed to load open orders!', e); @@ -106,8 +95,10 @@ export function useRichOrders( }), }); + /** + * Fetch completed orders + */ const { data: completedOrdersData } = useQuery({ - suspense: true, queryKey: ['useSwapOrdersByStatusCompleted', stakingKey], queryFn: () => order.list.byStatusCompleted().catch(e => { console.error('Failed to load completed orders!', e); @@ -117,43 +108,63 @@ export function useRichOrders( const [openOrders, setOpenOrders] = useState>([]); const [completedOrders, setCompletedOrders] = useState>([]); + const [transactionTimestamps, setTransactionTimestamps] = useState<{ [string]: Date }>({}); + /** + * Filter open orders by verified tokens when both are fetched + */ useEffect(() => { if (tokensMap && openOrdersData) { setOpenOrders( openOrdersData.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; + const txId = (o.utxo.split('#')[0]); + const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; + const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; return { + txId: txId.toLowerCase(), utxo: o.utxo, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, batcherFee: o.batcherFee, - valueAttached: o.valueAttached, deposit: o.deposit, provider: o.provider, sender: o.sender, + ...mapOrderAssets(from, to, o.valueAttached, defaultTokenInfo), }; - }).map(o => mapOpenOrder(o, defaultTokenInfo)) + }) ); } }, [tokensMap, openOrdersData]); + /** + * Filter completed orders by verified tokens when both are fetched + */ useEffect(() => { if (tokensMap && completedOrdersData) { setCompletedOrders( completedOrdersData.map(o => { - const fromToken = tokensMap[o.from.tokenId]; - const toToken = tokensMap[o.to.tokenId]; + const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; + const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; return { - txHash: o.txHash, - from: { quantity: o.from.quantity, token: fromToken }, - to: { quantity: o.to.quantity, token: toToken }, + txId: o.txHash.toLowerCase(), + ...mapOrderAssets(from, to, null, defaultTokenInfo), }; - }).map(o => mapCompletedOrder(o, defaultTokenInfo)) + }) ); } - }, [tokensMap, completedOrdersData]) + }, [tokensMap, completedOrdersData]); + + /** + * Fetch missing transaction timestamps any time open or completed orders change + */ + useEffect(() => { + const txHashes = [...openOrders, ...completedOrders].map(o => o.txId); + const existingSet = new Set(Object.keys(transactionTimestamps)); + const filteredTxHashes = txHashes.filter(x => !existingSet.has(x)); + if (filteredTxHashes.length === 0) return; + fetchTransactionTimestamps(filteredTxHashes).then(newTimestamps => { + setTransactionTimestamps(state => ({ ...state, ...newTimestamps })); + return null; + }).catch(e => console.error('Failed to load transaction timestamps!', e)); + }, [openOrders, completedOrders]); - return { openOrders, completedOrders }; + return { openOrders, completedOrders, transactionTimestamps }; } diff --git a/packages/yoroi-extension/app/containers/swap/orders/util.js b/packages/yoroi-extension/app/containers/swap/orders/util.js index 6c9467f391..7f590dd2e7 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/util.js +++ b/packages/yoroi-extension/app/containers/swap/orders/util.js @@ -9,13 +9,17 @@ export type FormattedTokenValue = {| ticker: string, |}; +export type OrderAsset = {| token: {| id: string, decimals: number, ticker: ?string |}, quantity: string |}; + export function createFormattedTokenValues({ entries, - order, + from, + to, defaultTokenInfo, }: {| entries: Array<{| id: string, amount: string |}>, - order: any, + from: OrderAsset, + to: OrderAsset, defaultTokenInfo: RemoteTokenInfo, |}): Array { const tokenAmountMap = entries.reduce( @@ -35,7 +39,7 @@ export function createFormattedTokenValues({ ticker: defaultTokenInfo.ticker ?? '-', }, ]; - [order.from.token, order.to.token].forEach(t => { + [from.token, to.token].forEach(t => { if (t.id !== '' && t.id !== '.') { maybe(tokenAmountMap[t.id], v => { const formattedValue = Quantities.format(v, t.decimals, t.decimals); diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js index a266779076..7e493b0a83 100644 --- a/packages/yoroi-extension/app/stores/ada/SwapStore.js +++ b/packages/yoroi-extension/app/stores/ada/SwapStore.js @@ -3,7 +3,7 @@ import Store from '../base/Store'; import type { ActionsMap } from '../../actions'; import type { StoresMap } from '../index'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable } from 'mobx'; import type { StorageField } from '../../api/localStorage'; import { createStorageFlag, loadSubmittedTransactions } from '../../api/localStorage'; import { PublicDeriver } from '../../api/ada/lib/storage/models/PublicDeriver'; @@ -38,7 +38,6 @@ const FRONTEND_FEE_ADDRESS_PREPROD = export default class SwapStore extends Store { @observable orderStep: number = 0; - @observable transactionTimestamps: { [string]: Date } = {}; swapDisclaimerAcceptanceFlag: StorageField = createStorageFlag( 'SwapStore.swapDisclaimerAcceptanceFlag', @@ -229,27 +228,21 @@ export default class SwapStore extends Store { fetchTransactionTimestamps: ({| wallet: PublicDeriver<>, txHashes: Array, - |}) => Promise = async ({ + |}) => Promise<{ [string]: Date }> = async ({ wallet, txHashes, }) => { - console.log('fetchTransactionTimestamps'); - const existingSet = new Set(Object.keys(this.transactionTimestamps)); - const filteredTxHashes = txHashes.filter(x => !existingSet.has(x.toLowerCase())); - if (filteredTxHashes.length === 0) { - return; + if (txHashes.length === 0) { + return {}; } const network = wallet.getParent().getNetworkInfo(); const globalSlotMap: { [string]: string } = await this.stores.substores.ada.stateFetchStore.fetcher - .getTransactionSlotsByHashes({ network, txHashes: filteredTxHashes }); + .getTransactionSlotsByHashes({ network, txHashes }); const timeCalcRequests = this.stores.substores.ada.time.getTimeCalcRequests(wallet); const { toRealTime } = timeCalcRequests.requests; const slotToTimestamp: string => Date = s => toRealTime({ absoluteSlotNum: Number(s) }); - runInAction(() => { - for (const [tx,slot] of listEntries(globalSlotMap)) { - this.transactionTimestamps[tx.toLowerCase()] = slotToTimestamp(slot); - } - }); + return listEntries(globalSlotMap).reduce((res, [tx,slot]) => + ({ ...res, [tx.toLowerCase()]: slotToTimestamp(slot) }), ({}: { [string]: Date })) } } From aa1d51bbf6f0089272b40119f738e0eae83ff343 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Sat, 22 Jun 2024 21:24:39 +0300 Subject: [PATCH 13/21] render list key fix --- .../yoroi-extension/app/components/swap/CancelOrderDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js b/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js index 233e2aab34..77d03a7177 100644 --- a/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js +++ b/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js @@ -102,9 +102,9 @@ export default function CancelSwapOrderDialog({ > {transactionParams ? ( transactionParams.returnValues.map((v, index) => ( - <> + {index > 0 && ' +'} {v.formattedValue} {v.ticker} - + )) ) : ( From 5a297d3c74cd1e3e7aea85b4b3e2da235f84248b Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Sat, 22 Jun 2024 23:28:52 +0300 Subject: [PATCH 14/21] comments fixed --- packages/yoroi-extension/app/containers/swap/orders/hooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/orders/hooks.js b/packages/yoroi-extension/app/containers/swap/orders/hooks.js index a38b437532..10beafa823 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js @@ -111,7 +111,7 @@ export function useRichOrders( const [transactionTimestamps, setTransactionTimestamps] = useState<{ [string]: Date }>({}); /** - * Filter open orders by verified tokens when both are fetched + * Map open orders with verified tokens when both are fetched */ useEffect(() => { if (tokensMap && openOrdersData) { @@ -135,7 +135,7 @@ export function useRichOrders( }, [tokensMap, openOrdersData]); /** - * Filter completed orders by verified tokens when both are fetched + * Map completed orders with verified tokens when both are fetched */ useEffect(() => { if (tokensMap && completedOrdersData) { From b34de3e02d1fec9d913c5ac308eab8eb3dd4c50d Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Sun, 23 Jun 2024 19:57:27 +0300 Subject: [PATCH 15/21] useMemo --- .../app/containers/swap/orders/hooks.js | 85 +++++++++---------- packages/yoroi-extension/app/reactUtils.js | 36 ++++++++ 2 files changed, 76 insertions(+), 45 deletions(-) create mode 100644 packages/yoroi-extension/app/reactUtils.js diff --git a/packages/yoroi-extension/app/containers/swap/orders/hooks.js b/packages/yoroi-extension/app/containers/swap/orders/hooks.js index 10beafa823..b2312a70bf 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js @@ -1,13 +1,14 @@ //@flow import { useSwap, } from '@yoroi/swap'; import { useQuery } from 'react-query'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; import { Quantities } from '../../../utils/quantities'; import { PRICE_PRECISION } from '../../../components/swap/common'; -import { maybe } from '../../../coreUtils'; +import { maybe} from '../../../coreUtils'; import type { FormattedTokenValue, OrderAsset } from './util'; import { createFormattedTokenValues } from './util'; +import { useAsyncMemo } from '../../../reactUtils'; function mapOrderAssets( from: OrderAsset, @@ -106,65 +107,59 @@ export function useRichOrders( }), }); - const [openOrders, setOpenOrders] = useState>([]); - const [completedOrders, setCompletedOrders] = useState>([]); - const [transactionTimestamps, setTransactionTimestamps] = useState<{ [string]: Date }>({}); - /** * Map open orders with verified tokens when both are fetched */ - useEffect(() => { - if (tokensMap && openOrdersData) { - setOpenOrders( - openOrdersData.map(o => { - const txId = (o.utxo.split('#')[0]); - const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; - const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; - return { - txId: txId.toLowerCase(), - utxo: o.utxo, - batcherFee: o.batcherFee, - deposit: o.deposit, - provider: o.provider, - sender: o.sender, - ...mapOrderAssets(from, to, o.valueAttached, defaultTokenInfo), - }; - }) - ); - } + const openOrders: Array = useMemo(() => { + if (!tokensMap || !openOrdersData) return []; + return openOrdersData.map(o => { + const txId = (o.utxo.split('#')[0]); + const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; + const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; + return { + txId: txId.toLowerCase(), + utxo: o.utxo, + batcherFee: o.batcherFee, + deposit: o.deposit, + provider: o.provider, + sender: o.sender, + ...mapOrderAssets(from, to, o.valueAttached, defaultTokenInfo), + }; + }); }, [tokensMap, openOrdersData]); /** * Map completed orders with verified tokens when both are fetched */ - useEffect(() => { - if (tokensMap && completedOrdersData) { - setCompletedOrders( - completedOrdersData.map(o => { - const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; - const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; - return { - txId: o.txHash.toLowerCase(), - ...mapOrderAssets(from, to, null, defaultTokenInfo), - }; - }) - ); - } + const completedOrders: Array = useMemo(() => { + if (!tokensMap || !completedOrdersData) return []; + return completedOrdersData.map(o => { + const from = { quantity: o.from.quantity, token: tokensMap[o.from.tokenId] }; + const to = { quantity: o.to.quantity, token: tokensMap[o.to.tokenId] }; + return { + txId: o.txHash.toLowerCase(), + ...mapOrderAssets(from, to, null, defaultTokenInfo), + }; + }); }, [tokensMap, completedOrdersData]); /** * Fetch missing transaction timestamps any time open or completed orders change */ - useEffect(() => { + const transactionTimestamps = useAsyncMemo<{ [string]: Date }>(async () => { const txHashes = [...openOrders, ...completedOrders].map(o => o.txId); const existingSet = new Set(Object.keys(transactionTimestamps)); const filteredTxHashes = txHashes.filter(x => !existingSet.has(x)); - if (filteredTxHashes.length === 0) return; - fetchTransactionTimestamps(filteredTxHashes).then(newTimestamps => { - setTransactionTimestamps(state => ({ ...state, ...newTimestamps })); - return null; - }).catch(e => console.error('Failed to load transaction timestamps!', e)); - }, [openOrders, completedOrders]); + if (filteredTxHashes.length > 0) { + try { + const newTimestamps = await fetchTransactionTimestamps(filteredTxHashes); + return state => ({ ...state, ...newTimestamps }); + } catch (e) { + console.error('Failed to load transaction timestamps!', e); + } + } + return useAsyncMemo.void; + }, [openOrders, completedOrders], {}); return { openOrders, completedOrders, transactionTimestamps }; } diff --git a/packages/yoroi-extension/app/reactUtils.js b/packages/yoroi-extension/app/reactUtils.js new file mode 100644 index 0000000000..592b515602 --- /dev/null +++ b/packages/yoroi-extension/app/reactUtils.js @@ -0,0 +1,36 @@ +// @flow +import { useEffect, useState } from 'react'; + +/** + * Similar to `useMemo` hook but allows async functions. + * The result of the producer function will be sent straight into a React state updater, + * so it can be either a new value directly or an updater function that accepts the previous state. + * + * @param create - the async producer function, returns a promise of: either the new version of the value or an updater function + * @param inputs - effect inputs to react to + * @param defaultValue - the value which will be returned until the async resolves + * @return {T} - returns the supplied default value until the producer function resolves and then returns whatever it has done to the state + */ +export function useAsyncMemo(create: () => Promise T)>, inputs: any, defaultValue: T): T { + const [res, setRes] = useState(defaultValue); + useEffect(() => { + create().then(res => { + if (res === useAsyncMemo.void) { + // ignore the void return + // just a tiny optimisation + } else { + // update the state + setRes(res); + } + return null; + }); + }, inputs) + return res; +} + +/** + * The value that can be returned when the result of the async producer function in the `useAsyncMemo` should not change the existing value. + * + * This is just an identity function which means the existing React state will be preserved. + */ +useAsyncMemo.void = (x: T): T => x; \ No newline at end of file From 0b3f29f866a9c50993ea51ca965c6584652cab01 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 10:47:47 +0300 Subject: [PATCH 16/21] fixing swap reacting to asset inputs --- .../containers/swap/context/swap-form/SwapFormProvider.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 2d09f15de1..7dca1bba4e 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -101,6 +101,11 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { ...defaultSwapFormState, }); + const { + sellQuantity: { isTouched: sellTouched }, + buyQuantity: { isTouched: buyTouched }, + } = swapFormState; + const actions = { sellTouched: (token?: AssetAmount) => dispatch({ type: SwapFormActionTypeValues.SellTouched, token }), @@ -162,7 +167,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { .then(poolsArray => poolPairsChanged(poolsArray)) .catch(err => console.error(`Failed to fetch pools for pair: ${sellTokenId}/${buyTokenId}`, err)); } - }, [sellTokenId, buyTokenId]); + }, [sellTokenId, buyTokenId, sellTouched, buyTouched]); const clearErrors = useCallback(() => { if (swapFormState.sellQuantity.error != null) actions.sellAmountErrorChanged(null); From 4efd5db15db6816e16539efb5b3818a653c9de98 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 15:12:43 +0300 Subject: [PATCH 17/21] fixed asset updating effects in swap form --- .../containers/swap/asset-swap/SwapPage.js | 3 +- .../context/swap-form/SwapFormProvider.js | 50 +++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js index 5249a67914..ff084aa017 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js @@ -47,7 +47,7 @@ function SwapPage(props: StoresAndActionsProps): Node { }, frontendFeeTiersChanged, } = useSwap(); - const { sellTokenInfo, buyTokenInfo } = useSwapForm(); + const { sellTokenInfo, buyTokenInfo, resetSwapForm } = useSwapForm(); const isMarketOrder = orderType === 'market'; const impact = isMarketOrder ? Number(selectedPoolCalculation?.prices.priceImpact ?? 0) : 0; @@ -239,6 +239,7 @@ function SwapPage(props: StoresAndActionsProps): Node { refreshWallet: () => props.stores.wallets.refreshWalletFromRemote(wallet), }); setOrderStepValue(2); + resetSwapForm(); } catch (e) { handleTransactionError(e); } finally { diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index 7dca1bba4e..fa28429735 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -102,8 +102,8 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { }); const { - sellQuantity: { isTouched: sellTouched }, - buyQuantity: { isTouched: buyTouched }, + sellTokenInfo: { ticker: sellTicker }, + buyTokenInfo: { ticker: buyTicker }, } = swapFormState; const actions = { @@ -141,33 +141,43 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { dispatch({ type: SwapFormActionTypeValues.SellAmountErrorChanged, error }), }; - // on mount + /** + * On mount + */ + useEffect(() => actions.resetSwapForm(), []); + /** + * On unmount + */ + useEffect(() => () => actions.resetSwapForm(), []); + + /** + * On sell asset changes - set default asset in case none is selected + */ useEffect(() => { - // RESET - actions.resetSwapForm(); - // SELECT DEFAULT SELL - const assets = swapStore.assets; - const defaultAsset = assets[0]; - if (defaultAsset != null) { - actions.sellTouched({ ...defaultAsset }); - sellTokenInfoChanged({ - id: defaultAsset.id, - decimals: defaultAsset.decimals, - }); + if (sellTokenId === '' && sellTicker == null) { + // SELECT DEFAULT SELL + const assets = swapStore.assets; + const defaultAsset = assets[0]; + if (defaultAsset != null) { + actions.sellTouched({ ...defaultAsset }); + sellTokenInfoChanged({ + id: defaultAsset.id, + decimals: defaultAsset.decimals, + }); + } } - }, []); - - // on unmount - useEffect(() => () => actions.resetSwapForm(), []); + }, [sellTokenId, sellTicker]); - // on token pair changes + /** + * On token pair changes - fetch pools for pair + */ useEffect(() => { if (sellTokenId != null && buyTokenId != null && sellTokenId !== buyTokenId) { pools.list.byPair({ tokenA: sellTokenId, tokenB: buyTokenId }) .then(poolsArray => poolPairsChanged(poolsArray)) .catch(err => console.error(`Failed to fetch pools for pair: ${sellTokenId}/${buyTokenId}`, err)); } - }, [sellTokenId, buyTokenId, sellTouched, buyTouched]); + }, [sellTokenId, buyTokenId, sellTicker, buyTicker]); const clearErrors = useCallback(() => { if (swapFormState.sellQuantity.error != null) actions.sellAmountErrorChanged(null); From f129fde2d9e9e704656255a0e2a3ab1fb6c7f112 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 15:21:22 +0300 Subject: [PATCH 18/21] lint fixes --- packages/yoroi-extension/app/reactUtils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/yoroi-extension/app/reactUtils.js b/packages/yoroi-extension/app/reactUtils.js index 592b515602..f3481bdb37 100644 --- a/packages/yoroi-extension/app/reactUtils.js +++ b/packages/yoroi-extension/app/reactUtils.js @@ -14,16 +14,16 @@ import { useEffect, useState } from 'react'; export function useAsyncMemo(create: () => Promise T)>, inputs: any, defaultValue: T): T { const [res, setRes] = useState(defaultValue); useEffect(() => { - create().then(res => { - if (res === useAsyncMemo.void) { + create().then(r => { + if (r === useAsyncMemo.void) { // ignore the void return // just a tiny optimisation } else { // update the state - setRes(res); + setRes(r); } return null; - }); + }).catch(e => { throw e; }); }, inputs) return res; } From c17d110b5a6bc1b50a36a8f461dc87df782ac1be Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 18:41:19 +0300 Subject: [PATCH 19/21] swap input editing fix --- packages/yoroi-extension/app/components/swap/SwapInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yoroi-extension/app/components/swap/SwapInput.js b/packages/yoroi-extension/app/components/swap/SwapInput.js index ee6552c62c..ae433227df 100644 --- a/packages/yoroi-extension/app/components/swap/SwapInput.js +++ b/packages/yoroi-extension/app/components/swap/SwapInput.js @@ -40,7 +40,7 @@ export default function SwapInput({ const { id, amount: quantity = undefined, ticker } = tokenInfo || {}; const handleChange = e => { - if (!disabled && value !== quantity) { + if (!disabled) { handleAmountChange(e.target.value); } }; From fb87d4b7e1a809e909d3ea081690454d76fb61df Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 18:54:03 +0300 Subject: [PATCH 20/21] fix swap input error handling --- .../containers/swap/asset-swap/SwapPage.js | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js index ff084aa017..741669c5b8 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js @@ -47,7 +47,13 @@ function SwapPage(props: StoresAndActionsProps): Node { }, frontendFeeTiersChanged, } = useSwap(); - const { sellTokenInfo, buyTokenInfo, resetSwapForm } = useSwapForm(); + const { + sellTokenInfo, + buyTokenInfo, + resetSwapForm, + sellQuantity, + buyQuantity, + } = useSwapForm(); const isMarketOrder = orderType === 'market'; const impact = isMarketOrder ? Number(selectedPoolCalculation?.prices.priceImpact ?? 0) : 0; @@ -71,17 +77,20 @@ function SwapPage(props: StoresAndActionsProps): Node { ); const swapFormCanContinue = - selectedPoolCalculation != null && - sell.quantity !== '0' && - buy.quantity !== '0' && - isValidTickers; + selectedPoolCalculation != null + && sell.quantity !== '0' + && buy.quantity !== '0' + && sellQuantity.error == null + && buyQuantity.error == null + && isValidTickers; const confirmationCanContinue = userPasswordState.value !== '' && signRequest != null; const isButtonLoader = orderStep === 1 && signRequest == null; const isSwapEnabled = - (orderStep === 0 && swapFormCanContinue) || (orderStep === 1 && confirmationCanContinue); + (orderStep === 0 && swapFormCanContinue) + || (orderStep === 1 && confirmationCanContinue); const wallet = props.stores.wallets.selectedOrFail; const network = wallet.getParent().getNetworkInfo(); From af8f0cd4346eb1caed6532c7f6f62eea9047af39 Mon Sep 17 00:00:00 2001 From: vantuz-subhuman Date: Mon, 24 Jun 2024 19:00:22 +0300 Subject: [PATCH 21/21] Version bump: 5.2.002 (nightly) --- packages/yoroi-extension/package-lock.json | 4 ++-- packages/yoroi-extension/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/yoroi-extension/package-lock.json b/packages/yoroi-extension/package-lock.json index 8682704c36..2abac3993f 100644 --- a/packages/yoroi-extension/package-lock.json +++ b/packages/yoroi-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "yoroi", - "version": "5.2.001", + "version": "5.2.002", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yoroi", - "version": "5.2.001", + "version": "5.2.002", "license": "MIT", "dependencies": { "@amplitude/analytics-browser": "^2.1.3", diff --git a/packages/yoroi-extension/package.json b/packages/yoroi-extension/package.json index b7528dc5b0..d099709fd2 100644 --- a/packages/yoroi-extension/package.json +++ b/packages/yoroi-extension/package.json @@ -1,6 +1,6 @@ { "name": "yoroi", - "version": "5.2.001", + "version": "5.2.002", "description": "Cardano ADA wallet", "scripts": { "dev-mv2": "rimraf dev/ && NODE_OPTIONS=--openssl-legacy-provider babel-node scripts-mv2/build --type=debug --env 'mainnet'",