Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup exchange #528

Merged
merged 3 commits into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 28 additions & 47 deletions src/components/TdexOrderInput/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ export function createAmountAndUnit(assetsRegistry: Record<string, AssetConfig>,
};
}

// calculate price is a wrapper of marketPrice rpc call
// it returns the price of the asset in sats
const calculatePrice =
(sats: number, asset: string) =>
async (order: TradeOrder): Promise<SatsAsset> =>
marketPriceRequest(order, sats, asset);

// custom state hook using to represent an asset/sats pair
function useAssetSats(initialAssetHash?: string) {
const [assetHash, setAssetHash] = useState<string | undefined>(initialAssetHash);
Expand Down Expand Up @@ -78,56 +71,44 @@ export function useTradeState(markets: TDEXMarket[]) {
resetErrors();
};

const discoverFunction = () => {
try {
return discoverBestOrder(markets, sendAsset, receiveAsset);
} catch (err) {
console.error('Best order discovery error', err);
throw err;
}
};

const computePriceAndUpdate =
(sats: number, asset: string, type: 'send' | 'receive') => async (order: TradeOrder) => {
const assetSats = await calculatePrice(sats, asset)(order);
if (type === 'send') {
setReceiveSats(assetSats.sats);
} else {
setSendSats(assetSats.sats);
}
return order;
};

const updateReceiveSats = (newSendSats: number) => {
const updateReceiveSats = async (newSendSats: number) => {
if (receiveAssetHasChanged) {
setReceiveAssetHasChanged(false);
return;
}
if (receiveLoader || !sendAsset || (focus === 'receive' && !hasBeenSwapped && !sendAssetHasChanged)) return;
setReceiveLoader(true);
return discoverFunction()(newSendSats ?? 0, sendAsset)
.then(computePriceAndUpdate(newSendSats ?? 0, sendAsset, 'send')) // set receive sats
.then((tradeOrder: TradeOrder) => {
setBestOrder(tradeOrder);
resetErrors();
})
.catch(setSendError)
.finally(() => setReceiveLoader(false));
try {
setReceiveLoader(true);
const bestOrder = await discoverBestOrder(markets, sendAsset, receiveAsset)(newSendSats ?? 0, sendAsset);
const assetSats = await marketPriceRequest(bestOrder, newSendSats ?? 0, sendAsset);
setReceiveSats(assetSats.sats);
setBestOrder(bestOrder);
resetErrors();
} catch (err) {
console.error(err);
setSendError(err as any);
} finally {
setReceiveLoader(false);
}
};

const updateSendSats = (newReceiveSats: number) => {
const updateSendSats = async (newReceiveSats: number) => {
if (!receiveAsset || (focus === 'send' && !receiveAssetHasChanged) || (focus === 'receive' && hasBeenSwapped)) {
return;
}
setSendLoader(true);
return discoverFunction()(newReceiveSats ?? 0, receiveAsset)
.then(computePriceAndUpdate(newReceiveSats ?? 0, receiveAsset, 'receive')) // set send sats
.then((tradeOrder: TradeOrder) => {
setBestOrder(tradeOrder);
resetErrors();
})
.catch(setReceiveError)
.finally(() => setSendLoader(false));
try {
setSendLoader(true);
const bestOrder = await discoverBestOrder(markets, sendAsset, receiveAsset)(newReceiveSats ?? 0, receiveAsset);
const assetSats = await marketPriceRequest(bestOrder, newReceiveSats ?? 0, receiveAsset);
setSendSats(assetSats.sats);
setBestOrder(bestOrder);
resetErrors();
} catch (err) {
console.error(err);
setReceiveError(err as any);
} finally {
setSendLoader(false);
}
};

// send sats setter
Expand Down
2 changes: 1 addition & 1 deletion src/components/TdexOrderInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const TdexOrderInput: React.FC<Props> = ({
// Set receive asset to first tradable asset
const tradableAssets = getTradablesAssets(markets, sendAsset);
setReceiveAsset(tradableAssets[0]);
});
}, [markets, sendAsset]);

useIonViewDidLeave(() => {
setAccessoryBar(false).catch(console.error);
Expand Down
124 changes: 60 additions & 64 deletions src/pages/Exchange/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ import {
IonLabel,
IonIcon,
IonAlert,
useIonAlert,
useIonViewWillEnter,
IonSpinner,
} from '@ionic/react';
import classNames from 'classnames';
import { closeOutline } from 'ionicons/icons';
import type { StateRestorerOpts } from 'ldk';
import { mnemonicRestorerFromState } from 'ldk';
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import type { RouteComponentProps } from 'react-router';
import type { NetworkString, UnblindedOutput } from 'tdex-sdk';
Expand Down Expand Up @@ -83,7 +81,6 @@ const Exchange: React.FC<Props> = ({
const dispatch = useTypedDispatch();
const [tdexOrderInputResult, setTdexOrderInputResult] = useState<TdexOrderInputResult>();
const [excludedProviders, setExcludedProviders] = useState<TDEXProvider[]>([]);
const [presentNoProvidersAvailableAlert, dismissNoProvidersAvailableAlert] = useIonAlert();
const [showExcludedProvidersAlert, setShowExcludedProvidersAlert] = useState(false);
const [tradeError, setTradeError] = useState<AppError>();

Expand Down Expand Up @@ -112,7 +109,11 @@ const Exchange: React.FC<Props> = ({
return true;
};

const getAllMarketsFromNotExcludedProviders = () => markets.filter(withoutProviders(...excludedProviders));
const getAllMarketsFromNotExcludedProviders = useCallback(
() => markets.filter(withoutProviders(...excludedProviders)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[markets]
);

const getAllMarketsFromNotExcludedProvidersAndOnlySelectedPair = (providerToBan: TDEXProvider) => {
return markets
Expand All @@ -127,13 +128,17 @@ const Exchange: React.FC<Props> = ({
const getAllProvidersExceptExcluded = () =>
providers.filter((p) => !excludedProviders.map((p) => p.endpoint).includes(p.endpoint));

useIonViewWillEnter(() => {
// Prevent entering Exchange page if no provider or no markets
// We may have the default provider set but no markets, so we need to check both
if (providers.length === 0 || markets.length === 0) {
openNoProvidersAvailableAlert();
const [showNoProvidersAvailableAlert, setShowNoProvidersAvailableAlert] = useState<boolean>(
!!getAllMarketsFromNotExcludedProviders().length
);

useEffect(() => {
if (getAllMarketsFromNotExcludedProviders().length > 0) {
setShowNoProvidersAvailableAlert(false);
} else {
setShowNoProvidersAvailableAlert(true);
}
}, [providers]);
}, [getAllMarketsFromNotExcludedProviders]);

const [
bestOrder,
Expand All @@ -158,40 +163,6 @@ const Exchange: React.FC<Props> = ({
setReceiveAssetHasChanged,
] = useTradeState(getAllMarketsFromNotExcludedProviders());

const openNoProvidersAvailableAlert = () => {
if (history.location.pathname === routerLinks.exchange) {
presentNoProvidersAvailableAlert({
header: 'No providers available',
message:
'Liquidity providers on Tor network can take a long time to respond or all your providers are offline.',
backdropDismiss: false,
buttons: [
{
text: 'Go To Wallet',
handler: () => {
dismissNoProvidersAvailableAlert()
.then(() => {
history.replace(routerLinks.wallet);
})
.catch(console.error);
},
},
{
text: 'Retry',
handler: () => {
dispatch(updateMarkets());
if (getAllMarketsFromNotExcludedProviders().length === 0 || tdexOrderInputResult === undefined) {
dismissNoProvidersAvailableAlert().then(() => {
openNoProvidersAvailableAlert();
});
}
},
},
],
}).catch(console.error);
}
};

const getIdentity = async (pin: string) => {
try {
const toRestore = await getConnectedTDexMnemonic(pin, dispatch, network);
Expand Down Expand Up @@ -297,13 +268,18 @@ const Exchange: React.FC<Props> = ({
isFetchingMarkets &&
!isBusyMakingTrade &&
!tradeError &&
getAllProvidersExceptExcluded().length > 0
getAllProvidersExceptExcluded().length > 0 &&
// At least one market is available, otherwise we display NoProvidersAvailableAlert
markets.length > 0
}
message="Discovering TDEX providers with best liquidity..."
delay={0}
backdropDismiss={true}
duration={15000}
onDidDismiss={() => providers.length === 0 && openNoProvidersAvailableAlert()}
onDidDismiss={() => {
if (providers.length === 0 || getAllMarketsFromNotExcludedProviders().length === 0)
setShowNoProvidersAvailableAlert(true);
}}
/>
<ExchangeErrorModal
result={tdexOrderInputResult}
Expand All @@ -312,22 +288,42 @@ const Exchange: React.FC<Props> = ({
onClickRetry={() => tdexOrderInputResult !== undefined && setPINModalOpen(true)}
onClickTryNext={onClickTryNext}
/>

{getAllMarketsFromNotExcludedProviders().length > 0 && (
<PinModal
open={tdexOrderInputResult !== undefined && PINModalOpen}
title="Unlock your seed"
description={getPinModalDescription()}
onConfirm={onPinConfirm}
onClose={() => {
setPINModalOpen(false);
}}
isWrongPin={isWrongPin}
needReset={needReset}
setNeedReset={setNeedReset}
setIsWrongPin={setIsWrongPin}
/>
)}
<PinModal
open={tdexOrderInputResult !== undefined && PINModalOpen}
title="Unlock your seed"
description={getPinModalDescription()}
onConfirm={onPinConfirm}
onClose={() => {
setPINModalOpen(false);
}}
isWrongPin={isWrongPin}
needReset={needReset}
setNeedReset={setNeedReset}
setIsWrongPin={setIsWrongPin}
/>
<IonAlert
isOpen={showNoProvidersAvailableAlert && history.location.pathname === routerLinks.exchange}
header="No providers available"
message="Liquidity providers on Tor network can take a long time to respond or all your providers are offline."
backdropDismiss={false}
buttons={[
{
text: 'Go To Wallet',
handler: () => {
history.replace(routerLinks.wallet);
},
},
{
text: 'Retry',
handler: () => {
dispatch(updateMarkets());
// false then true to trigger rerender if already true
setShowNoProvidersAvailableAlert(false);
setShowNoProvidersAvailableAlert(true);
},
},
]}
/>

{getAllMarketsFromNotExcludedProviders().length > 0 && (
<IonContent className="exchange-content">
Expand Down Expand Up @@ -422,7 +418,7 @@ const Exchange: React.FC<Props> = ({
</IonCol>
</IonRow>

{tdexOrderInputResult && (
{tdexOrderInputResult && sendSats !== 0 && (
<IonRow className="market-provider ion-margin-vertical-x2 ion-text-center">
<IonCol size="10" offset="1">
<IonText className="trade-info" color="light">
Expand Down
9 changes: 9 additions & 0 deletions src/redux/actions/tdexActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ export const CLEAR_PROVIDERS = 'CLEAR_PROVIDERS';
export const UPDATE_MARKETS = 'UPDATE_MARKETS';
export const ADD_MARKETS = 'ADD_MARKETS';
export const CLEAR_MARKETS = 'CLEAR_MARKETS';
export const REPLACE_MARKETS_OF_PROVIDER = 'REPLACE_MARKETS_OF_PROVIDER';

export const clearMarkets = (): ActionType => ({
type: CLEAR_MARKETS,
});

export const replaceMarketsOfProvider = (
providerToUpdate: TDEXProvider,
markets: TDEXMarket[]
): ActionType<{ providerToUpdate: TDEXProvider; markets: TDEXMarket[] }> => ({
type: REPLACE_MARKETS_OF_PROVIDER,
payload: { providerToUpdate, markets },
});

export const addMarkets = (markets: TDEXMarket[]): ActionType<TDEXMarket[]> => {
return {
type: ADD_MARKETS,
Expand Down
16 changes: 15 additions & 1 deletion src/redux/reducers/tdexReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { createSelector } from 'reselect';

import type { ActionType } from '../../utils/types';
import type { TDEXMarket, TDEXProvider } from '../actionTypes/tdexActionTypes';
import { ADD_MARKETS, ADD_PROVIDERS, CLEAR_MARKETS, CLEAR_PROVIDERS, DELETE_PROVIDER } from '../actions/tdexActions';
import {
ADD_MARKETS,
ADD_PROVIDERS,
CLEAR_MARKETS,
REPLACE_MARKETS_OF_PROVIDER,
CLEAR_PROVIDERS,
DELETE_PROVIDER,
} from '../actions/tdexActions';
import type { RootState } from '../types';

export interface TDEXState {
Expand All @@ -22,6 +29,13 @@ const TDEXReducer = (
switch (action.type) {
case ADD_MARKETS:
return { ...state, markets: [...state.markets, ...action.payload] };
case REPLACE_MARKETS_OF_PROVIDER: {
// Remove markets of provider received in arg
const marketsWithoutProviderToUpdate = state.markets.filter(
(market) => market.provider.endpoint !== (action.payload.providerToUpdate as TDEXProvider).endpoint
);
return { ...state, markets: [...marketsWithoutProviderToUpdate, ...action.payload.markets] };
}
case CLEAR_MARKETS:
return { ...state, markets: [] };
case ADD_PROVIDERS: {
Expand Down
Loading