diff --git a/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js b/packages/yoroi-extension/app/components/swap/CancelOrderDialog.js
index 61c3f629f6..77d03a7177 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,
@@ -102,9 +102,9 @@ export default function CancelSwapOrderDialog({
>
{transactionParams ? (
transactionParams.returnValues.map((v, index) => (
- <>
+
{index > 0 && ' +'} {v.formattedValue} {v.ticker}
- >
+
))
) : (
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);
}
};
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..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 } = 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();
@@ -239,6 +248,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 2d09f15de1..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
@@ -101,6 +101,11 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node {
...defaultSwapFormState,
});
+ const {
+ sellTokenInfo: { ticker: sellTicker },
+ buyTokenInfo: { ticker: buyTicker },
+ } = swapFormState;
+
const actions = {
sellTouched: (token?: AssetAmount) =>
dispatch({ type: SwapFormActionTypeValues.SellTouched, token }),
@@ -136,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,
+ });
+ }
}
- }, []);
+ }, [sellTokenId, sellTicker]);
- // on unmount
- useEffect(() => () => actions.resetSwapForm(), []);
-
- // 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]);
+ }, [sellTokenId, buyTokenId, sellTicker, buyTicker]);
const clearErrors = useCallback(() => {
if (swapFormState.sellQuantity.error != null) actions.sellAmountErrorChanged(null);
diff --git a/packages/yoroi-extension/app/containers/swap/hooks.js b/packages/yoroi-extension/app/containers/swap/hooks.js
index 2a6c4d8df6..cf059361fc 100644
--- a/packages/yoroi-extension/app/containers/swap/hooks.js
+++ b/packages/yoroi-extension/app/containers/swap/hooks.js
@@ -1,9 +1,6 @@
//@flow
import {
useSwap,
- useSwapOrdersByStatusCompleted,
- useSwapOrdersByStatusOpen,
- useSwapTokensOnlyVerified,
} from '@yoroi/swap';
import { Quantities } from '../../utils/quantities';
import { useSwapForm } from './context/swap-form';
@@ -72,72 +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 [];
- }
-}
diff --git a/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js b/packages/yoroi-extension/app/containers/swap/orders/OrdersPage.js
index 388a1a96d2..7b5db97f39 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 } 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 { 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';
-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,119 +82,9 @@ 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: { cancel: swapCancelOrder },
+ order: orderApi,
} = useSwap();
const [showCompletedOrders, setShowCompletedOrders] = useState(false);
@@ -207,32 +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');
- const openOrders = useRichOpenOrders().map(o => mapOpenOrder(o, defaultTokenInfo));
- const completedOrders = useRichCompletedOrders().map(o => mapCompletedOrder(o, 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;
@@ -242,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;
@@ -270,7 +158,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,
@@ -286,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(
@@ -358,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,
});
@@ -440,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
new file mode 100644
index 0000000000..b2312a70bf
--- /dev/null
+++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js
@@ -0,0 +1,165 @@
+//@flow
+import { useSwap, } from '@yoroi/swap';
+import { useQuery } from 'react-query';
+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 type { FormattedTokenValue, OrderAsset } from './util';
+import { createFormattedTokenValues } from './util';
+import { useAsyncMemo } from '../../../reactUtils';
+
+function mapOrderAssets(
+ from: OrderAsset,
+ to: OrderAsset,
+ valueAttached: ?any,
+ defaultTokenInfo: RemoteTokenInfo
+): {|
+ price: string,
+ amount: string,
+ totalValues: ?Array,
+ from: any,
+ to: any,
+|} {
+ 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(
+ to.quantity,
+ toDecimals,
+ toDecimals
+ );
+ const formattedAttachedValues = maybe(valueAttached, val =>
+ createFormattedTokenValues({
+ entries: val.map(({ token: id, amount }) => ({ id, amount })),
+ from,
+ to,
+ defaultTokenInfo,
+ })
+ );
+ return {
+ price: formattedPrice,
+ amount: formattedToQuantity,
+ totalValues: formattedAttachedValues,
+ from,
+ to,
+ };
+}
+
+export type MappedOrder = {|
+ txId: string,
+ utxo?: string,
+ sender?: string,
+ provider?: string,
+ price: string,
+ amount: string,
+ totalValues: ?Array,
+ from: any,
+ to: any,
+|};
+
+export function useRichOrders(
+ 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'],
+ 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;
+ }),
+ });
+
+ /**
+ * Fetch open orders
+ */
+ const { data: openOrdersData } = useQuery({
+ queryKey: ['useSwapOrdersByStatusOpen', stakingKey],
+ queryFn: () => order.list.byStatusOpen().catch(e => {
+ console.error('Failed to load open orders!', e);
+ throw e;
+ }),
+ });
+
+ /**
+ * Fetch completed orders
+ */
+ const { data: completedOrdersData } = useQuery({
+ queryKey: ['useSwapOrdersByStatusCompleted', stakingKey],
+ queryFn: () => order.list.byStatusCompleted().catch(e => {
+ console.error('Failed to load completed orders!', e);
+ throw e;
+ }),
+ });
+
+ /**
+ * Map open orders with verified tokens when both are fetched
+ */
+ 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
+ */
+ 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
+ */
+ 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) {
+ 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/containers/swap/orders/util.js b/packages/yoroi-extension/app/containers/swap/orders/util.js
new file mode 100644
index 0000000000..7f590dd2e7
--- /dev/null
+++ b/packages/yoroi-extension/app/containers/swap/orders/util.js
@@ -0,0 +1,55 @@
+// @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 type OrderAsset = {| token: {| id: string, decimals: number, ticker: ?string |}, quantity: string |};
+
+export function createFormattedTokenValues({
+ entries,
+ from,
+ to,
+ defaultTokenInfo,
+}: {|
+ entries: Array<{| id: string, amount: string |}>,
+ from: OrderAsset,
+ to: OrderAsset,
+ 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 ?? '-',
+ },
+ ];
+ [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);
+ formattedTokenValues.push({
+ value: v,
+ formattedValue,
+ ticker: t.ticker ?? '-',
+ });
+ });
+ }
+ });
+ return formattedTokenValues;
+}
\ No newline at end of file
diff --git a/packages/yoroi-extension/app/reactUtils.js b/packages/yoroi-extension/app/reactUtils.js
new file mode 100644
index 0000000000..f3481bdb37
--- /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(r => {
+ if (r === useAsyncMemo.void) {
+ // ignore the void return
+ // just a tiny optimisation
+ } else {
+ // update the state
+ setRes(r);
+ }
+ return null;
+ }).catch(e => { throw e; });
+ }, 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
diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js
index d538ac620d..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,26 +228,21 @@ export default class SwapStore extends Store {
fetchTransactionTimestamps: ({|
wallet: PublicDeriver<>,
txHashes: Array,
- |}) => Promise = async ({
+ |}) => Promise<{ [string]: Date }> = async ({
wallet,
txHashes,
}) => {
- 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 }))
}
}
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'",