Merge branch 'development' of https://github.com/ecency/ecency-mobile into sa/hive-uri-support

This commit is contained in:
Sadaqat Ali 2023-08-09 13:15:12 +05:00
commit fbe067906f
43 changed files with 1464 additions and 272 deletions

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
};

View File

@ -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",

View File

@ -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}`,

View File

@ -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',

View File

@ -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

View File

@ -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);

View 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,
};
};

View 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,
};
};

View 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;
}

View File

@ -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');

View File

@ -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);

View File

@ -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 },

View File

@ -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;

View File

@ -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 = {

View File

@ -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));

View File

@ -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,

View 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>
);
};

View 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>
);
};

View File

@ -0,0 +1,5 @@
export * from './swapTokenContent';
export * from './swapAmountInput';
export * from './swapFeeSection';
export * from './errorSection';
export * from './assetChangeBtn';

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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,
};
};

View File

@ -0,0 +1,3 @@
import TradeScreen from './screen/tradeScreen';
export default TradeScreen;

View 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;

View 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,
})

View 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'
}
})

View 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'
},
})

View 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'
}
})

View 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',
}
})

View File

@ -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}>

View File

@ -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';

View File

@ -16,7 +16,7 @@ export default EStyleSheet.create({
right: 0,
alignItems: 'center',
},
rightIconContainer:{
marginHorizontal:8
}
rightIconContainer: {
marginHorizontal: 8,
},
});

View File

@ -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
View 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;
}
};

View File

@ -4,12 +4,12 @@
* @returns formated human readable string
*/
export const getHumanReadableKeyString = (intlKey:string) => {
export const getHumanReadableKeyString = (intlKey: string) => {
const words = intlKey.split('_');
const capitalizedWords = words.map(word => {
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(' ');
}
};

View File

@ -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 => {

View File

@ -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"