mirror of
https://github.com/ecency/ecency-mobile.git
synced 2024-11-27 03:14:56 +03:00
Merge branch 'development' of https://github.com/ecency/ecency-mobile into sa/hive-uri-support
This commit is contained in:
commit
fbe067906f
File diff suppressed because one or more lines are too long
@ -455,9 +455,9 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-cameraroll (1.8.1):
|
||||
- React
|
||||
- react-native-config (1.5.0):
|
||||
- react-native-config/App (= 1.5.0)
|
||||
- react-native-config/App (1.5.0):
|
||||
- react-native-config (1.5.1):
|
||||
- react-native-config/App (= 1.5.1)
|
||||
- react-native-config/App (1.5.1):
|
||||
- React-Core
|
||||
- react-native-date-picker (4.2.9):
|
||||
- React-Core
|
||||
@ -1045,7 +1045,7 @@ SPEC CHECKSUMS:
|
||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-cameraroll: e2917a5e62da9f10c3d525e157e25e694d2d6dfa
|
||||
react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727
|
||||
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
|
||||
react-native-date-picker: c063a8967058c58a02d7d0e1d655f0453576fb0d
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-flipper: c33a4995958ef12a2b2f8290d63bed7adeed7634
|
||||
|
@ -105,7 +105,7 @@
|
||||
"react-native-bootsplash": "^4.3.2",
|
||||
"react-native-camera": "^4.2.1",
|
||||
"react-native-chart-kit": "^6.11.0",
|
||||
"react-native-config": "luggit/react-native-config#master",
|
||||
"react-native-config": "^1.5.1",
|
||||
"react-native-crypto": "^2.2.0",
|
||||
"react-native-date-picker": "^4.2.0",
|
||||
"react-native-device-info": "^10.7.0",
|
||||
|
@ -5,11 +5,15 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
import { hideActionModal } from '../../../redux/actions/uiAction';
|
||||
import ActionModalView, { ActionModalRef } from '../view/actionModalView';
|
||||
|
||||
interface ExtendedAlertButton extends AlertButton {
|
||||
textId: string;
|
||||
}
|
||||
|
||||
export interface ActionModalData {
|
||||
title: string;
|
||||
body: string;
|
||||
para?: string;
|
||||
buttons: AlertButton[];
|
||||
buttons: ExtendedAlertButton[];
|
||||
headerImage?: Source;
|
||||
onClosed: () => void;
|
||||
headerContent?: React.ReactNode;
|
||||
|
@ -3,6 +3,7 @@ import { View, Text } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import ActionSheet from 'react-native-actions-sheet';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styles from './actionModalStyles';
|
||||
|
||||
import { ActionModalData } from '../container/actionModalContainer';
|
||||
@ -21,6 +22,8 @@ interface ActionModalViewProps {
|
||||
const ActionModalView = ({ onClose, data }: ActionModalViewProps, ref) => {
|
||||
const sheetModalRef = useRef<ActionSheet>();
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
showModal: () => {
|
||||
console.log('Showing action modal');
|
||||
@ -59,7 +62,7 @@ const ActionModalView = ({ onClose, data }: ActionModalViewProps, ref) => {
|
||||
buttons.map((props) => (
|
||||
<MainButton
|
||||
key={props.text}
|
||||
text={props.text}
|
||||
text={props.textId ? intl.formatMessage({ id: props.textId }) : props.text}
|
||||
onPress={(evn) => {
|
||||
sheetModalRef.current?.setModalVisible(false);
|
||||
props.onPress(evn);
|
||||
|
@ -50,7 +50,7 @@ import { CacheStatus } from '../../../redux/reducers/cacheReducer';
|
||||
import showLoginAlert from '../../../utils/showLoginAlert';
|
||||
import { delay } from '../../../utils/editor';
|
||||
|
||||
interface Props {}
|
||||
interface Props { }
|
||||
interface PopoverOptions {
|
||||
anchorRect: Rect;
|
||||
content: any;
|
||||
@ -65,7 +65,7 @@ interface PopoverOptions {
|
||||
*
|
||||
*/
|
||||
|
||||
const UpvotePopover = forwardRef(({}: Props, ref) => {
|
||||
const UpvotePopover = forwardRef(({ }: Props, ref) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -237,7 +237,6 @@ const UpvotePopover = forwardRef(({}: Props, ref) => {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setIsDownVoted(false);
|
||||
}
|
||||
};
|
||||
@ -280,7 +279,6 @@ const UpvotePopover = forwardRef(({}: Props, ref) => {
|
||||
_onVotingStart ? _onVotingStart(0) : null;
|
||||
});
|
||||
} else {
|
||||
setSliderValue(1);
|
||||
setIsDownVoted(true);
|
||||
}
|
||||
};
|
||||
|
@ -83,6 +83,7 @@
|
||||
"withdraw_hive": "Withdraw Savings",
|
||||
"withdraw_hbd": "Withdraw Savings",
|
||||
"transfer_to_savings": "To Savings",
|
||||
"swap_token":"Swap Token",
|
||||
"convert": "Convert",
|
||||
"convert_request":"Convert Request",
|
||||
"escrow_transfer": "Escrow Transfer",
|
||||
@ -575,6 +576,7 @@
|
||||
"move": "Move",
|
||||
"continue": "Continue",
|
||||
"okay":"Okay",
|
||||
"done":"Done",
|
||||
"move_question": "Are you sure to move to drafts?",
|
||||
"success_shared": "Success! Content submitted!",
|
||||
"success_moved": "Moved to draft",
|
||||
@ -835,6 +837,18 @@
|
||||
"amount_select_description": "Enter amount within maximum available balance {suffix}",
|
||||
"amount_select_desc_limit":" and must be greater than 0.001"
|
||||
},
|
||||
"trade":{
|
||||
"swap_token":"Swap Your Funds",
|
||||
"more-than-balance": "Entered amount is more than your available balance",
|
||||
"offer-unavailable": "Offer not available for entered amount. Please reduce amount",
|
||||
"too-much-slippage": "We highly recommend you to reduce amount for better pricing",
|
||||
"fee":"Fee",
|
||||
"free":"Free",
|
||||
"confirm_swap":"Confirm Swap",
|
||||
"swap_for" :"Swapping {fromAmount} for {toAmount}",
|
||||
"swap_successful":"Successfully Swapped!",
|
||||
"new_swap":"New Swap"
|
||||
},
|
||||
"boost": {
|
||||
"title": "Get Points",
|
||||
"buy": "GET Points",
|
||||
|
@ -38,6 +38,7 @@ const ROUTES = {
|
||||
EDIT_HISTORY: `EditHistory${SCREEN_SUFFIX}`,
|
||||
WELCOME: `Welcome${SCREEN_SUFFIX}`,
|
||||
BACKUP_KEYS: `BackupKeys${SCREEN_SUFFIX}`,
|
||||
TRADE: `Trade${SCREEN_SUFFIX}`,
|
||||
},
|
||||
MODALS: {
|
||||
ASSETS_SELECT: `AssetsSelect${MODAL_SUFFIX}`,
|
||||
|
@ -7,6 +7,7 @@ const TransferTypes = {
|
||||
POINTS: 'points',
|
||||
WITHDRAW_HIVE: 'withdraw_hive',
|
||||
WITHDRAW_HBD: 'withdraw_hbd',
|
||||
SWAP_TOKEN: 'swap_token',
|
||||
DELEGATE: 'delegate',
|
||||
POWER_DOWN: 'power_down',
|
||||
ADDRESS_VIEW: 'address_view',
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
Settings,
|
||||
SpinGame,
|
||||
Transfer,
|
||||
TradeScreen,
|
||||
Voters,
|
||||
AccountBoost,
|
||||
TagResult,
|
||||
@ -67,6 +68,7 @@ const MainStackNavigator = () => {
|
||||
<MainStack.Screen name={ROUTES.SCREENS.VOTERS} component={Voters} />
|
||||
<MainStack.Screen name={ROUTES.SCREENS.FOLLOWS} component={Follows} />
|
||||
<MainStack.Screen name={ROUTES.SCREENS.TRANSFER} component={Transfer} />
|
||||
<MainStack.Screen name={ROUTES.SCREENS.TRADE} component={TradeScreen} />
|
||||
<MainStack.Screen name={ROUTES.SCREENS.EDITOR} component={Editor} />
|
||||
<MainStack.Screen name={ROUTES.SCREENS.BACKUP_KEYS} component={BackupKeysScreen} />
|
||||
<MainStack.Screen
|
||||
|
@ -31,9 +31,9 @@ import {
|
||||
* ************************************
|
||||
*/
|
||||
|
||||
export const getCurrencyRate = (currency) =>
|
||||
export const getFiatHbdRate = (fiatCode:string) =>
|
||||
ecencyApi
|
||||
.get(`/private-api/market-data/${currency}/hbd?fixed=1`)
|
||||
.get(`/private-api/market-data/${fiatCode}/hbd`)
|
||||
.then((resp) => resp.data)
|
||||
.catch((err) => {
|
||||
bugsnagInstance.notify(err);
|
||||
|
26
src/providers/hive-trade/converters.ts
Normal file
26
src/providers/hive-trade/converters.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { MarketAsset, SwapOptions, TransactionType } from './hiveTrade.types';
|
||||
|
||||
export const convertSwapOptionsToLimitOrder = (data: SwapOptions) => {
|
||||
let amountToSell = 0;
|
||||
let minToRecieve = 0;
|
||||
let transactionType = TransactionType.None;
|
||||
|
||||
switch (data.fromAsset) {
|
||||
case MarketAsset.HIVE:
|
||||
amountToSell = data.toAmount;
|
||||
minToRecieve = data.fromAmount;
|
||||
transactionType = TransactionType.Sell;
|
||||
break;
|
||||
case MarketAsset.HBD:
|
||||
amountToSell = data.fromAmount;
|
||||
minToRecieve = data.toAmount;
|
||||
transactionType = TransactionType.Buy;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
amountToSell,
|
||||
minToRecieve,
|
||||
transactionType,
|
||||
};
|
||||
};
|
168
src/providers/hive-trade/hiveTrade.ts
Normal file
168
src/providers/hive-trade/hiveTrade.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { PrivateKey } from '@esteemapp/dhive';
|
||||
import { Operation } from '@hiveio/dhive';
|
||||
import {
|
||||
getAnyPrivateKey,
|
||||
getDigitPinCode,
|
||||
getMarketStatistics,
|
||||
sendHiveOperations,
|
||||
} from '../hive/dhive';
|
||||
import {
|
||||
MarketAsset,
|
||||
MarketStatistics,
|
||||
OrderIdPrefix,
|
||||
SwapOptions,
|
||||
TransactionType,
|
||||
} from './hiveTrade.types';
|
||||
import bugsnapInstance from '../../config/bugsnag';
|
||||
import { convertSwapOptionsToLimitOrder } from './converters';
|
||||
|
||||
// This operation creates a limit order and matches it against existing open orders.
|
||||
// The maximum expiration time for any limit order is 28 days from head_block_time()
|
||||
export const limitOrderCreate = (
|
||||
currentAccount: any,
|
||||
pinHash: string,
|
||||
amountToSell: number,
|
||||
minToReceive: number,
|
||||
orderType: TransactionType,
|
||||
idPrefix = OrderIdPrefix.EMPTY,
|
||||
) => {
|
||||
const digitPinCode = getDigitPinCode(pinHash);
|
||||
const key = getAnyPrivateKey(
|
||||
{
|
||||
activeKey: currentAccount?.local?.activeKey,
|
||||
},
|
||||
digitPinCode,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
const privateKey = PrivateKey.fromString(key);
|
||||
|
||||
let expiration: any = new Date(Date.now());
|
||||
expiration.setDate(expiration.getDate() + 27);
|
||||
expiration = expiration.toISOString().split('.')[0];
|
||||
|
||||
const data = getLimitOrderCreateOpData(
|
||||
currentAccount.username,
|
||||
amountToSell,
|
||||
minToReceive,
|
||||
orderType,
|
||||
idPrefix,
|
||||
);
|
||||
|
||||
const args: Operation[] = [['limit_order_create', data]];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
sendHiveOperations(args, privateKey)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
bugsnapInstance.notify(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
new Error('Check private key permission! Required private active key or above.'),
|
||||
);
|
||||
};
|
||||
|
||||
export const generateHsLimitOrderCreatePath = (
|
||||
currentAccount: any,
|
||||
amountToSell: number,
|
||||
minToReceive: number,
|
||||
orderType: TransactionType,
|
||||
idPrefix = OrderIdPrefix.EMPTY,
|
||||
) => {
|
||||
const data = getLimitOrderCreateOpData(
|
||||
currentAccount.username,
|
||||
amountToSell,
|
||||
minToReceive,
|
||||
orderType,
|
||||
idPrefix,
|
||||
);
|
||||
|
||||
const query = new URLSearchParams(data).toString();
|
||||
|
||||
return `sign/limitOrderCreate?${query}`;
|
||||
};
|
||||
|
||||
export const generateHsSwapTokenPath = (currentAccount: any, data: SwapOptions) => {
|
||||
const { amountToSell, minToRecieve, transactionType } = convertSwapOptionsToLimitOrder(data);
|
||||
|
||||
return generateHsLimitOrderCreatePath(
|
||||
currentAccount,
|
||||
amountToSell,
|
||||
minToRecieve,
|
||||
transactionType,
|
||||
OrderIdPrefix.SWAP,
|
||||
);
|
||||
};
|
||||
|
||||
export const swapToken = async (currentAccount: any, pinHash: string, data: SwapOptions) => {
|
||||
try {
|
||||
const { amountToSell, minToRecieve, transactionType } = convertSwapOptionsToLimitOrder(data);
|
||||
|
||||
await limitOrderCreate(
|
||||
currentAccount,
|
||||
pinHash,
|
||||
amountToSell,
|
||||
minToRecieve,
|
||||
transactionType,
|
||||
OrderIdPrefix.SWAP,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn('Failed to swap token', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchHiveMarketRate = async (asset: MarketAsset): Promise<number> => {
|
||||
try {
|
||||
const market: MarketStatistics = await getMarketStatistics();
|
||||
const _lowestAsk = Number(market?.lowest_ask);
|
||||
|
||||
if (!_lowestAsk) {
|
||||
throw new Error('Invalid market lowest ask');
|
||||
}
|
||||
|
||||
switch (asset) {
|
||||
case MarketAsset.HIVE:
|
||||
return _lowestAsk;
|
||||
case MarketAsset.HBD:
|
||||
return 1 / _lowestAsk;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('failed to get hive market rate');
|
||||
bugsnapInstance.notify(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getLimitOrderCreateOpData = (username, amountToSell, minToReceive, orderType, idPrefix) => {
|
||||
let expiration: any = new Date(Date.now());
|
||||
expiration.setDate(expiration.getDate() + 27);
|
||||
expiration = expiration.toISOString().split('.')[0];
|
||||
|
||||
return {
|
||||
owner: username,
|
||||
orderid: Number(
|
||||
`${idPrefix}${Math.floor(Date.now() / 1000)
|
||||
.toString()
|
||||
.slice(2)}`,
|
||||
),
|
||||
amount_to_sell: `${
|
||||
orderType === TransactionType.Buy ? amountToSell.toFixed(3) : minToReceive.toFixed(3)
|
||||
} ${orderType === TransactionType.Buy ? MarketAsset.HBD : MarketAsset.HIVE}`,
|
||||
min_to_receive: `${
|
||||
orderType === TransactionType.Buy ? minToReceive.toFixed(3) : amountToSell.toFixed(3)
|
||||
} ${orderType === TransactionType.Buy ? MarketAsset.HIVE : MarketAsset.HBD}`,
|
||||
fill_or_kill: false,
|
||||
expiration,
|
||||
};
|
||||
};
|
42
src/providers/hive-trade/hiveTrade.types.ts
Normal file
42
src/providers/hive-trade/hiveTrade.types.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export enum TransactionType {
|
||||
None = 0,
|
||||
Sell = 2,
|
||||
Buy = 1,
|
||||
Cancel = 3,
|
||||
}
|
||||
|
||||
export enum OrderIdPrefix {
|
||||
EMPTY = '',
|
||||
SWAP = '9',
|
||||
}
|
||||
|
||||
export enum MarketAsset {
|
||||
HIVE = 'HIVE',
|
||||
HBD = 'HBD',
|
||||
}
|
||||
|
||||
export interface SwapOptions {
|
||||
fromAsset: MarketAsset;
|
||||
fromAmount: number;
|
||||
toAmount: number;
|
||||
}
|
||||
|
||||
export interface MarketStatistics {
|
||||
hbd_volume: string;
|
||||
highest_bid: string;
|
||||
hive_volume: string;
|
||||
latest: string;
|
||||
lowest_ask: string;
|
||||
percent_change: string;
|
||||
}
|
||||
|
||||
export interface OrdersDataItem {
|
||||
created: string;
|
||||
hbd: number;
|
||||
hive: number;
|
||||
order_price: {
|
||||
base: string;
|
||||
quote: string;
|
||||
};
|
||||
real_price: string;
|
||||
}
|
@ -176,6 +176,11 @@ export const getDynamicGlobalProperties = () => client.database.getDynamicGlobal
|
||||
|
||||
export const getRewardFund = () => client.database.call('get_reward_fund', ['post']);
|
||||
|
||||
export const getMarketStatistics = () => client.call('condenser_api', 'get_ticker', []);
|
||||
|
||||
export const getOrderBook = (limit = 500) =>
|
||||
client.call('condenser_api', 'get_order_book', [limit]);
|
||||
|
||||
export const getFeedHistory = async () => {
|
||||
try {
|
||||
const feedHistory = await client.database.call('get_feed_history');
|
||||
|
@ -254,9 +254,6 @@ export const useClaimRewardsMutation = () => {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const useActivitiesQuery = (assetId: string) => {
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const globalProps = useAppSelector((state) => state.account.globalProps);
|
||||
@ -278,7 +275,7 @@ export const useActivitiesQuery = (assetId: string) => {
|
||||
globalProps,
|
||||
startIndex: pageParam,
|
||||
limit: ACTIVITIES_FETCH_LIMIT,
|
||||
isEngine: assetData.isEngine
|
||||
isEngine: assetData.isEngine,
|
||||
});
|
||||
|
||||
console.log('new page fetched', _activites);
|
||||
@ -330,7 +327,6 @@ export const useActivitiesQuery = (assetId: string) => {
|
||||
setPageParams([...pageParams]);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const _data = useMemo(() => {
|
||||
@ -347,9 +343,6 @@ export const useActivitiesQuery = (assetId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const usePendingRequestsQuery = (assetId: string) => {
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const selectedCoins = useAppSelector((state) => state.wallet.selectedCoins);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import getSymbolFromCurrency from 'currency-symbol-map';
|
||||
import { getCurrencyRate } from '../../providers/ecency/ecency';
|
||||
import { getFiatHbdRate } from '../../providers/ecency/ecency';
|
||||
import {
|
||||
CHANGE_COMMENT_NOTIFICATION,
|
||||
CHANGE_FOLLOW_NOTIFICATION,
|
||||
@ -166,7 +166,13 @@ export const isDefaultFooter = (payload) => ({
|
||||
export const setCurrency = (currency) => async (dispatch) => {
|
||||
const currencySymbol = getSymbolFromCurrency(currency);
|
||||
|
||||
const currencyRate = await getCurrencyRate(currency);
|
||||
let currencyRate = 1;
|
||||
if (currency !== 'usd') {
|
||||
const _usdRate = await getFiatHbdRate('usd');
|
||||
const _fiatRate = await getFiatHbdRate(currency);
|
||||
currencyRate = _fiatRate / _usdRate;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_CURRENCY,
|
||||
payload: { currency, currencyRate, currencySymbol },
|
||||
|
@ -13,6 +13,7 @@ import notifee, { EventType } from '@notifee/react-native';
|
||||
import { isEmpty, some, get } from 'lodash';
|
||||
import messaging from '@react-native-firebase/messaging';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { setDeviceOrientation, setLockedOrientation } from '../../../redux/actions/uiAction';
|
||||
import { orientations } from '../../../redux/constants/orientationsConstants';
|
||||
@ -21,16 +22,17 @@ import darkTheme from '../../../themes/darkTheme';
|
||||
import lightTheme from '../../../themes/lightTheme';
|
||||
import { useUserActivityMutation } from '../../../providers/queries';
|
||||
import THEME_OPTIONS from '../../../constants/options/theme';
|
||||
import { setIsDarkTheme } from '../../../redux/actions/applicationActions';
|
||||
import { setCurrency, setIsDarkTheme } from '../../../redux/actions/applicationActions';
|
||||
import { markNotifications } from '../../../providers/ecency/ecency';
|
||||
import { updateUnreadActivityCount } from '../../../redux/actions/accountAction';
|
||||
import RootNavigation from '../../../navigation/rootNavigation';
|
||||
import ROUTES from '../../../constants/routeNames';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export const useInitApplication = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isDarkTheme, colorTheme, isPinCodeOpen } = useAppSelector((state) => state.application);
|
||||
const { isDarkTheme, colorTheme, isPinCodeOpen, currency } = useAppSelector(
|
||||
(state) => state.application,
|
||||
);
|
||||
|
||||
const systemColorScheme = useColorScheme();
|
||||
|
||||
@ -74,6 +76,9 @@ export const useInitApplication = () => {
|
||||
|
||||
userActivityMutation.lazyMutatePendingActivities();
|
||||
|
||||
// update fiat currency rate usd:fiat
|
||||
dispatch(setCurrency(currency.currency));
|
||||
|
||||
_initPushListener();
|
||||
|
||||
return _cleanup;
|
||||
|
@ -135,6 +135,10 @@ const AssetDetailsScreen = ({ navigation, route }: AssetDetailsScreenProps) => {
|
||||
case TransferTypes.WITHDRAW_HBD:
|
||||
balance = coinData.savings ?? 0;
|
||||
break;
|
||||
|
||||
case TransferTypes.SWAP_TOKEN:
|
||||
navigateTo = ROUTES.SCREENS.TRADE;
|
||||
break;
|
||||
}
|
||||
|
||||
navigateParams = {
|
||||
|
@ -2,12 +2,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
// Constants
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import ROUTES from '../../../constants/routeNames';
|
||||
|
||||
import { useDispatch, connect } from 'react-redux';
|
||||
import { showProfileModal } from '../../../redux/actions/uiAction';
|
||||
// Components
|
||||
import { BasicHeader, UserListItem } from '../../../components';
|
||||
|
||||
@ -29,15 +28,7 @@ class FollowsScreen extends PureComponent {
|
||||
|
||||
// Component Functions
|
||||
_handleOnUserPress = (username) => {
|
||||
const { navigation } = this.props;
|
||||
|
||||
navigation.navigate({
|
||||
name: ROUTES.SCREENS.PROFILE,
|
||||
params: {
|
||||
username,
|
||||
},
|
||||
key: username,
|
||||
});
|
||||
this.props.dispatch(showProfileModal(username));
|
||||
};
|
||||
|
||||
_renderItem = ({ item, index }) => {
|
||||
@ -90,11 +81,4 @@ class FollowsScreen extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapHooksToProps = (props) => {
|
||||
const navigation = useNavigation();
|
||||
return <FollowsScreen {...props} navigation={navigation} />;
|
||||
};
|
||||
|
||||
export default injectIntl(mapHooksToProps);
|
||||
/* eslint-enable */
|
||||
export default connect()(injectIntl(FollowsScreen));
|
||||
|
@ -20,6 +20,7 @@ import Redeem from './redeem/screen/redeemScreen';
|
||||
import HiveSigner from './steem-connect/hiveSigner';
|
||||
import { WebBrowser } from './webBrowser';
|
||||
import Transfer from './transfer';
|
||||
import TradeScreen from './trade';
|
||||
import Voters from './voters';
|
||||
import AccountBoost from './accountBoost/screen/accountBoostScreen';
|
||||
import Register from './register/registerScreen';
|
||||
@ -56,6 +57,7 @@ export {
|
||||
SpinGame,
|
||||
HiveSigner,
|
||||
Transfer,
|
||||
TradeScreen,
|
||||
Voters,
|
||||
Wallet,
|
||||
TagResult,
|
||||
|
27
src/screens/trade/children/assetChangeBtn.tsx
Normal file
27
src/screens/trade/children/assetChangeBtn.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import styles from '../styles/assetChangeBtn.styles';
|
||||
import { IconButton } from '../../../components';
|
||||
|
||||
interface Props {
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
// Reusable component for label, text input, and bottom text
|
||||
export const AssetChangeBtn = ({ onPress }: Props) => {
|
||||
return (
|
||||
<View style={styles.changeBtnContainer} pointerEvents="box-none">
|
||||
<View style={styles.changeBtn}>
|
||||
<IconButton
|
||||
style={styles.changeBtnSize}
|
||||
color={EStyleSheet.value('$primaryBlue')}
|
||||
iconType="MaterialIcons"
|
||||
name="swap-vert"
|
||||
onPress={onPress}
|
||||
size={44}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
28
src/screens/trade/children/errorSection.tsx
Normal file
28
src/screens/trade/children/errorSection.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import styles from '../styles/errorSection.styles';
|
||||
import { Icon } from '../../../components';
|
||||
|
||||
interface Props {
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
// Reusable component for label, text input, and bottom text
|
||||
export const ErrorSection = ({ message }: Props) => {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{message}</Text>
|
||||
<Icon
|
||||
iconType="MaterialIcons"
|
||||
name="error"
|
||||
color={EStyleSheet.value('$pureWhite')}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
5
src/screens/trade/children/index.ts
Normal file
5
src/screens/trade/children/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './swapTokenContent';
|
||||
export * from './swapAmountInput';
|
||||
export * from './swapFeeSection';
|
||||
export * from './errorSection';
|
||||
export * from './assetChangeBtn';
|
53
src/screens/trade/children/swapAmountInput.tsx
Normal file
53
src/screens/trade/children/swapAmountInput.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { View, TextInput, Text } from 'react-native';
|
||||
import styles from '../styles/swapAmountInput.styles';
|
||||
import { useAppSelector } from '../../../hooks';
|
||||
import { formatNumberInputStr } from '../../../utils/number';
|
||||
|
||||
interface SwapInputProps {
|
||||
label: string;
|
||||
onChangeText?: (text: string) => void;
|
||||
value: string;
|
||||
fiatPrice: number;
|
||||
symbol: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// Reusable component for label, text input, and bottom text
|
||||
export const SwapAmountInput = ({
|
||||
label,
|
||||
onChangeText,
|
||||
value,
|
||||
fiatPrice,
|
||||
symbol,
|
||||
}: SwapInputProps) => {
|
||||
const currency = useAppSelector((state) => state.application.currency);
|
||||
|
||||
const _fiatValue = ((Number(value) || 0) * fiatPrice).toFixed(3);
|
||||
|
||||
const _onChangeText = (text: string) => {
|
||||
if (onChangeText) {
|
||||
onChangeText(formatNumberInputStr(text, 3));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
editable={!!onChangeText}
|
||||
onChangeText={_onChangeText}
|
||||
value={value}
|
||||
keyboardType="numeric"
|
||||
style={styles.input}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<View style={styles.symbolContainer}>
|
||||
<Text style={styles.symbol}>{symbol}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.fiat}>{currency.currencySymbol + _fiatValue}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
18
src/screens/trade/children/swapFeeSection.tsx
Normal file
18
src/screens/trade/children/swapFeeSection.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styles from '../styles/swapFeeSection.styles';
|
||||
|
||||
// Reusable component for label, text input, and bottom text
|
||||
export const SwapFeeSection = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{intl.formatMessage({ id: 'trade.fee' })}</Text>
|
||||
<View style={styles.freeContainer}>
|
||||
<Text style={styles.free}>{intl.formatMessage({ id: 'trade.free' })}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
303
src/screens/trade/children/swapTokenContent.tsx
Normal file
303
src/screens/trade/children/swapTokenContent.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, Alert, RefreshControl } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import styles from '../styles/tradeScreen.styles';
|
||||
import { AssetChangeBtn, ErrorSection, SwapAmountInput, SwapFeeSection } from '.';
|
||||
import { Icon, MainButton } from '../../../components';
|
||||
import {
|
||||
fetchHiveMarketRate,
|
||||
generateHsSwapTokenPath,
|
||||
swapToken,
|
||||
} from '../../../providers/hive-trade/hiveTrade';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks';
|
||||
import { MarketAsset, SwapOptions } from '../../../providers/hive-trade/hiveTrade.types';
|
||||
import { ASSET_IDS } from '../../../constants/defaultAssets';
|
||||
import { showActionModal } from '../../../redux/actions/uiAction';
|
||||
import { walletQueries } from '../../../providers/queries';
|
||||
import { useSwapCalculator } from './useSwapCalculator';
|
||||
import AUTH_TYPE from '../../../constants/authType';
|
||||
import { delay } from '../../../utils/editor';
|
||||
|
||||
interface Props {
|
||||
initialSymbol: MarketAsset;
|
||||
handleHsTransfer: (hsSignPath: string) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export const SwapTokenContent = ({ initialSymbol, handleHsTransfer, onSuccess }: Props) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
|
||||
// queres
|
||||
const assetsQuery = walletQueries.useAssetsQuery();
|
||||
|
||||
const currentAccount = useAppSelector((state) => state.account.currentAccount);
|
||||
const currency = useAppSelector((state) => state.application.currency);
|
||||
|
||||
const assetsData = useAppSelector((state) => state.wallet.coinsData);
|
||||
const pinHash = useAppSelector((state) => state.application.pin);
|
||||
const isDarkTheme = useAppSelector((state) => state.application.isDarkTheme);
|
||||
|
||||
const [fromAssetSymbol, setFromAssetSymbol] = useState(initialSymbol || MarketAsset.HIVE);
|
||||
const [marketPrice, setMarketPrice] = useState(0);
|
||||
const [isMoreThanBalance, setIsMoreThanBalance] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [swapping, setSwapping] = useState(false);
|
||||
const [fromAmount, setFromAmount] = useState('0');
|
||||
|
||||
const _toAssetSymbol = useMemo(
|
||||
() => (fromAssetSymbol === MarketAsset.HBD ? MarketAsset.HIVE : MarketAsset.HBD),
|
||||
[fromAssetSymbol],
|
||||
);
|
||||
|
||||
// this method makes sure amount is only updated when new order book is fetched after asset change
|
||||
// this avoid wrong from and to swap value on changing source asset
|
||||
const _onAssetChangeComplete = () => {
|
||||
setFromAmount(_toAmountStr);
|
||||
};
|
||||
|
||||
const {
|
||||
toAmount,
|
||||
offerUnavailable,
|
||||
tooMuchSlippage,
|
||||
isLoading: _isFetchingOrders,
|
||||
} = useSwapCalculator(fromAssetSymbol, Number(fromAmount) || 0, _onAssetChangeComplete);
|
||||
|
||||
const _errorMessage = useMemo(() => {
|
||||
let msg = '';
|
||||
if (isMoreThanBalance) {
|
||||
msg += `${intl.formatMessage({ id: 'trade.more-than-balance' })}\n`;
|
||||
}
|
||||
if (offerUnavailable) {
|
||||
msg += `${intl.formatMessage({ id: 'trade.offer-unavailable' })}\n`;
|
||||
}
|
||||
if (tooMuchSlippage) {
|
||||
msg += `${intl.formatMessage({ id: 'trade.too-much-slippage' })}\n`;
|
||||
}
|
||||
return msg.trim();
|
||||
}, [tooMuchSlippage, offerUnavailable, isMoreThanBalance]);
|
||||
|
||||
// accumulate asset data properties
|
||||
const _fromAssetData =
|
||||
assetsData[fromAssetSymbol === MarketAsset.HBD ? ASSET_IDS.HBD : ASSET_IDS.HIVE];
|
||||
const _balance = _fromAssetData.balance;
|
||||
const _fromFiatPrice = _fromAssetData.currentPrice;
|
||||
const _toFiatPrice =
|
||||
assetsData[_toAssetSymbol === MarketAsset.HBD ? ASSET_IDS.HBD : ASSET_IDS.HIVE].currentPrice;
|
||||
const _marketFiatPrice = marketPrice * _toFiatPrice;
|
||||
|
||||
const _toAmountStr = toAmount.toFixed(3);
|
||||
|
||||
// initialize market data
|
||||
useEffect(() => {
|
||||
_fetchMarketRate();
|
||||
}, [fromAssetSymbol]);
|
||||
|
||||
// post process updated amount value
|
||||
useEffect(() => {
|
||||
const _value = Number(fromAmount);
|
||||
// check for amount validity
|
||||
setIsMoreThanBalance(_value > _balance);
|
||||
}, [fromAmount]);
|
||||
|
||||
// fetches and sets market rate based on selected assetew
|
||||
const _fetchMarketRate = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: update marketPrice
|
||||
const _marketPrice = await fetchHiveMarketRate(fromAssetSymbol);
|
||||
setMarketPrice(_marketPrice);
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
Alert.alert('fail', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const _reset = () => {
|
||||
setFromAmount('0');
|
||||
};
|
||||
|
||||
const _onSwapSuccess = () => {
|
||||
const headerContent = (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: EStyleSheet.value('$primaryGreen'),
|
||||
borderRadius: 56,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
style={{ borderWidth: 0 }}
|
||||
size={64}
|
||||
color={EStyleSheet.value('$pureWhite')}
|
||||
name="check"
|
||||
iconType="MaterialIcons"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
dispatch(
|
||||
showActionModal({
|
||||
headerContent,
|
||||
title: intl.formatMessage({ id: 'trade.swap_successful' }),
|
||||
buttons: [
|
||||
{ textId: 'trade.new_swap', onPress: _reset },
|
||||
{ textId: 'alert.done', onPress: () => navigation.goBack() },
|
||||
],
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// initiates swaping action on confirmation
|
||||
const _confirmSwap = async () => {
|
||||
const _fromAmount = Number(fromAmount);
|
||||
|
||||
const data: SwapOptions = {
|
||||
fromAsset: fromAssetSymbol,
|
||||
fromAmount: _fromAmount,
|
||||
toAmount,
|
||||
};
|
||||
|
||||
if (currentAccount.local.authType === AUTH_TYPE.STEEM_CONNECT) {
|
||||
await delay(500); // NOTE: it's required to avoid modal mis fire
|
||||
handleHsTransfer(generateHsSwapTokenPath(currentAccount, data));
|
||||
} else {
|
||||
try {
|
||||
setSwapping(true);
|
||||
|
||||
await swapToken(currentAccount, pinHash, data);
|
||||
|
||||
onSuccess();
|
||||
setSwapping(false);
|
||||
_onSwapSuccess();
|
||||
} catch (err) {
|
||||
Alert.alert('fail', err.message);
|
||||
setSwapping(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// prompts user to verify swap action;
|
||||
const handleContinue = () => {
|
||||
dispatch(
|
||||
showActionModal({
|
||||
title: intl.formatMessage({ id: 'trade.confirm_swap' }),
|
||||
body: intl.formatMessage(
|
||||
{ id: 'trade.swap_for' },
|
||||
{
|
||||
fromAmount: `${fromAmount} ${fromAssetSymbol}`,
|
||||
toAmount: `${_toAmountStr} ${_toAssetSymbol}`,
|
||||
},
|
||||
),
|
||||
buttons: [
|
||||
{ textId: 'alert.cancel', onPress: () => {} },
|
||||
{ textId: 'alert.confirm', onPress: _confirmSwap },
|
||||
],
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// refreshes wallet data and market rate
|
||||
const _refresh = async () => {
|
||||
setLoading(true);
|
||||
assetsQuery.refetch();
|
||||
_fetchMarketRate();
|
||||
};
|
||||
|
||||
const handleAssetChange = () => {
|
||||
setFromAssetSymbol(_toAssetSymbol);
|
||||
};
|
||||
|
||||
const _disabledContinue =
|
||||
_isFetchingOrders ||
|
||||
loading ||
|
||||
isMoreThanBalance ||
|
||||
offerUnavailable ||
|
||||
!Number(fromAmount) ||
|
||||
!Number(toAmount);
|
||||
|
||||
const _renderBalance = () => (
|
||||
<Text style={styles.balance}>
|
||||
{'Balance: '}
|
||||
<Text
|
||||
style={{ color: EStyleSheet.value('$primaryBlue') }}
|
||||
onPress={() => {
|
||||
setFromAmount(`${_balance}`);
|
||||
}}
|
||||
>
|
||||
{`${_balance} ${fromAssetSymbol}`}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
|
||||
const _renderInputs = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SwapAmountInput
|
||||
label={intl.formatMessage({ id: 'transfer.from' })}
|
||||
onChangeText={setFromAmount}
|
||||
value={fromAmount}
|
||||
symbol={fromAssetSymbol}
|
||||
fiatPrice={_fromFiatPrice}
|
||||
/>
|
||||
|
||||
<SwapAmountInput
|
||||
label={intl.formatMessage({ id: 'transfer.to' })}
|
||||
value={_toAmountStr}
|
||||
symbol={_toAssetSymbol}
|
||||
fiatPrice={_toFiatPrice}
|
||||
/>
|
||||
<AssetChangeBtn onPress={handleAssetChange} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const _renderMainBtn = () => (
|
||||
<View style={styles.mainBtnContainer}>
|
||||
<MainButton
|
||||
style={styles.mainBtn}
|
||||
isDisable={_disabledContinue}
|
||||
onPress={handleContinue}
|
||||
isLoading={swapping}
|
||||
>
|
||||
<Text style={styles.buttonText}>{intl.formatMessage({ id: 'transfer.next' })}</Text>
|
||||
</MainButton>
|
||||
</View>
|
||||
);
|
||||
|
||||
const _renderMarketPrice = () => (
|
||||
<Text style={styles.marketRate}>
|
||||
{`1 ${fromAssetSymbol} = ${marketPrice.toFixed(3)} ` +
|
||||
`${_toAssetSymbol} (${currency.currencySymbol + _marketFiatPrice.toFixed(3)})`}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={styles.container}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={loading}
|
||||
onRefresh={_refresh}
|
||||
progressBackgroundColor="#357CE6"
|
||||
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
|
||||
titleColor="#fff"
|
||||
colors={['#fff']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{_renderBalance()}
|
||||
{_renderInputs()}
|
||||
{_renderMarketPrice()}
|
||||
|
||||
<SwapFeeSection />
|
||||
<ErrorSection message={_errorMessage} />
|
||||
|
||||
{_renderMainBtn()}
|
||||
</KeyboardAwareScrollView>
|
||||
);
|
||||
};
|
176
src/screens/trade/children/useSwapCalculator.tsx
Normal file
176
src/screens/trade/children/useSwapCalculator.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { MarketAsset, OrdersDataItem } from "../../../providers/hive-trade/hiveTrade.types";
|
||||
import bugsnapInstance from "../../../config/bugsnag";
|
||||
import { Alert } from "react-native";
|
||||
import { getOrderBook } from "../../../providers/hive/dhive";
|
||||
import { stripDecimalPlaces } from "../../../utils/number";
|
||||
|
||||
|
||||
export namespace HiveMarket {
|
||||
interface ProcessingResult {
|
||||
tooMuchSlippage?: boolean;
|
||||
invalidAmount?: boolean;
|
||||
toAmount?: number;
|
||||
emptyOrderBook?: boolean;
|
||||
}
|
||||
|
||||
function calculatePrice(intAmount: number, book: OrdersDataItem[], asset: "hive" | "hbd") {
|
||||
let available = book[0][asset] / 1000;
|
||||
let index = 0;
|
||||
while (available < intAmount && book.length > index + 1) {
|
||||
available += book[index][asset] / 1000;
|
||||
index++;
|
||||
}
|
||||
return +book[index].real_price;
|
||||
}
|
||||
|
||||
export async function fetchHiveOrderBook() {
|
||||
try {
|
||||
return await getOrderBook();
|
||||
} catch (e) {
|
||||
bugsnapInstance.notify(e)
|
||||
Alert.alert("Order book is empty")
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function processHiveOrderBook(
|
||||
buyOrderBook: OrdersDataItem[],
|
||||
sellOrderBook: OrdersDataItem[],
|
||||
fromAmount: number,
|
||||
asset: string
|
||||
): ProcessingResult {
|
||||
if (buyOrderBook.length <= 0 || sellOrderBook.length <= 0) return { emptyOrderBook: true };
|
||||
|
||||
let tooMuchSlippage,
|
||||
invalidAmount = false;
|
||||
let availableInOrderBook,
|
||||
price = 0;
|
||||
let firstPrice = Infinity;
|
||||
let toAmount = 0;;
|
||||
let resultToAmount;
|
||||
|
||||
if (asset === MarketAsset.HIVE) {
|
||||
availableInOrderBook =
|
||||
buyOrderBook.map((item) => item.hive).reduce((acc, item) => acc + item, 0) / 1000;
|
||||
price = calculatePrice(fromAmount, buyOrderBook, "hive");
|
||||
toAmount = fromAmount * price;
|
||||
firstPrice = +buyOrderBook[0].real_price;
|
||||
} else if (asset === MarketAsset.HBD) {
|
||||
availableInOrderBook =
|
||||
sellOrderBook.map((item) => item.hbd).reduce((acc, item) => acc + item, 0) / 1000;
|
||||
price = calculatePrice(fromAmount, sellOrderBook, "hbd");
|
||||
toAmount = fromAmount / price;
|
||||
firstPrice = +sellOrderBook[0].real_price;
|
||||
}
|
||||
|
||||
if (!availableInOrderBook) return { emptyOrderBook: true };
|
||||
|
||||
const slippage = Math.abs(price - firstPrice);
|
||||
tooMuchSlippage = slippage > 0.01;
|
||||
|
||||
if (fromAmount > availableInOrderBook) {
|
||||
invalidAmount = true;
|
||||
} else if (toAmount) {
|
||||
resultToAmount = toAmount;
|
||||
invalidAmount = false;
|
||||
}
|
||||
return { toAmount: resultToAmount, tooMuchSlippage, invalidAmount };
|
||||
}
|
||||
|
||||
export async function getNewAmount(toAmount: string, fromAmount: number, asset: MarketAsset) {
|
||||
const book = await HiveMarket.fetchHiveOrderBook();
|
||||
const { toAmount: newToAmount } = HiveMarket.processHiveOrderBook(
|
||||
book?.bids ?? [],
|
||||
book?.asks ?? [],
|
||||
fromAmount,
|
||||
asset
|
||||
);
|
||||
if (newToAmount) {
|
||||
return newToAmount;
|
||||
}
|
||||
return toAmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const useSwapCalculator = (
|
||||
asset: MarketAsset,
|
||||
fromAmount: number,
|
||||
onAssetChangeComplete: () => void,
|
||||
) => {
|
||||
const [buyOrderBook, setBuyOrderBook] = useState<OrdersDataItem[]>([]);
|
||||
const [sellOrderBook, setSellOrderBook] = useState<OrdersDataItem[]>([]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [toAmount, setToAmount] = useState(0);
|
||||
const [tooMuchSlippage, setTooMuchSlippage] = useState(false);
|
||||
const [offerUnavailable, setOfferUnavailable] = useState(false);
|
||||
|
||||
const assetRef = useRef(asset);
|
||||
|
||||
|
||||
let updateInterval: any;
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrderBook();
|
||||
updateInterval = setInterval(() => fetchOrderBook(), 60000);
|
||||
return () => {
|
||||
clearInterval(updateInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrderBook().then(() => {
|
||||
if (assetRef.current !== asset) {
|
||||
assetRef.current = asset;
|
||||
onAssetChangeComplete();
|
||||
}
|
||||
});
|
||||
}, [asset]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
processOrderBook();
|
||||
}, [fromAmount]);
|
||||
|
||||
|
||||
|
||||
const processOrderBook = () => {
|
||||
const { tooMuchSlippage: _tooMuchSlippage, invalidAmount: _invalidAmount, toAmount: _toAmount } = HiveMarket.processHiveOrderBook(
|
||||
buyOrderBook,
|
||||
sellOrderBook,
|
||||
fromAmount,
|
||||
asset
|
||||
);
|
||||
setTooMuchSlippage(!!_tooMuchSlippage);
|
||||
setOfferUnavailable(!!_invalidAmount);
|
||||
if (_toAmount) {
|
||||
setToAmount(stripDecimalPlaces(_toAmount));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchOrderBook = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const book = await HiveMarket.fetchHiveOrderBook();
|
||||
if (book) {
|
||||
setBuyOrderBook(book.bids);
|
||||
setSellOrderBook(book.asks);
|
||||
}
|
||||
processOrderBook();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
toAmount,
|
||||
offerUnavailable,
|
||||
tooMuchSlippage,
|
||||
isLoading,
|
||||
};
|
||||
};
|
3
src/screens/trade/index.ts
Normal file
3
src/screens/trade/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import TradeScreen from './screen/tradeScreen';
|
||||
|
||||
export default TradeScreen;
|
84
src/screens/trade/screen/tradeScreen.tsx
Normal file
84
src/screens/trade/screen/tradeScreen.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useIntl } from 'react-intl';
|
||||
import WebView from 'react-native-webview';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styles from '../styles/tradeScreen.styles';
|
||||
import { SwapTokenContent } from '../children';
|
||||
import { BasicHeader, Modal } from '../../../components';
|
||||
|
||||
import TransferTypes from '../../../constants/transferTypes';
|
||||
import { hsOptions } from '../../../constants/hsOptions';
|
||||
import { walletQueries } from '../../../providers/queries';
|
||||
import { delay } from '../../../utils/editor';
|
||||
|
||||
const TradeScreen = ({ route, navigation }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const assetsQuery = walletQueries.useAssetsQuery();
|
||||
|
||||
const transferType = route?.params?.transferType;
|
||||
const fundType = route?.params?.fundType;
|
||||
|
||||
const [showHsModal, setShowHsModal] = useState(false);
|
||||
const [hsSignPath, setHsSignPath] = useState('');
|
||||
|
||||
const _delayedRefreshCoinsData = () => {
|
||||
setTimeout(() => {
|
||||
assetsQuery.refetch();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const _handleOnModalClose = async () => {
|
||||
setShowHsModal(false);
|
||||
setHsSignPath('');
|
||||
|
||||
await delay(300);
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const _onSuccess = () => {
|
||||
_delayedRefreshCoinsData();
|
||||
};
|
||||
|
||||
const _handleHsTransfer = (_hsSignPath: string) => {
|
||||
setHsSignPath(_hsSignPath);
|
||||
setShowHsModal(true);
|
||||
};
|
||||
|
||||
let _content: any = null;
|
||||
switch (transferType) {
|
||||
case TransferTypes.SWAP_TOKEN:
|
||||
_content = (
|
||||
<SwapTokenContent
|
||||
initialSymbol={fundType}
|
||||
handleHsTransfer={_handleHsTransfer}
|
||||
onSuccess={_onSuccess}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
// NOTE: when we add support for different modes of trade, those section will separatly rendered from here.
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<BasicHeader title={intl.formatMessage({ id: `trade.${transferType}` })} />
|
||||
{_content}
|
||||
|
||||
{!!hsSignPath && (
|
||||
<Modal
|
||||
isOpen={showHsModal}
|
||||
isFullScreen
|
||||
isCloseButton
|
||||
handleOnModalClose={_handleOnModalClose}
|
||||
title={intl.formatMessage({ id: 'transfer.steemconnect_title' })}
|
||||
>
|
||||
<WebView source={{ uri: `${hsOptions.base_url}${hsSignPath}` }} />
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeScreen;
|
27
src/screens/trade/styles/assetChangeBtn.styles.ts
Normal file
27
src/screens/trade/styles/assetChangeBtn.styles.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
changeBtnContainer:{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
} as ViewStyle,
|
||||
changeBtn: {
|
||||
justifyContent:'center',
|
||||
alignItems:'center',
|
||||
backgroundColor: '$primaryLightBackground',
|
||||
borderRadius: 28,
|
||||
borderWidth: 8,
|
||||
borderColor: '$primaryBackgroundColor',
|
||||
} as ViewStyle,
|
||||
changeBtnSize:{
|
||||
height: 60,
|
||||
width: 60,
|
||||
} as ViewStyle,
|
||||
})
|
||||
|
38
src/screens/trade/styles/errorSection.styles.ts
Normal file
38
src/screens/trade/styles/errorSection.styles.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
margin: 12,
|
||||
padding: 14,
|
||||
borderRadius: 16,
|
||||
flexDirection:'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems:'center',
|
||||
backgroundColor: '$primaryRed'
|
||||
} as ViewStyle,
|
||||
label: {
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
paddingRight:12,
|
||||
color: '$pureWhite',
|
||||
} as TextStyle,
|
||||
freeContainer:{
|
||||
paddingVertical:4,
|
||||
paddingHorizontal:8,
|
||||
borderRadius:6,
|
||||
marginHorizontal:8,
|
||||
backgroundColor:'$primaryGreen'
|
||||
} as ViewStyle,
|
||||
free: {
|
||||
borderWidth: 0,
|
||||
color: '$primaryDarkText',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
} as TextStyle,
|
||||
fiat: {
|
||||
fontSize: 14,
|
||||
padding: 10,
|
||||
color: '$iconColor'
|
||||
}
|
||||
})
|
52
src/screens/trade/styles/swapAmountInput.styles.ts
Normal file
52
src/screens/trade/styles/swapAmountInput.styles.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: 12,
|
||||
marginVertical:6,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom:32,
|
||||
paddingTop:12,
|
||||
borderWidth: 1,
|
||||
borderRadius: 16,
|
||||
borderColor: '$primaryLightBackground',
|
||||
backgroundColor: '$primaryLightBackground'
|
||||
} as ViewStyle,
|
||||
label: {
|
||||
fontSize: 18,
|
||||
color: '$primaryDarkText',
|
||||
paddingVertical: 6,
|
||||
} as TextStyle,
|
||||
inputContainer:{
|
||||
flexDirection:'row',
|
||||
justifyContent:'space-between',
|
||||
alignItems:'center',
|
||||
} as ViewStyle,
|
||||
input: {
|
||||
flex: 1,
|
||||
borderWidth: 0,
|
||||
color: '$primaryDarkText',
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
paddingVertical: 6,
|
||||
marginTop: 10,
|
||||
} as TextStyle,
|
||||
symbolContainer:{
|
||||
padding:6,
|
||||
paddingHorizontal:12,
|
||||
backgroundColor:'$primaryDarkGray',
|
||||
borderRadius:24,
|
||||
} as ViewStyle,
|
||||
symbol:{
|
||||
fontSize:16,
|
||||
fontWeight:'bold',
|
||||
color: '$white',
|
||||
} as TextStyle,
|
||||
fiat: {
|
||||
fontSize: 14,
|
||||
paddingVertical: 6,
|
||||
color: '$iconColor'
|
||||
},
|
||||
|
||||
})
|
37
src/screens/trade/styles/swapFeeSection.styles.ts
Normal file
37
src/screens/trade/styles/swapFeeSection.styles.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
margin: 12,
|
||||
padding: 6,
|
||||
borderRadius: 16,
|
||||
flexDirection:'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems:'center',
|
||||
backgroundColor: '$primaryLightBackground'
|
||||
} as ViewStyle,
|
||||
label: {
|
||||
fontSize: 18,
|
||||
color: '$primaryDarkText',
|
||||
padding: 10,
|
||||
} as TextStyle,
|
||||
freeContainer:{
|
||||
paddingVertical:4,
|
||||
paddingHorizontal:8,
|
||||
borderRadius:6,
|
||||
marginHorizontal:8,
|
||||
backgroundColor:'$primaryGreen'
|
||||
} as ViewStyle,
|
||||
free: {
|
||||
borderWidth: 0,
|
||||
color: '$primaryDarkText',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
} as TextStyle,
|
||||
fiat: {
|
||||
fontSize: 14,
|
||||
padding: 10,
|
||||
color: '$iconColor'
|
||||
}
|
||||
})
|
36
src/screens/trade/styles/tradeScreen.styles.ts
Normal file
36
src/screens/trade/styles/tradeScreen.styles.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import EStyleSheet from 'react-native-extended-stylesheet';
|
||||
|
||||
export default EStyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '$primaryBackgroundColor'
|
||||
},
|
||||
balance: {
|
||||
marginHorizontal: 16,
|
||||
fontSize: 14,
|
||||
color: '$iconColor',
|
||||
alignSelf: 'flex-end'
|
||||
} as TextStyle,
|
||||
marketRate: {
|
||||
padding: 10,
|
||||
marginHorizontal: 10,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '$primaryDarkText'
|
||||
} as TextStyle,
|
||||
|
||||
mainBtnContainer:{
|
||||
alignItems:'center'
|
||||
} as ViewStyle,
|
||||
mainBtn: {
|
||||
width: '$deviceWidth / 3',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginVertical: 16,
|
||||
} as ViewStyle,
|
||||
buttonText:{
|
||||
color: 'white',
|
||||
}
|
||||
})
|
@ -7,6 +7,7 @@ import TransferView from './screen/transferScreen';
|
||||
import AddressView from './screen/addressScreen';
|
||||
import PowerDownView from './screen/powerDownScreen';
|
||||
import DelegateView from './screen/delegateScreen';
|
||||
import TransferTypes from '../../constants/transferTypes';
|
||||
|
||||
const Transfer = ({ navigation, route }) => (
|
||||
<TransferContainer navigation={navigation} route={route}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Button, Text, View } from 'react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { get, debounce } from 'lodash';
|
||||
|
@ -16,7 +16,7 @@ export default EStyleSheet.create({
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
rightIconContainer:{
|
||||
marginHorizontal:8
|
||||
}
|
||||
rightIconContainer: {
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
});
|
||||
|
@ -1,11 +0,0 @@
|
||||
export const countDecimals = (value) => {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Math.floor(value) === value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value.toString().split('.')[1].length || 0;
|
||||
};
|
51
src/utils/number.ts
Normal file
51
src/utils/number.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export const countDecimals = (value) => {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Math.floor(value) === value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value.toString().split('.')[1].length || 0;
|
||||
};
|
||||
|
||||
export const stripDecimalPlaces = (value: number, precision: number = 3) => {
|
||||
if (!Number(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const power = Math.pow(10, precision);
|
||||
|
||||
return Math.floor(value * power) / power;
|
||||
};
|
||||
|
||||
export const getDecimalPlaces = (value: number) => {
|
||||
const regex = /(?<=\.)\d+/;
|
||||
const match = value.toString().match(regex);
|
||||
return match ? match[0].length : 0;
|
||||
};
|
||||
|
||||
export const formatNumberInputStr = (text: string, precision: number = 10) => {
|
||||
if (text.includes(',')) {
|
||||
text = text.replace(',', '.');
|
||||
}
|
||||
|
||||
const _num = parseFloat(text);
|
||||
|
||||
if (_num) {
|
||||
let _retVal = text;
|
||||
if ((text.startsWith('0') && _num >= 1) || text.startsWith('.')) {
|
||||
_retVal = `${_num}`;
|
||||
}
|
||||
|
||||
if (getDecimalPlaces(_num) > precision) {
|
||||
_retVal = `${stripDecimalPlaces(_num, precision)}`;
|
||||
}
|
||||
return _retVal;
|
||||
} else if (text === '') {
|
||||
return '0';
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
};
|
@ -4,12 +4,12 @@
|
||||
* @returns formated human readable string
|
||||
*/
|
||||
|
||||
export const getHumanReadableKeyString = (intlKey:string) => {
|
||||
const words = intlKey.split('_');
|
||||
const capitalizedWords = words.map(word => {
|
||||
const firstLetter = word.charAt(0).toUpperCase();
|
||||
const remainingLetters = word.slice(1).replace(/([A-Z])/g, ' $1');
|
||||
return firstLetter + remainingLetters;
|
||||
})
|
||||
return capitalizedWords.join(' ');
|
||||
}
|
||||
export const getHumanReadableKeyString = (intlKey: string) => {
|
||||
const words = intlKey.split('_');
|
||||
const capitalizedWords = words.map((word) => {
|
||||
const firstLetter = word.charAt(0).toUpperCase();
|
||||
const remainingLetters = word.slice(1).replace(/([A-Z])/g, ' $1');
|
||||
return firstLetter + remainingLetters;
|
||||
});
|
||||
return capitalizedWords.join(' ');
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import get from 'lodash/get';
|
||||
import isArray from 'lodash/isArray';
|
||||
import { operationOrders } from '@hiveio/dhive/lib/utils';
|
||||
import { utils } from '@hiveio/dhive';
|
||||
import parseDate from './parseDate';
|
||||
@ -37,9 +36,8 @@ import {
|
||||
import { EngineActions, EngineOperations, HistoryItem } from '../providers/hive-engine/hiveEngine.types';
|
||||
import { ClaimsCollection } from '../redux/reducers/cacheReducer';
|
||||
import { fetchSpkWallet } from '../providers/hive-spk/hiveSpk';
|
||||
import { SpkActions } from '../providers/hive-spk/hiveSpk.types';
|
||||
import TransferTypes from '../constants/transferTypes';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
|
||||
export const transferTypes = [
|
||||
'curation_reward',
|
||||
@ -69,8 +67,15 @@ const HIVE_ACTIONS = [
|
||||
'transfer_to_savings',
|
||||
'transfer_to_vesting',
|
||||
'withdraw_hive',
|
||||
'swap_token'
|
||||
];
|
||||
const HBD_ACTIONS = [
|
||||
'transfer_token',
|
||||
'transfer_to_savings',
|
||||
'convert',
|
||||
'withdraw_hbd',
|
||||
'swap_token'
|
||||
];
|
||||
const HBD_ACTIONS = ['transfer_token', 'transfer_to_savings', 'convert', 'withdraw_hbd'];
|
||||
const HIVE_POWER_ACTIONS = ['delegate', 'power_down'];
|
||||
|
||||
export const groomingTransactionData = (transaction, hivePerMVests): CoinActivity | null => {
|
||||
|
@ -8966,9 +8966,10 @@ react-native-codegen@^0.70.6:
|
||||
jscodeshift "^0.13.1"
|
||||
nullthrows "^1.1.1"
|
||||
|
||||
react-native-config@luggit/react-native-config#master:
|
||||
version "1.5.0"
|
||||
resolved "https://codeload.github.com/luggit/react-native-config/tar.gz/4ceb1dc4d05415f352c180469b511714e00cf5bd"
|
||||
react-native-config@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.5.1.tgz#73c94f511493e9b7ff9350cdf351d203a1b05acc"
|
||||
integrity sha512-g1xNgt1tV95FCX+iWz6YJonxXkQX0GdD3fB8xQtR1GUBEqweB9zMROW77gi2TygmYmUkBI7LU4pES+zcTyK4HA==
|
||||
|
||||
react-native-crypto-js@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user