Skip to content

Commit

Permalink
chore(trading): add buy middleware and thunks
Browse files Browse the repository at this point in the history
  • Loading branch information
adderpositive committed Feb 10, 2025
1 parent 2f6dda3 commit 012bb64
Show file tree
Hide file tree
Showing 19 changed files with 328 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState } from 'react';

import { INVITY_API_RELOAD_QUOTES_AFTER_SECONDS } from '@suite-common/trading';
import { useTimer } from '@trezor/react-utils';

import { INVITY_API_RELOAD_QUOTES_AFTER_SECONDS } from 'src/constants/wallet/trading/metadata';
import { useDevice } from 'src/hooks/suite';
import { useServerEnvironment } from 'src/hooks/wallet/trading/useServerEnviroment';
import { UseTradingCommonProps, UseTradingCommonReturnProps } from 'src/types/trading/trading';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { TradingType } from '@suite-common/trading';
import { type TradingType, getTradeTypeByRoute } from '@suite-common/trading';

import { useSelector } from 'src/hooks/suite';
import { selectRouter } from 'src/reducers/suite/routerReducer';
import { getTradeTypeByRoute } from 'src/utils/wallet/trading/tradingUtils';

export const useTradingPreviousRoute = (tradeType: TradingType) => {
const {
Expand Down
8 changes: 5 additions & 3 deletions packages/suite/src/middlewares/wallet/tradingMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { MiddlewareAPI } from 'redux';

import { invityAPI } from '@suite-common/trading';
import {
INVITY_API_RELOAD_DATA_AFTER_MS,
getTradeTypeByRoute,
invityAPI,
} from '@suite-common/trading';
import { UI } from '@trezor/connect';

import { MODAL, ROUTER } from 'src/actions/suite/constants';
Expand All @@ -10,9 +14,7 @@ import * as tradingBuyActions from 'src/actions/wallet/tradingBuyActions';
import * as tradingExchangeActions from 'src/actions/wallet/tradingExchangeActions';
import * as tradingInfoAction from 'src/actions/wallet/tradingInfoActions';
import * as tradingSellActions from 'src/actions/wallet/tradingSellActions';
import { INVITY_API_RELOAD_DATA_AFTER_MS } from 'src/constants/wallet/trading/metadata';
import { Action, AppState, Dispatch } from 'src/types/suite';
import { getTradeTypeByRoute } from 'src/utils/wallet/trading/tradingUtils';

/**
* In the Sell and Swap section an account can be changed by a user in the select
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import styled from 'styled-components';

import { INVITY_API_RELOAD_QUOTES_AFTER_SECONDS } from '@suite-common/trading';
import { H2 } from '@trezor/components';
import { SCREEN_QUERY } from '@trezor/components/src/config/variables';
import { spacingsPx } from '@trezor/theme';

import { Translation } from 'src/components/suite';
import { INVITY_API_RELOAD_QUOTES_AFTER_SECONDS } from 'src/constants/wallet/trading/metadata';
import { useTradingFormContext } from 'src/hooks/wallet/trading/form/useTradingCommonForm';
import { ExtendedMessageDescriptor } from 'src/types/suite';
import {
Expand Down
1 change: 1 addition & 0 deletions suite-common/trading/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@reduxjs/toolkit": "1.9.5",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-common/wallet-types": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/env-utils": "workspace:*",
Expand Down
3 changes: 1 addition & 2 deletions suite-common/trading/src/actions/buyActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BuyTrade, BuyTradeQuoteRequest } from 'invity-api';
import { createAction } from '@reduxjs/toolkit';
import { BuyTrade, BuyTradeQuoteRequest } from 'invity-api';

import { BuyInfo } from '../reducers/buyReducer';

Expand Down Expand Up @@ -38,7 +38,6 @@ const saveSelectedQuote = createAction(SAVE_QUOTE, (payload: BuyTrade | undefine

const clearQuotes = createAction(CLEAR_QUOTES);

// TODO: verifyAddress thunk
const verifyAddress = createAction(VERIFY_ADDRESS, (payload: string | undefined) => ({ payload }));

export const tradingBuyActions = {
Expand Down
8 changes: 4 additions & 4 deletions suite-common/trading/src/actions/tradingActions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createAction } from '@reduxjs/toolkit';
import { CryptoId, InfoResponse } from 'invity-api';

import { TradingPaymentMethodListProps, TradingTransaction, TradingType } from '../types';
import { TradingComposedTransactionInfo } from '../reducers/tradingReducer';
import { TradingPaymentMethodListProps, TradingTransaction, TradingType } from '../types';

// TODO: maybe move to constants
const TRADING_COMMON_PREFIX = '@trading-common';
Expand Down Expand Up @@ -44,10 +44,10 @@ const loadInvityData = createAction(LOAD_DATA);

const setLoading = createAction(
SET_LOADING,
(payload: { isLoading: boolean; lastLoadedTimestamp?: number }) => ({
(isLoading: boolean, lastLoadedTimestamp?: number) => ({
payload: {
...payload,
lastLoadedTimestamp: payload.lastLoadedTimestamp ?? 0,
isLoading,
lastLoadedTimestamp: lastLoadedTimestamp ?? 0,
},
}),
);
Expand Down
3 changes: 3 additions & 0 deletions suite-common/trading/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const INVITY_API_RELOAD_DATA_AFTER_MS = 10 * 60 * 1000; // 10 minutes
export const INVITY_API_RELOAD_QUOTES_AFTER_SECONDS = 30;

export const CRYPTO_PLATFORM_SEPARATOR = '--';
/**
* Used for for L2 networks (e.g. base, op)
Expand Down
105 changes: 105 additions & 0 deletions suite-common/trading/src/middlewares/tradingMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { createMiddleware } from '@suite-common/redux-utils';

import { tradingBuyActions } from '../actions/buyActions';
import * as tradingActions from '../actions/tradingActions';
import { INVITY_API_RELOAD_DATA_AFTER_MS } from '../constants';
import { invityAPI } from '../invityAPI';
import { TradingRootState, selectState } from '../selectors/tradingSelectors';
import { buyThunks } from '../thunks/buyThunks';
import { getTradeTypeByRoute } from '../utils';

const LOCATION_CHANGE = '@router/location-change';

/**
* In the Sell and Swap section an account can be changed by a user in the select
*/
export const getAccountAccordingToRoute = (state: TradingRootState) => {
const {
selectedAccount: { account },
// accounts,
} = state.wallet;

return account;
};

export const tradingMiddleware = createMiddleware(async (action, { dispatch, next, getState }) => {
const state = selectState(getState());
const { isLoading, lastLoadedTimestamp } = state.wallet.trading;
const isRouteChange = action.type === LOCATION_CHANGE;

if (action.type === tradingActions.LOAD_DATA) {
const account = getAccountAccordingToRoute(state);
const { platforms, coins } = state.wallet.trading.info;
const { buyInfo } = state.wallet.trading.buy;

const currentAccountDescriptor = invityAPI.getCurrentAccountDescriptor();
const isDifferentAccount = currentAccountDescriptor !== account?.descriptor;

if (
account &&
!isLoading &&
(isDifferentAccount ||
lastLoadedTimestamp + INVITY_API_RELOAD_DATA_AFTER_MS < Date.now())
) {
dispatch(tradingActions.tradingActions.setLoading(true));

const { invityServerEnvironment } = state.suite.settings.debug;
if (invityServerEnvironment) {
invityAPI.setInvityServersEnvironment(invityServerEnvironment);
}

const tradeType = getTradeTypeByRoute(state.router.route?.name);
if (tradeType) {
dispatch(tradingActions.tradingActions.setTradingActiveSection(tradeType));
}

invityAPI.createInvityAPIKey(account.descriptor);

if (isDifferentAccount || !platforms || !coins) {
const info = await invityAPI.getInfo();

dispatch(tradingActions.tradingActions.saveInfo(info));
}

if (isDifferentAccount || !buyInfo) {
const buyInfoData = await dispatch(buyThunks.loadInfoThunk()).unwrap();

dispatch(tradingBuyActions.saveBuyInfo(buyInfoData));
}

dispatch(tradingActions.tradingActions.setLoading(false, Date.now()));
}
}

next(action);

// get the new state after the action has been processed
const newState = selectState(getState());

// TODO: native route change control?
if (isRouteChange) {
const routeName = newState.router.route?.name;
const isBuy = routeName === 'wallet-trading-buy';
const isSell = routeName === 'wallet-trading-sell';
const isExchange = routeName === 'wallet-trading-exchange';

if (isBuy) {
dispatch(tradingActions.tradingActions.setTradingActiveSection('buy'));
}

const wasBuy = state.router.route?.name === 'wallet-trading-buy';
const wasSell = state.router.route?.name === 'wallet-trading-sell';
const isBuyToSell = wasBuy && isSell;
const isSellToBuy = wasSell && isBuy;

const cleanupPrefilledFromCryptoId =
!!newState.wallet.trading.prefilledFromCryptoId &&
((!isSell && !isExchange && !isBuy) || isBuyToSell || isSellToBuy);

if (cleanupPrefilledFromCryptoId) {
dispatch(tradingActions.tradingActions.setTradingFromPrefilledCryptoId(undefined));
}
}

return action;
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BuyTrade, BuyTradeQuoteRequest, CryptoId } from 'invity-api';

import { BuyInfo, TradingBuyState } from '../buyReducer';
import * as BUY_TRADING_ACTIONS from '../../actions/buyActions';
import { BuyInfo, TradingBuyState } from '../buyReducer';

export const buyInitialState: TradingBuyState = {
transactionId: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { combineReducers } from '@reduxjs/toolkit';

import { configureMockStore, extraDependenciesMock } from '@suite-common/test-utils';

import { prepareTradingReducer } from '../tradingReducer';
import { tradingFixtures } from '../__fixtures__/tradingReducer';
import { prepareTradingReducer } from '../tradingReducer';

const tradingReducer = prepareTradingReducer(extraDependenciesMock);

Expand Down
2 changes: 1 addition & 1 deletion suite-common/trading/src/reducers/buyReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {

import { createReducerWithExtraDeps } from '@suite-common/redux-utils';

import type { TradingFiatCurrenciesProps } from '../types';
import { tradingBuyActions } from '../actions/buyActions';
import type { TradingFiatCurrenciesProps } from '../types';

// TODO: refactor Map and Set https://redux.js.org/style-guide/#do-not-put-non-serializable-values-in-state-or-actions
export interface BuyInfo {
Expand Down
8 changes: 4 additions & 4 deletions suite-common/trading/src/reducers/tradingReducer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Coins, CryptoId, Platforms } from 'invity-api';
import { AnyAction } from '@reduxjs/toolkit';
import { Coins, CryptoId, Platforms } from 'invity-api';

import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
import { AccountKey, PrecomposedTransactionFinal } from '@suite-common/wallet-types';
import { FeeLevel } from '@trezor/connect';
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';

import { TradingTransaction, TradingPaymentMethodListProps, TradingType } from '../types';
import { buyInitialState, prepareBuyReducer, TradingBuyState } from './buyReducer';
import { TradingPaymentMethodListProps, TradingTransaction, TradingType } from '../types';
import { TradingBuyState, buyInitialState, prepareBuyReducer } from './buyReducer';
import { tradingActions } from '../actions/tradingActions';

export interface TradingComposedTransactionInfo {
Expand Down
32 changes: 32 additions & 0 deletions suite-common/trading/src/selectors/tradingSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Route } from '@suite-common/suite-types/src/route';
import { Account, SelectedAccountStatus } from '@suite-common/wallet-types';
import { AddressDisplayOptions } from '@suite-common/wallet-types/src/settings';

import type { TradingState } from '../reducers/tradingReducer';
import { InvityServerEnvironment } from '../types';

// partial copy of Suite state
export type TradingRootState = {
wallet: {
selectedAccount: SelectedAccountStatus;
accounts: Account[];
trading: TradingState;
};
suite: {
settings: {
addressDisplayType: AddressDisplayOptions;
debug: {
invityServerEnvironment: InvityServerEnvironment;
};
};
};
router: {
url: string;
pathname: string;
route: {
name: Route['name'];
};
};
};

export const selectState = (state: TradingRootState) => state;
59 changes: 59 additions & 0 deletions suite-common/trading/src/thunks/buyThunks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { BuyProviderInfo, CryptoId, FiatCurrencyCode } from 'invity-api';

import { createThunk } from '@suite-common/redux-utils';

import { invityAPI } from '../invityAPI';
import { BuyInfo } from '../reducers/buyReducer';
import { regional } from '../regional';
import { TradingFiatCurrenciesProps } from '../types';

const loadInfoThunk = createThunk<BuyInfo>('buy/loadInfo', async () => {
const buyInfo = await invityAPI.getBuyList();
const defaultAmountsOfFiatCurrencies: TradingFiatCurrenciesProps = new Map();

if (!buyInfo || !buyInfo.providers) {
return {
buyInfo: {
country: regional.UNKNOWN_COUNTRY,
providers: [],
defaultAmountsOfFiatCurrencies,
},
providerInfos: {},
supportedFiatCurrencies: new Set(),
supportedCryptoCurrencies: new Set(),
};
}

const providerInfos: { [name: string]: BuyProviderInfo } = {};

buyInfo.providers.forEach(e => (providerInfos[e.name] = e));

const tradedFiatCurrencies: string[] = [];
const tradedCoins: CryptoId[] = [];
buyInfo.providers.forEach(p => {
tradedFiatCurrencies.push(...p.tradedFiatCurrencies.map(c => c.toLowerCase()));
tradedCoins.push(...p.tradedCoins);
});
const supportedFiatCurrencies = new Set(tradedFiatCurrencies);
const supportedCryptoCurrencies = new Set(tradedCoins);

if (buyInfo.defaultAmountsOfFiatCurrencies) {
Object.entries(buyInfo.defaultAmountsOfFiatCurrencies).forEach(([key, value]) => {
defaultAmountsOfFiatCurrencies.set(key as FiatCurrencyCode, value.toString());
});
}

return {
buyInfo: {
...buyInfo,
defaultAmountsOfFiatCurrencies,
},
providerInfos,
supportedFiatCurrencies,
supportedCryptoCurrencies,
};
});

export const buyThunks = {
loadInfoThunk,
};
Loading

0 comments on commit 012bb64

Please sign in to comment.