diff --git a/.env.development b/.env.development index 3350a645a..2eca5e063 100644 --- a/.env.development +++ b/.env.development @@ -10,7 +10,10 @@ NEXT_PUBLIC_PORTAL_ID=20146426 NEXT_PUBLIC_SUBSCRIBER_FORM_ID=f4023600-6995-433b-894a-2a1ab09dc2f6 NEXT_PUBLIC_SUPPORT_FORM_ID=282959ce-3633-4a73-907a-ef84cebe1123 +NEXT_PUBLIC_HUBSPOT_APIKEY=3382e6ff-1c9c-43f1-a622-52f60c30cce3 + +ANALYTICS_TINYMAN_V1_API=https://testnet.analytics.tinyman.org/api/v1 HUBSPOT_APIKEY= ALGODEX_API_V2=http://testnet-services-2.algodex.com:8080 -SKIP_PRERENDER_EXCEPT_DEFAULT=1 \ No newline at end of file +SKIP_PRERENDER_EXCEPT_DEFAULT=1 diff --git a/.env.test b/.env.test index 28dd527ed..27b9749f9 100644 --- a/.env.test +++ b/.env.test @@ -2,4 +2,6 @@ NEXT_PUBLIC_ENV=development NEXT_PUBLIC_API=https://api-testnet-public.algodex.com NEXT_PUBLIC_DEBUG=true DISABLE_SENTRY=true + +ANALYTICS_TINYMAN_V1_API=https://testnet.analytics.tinyman.org/api/v1 ALGODEX_API_V2=http://testnet-services-2.algodex.com:3006 diff --git a/components/Asset/Chart/Chart.jsx b/components/Asset/Chart/Chart.jsx index 7fa2955d2..97f5c5eb5 100644 --- a/components/Asset/Chart/Chart.jsx +++ b/components/Asset/Chart/Chart.jsx @@ -21,6 +21,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import ChartOverlay from './ChartOverlay' import ChartSettings from './ChartSettings' import PropTypes from 'prop-types' +import floatToFixed from '@algodex/algodex-sdk/lib/utils/format/floatToFixed' import millify from 'millify' import styled from '@emotion/styled' import useAreaChart from './hooks/useAreaChart' @@ -117,18 +118,30 @@ export function Chart({ interval: _interval, mode: _mode, volume, + algoVolume, ohlc, overlay: _overlay, onChange }) { // console.log(`Chart(`, arguments[0], `)`) + const [interval, setInterval] = useState(_interval) const [overlay, setOverlay] = useState(_overlay) const [chartMode, setChartMode] = useState(_mode) const [currentLogical, setCurrentLogical] = useState(ohlc.length - 1) + // Update ohlc data when it is stable asset + algoVolume = [...volume] // temporary solution: should be updated from backend + if (asset.isStable) { + ohlc.forEach((ele, index) => { + ohlc[index].open = ele.open != 0 ? floatToFixed(1 / ele.open) : 'Invalid' + ohlc[index].low = ele.low != 0 ? floatToFixed(1 / ele.low) : 'Invalid' + ohlc[index].high = ele.high != 0 ? floatToFixed(1 / ele.high) : 'Invalid' + ohlc[index].close = ele.close != 0 ? floatToFixed(1 / ele.close) : 'Invalid' + }) + } useEffect(() => { - setOverlay(_overlay) + // setOverlay(_overlay) setCurrentLogical(ohlc.length - 1) }, [ohlc, _overlay, setOverlay]) @@ -167,23 +180,36 @@ export function Chart({ const updateHoverPrices = useCallback( (logical) => { - if (ohlc == null || volume == null) { - return + if (asset.isStable) { + if (ohlc == null || algoVolume == null) { + return + } + } else { + if (ohlc == null || volume == null) { + return + } } + const priceEntry = ohlc[logical] const volumeEntry = volume[logical] - + const algoVolumeEntry = algoVolume[logical] setOverlay({ ...overlay, ohlc: priceEntry, - volume: volumeEntry != null ? millify(volumeEntry.value) : '0' + volume: volumeEntry != null ? millify(volumeEntry.value) : '0', + algoVolume: algoVolumeEntry != null ? millify(algoVolumeEntry.value) : '0' }) }, - [ohlc, volume, setOverlay, overlay] + [ohlc, volume, setOverlay, overlay, asset, algoVolume] ) const mouseOut = useCallback(() => { - setOverlay(_overlay) + if (asset.isStable) { + const __overlay = { ...overlay, ..._overlay } + setOverlay(__overlay) + } else { + setOverlay(_overlay) + } }, [setOverlay, _overlay]) const mouseMove = useCallback( @@ -197,12 +223,17 @@ export function Chart({ const rect = ReactDOM.findDOMNode(ev.target).getBoundingClientRect() const x = ev.clientX - rect.left const logical = candleChart.timeScale().coordinateToLogical(x) - - if (logical >= ohlc.length || logical >= volume.length) { - setOverlay(_overlay) - return + if (asset.isStable) { + if (logical >= ohlc.length || logical >= algoVolume.length) { + // setOverlay(_overlay) + return + } + } else { + if (logical >= ohlc.length || logical >= volume.length) { + // setOverlay(_overlay) + return + } } - if (logical !== currentLogical) { setCurrentLogical(logical) updateHoverPrices(logical) @@ -218,6 +249,8 @@ export function Chart({ setCurrentLogical, updateHoverPrices, volume, + algoVolume, + asset.isStable, ohlc ] ) @@ -247,7 +280,7 @@ export function Chart({ bid={overlay.orderbook.bid} ask={overlay.orderbook.ask} spread={overlay.orderbook.spread} - volume={overlay.volume} + volume={asset.isStable ? overlay.algoVolume : overlay.volume} /> )} {typeof overlay.ohlc === 'undefined' && ( @@ -257,7 +290,7 @@ export function Chart({ bid={_overlay.orderbook.bid} ask={_overlay.orderbook.ask} spread={_overlay.orderbook.spread} - volume={_overlay.volume} + volume={asset.isStable ? _overlay.algoVolume : _overlay.volume} /> )} @@ -270,7 +303,8 @@ export function Chart({ Chart.propTypes = { asset: PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired + decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + isStable: PropTypes.bool }).isRequired, interval: PropTypes.string.isRequired, mode: PropTypes.string.isRequired, @@ -282,6 +316,7 @@ Chart.propTypes = { close: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }), volume: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + algoVolume: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), orderbook: PropTypes.shape({ bid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), ask: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -290,6 +325,7 @@ Chart.propTypes = { }), ohlc: PropTypes.array.isRequired, volume: PropTypes.array.isRequired, + algoVolume: PropTypes.array, onChange: PropTypes.func } diff --git a/components/Asset/Chart/ChartOverlay.jsx b/components/Asset/Chart/ChartOverlay.jsx index 8711c52b5..40f05d3b9 100644 --- a/components/Asset/Chart/ChartOverlay.jsx +++ b/components/Asset/Chart/ChartOverlay.jsx @@ -238,9 +238,16 @@ function ChartOverlay(props) { color={theme.palette.gray['500']} /> )} -
-  {`${asset.name} `} / ALGO -
+ {!asset.isStable && ( +
+  {`${asset.name} `} / ALGO +
+ )} + {asset.isStable && ( +
+ ALGO / {`${asset.name} `} +
+ )}
@@ -267,6 +274,10 @@ function ChartOverlay(props) {
{openCloseChange}
+ {/* +
Volume:
+
{volume}
+
*/} @@ -277,7 +288,9 @@ function ChartOverlay(props) {
Vol:
-
{`${volume} ${asset.name}`}
+ {/*
{`${volume} ${asset.name}`}
*/} + {asset.isStable &&
{`${volume} ALGO`}
} + {!asset.isStable &&
{`${volume} ${asset.name}`}
}
@@ -292,5 +305,4 @@ ChartOverlay.propTypes = { spread: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), volume: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) } - export default ChartOverlay diff --git a/components/Asset/OrderBook/OrderBook.jsx b/components/Asset/OrderBook/OrderBook.jsx index b5e2bd742..389b08905 100644 --- a/components/Asset/OrderBook/OrderBook.jsx +++ b/components/Asset/OrderBook/OrderBook.jsx @@ -382,9 +382,11 @@ const DECIMALS_MAP = { },[asset.decimals, maxSpendableAlgo]) const reduceOrders = useCallback((result, order) => { - const _price = floatToFixedDynamic(order.price, selectedPrecision, selectedPrecision) - - const _amount = order.amount + // const _price = floatToFixedDynamic(order.price, selectedPrecision, selectedPrecision) + const _amount = asset.isStable ? order.amount * order.price : order.amount + const _price = asset.isStable + ? floatToFixedDynamic(1 / order.price, selectedPrecision, selectedPrecision) + : floatToFixedDynamic(order.price, selectedPrecision, selectedPrecision) const index = result.findIndex( (obj) => floatToFixedDynamic(obj.price, selectedPrecision, selectedPrecision) === _price ) @@ -413,6 +415,14 @@ const DECIMALS_MAP = { return orders.sell.reduce(reduceOrders, []) }, [orders.sell, reduceOrders]) + const sortedBuyOrder = useMemo(() => { + return aggregatedBuyOrder.sort((a, b) => b.price - a.price) + }, [aggregatedBuyOrder]) + + const sortedSellOrder = useMemo(() => { + return aggregatedSellOrder.sort((a, b) => b.price - a.price) + }, [aggregatedSellOrder]) + const isGeoBlocked = useMemo(() => getIsRestrictedCountry(query) && getIsRestricted(asset.id) , [asset.id, query]) @@ -508,16 +518,40 @@ const DECIMALS_MAP = {
- + {/* {t('amount')} ({assetVeryShortName}) - + */} + + + + + {t('amount')} ({asset.isStable ? 'ALGO' : assetVeryShortName}) + + + {t('total')} ({asset.isStable ? 'ALGO' : assetVeryShortName}) +
- {renderedSellOrders} + {/* {renderedSellOrders} */} + + {renderOrders(asset.isStable ? sortedBuyOrder : aggregatedSellOrder, 'sell')} + @@ -527,6 +561,7 @@ const DECIMALS_MAP = { {renderedBuyOrders} + {/* {renderOrders(asset.isStable ? sortedSellOrder : aggregatedBuyOrder, 'buy')} */} diff --git a/components/Asset/OrderBook/OrderBookPriceInfo.js b/components/Asset/OrderBook/OrderBookPriceInfo.js index 2f36a5cca..88ab3aeb1 100644 --- a/components/Asset/OrderBook/OrderBookPriceInfo.js +++ b/components/Asset/OrderBook/OrderBookPriceInfo.js @@ -26,6 +26,7 @@ import convertFromAsaUnits from '@algodex/algodex-sdk/lib/utils/units/fromAsaUni import floatToFixed from '@algodex/algodex-sdk/lib/utils/format/floatToFixed' import { formatUSDPrice } from '@/components/helpers' import { mdiApproximatelyEqual } from '@mdi/js' +// import { useMemo } from 'react' import { withAlgorandPriceQuery } from '@algodex/algodex-hooks' const getPriceDecimals = (price) => { @@ -76,6 +77,36 @@ PriceInfoView.propTypes = { asaValue: PropTypes.any } export function OrderBookPriceInfo({ algoPrice, asset }) { + // const percentageChange = useMemo(() => { + // return asset?.price_info && floatToFixed(asset?.price_info?.price24Change, 2) + // }, [asset]) + // const asaUnit = convertFromAsaUnits(asset?.price_info?.price, asset.decimals) + // const asaValue = !asset.isStable + // ? floatToFixed(asaUnit) + // : asaUnit === 0 + // ? 'Invalid Number' + // : floatToFixed(1 / asaUnit) + + // return ( + // <> + // + // {asaValue} + // + // {asset && asset.price_info && ( + // + // {(asset?.price_info?.price24Change && `${percentageChange}%`) || '0.00%'} + // + // )} + //
+ // + // + // ${formatUSDPrice(algoPrice * asaValue)} + // + //
+ // + // ) + + const decimals = getPriceDecimals(asset?.price_info?.price || 0) const asaValue = floatToFixed(asset?.price_info?.price || 0, decimals, 6) return typeof asset?.price_info === 'undefined' ? : diff --git a/components/Asset/TradeHistory/TradeHistory.jsx b/components/Asset/TradeHistory/TradeHistory.jsx index 0e1c1b4bb..009eeecb0 100644 --- a/components/Asset/TradeHistory/TradeHistory.jsx +++ b/components/Asset/TradeHistory/TradeHistory.jsx @@ -126,16 +126,21 @@ const PriceHeaderText = styled(Typography)` } ` -const PriceHeader = () => { +const PriceHeader = ({ currencySymbol }) => { const { t } = useTranslation('common') return ( {t('price')} - + {!currencySymbol && } + {currencySymbol &&  {currencySymbol}} ) } +PriceHeader.propTypes = { + currencySymbol: PropTypes.string +} + /** * Asset Trade History * @@ -180,10 +185,14 @@ export function TradeHistory({ asset, orders: tradesData }) { }) .map((row) => { const amount = new Big(row.amount) - + if (row.price === 0) { + return + } + const price = asset.isStable ? 1 / row.price : row.price return ( + {/* {floatToFixed(price)} */} {floatToFixed(row.price, priceDecimals, 6)}
- + - {t('amount')} ({assetVeryShortName}) + {t('amount')} ({asset.isStable ? 'ALGO' : assetVeryShortName}) {t('time')} diff --git a/components/Nav/SearchSidebar/SearchTable.jsx b/components/Nav/SearchSidebar/SearchTable.jsx index 9e1e84996..f8278084d 100644 --- a/components/Nav/SearchSidebar/SearchTable.jsx +++ b/components/Nav/SearchSidebar/SearchTable.jsx @@ -30,6 +30,7 @@ import { DelistedAssets } from '@/components/DelistedAssets' import Icon from '@mdi/react' import PropTypes from 'prop-types' import SearchFlyover from './SearchFlyover' +import { StableAssets } from '@/components/StableAssets' import Table from '@/components/Table' import Tooltip from 'components/Tooltip' import { flatten } from 'lodash' @@ -73,7 +74,8 @@ export const mapToSearchResults = ({ unitName, isGeoBlocked, formattedASALiquidity, - formattedAlgoLiquidity + formattedAlgoLiquidity, + isStable }) => { const price = formattedPrice ? floatToFixedDisplay(formattedPrice) : hasOrders ? '--' : null @@ -94,6 +96,7 @@ export const mapToSearchResults = ({ liquidityAsa: formattedASALiquidity, price, change, + isStable, decimals } } @@ -233,6 +236,10 @@ export const NavSearchTable = ({ }, [favoritesState] ) + const formattedStableAsa = {} + const formattedAssets = StableAssets.forEach( + (asa, index) => (formattedStableAsa[StableAssets[index]] = asa) + ) const handleRestrictedAsset = useCallback((assetsList) => { if (typeof assetsList !== 'undefined') { @@ -268,7 +275,6 @@ export const NavSearchTable = ({ // Geoformatted assets const geoFormattedAssets = handleRestrictedAsset(_acceptedAssets) const filteredList = sortBy(geoFormattedAssets.assets, { isGeoBlocked: true }) - // Return List if (!filteredList || !Array.isArray(filteredList) || filteredList.length === 0) { return [] @@ -320,6 +326,7 @@ export const NavSearchTable = ({ const AssetNameCell = useCallback( ({ value, row }) => { + // console.log(row, value, 'row and value') return (
- - {value} - {`/`} - - ALGO - {/* {row.original.verified && } */} - - + {formattedStableAsa[row?.original.id] && ( + + ALGO + {`/`} + + {value} + {/* {row.original.verified && } */} + + + )} + {!formattedStableAsa[row?.original.id] && ( + + {value} + {`/`} + + ALGO + {/* {row.original.verified && } */} + + + )}
{/*
*/}
diff --git a/components/StableAssets b/components/StableAssets new file mode 100644 index 000000000..e3933b7f6 --- /dev/null +++ b/components/StableAssets @@ -0,0 +1,9 @@ +// Current StableCoin List. +// Should check whether to decide using of assetid or asset name +export const StableAssets = [ + 51435943, // USDC, + 37074699, // USDC, + 38718614, // USDC, + 42279195, // USDT + 94115664 //USDT +] \ No newline at end of file diff --git a/components/StableAssets.js b/components/StableAssets.js new file mode 100644 index 000000000..02b696a13 --- /dev/null +++ b/components/StableAssets.js @@ -0,0 +1,9 @@ +// Current StableCoin List. +// Should check whether to decide using of assetid or asset name +export const StableAssets = [ + 51435943, // USDC, + 37074699, // USDC, + 38718614, // USDC, + 42279195, // USDT + 94115664 //USDT +] diff --git a/components/Table/Cell.jsx b/components/Table/Cell.jsx index d0d6918be..979202cd5 100644 --- a/components/Table/Cell.jsx +++ b/components/Table/Cell.jsx @@ -27,8 +27,8 @@ import useTranslation from 'next-translate/useTranslation' import { getActiveNetwork } from 'services/environment' const OrderTypeSpan = styled.span` - color: ${({ theme, value }) => - ('' + value).toUpperCase() === 'BUY' ? theme.palette.green[500] : theme.palette.red[500]}; + color: ${({ theme, value }) => + ('' + value).toUpperCase() === 'BUY' ? theme.palette.green[500] : theme.palette.red[500]} ` const TradeDetailLink = styled.a` @@ -51,6 +51,15 @@ export const AssetNameCell = ({ value, row }) => { const onClick = useCallback(() => { dispatcher('clicked', 'asset') }, [dispatcher]) + + const formattedPair = (value) => { + const splittedPair = value.split('/') + if (row.original.isStable && typeof splittedPair[1] !== 'undefined') { + return `${splittedPair[1]}/${splittedPair[0]}` + } else { + return value + } + } return ( {/* */} @@ -76,11 +85,22 @@ AssetNameCell.propTypes = { row: PropTypes.any, value: PropTypes.any } * @returns {JSX.Element} * @constructor */ -export const OrderTypeCell = ({ value }) => { +export const OrderTypeCell = ({ row, value }) => { const { t } = useTranslation('orders') + + const formattedPair = (value) => { + if (row.original.isStable) { + return value === 'BUY' ? t('sell') : t('buy') + } else { + return t(value.toLowerCase()) + // console.log(value, 'value here') + } + } + return ( - - {t(value.toLowerCase())} + + {formattedPair(value)} + {/* {t(value.toLowerCase())} */} ) } diff --git a/components/Table/PriceHeader.jsx b/components/Table/PriceHeader.jsx index cce9a35bd..a7769fb20 100644 --- a/components/Table/PriceHeader.jsx +++ b/components/Table/PriceHeader.jsx @@ -39,11 +39,19 @@ export const TablePriceHeader = ({ title, textAlign }) => { ) + + // const { t } = useTranslation('common') + // return ( + // + // {t('price')} + // {!currencySymbol && } + // {currencySymbol &&  {currencySymbol}} } TablePriceHeader.propTypes = { title: PropTypes.string.isRequired, - textAlign: PropTypes.string.isRequired + textAlign: PropTypes.string.isRequired, + currencySymbol: PropTypes.string } export default TablePriceHeader diff --git a/components/Table/Table.jsx b/components/Table/Table.jsx index ffcdf61b5..a5707fb6d 100644 --- a/components/Table/Table.jsx +++ b/components/Table/Table.jsx @@ -111,6 +111,7 @@ const Container = styled.div` line-height: 1.25; border-right: solid 1px ${({ theme }) => theme.palette.gray['700']}; border-bottom: solid 1px ${({ theme }) => theme.palette.gray['700']}; + &:first-of-type { padding-left: 1.125rem; box-sizing: border-box; diff --git a/components/Wallet/PlaceOrder/Form.jsx b/components/Wallet/PlaceOrder/Form.jsx index 16e265a19..1febc9dfc 100644 --- a/components/Wallet/PlaceOrder/Form.jsx +++ b/components/Wallet/PlaceOrder/Form.jsx @@ -45,7 +45,6 @@ export const Form = styled.form` display: none; } ` - function shallowEqual(object1, object2) { const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); @@ -59,7 +58,6 @@ function shallowEqual(object1, object2) { } return true; } - /** * # 📝 Place Order Form * @@ -211,8 +209,14 @@ export function PlaceOrderForm({ showTitle = true, asset, onSubmit, components: const buttonProps = useMemo( () => ({ - buy: { variant: 'primary', text: `${t('buy')} ${asset.name || asset.id}` }, - sell: { variant: 'danger', text: `${t('sell')} ${asset.name || asset.id}` } + buy: { + variant: 'primary', + text: `${t('buy')} ${(asset.isStable ? 'ALGO' : asset.name) || asset.id}` + }, + sell: { + variant: 'danger', + text: `${t('sell')} ${(asset.isStable ? 'ALGO' : asset.name) || asset.id}` + } }), [asset] ) @@ -355,6 +359,8 @@ export function PlaceOrderForm({ showTitle = true, asset, onSubmit, components: } lastToastId = toast.loading(msg, { duration: 30 * 60 * 1000 }) // Awaiting signature, or awaiting confirmations } + const { isStable } = asset + const _order = { ...order, type: isStable && order.type === 'buy' ? 'sell' : 'buy' } if (typeof onSubmit === 'function') { // What is the purpose of this conditional? // I have checked everywhere in the codebase and no other componenet passes an onSubmit prop to this component @@ -366,6 +372,7 @@ export function PlaceOrderForm({ showTitle = true, asset, onSubmit, components: } else { console.log( { + _order, ...formattedOrder, address: wallet.address, wallet, @@ -375,13 +382,13 @@ export function PlaceOrderForm({ showTitle = true, asset, onSubmit, components: }, { wallet } ) - const awaitPlaceOrder = async () => { try { notifier('Initializing order') await placeOrder( { - ...formattedOrder, + _order, + ...formattedOrder, address: wallet.address, wallet, asset, @@ -408,7 +415,7 @@ export function PlaceOrderForm({ showTitle = true, asset, onSubmit, components: wallet }) }, - [onSubmit, asset, order, wallet] + [onSubmit, asset, asset, order, wallet] ) const handleMarketTabSwitching = (e, tabId) => { setTabSwitch(tabId) @@ -553,7 +560,8 @@ PlaceOrderForm.propTypes = { id: PropTypes.number.isRequired, decimals: PropTypes.number.isRequired, name: PropTypes.string, - isGeoBlocked: PropTypes.bool + isGeoBlocked: PropTypes.bool, + isStable: PropTypes.bool }).isRequired, /** * Wallet to execute Orders from diff --git a/components/Wallet/PlaceOrder/Form/AvailableBalance.jsx b/components/Wallet/PlaceOrder/Form/AvailableBalance.jsx index dbb4f7faa..f552a29a3 100644 --- a/components/Wallet/PlaceOrder/Form/AvailableBalance.jsx +++ b/components/Wallet/PlaceOrder/Form/AvailableBalance.jsx @@ -66,6 +66,33 @@ const IconButton = styled.button` } ` +const AsaBalance = ({ amount, asaName, type, decimal }) => { + const _amount = type === 'others' ? fromBaseUnits(amount, decimal) : fromBaseUnits(amount) + const _usdPrice = type === 'others' ? fromBaseUnits(amount, decimal) : fromBaseUnits(amount) + return ( + + + {asaName} + + + + {_amount} + + + + + + + ) +} + +AsaBalance.propTypes = { + amount: PropTypes.number, + asaName: PropTypes.string, + type: PropTypes.string, + decimal: PropTypes.number +} + export const AvailableBalance = ({ wallet, asset }) => { const { t } = useTranslation('place-order') const maxSpendableAlgo = useMaxSpendableAlgo() @@ -141,6 +168,27 @@ export const AvailableBalance = ({ wallet, asset }) => { + {/* {asset.isStable ? ( + <> + + + + ) : ( + <> + + + + )} */} ALGO @@ -177,6 +225,7 @@ AvailableBalance.propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string, decimals: PropTypes.number.isRequired, + isStable: PropTypes.bool, price_info: PropTypes.object }), wallet: PropTypes.shape({ diff --git a/components/Wallet/PlaceOrder/Form/TradeInputs.jsx b/components/Wallet/PlaceOrder/Form/TradeInputs.jsx index b8dde8be8..07bc454d1 100644 --- a/components/Wallet/PlaceOrder/Form/TradeInputs.jsx +++ b/components/Wallet/PlaceOrder/Form/TradeInputs.jsx @@ -86,6 +86,82 @@ NumberFormatCustom.defaultProps = { } +const OutlinedInputComp = ({ + microAlgo, + value, + id, + name, + assetName, + errorMsgVisible, + executionType, + handleChange, + fieldName +}) => { + const { t } = useTranslation('place-order') + return ( + <> + + {fieldName} + + } + endAdornment={ + + {assetName} + + } + error={errorMsgVisible} + /> + {errorMsgVisible && executionType !== 'market' ? ( + + Price cannot be less than {microAlgo} + + ) : + name === 'price' && + } + + ) +} + +OutlinedInputComp.propTypes = { + microAlgo: PropTypes.number, + value: PropTypes.string, + assetName: PropTypes.string, + errorMsgVisible: PropTypes.bool, + executionType: PropTypes.string, + handleChange: PropTypes.func, + name: PropTypes.string, + id: PropTypes.string, + fieldName: PropTypes.string +} + export const TradeInputs = ({ order, handleChange, @@ -146,7 +222,7 @@ export const TradeInputs = ({ }, [order, microAlgo]) return ( - ) : ( - + + )} */} + {asset.isStable ? ( + <> + + + + ) : ( + <> + + + )} - - */} + {/* {asset.name} } - /> + /> */} - ALGO + + {asset.isStable ? asset.name : 'ALGO'} + } /> diff --git a/components/Wallet/PlaceOrder/Original/amount-range/index.jsx b/components/Wallet/PlaceOrder/Original/amount-range/index.jsx new file mode 100644 index 000000000..182bea842 --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/amount-range/index.jsx @@ -0,0 +1,119 @@ +import Big from 'big.js' +import { Container } from './amount-range.css' +import PropTypes from 'prop-types' +import { default as Slider } from 'components/Input/Slider' + +function AmountRange(props) { + const { order, algoBalance: _algoBalance, asaBalance: _asaBalance, asset, onChange } = props + const isBuyOrder = order.type === 'buy' + const price = new Big(order.price || 0).toString() + const amount = new Big(order.amount || 0).toString() + const algoBalance = new Big(_algoBalance).toString() + const asaBalance = new Big(_asaBalance).toString() + const currentPrice = new Big(asset.price_info.price || 0).toString() + + // @todo: calculate txn fees + // const value = isBuyOrder + // ? ((price * amount + txnFee) * 100) / algoBalance + // : (amount * 100) / asaBalance + const calculateValue = () => { + if (isBuyOrder) { + if (_algoBalance === 0) { + return 0 + } + return new Big(price).times(amount).times(100).div(algoBalance).toNumber() + } else { + if (_asaBalance === 0) { + return 0 + } + return new Big(amount).times(100).div(asaBalance).toNumber() + } + } + + const value = calculateValue() + // @todo: calculate txn fees + // const handleChange = (e) => { + // const adjustAlgoBalance = algoBalance - txnFee + + // if (isBuyOrder && !price) { + // onChange({ + // price: currentPrice, + // amount: ((adjustAlgoBalance * (Number(e.target.value) / 100)) / currentPrice).toFixed(6) + // }) + // return + // } + + // const newAmount = isBuyOrder + // ? ((adjustAlgoBalance * (Number(e.target.value) / 100)) / price).toFixed(6) + // : (asaBalance * (Number(e.target.value) / 100)).toFixed(6) + + // onChange({ + // amount: newAmount + // }) + // } + + const handleChange = (e) => { + if (isBuyOrder && price === '0') { + onChange({ + price: currentPrice, + amount: new Big(e.target.value).div(100).times(algoBalance).div(currentPrice).toString() + }) + return + } + + const newAmount = isBuyOrder + ? new Big(e.target.value).div(100).times(algoBalance).div(price).toString() + : new Big(e.target.value).div(100).times(asaBalance).toString() + + onChange({ + amount: newAmount + }) + } + + const marks = [ + { + value: 0, + label: '0%' + }, + { + value: 25, + label: '25%' + }, + { + value: 50, + label: '50%' + }, + { + value: 75, + label: '75%' + }, + { + value: 100, + label: '100%' + } + ] + + return ( + + + + ) +} + +AmountRange.propTypes = { + order: PropTypes.object.isRequired, + algoBalance: PropTypes.number.isRequired, + asaBalance: PropTypes.number.isRequired, + asset: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired +} + +export default AmountRange diff --git a/components/Wallet/PlaceOrder/Original/order-form.jsx b/components/Wallet/PlaceOrder/Original/order-form.jsx new file mode 100644 index 000000000..c4d91eed1 --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/order-form.jsx @@ -0,0 +1,287 @@ +import { BodyCopy, BodyCopyTiny } from '@/components/Typography' + +import AmountRange from './amount-range' +import Big from 'big.js' +import OrderInput from './order-input' +import OrderOptions from './order-options' +import PropTypes from 'prop-types' +import React from 'react' +import StableAssetUSDPrice from '@/components/Wallet/PriceConversion/StableAssetUSDPrice' +import USDPrice from '@/components/Wallet/PriceConversion/USDPrice' +import useTranslation from 'next-translate/useTranslation' + +/** + * + * Render USD Price for an input component + * @param {*} { value, id } + * @return {*} + */ +export const USDInputPrice = ({ value, id, showFee }) => { + return ( + <> + {showFee && ( +
+ Fee + + + USD + +
+ )} +
+ USD {id === 'price' ? 'Price' : 'Total'} + + + USD + +
+ + ) +} + +USDInputPrice.propTypes = { + showFee: PropTypes.bool, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + id: PropTypes.string, + asset: PropTypes.object +} + +/** + * + * Render Stable Asset USD Price for an input component + * @param {*} { value, id } + * @return {*} + */ +export const StableAssetUSDInputPrice = ({ value, id, assetId }) => { + return ( + <> +
+ USD {id === 'price' ? 'Price' : 'Total'} + + + USD + +
+ + ) +} + +StableAssetUSDInputPrice.propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + id: PropTypes.string, + assetId: PropTypes.number +} + +export const OrderForm = ({ + order, + handleChange, + asset, + maxSpendableAlgo, + asaBalance, + handleRangeChange, + enableOrder, + handleOptionsChange, + newOrderSizeFilter, + setNewOrderSizeFilter, + orderType, + microAlgo +}) => { + const { t } = useTranslation('place-order') + if (!enableOrder[order.type]) { + // @todo: make this better, this is a placeholder + return ( + + {t('insufficient-balance')} + + ) + } + + const isErrorMsgVisible = () => { + if (order.price === '0.00' || order.price === '') { + return false + } + if (order.price < microAlgo) { + return true + } + } + + return ( + <> + {!asset.isStable && ( +
+ + + + { + handleChange(e) + }} + autocomplete="false" + min="0" + step={new Big(10).pow(-1 * asset.decimals).toString()} + inputMode="decimal" + /> + + + + {/* + + Algorand transaction fees: {' '} + {txnFee.toFixed(3)} + + */} + {orderType === 'limit' && ( + console.log('Blocked') : handleOptionsChange} + allowTaker={typeof asset !== 'undefined'} + orderFilter={newOrderSizeFilter} + setOrderFilter={setNewOrderSizeFilter} + /> + )} +
+ )} + {asset.isStable && ( +
+ + + + { + handleChange(e) + }} + autocomplete="false" + min="0" + step={new Big(10).pow(-1 * asset.decimals).toString()} + inputMode="decimal" + /> + + + + {/* + + Algorand transaction fees: {' '} + {txnFee.toFixed(3)} + + */} + {orderType === 'limit' && ( + console.log('Blocked') : handleOptionsChange} + allowTaker={typeof asset !== 'undefined'} + orderFilter={newOrderSizeFilter} + setOrderFilter={setNewOrderSizeFilter} + /> + )} +
+ )} + + ) +} + +OrderForm.propTypes = { + orderType: PropTypes.string.isRequired, + order: PropTypes.object.isRequired, + asset: PropTypes.object.isRequired, + handleChange: PropTypes.func.isRequired, + maxSpendableAlgo: PropTypes.number.isRequired, + asaBalance: PropTypes.number.isRequired, + handleRangeChange: PropTypes.func, + handleOptionsChange: PropTypes.func, + enableOrder: PropTypes.object, + newOrderSizeFilter: PropTypes.number, + setNewOrderSizeFilter: PropTypes.func, + microAlgo: PropTypes.number +} diff --git a/components/Wallet/PlaceOrder/Original/order-input/index.jsx b/components/Wallet/PlaceOrder/Original/order-input/index.jsx new file mode 100644 index 000000000..507c19a69 --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/order-input/index.jsx @@ -0,0 +1,59 @@ +import { Asset, AssetUSD, Container, Input, Label } from './order-input.css' + +import { LabelSm } from '@/components/Typography' +import PropTypes from 'prop-types' +import USDPrice from '@/components/Wallet/PriceConversion/USDPrice' +import { ValidationMessage } from '@/components/InputValidations/ValidationMessage' + +function OrderInput({ label, asset, orderType, usdEquivalent, hasError, errorMessage, ...props }) { + const condenseAssetName = asset?.length > 5 + + if (usdEquivalent) { + return ( + <> + + + + + {asset} + {usdEquivalent && ( + <> +
+ USD + + )} +
+ {usdEquivalent && ( + + + + )} +
+ {hasError && } + + ) + } + return ( + <> + + + + {/* {asset} */} + {asset} + + {hasError && } + + ) +} + +OrderInput.propTypes = { + label: PropTypes.string, + asset: PropTypes.string, + decimals: PropTypes.number, + orderType: PropTypes.oneOf(['buy', 'sell']), + usdEquivalent: PropTypes.string, + hasError: PropTypes.bool, + errorMessage: PropTypes.string +} + +export default OrderInput diff --git a/components/Wallet/PlaceOrder/Original/order-options/order-options.css.js b/components/Wallet/PlaceOrder/Original/order-options/order-options.css.js new file mode 100644 index 000000000..6d4a8965d --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/order-options/order-options.css.js @@ -0,0 +1,147 @@ +import styled from '@emotion/styled' +import { lighten } from 'polished' +import Button from '@/components/Button' + +export const ExpandToggle = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + border-radius: 3px; + padding: 0.25rem 0.25rem; + position: relative; + left: -0.25rem; + width: calc(100% + 0.5rem); +` +ExpandToggle.defaultProps = { + execution: 'button' +} +export const ArrowContainer = styled.div` + line-height: 0; + transition: transform 200ms ease 0s; + + svg { + color: ${({ theme }) => theme.colors.gray['500']}; + width: 1rem; + height: 1rem; + transition: transform 200ms ease-in-out; + } +` + +export const ExpandContainer = styled.div` + z-index: 1; + transition: height 200ms ease-in-out; + position: relative; +` + +export const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + ${ExpandToggle} { + &:focus { + outline: 0; + box-shadow: ${({ theme, type }) => { + const color = type === 'buy' ? 'green' : 'red' + return `0 0 0 0.2rem ${theme.colors.focus[color]}` + }}; + } + } + + ${ArrowContainer} { + transform: ${({ isExpanded }) => (isExpanded ? 'scaleY(-1)' : 'scaleY(1)')}; + } + + ${ExpandContainer} { + height: ${({ isExpanded }) => (isExpanded ? 'auto' : '0')}; + overflow: ${({ isExpanded }) => (isExpanded ? 'visible' : 'auto')}; + } +` + +export const ExpandContentWrapper = styled.div` + inset: -0.5rem; + padding: 0.5rem 0; +` + +export const ExpandContent = styled.div` + display: flex; + flex-direction: column; +` + +export const OptionsWrapper = styled.div` + display: flex; + width: 100%; + padding: 1rem 0; +` + +export const OptionsInput = styled.input` + opacity: 0; + position: absolute; +` + +export const OptionsButton = styled(Button)` + flex: 1 1 auto; + display: flex; + justify-content: center; + margin: 0; + line-height: 1.25; + border-radius: 0; + padding: 0.375rem; + font-size: 0.625rem; + text-align: center; + text-transform: none; + margin-right: 1px; + background-color: ${({ theme }) => theme.colors.gray['700']}; + + &:hover { + background-color: ${({ theme }) => lighten(0.05, theme.colors.gray['700'])}; + } + + &:nth-of-type(2) { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + + &:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + margin-right: 0; + } + + && { + ${OptionsInput}:checked + & { + background-color: ${({ theme, type }) => { + const color = type === 'buy' ? 'green' : 'red' + return theme.colors[color]['500'] + }}; + } + + ${OptionsInput}:checked + &:hover { + background-color: ${({ theme, type }) => { + const color = type === 'buy' ? 'green' : 'red' + return lighten(0.05, theme.colors[color]['500']) + }}; + } + + ${OptionsInput}:focus + & { + box-shadow: ${({ theme, type }) => { + const color = type === 'buy' ? 'green' : 'red' + return `0 0 0 0.2rem ${theme.colors.focus[color]}` + }}; + } + + ${OptionsInput}:disabled + & { + opacity: 0.25; + pointer-events: none; + cursor: default; + } + } + + && { + ${OptionsInput}:focus + & { + z-index: 1; + border-radius: 3px; + } + } +` diff --git a/components/Wallet/PlaceOrder/Original/place-order.css.js b/components/Wallet/PlaceOrder/Original/place-order.css.js new file mode 100644 index 000000000..3a130b54f --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/place-order.css.js @@ -0,0 +1,217 @@ +import styled from '@emotion/styled' +import { lighten } from 'polished' +import Button from '@/components/Button' +export const _Tabs = styled.div` + display: flex; + padding: 0 1.125rem; + border-bottom: 1px solid ${({ theme }) => theme.colors.gray[700]}; + + & > * { + margin: 0 1rem; + } + + justify-content: space-between; + @media (min-width: 996px) { + justify-content: flex-start; + & > * { + margin-left: 0; + margin-right: 6rem; + } + } +` +export const _Tab = styled.div` + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-size: 0.9rem; + color: ${({ theme }) => theme.colors.gray[100]}; + padding: 1rem 0; + transition: all 0.1s ease-in; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.2rem; + font-weight: 600; + line-height: 1.25; + + border-bottom: ${({ isActive, theme }) => + isActive ? `6px inset ${theme.colors.green[500]}` : `6px inset transparent`}; + + &:hover { + color: ${({ theme }) => theme.colors.gray[100]}; + } + + &:active { + color: ${({ theme }) => theme.colors.gray[100]}; + } + + @media (min-width: 1024px) { + color: ${({ isActive, theme }) => (isActive ? theme.colors.gray[100] : theme.colors.gray[500])}; + } +` +export const Container = styled.div` + flex: 1 1 0%; + display: flex; + flex-direction: column; + background-color: ${({ theme }) => theme.colors.background.dark}; + overflow: hidden scroll; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +` + +export const Header = styled.header` + padding: 1.125rem; +` + +export const Form = styled.form` + flex: 1 1 0%; + padding: 0 1.125rem 1.125rem; +` + +export const ToggleWrapper = styled.div` + display: flex; + padding: 0 0 1.5rem; +` + +export const ToggleInput = styled.input` + opacity: 0; + position: absolute; +` + +const ToggleBtn = styled(Button)` + flex: 1 1 auto; + display: flex; + justify-content: center; + margin: 0; + line-height: 1.25; + background-color: ${({ theme }) => theme.colors.gray['700']}; + + &:hover { + background-color: ${({ theme }) => lighten(0.05, theme.colors.gray['700'])}; + } + label { + cursor: pointer; + width: 100%; + } + && { + ${ToggleInput}:focus + & { + z-index: 1; + border-radius: 3px; + } + } +` + +export const BuyButton = styled(ToggleBtn)` + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + && { + ${ToggleInput}:checked + & { + background-color: ${({ theme }) => theme.colors.green['500']}; + } + + ${ToggleInput}:checked + &:hover { + background-color: ${({ theme }) => lighten(0.05, theme.colors.green['500'])}; + } + + ${ToggleInput}:focus + & { + box-shadow: 0 0 0 0.2rem #4b9064; + } + } +` + +export const SellButton = styled(ToggleBtn)` + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + && { + ${ToggleInput}:checked + & { + background-color: ${({ theme }) => theme.colors.red['500']}; + } + + ${ToggleInput}:checked + &:hover { + background-color: ${({ theme }) => lighten(0.05, theme.colors.red['500'])}; + } + + ${ToggleInput}:focus + & { + box-shadow: 0 0 0 0.2rem #b23639; + } + } +` + +export const AvailableBalance = styled.div` + margin-bottom: 1.25rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid ${({ theme }) => theme.colors.gray['700']}; +` + +export const IconTextContainer = styled.div` + display: flex; + align-items: center; + color: ${({ theme }) => theme.colors.gray['300']}; +` + +export const BalanceRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.25rem; +` + +export const Tab = styled(_Tab)` + font-size: 0.875rem; + padding: 0.625rem 0; + letter-spacing: 0.12rem; + border-bottom-width: 4px; + margin-right: 1rem; + + border-bottom-color: ${({ orderType, isActive, theme }) => + isActive && (orderType === 'sell' ? theme.colors.red['500'] : theme.colors.green['500'])}; +} +` + +export const Tabs = styled(_Tabs)` + padding: 0; + margin-bottom: 1rem; + justify-content: flex-start; +` + +// export const LimitOrder = styled.div` +// display: flex; +// flex-direction: column; +// margin-bottom: 1rem; +// ` + +// export const TxnFeeContainer = styled.div` +// margin-bottom: 0.75rem; +// text-align: right; + +// svg { +// margin-left: 0.25rem; +// } +// ` + +export const SubmitButton = styled(Button)` + &:focus { + box-shadow: 0 0 0 0.2rem ${({ orderType }) => (orderType === 'sell' ? '#b23639' : '#4b9064')}; + } +` + +export const IconButton = styled.button` + cursor: pointer; + pointer-events: all; + border: none; + background: transparent; + margin-left: 0.125rem; + padding: 0; + height: 15px; + + svg { + height: 15px; + fill: ${({ theme }) => theme.colors.gray[500]}; + color: ${({ theme }) => theme.colors.gray[900]}; + } +` diff --git a/components/Wallet/PlaceOrder/Original/view.jsx b/components/Wallet/PlaceOrder/Original/view.jsx new file mode 100644 index 000000000..3eb8d3432 --- /dev/null +++ b/components/Wallet/PlaceOrder/Original/view.jsx @@ -0,0 +1,638 @@ +import * as Sentry from '@sentry/browser' + +import { + AvailableBalance, + BalanceRow, + BuyButton, + Container, + Form, + Header, + IconButton, + IconTextContainer, + SellButton, + SubmitButton, + Tab, + Tabs, + ToggleInput, + ToggleWrapper +} from './place-order.css' +import { BodyCopyTiny, HeaderCaps, LabelMd, LabelSm } from '@/components/Typography' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import Big from 'big.js' +import Icon from '@/components/Icon' +import { Info } from 'react-feather' +import { LimitOrder } from './limit-order' +import { MarketOrder } from './market-order' +import MaterialIcon from '@mdi/react' +import OrderService from '@/services/order' +import PropTypes from 'prop-types' +import { Tooltip } from '@/components/Tooltip' +import USDPrice from '../../PriceConversion/USDPrice' +import WalletService from '@/services/wallet' +import { aggregateOrders } from './helpers' +import { convertFromAsaUnits } from '@/services/convert' +import { convertToAsaUnits } from 'services/convert' +import detectMobileDisplay from 'utils/detectMobileDisplay' +import { floatToFixed } from '@/services/display' +import { mdiAlertCircleOutline } from '@mdi/js' +import theme from 'theme' +import toast from 'react-hot-toast' +import { useRouter } from 'next/router' +import { useStore } from '@/store/use-store' +import useTranslation from 'next-translate/useTranslation' +import useUserStore from '@/store/use-user-state' +import { useWalletMinBalanceQuery } from 'hooks/useAlgodex' + +const DEFAULT_ORDER = { + type: 'buy', + price: '', + amount: '', + total: '0', + execution: 'both' +} + +function PlaceOrderView(props) { + const { asset, wallets, activeWalletAddress, orderBook } = props + const { t } = useTranslation('place-order') + const [sellOrders, setSellOrders] = useState() + const [buyOrders, setBuyOrders] = useState() + const newOrderSizeFilter = useUserStore((state) => state.newOrderSizeFilter) + const setNewOrderSizeFilter = useUserStore((state) => state.setNewOrderSizeFilter) + const { query } = useRouter() + + const activeWallet = wallets.find((wallet) => wallet.address === activeWalletAddress) + const algoBalance = activeWallet?.balance || 0 + const asaBalance = convertToAsaUnits(activeWallet?.assets?.[asset.id]?.balance, asset.decimals) + const [maxSpendableAlgo, setMaxSpendableAlgo] = useState(algoBalance) + + const [status, setStatus] = useState({ + submitted: false, + submitting: false + }) + const [orderView, setOrderView] = useState('limit') + + const LIMIT_PANEL = 'limit' + const MARKET_PANEL = 'market' + + const MICROALGO = 0.000001 + + // @todo: calculate transaction fees in total + // const isAsaOptedIn = !!activeWallet?.assets?.[asset.id] + // const txnFee = isAsaOptedIn ? 0.002 : 0.003 + + /** + * Buy orders are enabled if active wallet has an ALGO balance > 0 + * Sell orders are enabled if active wallet has an ASA balance > 0 + */ + const enableOrder = { + buy: asset.isStable ? asaBalance > 0 : maxSpendableAlgo > 0, + sell: asset.isStable ? maxSpendableAlgo > 0 : asaBalance > 0 + } + + const order = useStore((state) => state.order) + const setOrder = useStore((state) => state.setOrder) + const { + data: minBalance, + isLoading: isWalletBalanceLoading, + isError: isWalletBalanceError + } = useWalletMinBalanceQuery({ + wallet: wallets.find((wallet) => wallet.address === activeWalletAddress) + }) + + useEffect(() => { + if (!isWalletBalanceLoading && !isWalletBalanceError) { + const total = new Big(algoBalance) + const min = new Big(minBalance).div(1000000) + const max = total.minus(min).minus(0.1).round(6, Big.roundDown).toNumber() + setMaxSpendableAlgo(Math.max(0, max)) + } + }, [minBalance, algoBalance, isWalletBalanceLoading, isWalletBalanceError]) + + /** + * When asset or active wallet changes, reset the form + */ + useEffect(() => { + setOrder( + { + ...DEFAULT_ORDER + }, + asset + ) + }, [asset, activeWalletAddress, setOrder]) + + const handleChange = useCallback( + (e, field) => { + setOrder( + { + [field || e.target.id]: e.target.value + }, + asset + ) + }, + [asset, setOrder] + ) + + useEffect(() => { + setSellOrders(aggregateOrders(orderBook.sellOrders, asset.decimals, 'sell')) + setBuyOrders(aggregateOrders(orderBook.buyOrders, asset.decimals, 'buy')) + }, [orderBook, setSellOrders, setBuyOrders, asset]) + + const updateInitialState = () => { + if (order.type === 'buy') { + setOrder( + { + price: sellOrders?.length ? sellOrders[sellOrders.length - 1].price : '0.00' + }, + asset + ) + } + + if (order.type === 'sell') { + setOrder( + { + price: buyOrders?.length ? buyOrders[0].price : '0.00' + }, + asset + ) + } + } + + useEffect(() => { + updateInitialState() + }, [order.type, orderView, activeWalletAddress]) + + const handleRangeChange = useCallback( + (update) => { + setOrder(update, asset) + }, + [setOrder, asset] + ) + + const handleOptionsChange = useCallback( + (e) => { + setOrder( + { + execution: e.target.value + }, + asset + ) + }, + [setOrder, asset] + ) + + const placeOrder = (orderData) => { + // Filter buy and sell orders to only include orders with a microalgo amount greater than the set filter amount + let filteredOrderBook = { + buyOrders: orderBook.buyOrders.filter((order) => + new Big(order.algoAmount).gte(new Big(newOrderSizeFilter).times(1000000)) + ), + sellOrders: orderBook.sellOrders.filter((order) => { + const equivAlgoAmount = new Big(order.formattedASAAmount).times(order.formattedPrice) + return equivAlgoAmount.gte(new Big(newOrderSizeFilter)) + }) + } + return OrderService.placeOrder(orderData, filteredOrderBook) + } + + const checkPopupBlocker = () => { + return ('' + window.open).indexOf('[native code]') === -1 + } + + const handleSubmit = async (e) => { + handleOptionsChange({ + target: { value: orderView === LIMIT_PANEL ? order.execution : 'market' } + }) + + e.preventDefault() + setStatus((prev) => ({ ...prev, submitting: true })) + if (checkPopupBlocker()) { + setStatus((prev) => ({ ...prev, submitting: false })) + toast.error(t('disable-popup')) + return + } + const minWalletBalance = await WalletService.getMinWalletBalance(activeWallet) + //console.log('activeWallet', { activeWallet }) + if (activeWallet.balance * 1000000 < minWalletBalance + 500001) { + setStatus((prev) => ({ ...prev, submitting: false })) + toast.error(t('fund-wallet')) + return + } + + const orderData = { + ...order, + execution: orderView === LIMIT_PANEL ? order.execution : 'market', + address: activeWalletAddress, + asset + } + + console.log('Order Submitted: ', order) + + // Adjust OrderData if it is stablecoin + if (asset.isStable) { + orderData.amount = order.price + orderData.price = order.amount + orderData.type = order.type === 'buy' ? 'sell' : 'buy' + } + // amount: "0.999998" + // decimals: 6 + // execution: "both" + // price: "1.000000" + // total: "0.999998" + // type: "buy" + + // amount: "0.08973" + // decimals: 6 + // execution: "both" + // price: "1" + // total: "0.08973" + // type: "sell" + + // Changed + // address: "4VBQQU66PR2MOHJ5PXNQTBCRBNJ2WRLMIAX5O6EQWIZREAEKR4GSDG56N4" + // amount: "0.1" + // asset: {id: 37074699, deleted: false, txid: 'JGNZUFHKV6SANVFQBI5CBNCJZVQMR233TKQIVIII3RGYLMGRBT6Q', decimals: 6, name: 'USDC', …} + // decimals: 6 + // execution: "both" + // price: "1.00" + // total: "0.1" + // type: "sell" + + //Current + // address: "4VBQQU66PR2MOHJ5PXNQTBCRBNJ2WRLMIAX5O6EQWIZREAEKR4GSDG56N4" + // amount: "0.2" + // asset: {id: 15322902, deleted: false, txid: 'NOFSUK4EXHFFXJK3ZA6DZMGE6CAGQ7G5JT2X7FYTYQBSQEBZHY4Q', decimals: 6, name: 'LAMP', …} + // decimals: 6 + // execution: "both" + // price: "0.1" + // total: "0.02" + // type: "sell" + + console.log('Order Submitted: OrderData', orderData) + + Sentry.addBreadcrumb({ + category: 'order', + message: `${orderData.execution} ${orderData.type} order placed`, + data: { + order: orderData + }, + level: Sentry.Severity.Info + }) + + const orderPromise = placeOrder(orderData) + + toast.promise(orderPromise, { + loading: t('awaiting-confirmation'), + success: t('order-success'), + error: (err) => { + if (/PopupOpenError|blocked/.test(err)) { + return detectMobileDisplay() ? t('disable-popup-mobile') : t('disable-popup') + } + + if (/Operation cancelled/i.test(err)) { + return t('order-cancelled') + } + + return t('error-placing-order') + } + }) + + try { + const result = await orderPromise + console.debug(result) + setStatus({ submitted: true, submitting: false }) + + // reset order form if it is not a market order + if (order.execution !== 'market') { + setOrder( + { + ...DEFAULT_ORDER, + type: order.type + }, + asset + ) + } + } catch (err) { + setStatus({ submitted: false, submitting: false }) + console.error(err) + + if (/PopupOpenError|blocked/.test(err)) { + return + } + + // ALG-417 Don't capture user initiated cancels + if (/Operation cancelled/i.test(err)) { + return + } + + Sentry.captureException(err) + } + } + + const calcAsaWorth = useMemo( + () => floatToFixed(convertFromAsaUnits(asset?.price_info?.price, asset.decimals)), + [asset] + ) + + const renderSubmit = () => { + const buttonProps = { + buy: { variant: 'primary', text: `${t('buy')} ${asset.isStable ? 'ALGO' : asset.name}` }, + sell: { variant: 'danger', text: `${t('sell')} ${asset.isStable ? 'ALGO' : asset.name}` } + } + + const isBelowMinOrderAmount = () => { + // if asset is stable invert sell/buy option + let _type = asset.isStable ? (order.type === 'buy' ? 'sell' : 'buy') : order.type + if (_type === 'buy') { + console.log(`BuyCondition: isBelowMinOrderAmount: ${_type}: `, order.total) + return new Big(order.total).lt(0.5) + } + console.log(`BuyCondition: isBelowMinOrderAmount: ${_type}: `, order.total) + return new Big(order.total).eq(0) + } + + const isInvalid = () => { + return isNaN(parseFloat(order.price)) || isNaN(parseFloat(order.amount)) + } + + const isBalanceExceeded = () => { + // let _type = asset.isStable ? (order.type === 'buy' ? 'sell' : 'buy') : order.type + if (asset.isStable) { + if (order.type === 'sell') { + console.log('BuyCondition: order.price : ', order.price) + console.log('BuyCondition: order.amount : ', order.amount) + return new Big(order.amount).times(order.price).gt(maxSpendableAlgo) + } + console.log('BuyCondition: order.amount : ', order.amount) + console.log('BuyCondition: asaBalance : ', asaBalance) + return new Big(order.price).gt(asaBalance) + } else { + if (order.type === 'buy') { + console.log('BuyCondition: order.price : ', order.price) + console.log('BuyCondition: order.amount : ', order.amount) + return new Big(order.price).times(order.amount).gt(maxSpendableAlgo) + } + console.log('BuyCondition: order.amount : ', order.amount) + console.log('BuyCondition: asaBalance : ', asaBalance) + return new Big(order.amount).gt(asaBalance) + } + } + + const isLessThanMicroAlgo = () => { + return order.price < MICROALGO + } + + const isDisabled = + isBelowMinOrderAmount() || + isInvalid() || + isBalanceExceeded() || + isLessThanMicroAlgo() || + asset.isGeoBlocked || + status.submitting + + console.log('BuyCondition: isBelowMinOrderAmount: ', isBelowMinOrderAmount()) + console.log('BuyCondition: isInvalid: ', isInvalid()) + // console.log('BuyCondition: isBalanceExceeded: ', isBalanceExceeded()) + console.log('BuyCondition: isLessThanMicroAlgo: ', isLessThanMicroAlgo()) + console.log('BuyCondition: asset.isGeoBlocked: ', asset.isGeoBlocked) + console.log('BuyCondition: status.submitting: ', status.submitting) + + return ( + + {buttonProps[order.type].text} + + ) + } + + const renderForm = () => ( +
+ + !asset.isGeoBlocked && handleChange(e, 'type')} + /> + + + + !asset.isGeoBlocked && handleChange(e, 'type')} + /> + + + + + + + + {t('available-balance')} + ( + + + + )} + > + + + {t('orders:available')}: + + + + {maxSpendableAlgo} + + + + + + + {t('total')}: + + + + {algoBalance} + + + + + + +  * + {t('max-spend-explanation', { + amount: new Big(algoBalance).minus(new Big(maxSpendableAlgo)).round(6).toString() + })} + + + + + + {asset.isStable && ( + <> + + + {asset.name} + + + {asaBalance} +
+ + + +
+
+ + + ALGO + + + {maxSpendableAlgo} +
+ + + +
+
+ + )} + + {!asset.isStable && ( + <> + + + ALGO + + + {maxSpendableAlgo} +
+ + + +
+
+ + + {asset.name} + + + {asaBalance} +
+ + + +
+
+ + )} +
+ + + { + !asset.isGeoBlocked && setOrderView(LIMIT_PANEL) + !asset.isGeoBlocked && handleOptionsChange({ target: { value: 'both' } }) + }} + > + {t('limit')} + + { + !asset.isGeoBlocked && setOrderView(MARKET_PANEL) + !asset.isGeoBlocked && handleOptionsChange({ target: { value: 'market' } }) + }} + > + {t('market')} + + + {orderView === LIMIT_PANEL ? ( + console.log('Blocked') : handleChange} + asset={asset} + maxSpendableAlgo={maxSpendableAlgo} + asaBalance={asaBalance} + handleRangeChange={!asset.isGeoBlocked && handleRangeChange} + enableOrder={enableOrder} + handleOptionsChange={!asset.isGeoBlocked && handleOptionsChange} + newOrderSizeFilter={newOrderSizeFilter} + microAlgo={MICROALGO} + setNewOrderSizeFilter={setNewOrderSizeFilter} + /> + ) : ( + console.log('Blocked') : handleChange} + asset={asset} + maxSpendableAlgo={maxSpendableAlgo} + asaBalance={asaBalance} + handleRangeChange={!asset.isGeoBlocked && handleRangeChange} + enableOrder={enableOrder} + /> + )} + {renderSubmit()} + + ) + + return ( + +
+
+ + {t('place-order')} + +
+ {renderForm()} +
+ {asset.isGeoBlocked && ( +
+ {' '} +   +
+

+ This asset is not able to be traded in your country ({query.cc}) for legal reasons. + You can view the chart and book but will not be able to place trades for this asset. +

+ {/*

Learn More Here

*/} +
+
+ )} +
+ ) +} + +PlaceOrderView.propTypes = { + asset: PropTypes.object.isRequired, + wallets: PropTypes.array.isRequired, + activeWalletAddress: PropTypes.string.isRequired, + orderBook: PropTypes.object.isRequired +} + +export default PlaceOrderView diff --git a/components/Wallet/PriceConversion/StableAssetUSDPrice.jsx b/components/Wallet/PriceConversion/StableAssetUSDPrice.jsx new file mode 100644 index 000000000..118fc873e --- /dev/null +++ b/components/Wallet/PriceConversion/StableAssetUSDPrice.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types' +import { formatUSDPrice } from '@/components/helpers' +import { withCurrentAssetPricesQuery } from '@/hooks/withAlgoExplorer' + +export function StableAssetUSDPrice({ asaWorth, priceToConvert, currency, usdPrice }) { + return ( + + {currency} + {formatUSDPrice(asaWorth * priceToConvert * usdPrice)} + + ) +} + +StableAssetUSDPrice.propTypes = { + priceToConvert: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + asaWorth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + currency: PropTypes.string, + usdPrice: PropTypes.any +} + +StableAssetUSDPrice.defaultProps = { + priceToConvert: 0, + asaWorth: 1, + currency: '', + usdPrice: 1 +} + +export default withCurrentAssetPricesQuery(StableAssetUSDPrice) diff --git a/components/Wallet/PriceConversion/StableAssetUSDPrice.spec.js b/components/Wallet/PriceConversion/StableAssetUSDPrice.spec.js new file mode 100644 index 000000000..ca48f8a1b --- /dev/null +++ b/components/Wallet/PriceConversion/StableAssetUSDPrice.spec.js @@ -0,0 +1,19 @@ +import React from 'react' +import { render } from 'test/test-utils' +import { StableAssetUSDPrice } from './StableAssetUSDPrice' + +const usdPrice = 1 + +describe('StableAssetUSDPrice', () => { + it('should show price once it gets data', () => { + const { queryByTestId } = render( + + ) + + expect(queryByTestId('StableAssetUSDprice-element')).not.toBeNull() + }) +}) diff --git a/components/Wallet/PriceConversion/StableAssetUSDPrice.stories.jsx b/components/Wallet/PriceConversion/StableAssetUSDPrice.stories.jsx new file mode 100644 index 000000000..b4f404852 --- /dev/null +++ b/components/Wallet/PriceConversion/StableAssetUSDPrice.stories.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import { StableAssetUSDPrice as Component } from './StableAssetUSDPrice' + +export default { + title: '@algodex/recipes/Wallet/Stable Asset Price Conversion', + component: Component, + parameters: { layout: 'fullscreen' }, + args: { + isRegenerate: false, + algoPrice: 260, + priceToConvert: 3000, + currency: '$' + } +} + +/* eslint-disable */ +export const StableAssetUSDPrice = ({ isRegenerate, algoPrice, priceToConvert, currency, ...props }) => { + if (isRegenerate) { + algoPrice = 0 + } + return ( + + ) +} diff --git a/components/Wallet/PriceConversion/USDPrice.jsx b/components/Wallet/PriceConversion/USDPrice.jsx index 3d456a00d..e987e554a 100644 --- a/components/Wallet/PriceConversion/USDPrice.jsx +++ b/components/Wallet/PriceConversion/USDPrice.jsx @@ -28,8 +28,8 @@ export function USDPrice({ algoPrice, asaWorth, priceToConvert, currency }) { USDPrice.propTypes = { algoPrice: PropTypes.any, - priceToConvert: PropTypes.number, - asaWorth: PropTypes.number, + priceToConvert: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + asaWorth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, currency: PropTypes.string } diff --git a/components/Wallet/Table/AssetsTable.jsx b/components/Wallet/Table/AssetsTable.jsx index 50aee0659..5e60f98b9 100644 --- a/components/Wallet/Table/AssetsTable.jsx +++ b/components/Wallet/Table/AssetsTable.jsx @@ -17,6 +17,7 @@ import { AssetId, AssetNameBlock } from '@/components/Asset/Typography' import Table, { AssetNameCell, DefaultCell } from '@/components/Table' import { useCallback, useMemo } from 'react' +import { StableAssets } from '@/components/StableAssets' import Link from 'next/link' import PropTypes from 'prop-types' @@ -52,6 +53,7 @@ export const AssetCoinCell = (props) => { const onClick = useCallback(() => { dispatcher('clicked', 'asset') }, [dispatcher]) + // console.log(props.row, 'props row') return ( state.walletAssetsTableState) const setWalletAssetsTableState = useUserStore((state) => state.setWalletAssetsTableState) - + const formatAssetsList = assets.map((asset) => { + return { + ...asset, + isStable: StableAssets.includes(parseInt(asset.id)) + } + }) + // console.log(formatAssetsList, 'formattedAsset') const columns = useMemo( () => [ { @@ -121,7 +130,7 @@ export function AssetsTable({ assets }) { initialState={walletAssetsTableState} onStateChange={(state) => setWalletAssetsTableState(state)} columns={columns} - data={assets || []} + data={formatAssetsList || []} /> diff --git a/components/Wallet/Table/OpenOrdersTable.jsx b/components/Wallet/Table/OpenOrdersTable.jsx index 31af6175f..cdc441c2d 100644 --- a/components/Wallet/Table/OpenOrdersTable.jsx +++ b/components/Wallet/Table/OpenOrdersTable.jsx @@ -210,7 +210,7 @@ export function OpenOrdersTable({ orders: _orders }) { Cell: AssetNameCell }, { - Header: t('price') + ' (ALGO)', + Header: t('price'), accessor: 'price', Cell: DefaultCell }, diff --git a/components/Wallet/Table/TradeHistoryTable.jsx b/components/Wallet/Table/TradeHistoryTable.jsx index 839e8f884..712d0bed7 100644 --- a/components/Wallet/Table/TradeHistoryTable.jsx +++ b/components/Wallet/Table/TradeHistoryTable.jsx @@ -20,6 +20,7 @@ import Table, { ExpandTradeDetail, OrderTypeCell } from '@/components/Table' +import { StableAssets } from '@/components/StableAssets' import PropTypes from 'prop-types' import styled from '@emotion/styled' @@ -65,12 +66,13 @@ export function TradeHistoryTable({ orders }) { return orders.map((order) => { const _order = { ...order, - price: floatToFixedDisplay(order.price) + price: floatToFixedDisplay(order.price), + isStable: StableAssets.includes(parseInt(order.id)) } return _order }) }, [orders]) - + // console.log(_formattedOrders, 'formatted orders') const columns = useMemo( () => [ { @@ -93,7 +95,7 @@ export function TradeHistoryTable({ orders }) { }, { - Header: t('price') + ' (ALGO)', + Header: t('price'), accessor: 'price', Cell: DefaultCell }, diff --git a/hooks/useAlgoExplorer.js b/hooks/useAlgoExplorer.js new file mode 100644 index 000000000..b4246473c --- /dev/null +++ b/hooks/useAlgoExplorer.js @@ -0,0 +1,96 @@ +import { + fetchAlgorandPrice, + fetchExplorerAssetInfo, + fetchCurrentAssetPrices +} from 'services/algoexplorer' + +import { routeQueryError } from './useAlgodex' +import { useEffect, useMemo } from 'react' +import { useQuery } from 'react-query' +import { useRouter } from 'next/router' + +const refetchInterval = 3000 + +/** + * Use Asset + * @param id + * @param options + * @param select + * @returns {Object} + * @todo: Refactor to use Algorand + */ +export const useExplorerAssetInfo = ({ asset = {}, options }) => { + console.log(`useExplorerAssetInfo(`, asset, `)`) + const router = useRouter() + const { id } = asset + const { data, isError, error, ...rest } = useQuery( + ['explorerAsset', id], + () => fetchExplorerAssetInfo(id), + options + ) + //console.log(data) + useEffect(() => { + let mounted = true + if (mounted && typeof id !== 'undefined') { + routeQueryError({ isError, error, router }) + } + return () => (mounted = false) + }, [router, data, isError, error]) + + return { data, isError, error, ...rest } +} + +/** + * Use Search Results Query + * @param {Object} props The props of the parent + * @param {string} props.query Search Query + * @param {Object} [props.options] useQuery Options + * @returns {UseQueryResult<{assets: *}, unknown>} + */ +export const useAlgorandPriceQuery = ({ + query = '', + options = { + refetchInterval: query === '' ? refetchInterval : 20000 + } +} = {}) => useQuery(['fetchAlgorandPrice', { query }], () => fetchAlgorandPrice(query), options) + +/** + * Use Search Results Query + * Use Asset + * @param {Object} props The props of the parent + * @param {string} props.query Search Query + * @param {Object} [props.options] useQuery Options + * @returns {UseQueryResult<{assets: *}, unknown>} + */ +export const useCurrentAssetPricesQuery = ({ + assetId = -1, + options = { + refetchInterval: 20000 + } +} = {}) => + useQuery( + ['fetchCurrentAssetPrices', { assetId }], + () => fetchCurrentAssetPrices(assetId), + options + ) + +/** + * Use Search Results Query + * @param {Object} props The props of the parent + * @param {Object} [props.options] useQuery Options + * @returns {UseQueryResult<{assets: *}, unknown>} + */ +// export function useCurrentAssetPricesQuery({ options = { refetchInterval: 30000 } }) { +// // return useQuery(['currentAssetPrices'], () => fetchCurrentAssetPrices(), options) +// const { data: _prices } = useQuery( +// ['currentAssetPrices'], +// () => fetchCurrentAssetPrices(), +// options +// ) + +// const currentPrices = useMemo(() => { +// return _prices +// }, [_prices]) + +// return currentPrices +// } diff --git a/hooks/useAlgodex.js b/hooks/useAlgodex.js new file mode 100644 index 000000000..2d2165544 --- /dev/null +++ b/hooks/useAlgodex.js @@ -0,0 +1,739 @@ +import { calculateAsaBuyAmount, convertFromAsaUnits } from '@/services/convert' +import { + fetchAssetChart, + fetchAssetOrders, + fetchAssetPrice, + fetchAssetTradeHistory, + fetchWalletAssets, + fetchWalletOrders, + fetchWalletTradeHistory, + searchAssets +} from '@/services/algodex' +import { + getAssetTotalStatus, + getIsRestricted, + getIsRestrictedCountry +} from '@/utils/restrictedAssets' +import { StableAssets } from '@/components/StableAssets' + +import { useEffect, useMemo, useState } from 'react' + +import Big from 'big.js' +import WalletService from '@/services/wallet' +import dayjs from 'dayjs' +import { floatToFixed } from '@/services/display' +import millify from 'millify' +import { useQuery } from 'react-query' +import { useRouter } from 'next/router' + +/** + * Route based on Error + * @param isError + * @param error + * @param router + * @returns {function(): boolean} + */ +export function routeQueryError({ isError, error, router }) { + if (isError && error.message.match(404)) { + router.push('/404') + } else if (isError && error.message.match(500)) { + // Do nothing. The component will handle this. + } else if (isError) { + // router.push('/500') + console.error({ error }) + // router.push('/restricted') + } +} +const refetchInterval = 3000 + +/** + * Use Search Results Query + * @param {Object} props The props of the parent + * @param {string} props.query Search Query + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useSearchResultsQuery({ + query = '', + options = { + refetchInterval: query === '' ? refetchInterval : 20000 + } +} = {}) { + const router = useRouter() + const { + data: queryData, + isError, + error, + ...rest + } = useQuery(['searchResults', { query }], () => searchAssets(query), options) + routeQueryError({ isError, error, router }) + const data = useMemo(() => { + if (typeof queryData !== 'undefined' && typeof queryData.assets !== 'undefined') { + return { + assets: queryData.assets.map((asset) => { + const isRestricted = + getIsRestricted(`${asset.assetId}`) && getAssetTotalStatus(asset.total) + return { + ...asset, + isRestricted, + isGeoBlocked: getIsRestrictedCountry(router.query) && isRestricted, + isStable: StableAssets.includes(asset.assetId) + } + }) + } + } else { + return queryData + } + }, [queryData]) + return { data, isError, error, ...rest } +} + +/** + * Use Asset Price Query + * + * @param {Object} props The props of the parent + * @param {Object} props.asset An instance of an Asset + * @param {Object} [props.options] useQuery Options + * @todo: Consolidate with Search + * @returns {object} Massaged Query + */ +export function useAssetPriceQuery({ + asset: algorandAsset, + options = { + refetchInterval, + enabled: typeof algorandAsset !== 'undefined' && typeof algorandAsset.id !== 'undefined', + initialData: algorandAsset?.price_info + } +} = {}) { + //console.log(`useAssetPriceQuery(`, arguments[0], `)`) + const { id } = algorandAsset + const { data: dexAsset, ...rest } = useQuery( + ['assetPrice', { id }], + () => fetchAssetPrice(id), + options + ) + const asset = useMemo(() => { + return { + ...algorandAsset, + price_info: dexAsset + } + }, [algorandAsset, dexAsset]) + + return { data: { asset }, ...rest } +} + +function mapPriceData(data, isStableAsset) { + let prices = [] + // Use if-else condition for isStableAsset outside of iteration to speed up + if (isStableAsset) { + prices = + data?.chart_data.map( + ({ formatted_open, formatted_high, formatted_low, formatted_close, unixTime }) => { + const time = parseInt(unixTime) + return { + time: time, + open: floatToFixed(1 / formatted_open), + high: floatToFixed(1 / formatted_high), + low: floatToFixed(1 / formatted_low), + close: floatToFixed(1 / formatted_close) + } + } + ) || [] + } else { + prices = + data?.chart_data.map( + ({ formatted_open, formatted_high, formatted_low, formatted_close, unixTime }) => { + const time = parseInt(unixTime) + return { + time: time, + open: floatToFixed(formatted_open), + high: floatToFixed(formatted_high), + low: floatToFixed(formatted_low), + close: floatToFixed(formatted_close) + } + } + ) || [] + } + + return prices.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0)) +} + +function getOhlc(data, isStableAsset) { + let lastPriceData = {} + + if (data?.chart_data[0]) { + if (isStableAsset) { + lastPriceData = { + open: floatToFixed(1 / data?.chart_data[0].formatted_open), + low: floatToFixed(1 / data?.chart_data[0].formatted_high), + high: floatToFixed(1 / data?.chart_data[0].formatted_low), + close: floatToFixed(1 / data?.chart_data[0].formatted_close) + } + } else { + lastPriceData = { + open: floatToFixed(data?.chart_data[0].formatted_open), + high: floatToFixed(data?.chart_data[0].formatted_high), + low: floatToFixed(data?.chart_data[0].formatted_low), + close: floatToFixed(data?.chart_data[0].formatted_close) + } + } + } + + return lastPriceData +} + +function mapVolumeData(data, volUpColor, volDownColor) { + const mappedData = data?.chart_data?.map(({ asaVolume, unixTime }) => { + const time = parseInt(unixTime) + return { + time: time, + value: asaVolume + } + }) + const volumeColors = data?.chart_data.map(({ open, close }) => + open > close ? volDownColor : volUpColor + ) + return mappedData?.map((md, i) => ({ ...md, color: volumeColors[i] })) || [] +} + +function mapAlgoVolumeData(data, volUpColor, volDownColor) { + const mappedData = data?.chart_data?.map(({ algoVolume, unixTime }) => { + const time = parseInt(unixTime) + return { + time: time, + value: algoVolume + } + }) + const volumeColors = data?.chart_data.map(({ open, close }) => + open > close ? volDownColor : volUpColor + ) + return mappedData?.map((md, i) => ({ ...md, color: volumeColors[i] })) || [] +} + +function getBidAskSpread(orderBook, isStableAsset) { + const { buyOrders, sellOrders } = orderBook + const bidPrice = buyOrders.sort((a, b) => b.asaPrice - a.asaPrice)?.[0]?.formattedPrice || 0 + const askPrice = sellOrders.sort((a, b) => a.asaPrice - b.asaPrice)?.[0]?.formattedPrice || 0 + + let bid = floatToFixed(bidPrice) + let ask = floatToFixed(askPrice) + let spread = floatToFixed(new Big(ask).minus(bid).abs()) + + if (isStableAsset) { + bid = bidPrice === 0 ? 'Invalid Price' : floatToFixed(1 / bidPrice) + ask = askPrice === 0 ? 'Invalid Price' : floatToFixed(1 / askPrice) + + if (Number(bidPrice) === 0 || Number(bidPrice) === 0) { + spread = 'Invalid Price' + } else { + spread = floatToFixed(new Big(ask).minus(bid).abs()) + } + } + + return { bid, ask, spread } +} + +/** + * Use Asset Chart Query + * @param {Object} props The props of the parent + * @param {Object} props.asset An instance of an Asset + * @param {string} props.interval Interval to aggregate chart by + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useAssetChartQuery({ + interval, + asset, + options = { + refetchInterval + } +}) { + // console.log(`useAssetChartQuery(${JSON.stringify({ interval, asset })})`) + const { id } = asset + const { + data: assetOrders, + isLoading: isOrdersLoading, + isError: isOrdersError + } = useAssetOrdersQuery({ asset }) + + const VOLUME_UP_COLOR = '#2fb16c2c' + const VOLUME_DOWN_COLOR = '#e53e3e2c' + const orderBook = useMemo( + () => ({ + buyOrders: assetOrders?.buyASAOrdersInEscrow || [], + sellOrders: assetOrders?.sellASAOrdersInEscrow || [] + }), + [assetOrders] + ) + + const { + isLoading: isChartLoading, + isError: isChartError, + data, + ...rest + } = useQuery(['assetChart', { id, interval }], () => fetchAssetChart(id, interval), options) + + const { bid, ask, spread } = useMemo( + () => getBidAskSpread(orderBook, asset.isStable), + [orderBook] + ) + const priceData = useMemo(() => mapPriceData(data, asset.isStable), [data]) + const volumeData = useMemo(() => mapVolumeData(data, VOLUME_UP_COLOR, VOLUME_DOWN_COLOR), [data]) + const algoVolumeData = useMemo( + () => mapAlgoVolumeData(data, VOLUME_UP_COLOR, VOLUME_DOWN_COLOR), + [data] + ) + const ohlcOverlay = useMemo(() => getOhlc(data, asset.isStable), [data]) + + const volume = millify(data?.chart_data[data?.chart_data.length - 1]?.asaVolume || 0) + const algoVolume = millify(data?.chart_data[data?.chart_data.length - 1]?.algoVolume || 0) + const isLoading = isOrdersLoading || isChartLoading + const isError = isOrdersError || isChartError + + return { + data: { + overlay: { + ohlc: ohlcOverlay, + orderbook: { bid, ask, spread }, + volume, + algoVolume + }, + volume: volumeData, + algoVolume: algoVolumeData, + ohlc: priceData, + isLoading, + isError + }, + isLoading, + isError, + ...rest + } +} +/** + * @todo aggregate Orders in the API + * @param orders + * @param asaDecimals + * @param type + * @returns {*} + */ +function aggregateOrders(orders, asaDecimals, type, ascending) { + const isBuyOrder = type === 'buy' + let total = 0 + + const leftPriceDecimalsLength = orders.map((order) => { + const price = new Big(convertFromAsaUnits(order.asaPrice, asaDecimals)) + const left = Math.floor(price) + const right = price.sub(left) + return right !== 0 && right.toString().length > 2 ? right.toString().length - 2 : 0 + }) + + const decimalLength = + leftPriceDecimalsLength.length === 0 ? 0 : Math.max(...leftPriceDecimalsLength) + + const sortOrdersToAggregate = (a, b) => { + if (isBuyOrder) { + return b.asaPrice - a.asaPrice + } + return a.asaPrice - b.asaPrice + } + + const reduceAggregateData = (result, order) => { + const _price = convertFromAsaUnits(order.asaPrice, asaDecimals) + const price = floatToFixed(_price, 6, decimalLength) + + const orderAmount = isBuyOrder ? order.algoAmount : order.asaAmount + + const amount = isBuyOrder + ? calculateAsaBuyAmount(price, orderAmount) + : parseFloat(order.formattedASAAmount) + + total += amount + + const index = result.findIndex((obj) => obj.price === price) + + if (index !== -1) { + result[index].amount += amount + result[index].total += amount + return result + } + + result.push({ + price, + amount, + total + }) + return result + } + + const sortRowsByPrice = (a, b) => { + return b.price - a.price + } + + const sortRowsByPriceInAscending = (a, b) => { + return a.price - b.price + } + + return orders + .sort(sortOrdersToAggregate) + .reduce(reduceAggregateData, []) + .sort(ascending ? sortRowsByPriceInAscending : sortRowsByPrice) +} + +/** + * Use Asset Orders Query + * @param {Object} props The props of the parent + * @param {Object} props.asset An instance of an Asset + * @param {Object} [props.options] useQuery Options + * @returns {object} React Query Results + */ +export function useAssetOrderbookQuery({ + asset, + options = { + refetchInterval + } +} = {}) { + // console.log(`useAssetOrderbookQuery(${JSON.stringify({ asset })})`) + const { id, decimals, isStable } = asset + const [sell, setSellOrders] = useState([]) + const [buy, setBuyOrders] = useState([]) + + // Orderbook Query + const { data, isLoading, ...rest } = useQuery( + ['assetOrders', { id }], + () => fetchAssetOrders(id), + options + ) + + // Massage Orders + useEffect(() => { + if ( + data && + !isLoading && + typeof data.sellASAOrdersInEscrow !== 'undefined' && + typeof data.buyASAOrdersInEscrow !== 'undefined' + ) { + setSellOrders(aggregateOrders(data.sellASAOrdersInEscrow, decimals, 'sell', isStable && true)) + setBuyOrders(aggregateOrders(data.buyASAOrdersInEscrow, decimals, 'buy', isStable && true)) + } + }, [isLoading, data, setSellOrders, setBuyOrders, decimals]) + + // Return OrderBook + return { data: { orders: { sell, buy }, isLoading }, isLoading, ...rest } +} + +export function useAssetOrdersQuery({ asset, options = {} }) { + // console.log(`useAssetOrdersQuery(${JSON.stringify({ asset })})`) + const { id } = asset + return useQuery(['assetOrders', { id }], () => fetchAssetOrders(id), options) +} + +/** + * Use Asset Trade History Query + * @param {Object} props The props of the parent + * @param {Object} props.asset An instance of an Asset + * @param {Object} [props.options] useQuery Options + * @returns {object} Massaged React-Query + */ +export function useAssetTradeHistoryQuery({ + asset, + options = { + refetchInterval: 5000, + staleTime: 3000 + } +}) { + const { id } = asset + const { data, ...rest } = useQuery( + ['assetTradeHistory', { id }], + () => fetchAssetTradeHistory(id), + options + ) + + const tradesData = + data?.transactions.map((txn) => ({ + id: txn.PK_trade_history_id, + type: txn.tradeType, + price: floatToFixed(txn.formattedPrice), + amount: txn.formattedASAAmount, + groupId: encodeURIComponent(txn.group_id), + timestamp: txn.unix_time * 1000 + })) || [] + + return { data: { orders: tradesData }, ...rest } +} +/** + * @deprecated + * @param data + * @returns {null|*} + */ +export const mapAssetsData = (data) => { + if (!data || !data.allAssets || !data.allAssets.length) { + return null + } + + const { allAssets: assetsData } = data + + return assetsData.map( + ({ + unit_name, + name, + formattedTotalASAAmount, + formattedASAAvailable, + formattedASAInOrder, + formattedTotalAlgoEquiv, + assetId + }) => { + return { + unit: unit_name, + id: assetId, + name, + total: formattedTotalASAAmount || '', + available: formattedASAAvailable || '', + 'in-order': formattedASAInOrder || '', + 'algo-value': formattedTotalAlgoEquiv || '' + } + } + ) +} +/** + * Use Wallet Assets Query + * + * @param {Object} props The props of the parent + * @param {Object} props.wallet An instance of a Wallet + * @param {Object} [props.options] useQuery Options + * @todo: Fetch Wallet Assets from on-chain + * @returns {object} + */ +export function useWalletAssetsQuery({ + wallet: { address }, + options = { + enabled: typeof address !== 'undefined', + refetchInterval + } +}) { + const { data, ...rest } = useQuery( + ['walletAssets', { address }], + () => fetchWalletAssets(address), + options + ) + const assets = useMemo(() => mapAssetsData(data), [data]) + return { data: { assets }, ...rest } +} + +const getFormattedPairMap = (assetsList) => { + if (!assetsList?.data?.assets) { + return new Map() + } + return assetsList.data.assets.reduce((map, currentValue) => { + const key = currentValue.assetId + map.set(key, currentValue) + map.set(key, currentValue.unitName) + return map + }, new Map()) +} + +const mapOpenOrdersData = (data, assetList = []) => { + // if (!data || !data.buyASAOrdersInEscrow || !data.sellASAOrdersInEscrow || !data?.allAssets) { + if (!data || !data.buyASAOrdersInEscrow || !data.sellASAOrdersInEscrow) { + return null + } + const { + buyASAOrdersInEscrow: buyOrdersData, + sellASAOrdersInEscrow: sellOrdersData, + allAssets: assetsData + } = data + const assetsInfo = (assetsData || []).reduce((allAssetsInfo, currentAssetInfo) => { + allAssetsInfo[currentAssetInfo.index] = currentAssetInfo + return allAssetsInfo + }, {}) + + //FIXME: after 2.0 backend updates, this may not be necessary + const unitNameMap = + Object.keys(assetsInfo).length === 0 ? getFormattedPairMap(assetList) : new Map() + const buyOrders = buyOrdersData.map((order) => { + const { assetId, formattedPrice, formattedASAAmount, unix_time } = order + const unitName = assetsInfo[assetId]?.params['unit-name'] || unitNameMap.get(assetId) + let pair = `${unitName}/ALGO` + let price = floatToFixed(formattedPrice) + ' (ALGO)' + let amount = formattedASAAmount + ` (${unitName}) ` + + if (StableAssets.includes(assetId)) { + pair = `ALGO/${unitName}` + amount = `${formattedPrice * formattedASAAmount} (ALGO) ` + price = + formattedPrice !== 0 + ? floatToFixed(1 / formattedPrice) + ` (${unitName}) ` + : 'Invalid Price' + } + return { + asset: { id: assetId }, + date: dayjs.unix(unix_time).format('YYYY-MM-DD HH:mm:ss'), + // date: moment(unix_time, 'YYYY-MM-DD HH:mm').format(), + unix_time: unix_time, + price: price, + pair: pair, + type: 'BUY', + status: 'OPEN', + amount: amount, + metadata: order + } + }) + + const sellOrders = sellOrdersData.map((order) => { + const { assetId, formattedPrice, formattedASAAmount, unix_time } = order + const unitName = assetsInfo[assetId]?.params['unit-name'] || unitNameMap.get(assetId) + let pair = `${unitName}/ALGO` + let price = floatToFixed(formattedPrice) + ' (ALGO)' + let amount = formattedASAAmount + ` (${unitName}) ` + + if (StableAssets.includes(assetId)) { + pair = `ALGO/${unitName}` + amount = `${formattedPrice * formattedASAAmount} (1ALGO) ` + price = + formattedPrice !== 0 + ? floatToFixed(1 / formattedPrice) + ` (${unitName}) ` + : 'Invalid Price' + } + return { + asset: { id: assetId }, + date: dayjs.unix(unix_time).format('YYYY-MM-DD HH:mm:ss'), + unix_time: unix_time, + price: price, + pair: pair, + type: 'SELL', + status: 'OPEN', + amount: amount, + metadata: order + } + }) + const allOrders = [...buyOrders, ...sellOrders] + allOrders.sort((a, b) => (a.unix_time < b.unix_time ? 1 : -1)) + return allOrders +} + +/** + * Use Wallet Orders Query + * + * @param {Object} props The props of the parent + * @param {Object} props.wallet An instance of a Wallet + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useWalletOrdersQuery({ wallet, options = { refetchInterval } }) { + const { address } = wallet + const { data, ...rest } = useQuery( + ['walletOrders', { address }], + () => fetchWalletOrders(address), + options + ) + const assetsList = useSearchResultsQuery() + + const orders = useMemo(() => mapOpenOrdersData(data, assetsList), [data, assetsList]) + + return { data: { orders }, ...rest } +} +/** + * Use Wallet Trade History + * + * @param {Object} props The props of the parent + * @param {Object} props.wallet An instance of a Wallet + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useWalletTradeHistoryQuery({ + wallet, + options = { + refetchInterval + } +}) { + const { address } = wallet + const mapTradeHistoryData = (data, assetList = []) => { + const buyText = 'BUY' + const sellText = 'SELL' + if (!data || !data.transactions || !data.allAssets) { + return null + } + + const { transactions: tradeHistoryData, allAssets: assetsData } = data + + const assetsInfo = assetsData.reduce((allAssetsInfo, currentAssetInfo) => { + allAssetsInfo[currentAssetInfo.index] = currentAssetInfo + return allAssetsInfo + }, {}) + const unitNameMap = + Object.keys(assetsInfo).length === 0 ? getFormattedPairMap(assetList) : new Map() + return tradeHistoryData.map( + ({ unix_time, group_id, asset_id, tradeType, formattedPrice, formattedASAAmount }) => { + let side = tradeType === 'buyASA' ? buyText : sellText + // const unitName = assetsInfo[asset_id]?.params['unit-name'] || unitNameMap.get(asset_id) + const unitName = assetsInfo[asset_id].params['unit-name'] + let price = floatToFixed(formattedPrice) + ' (ALGO)' + let amount = formattedASAAmount + ` (${unitName}) ` + let pair = `${unitName}/ALGO` + + if (StableAssets.includes(asset_id)) { + pair = `ALGO/${unitName}` + amount = `${formattedPrice * formattedASAAmount} (ALGO) ` + price = + formattedPrice !== 0 + ? floatToFixed(1 / formattedPrice) + ` (${unitName}) ` + : 'Invalid Price' + side = side === buyText ? sellText : buyText + } + + return { + id: asset_id, + groupId: encodeURIComponent(group_id), + date: dayjs(unix_time * 1000).format('YYYY-MM-DD HH:mm:ss'), + price: price, + pair: pair, + side, + amount: amount + } + } + ) + } + const { data, ...rest } = useQuery( + ['walletTradeHistory', { address }], + () => fetchWalletTradeHistory(address), + options + ) + const assetsList = useSearchResultsQuery() + + const orders = useMemo(() => mapTradeHistoryData(data, assetsList), [data, assetsList]) + return { data: { orders }, ...rest } +} +/** + * Use Wallet Minimum Balance Query + * @param {Object} props The props of the parent + * @param {Object} props.wallet An instance of a Wallet + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useWalletMinBalanceQuery({ + wallet, + options = { + enabled: typeof wallet !== 'undefined' && typeof wallet.address !== 'undefined' + } +}) { + return useQuery( + ['walletMinBalance', { address: wallet?.address }], + async () => await WalletService.getMinWalletBalance(wallet), + options + ) +} +/** + * Use Wallets Query + * @param {Object} props The props of the parent + * @param {Object} props.wallets A list of Wallet Addresses + * @param {Object} [props.options] useQuery Options + * @returns {object} + */ +export function useWalletsQuery({ + wallets, + options = { + enabled: typeof wallets !== 'undefined', + refetchInterval + } +}) { + return useQuery('wallets', () => WalletService.fetchWallets(wallets), options) +} diff --git a/hooks/withAlgoExplorer.js b/hooks/withAlgoExplorer.js new file mode 100644 index 000000000..653c8f183 --- /dev/null +++ b/hooks/withAlgoExplorer.js @@ -0,0 +1,49 @@ +import { + useAlgorandPriceQuery, + useExplorerAssetInfo, + useCurrentAssetPricesQuery +} from '@/hooks/useAlgoExplorer' + +import ServiceError from '@/components/ServiceError' +import Spinner from '@/components/Spinner' +import withQuery from '@/hooks/withQuery' + +const components = { + Loading: Spinner, + ServiceError +} +/** + * With Algorand Price Query + * @param {JSX.Element| Function} Component Component to wrap + * @param {object} [options] Options to pass to withQuery + * @returns {JSX.Element} + */ +export function withAlgorandPriceQuery(Component, options) { + return withQuery(Component, { + hook: useAlgorandPriceQuery, + components, + ...options + }) +} + +export function withExplorerAssetInfo(Component, options) { + return withQuery(Component, { + hook: useExplorerAssetInfo, + components, + ...options + }) +} + +/** + * With Algorand Price Query + * @param {JSX.Element| Function} Component Component to wrap + * @param {object} [options] Options to pass to withQuery + * @returns {JSX.Element} + */ +export function withCurrentAssetPricesQuery(Component, options) { + return withQuery(Component, { + hook: useCurrentAssetPricesQuery, + components, + ...options + }) +} diff --git a/pages/trade/[id].js b/pages/trade/[id].js index a28ab1bdf..d4a07065b 100644 --- a/pages/trade/[id].js +++ b/pages/trade/[id].js @@ -38,6 +38,7 @@ import { useAssetPriceQuery } from '@/hooks/useAssetPriceQuery' import useDebounce from '@/hooks/useDebounce' import { useRouter } from 'next/router' import useUserStore from '@/store/use-user-state' +import { StableAssets } from '@/components/StableAssets' import useWallets from '@/hooks/useWallets' /** @@ -126,7 +127,8 @@ export async function getStaticProps({ params: { id } }) { if (typeof staticAssetPrice.isTraded !== 'undefined') { staticExplorerAsset.price_info = staticAssetPrice } - + staticExplorerAsset.isStable = StableAssets.includes(parseInt(id)) + console.log(staticExplorerAsset, 'hello here') if (typeof staticExplorerAsset.name === 'undefined') { staticExplorerAsset.name = '' } diff --git a/services/algoexplorer.js b/services/algoexplorer.js new file mode 100644 index 000000000..97d1ca02a --- /dev/null +++ b/services/algoexplorer.js @@ -0,0 +1,184 @@ +import axios from 'axios' +import { indexerAssetMap } from './algorand' +export const EXPLORER_API = + process.env.NEXT_PUBLIC_EXPLORER_API || 'https://node.testnet.algoexplorerapi.io' +export const EXPLORER_INDEXER_API = + process.env.NEXT_PUBLIC_EXPLORER_INDEXER_API || 'https://algoindexer.testnet.algoexplorerapi.io' +export const ALGO_EXPLORER_V1_API = + process.env.NEXT_PUBLIC_ALGO_EXPLORER_V1_API || 'https://testnet.algoexplorerapi.io' +export const ALGO_EXPLORER_V2_API = + process.env.NEXT_PUBLIC_ALGO_EXPLORER_V2_API || 'https://indexer.testnet.algoexplorerapi.io' +export const EXPLORER_ALGORAND_PRICE = 'https://price.algoexplorerapi.io/price/algo-usd' +export const EXPLORER_CURRENT_ASSET_PRICES = + 'https://testnet.analytics.tinyman.org/api/v1/current-asset-prices' + +// console.debug('NEXT_PUBLIC_EXPLORER_API: ' + process.env.NEXT_PUBLIC_EXPLORER_API) +// console.debug('EXPLORER_API: ' + EXPLORER_API) +// console.debug('NEXT_PUBLIC_EXPLORER_INDEXER_API: ' + process.env.NEXT_PUBLIC_EXPLORER_INDEXER_API) +// console.debug('EXPLORER_INDEXER_API: ' + EXPLORER_INDEXER_API) +// console.debug('EXPLORER_ALGORAND_PRICE: ' + EXPLORER_ALGORAND_PRICE) +// console.debug('EXPLORER_V2_API: ' + ALGO_EXPLORER_V2_API) +/** + * @see https://algoindexer.testnet.algoexplorerapi.io/v2/assets/185 + * @typedef {Object} ExplorerIndexAsset + * @property {number} assetId Unique asset identifier. + * @property {boolean} destroyed Whether or not this asset is currently deleted. + * @property {number} destroyed-at-round Round during which this asset was destroyed. + * @property {IndexAssetParams} params Specifies the parameters for an asset. + */ + +/** + * @see https://algoindexer.testnet.algoexplorerapi.io/v2/assets/185 + * @typedef {Object} ExplorerIndexVerifedInfo + * @property {string} clawback Address of account used to clawback holdings of this asset. If empty, clawback is not permitted. + * @property {string} creator The address that created this asset. This is the address where the parameters for this asset can be found, and also the address where unwanted asset units can be sent in the worst case. + * @property {number} decimals The number of digits to use after the decimal point when displaying this asset. If 0, the asset is not divisible. If 1, the base unit of the asset is in tenths. If 2, the base unit of the asset is in hundredths, and so on. This value must be between 0 and 19 (inclusive). + * @property {boolean} default-frozen Whether holdings of this asset are frozen by default. + * @property {string} freeze Address of account used to freeze holdings of this asset. If empty, freezing is not permitted. + * @property {string} manager Address of account used to manage the keys of this asset and to destroy it. + * @property {string} metadata-hash A commitment to some unspecified asset metadata. The format of this metadata is up to the application. + * @property {string} name Name of this asset, as supplied by the creator. + * @property {string} reserve Address of account holding reserve (non-minted) units of this asset. + * @property {number} total The total number of units of this asset. + * @property {string} unit-name Name of a unit of this asset, as supplied by the creator. + * @property {string} url URL where more information about the asset can be retrieved. + * + */ + +/** + * + * @see https://testnet.algoexplorerapi.io/v1/asset/408947/info + * @param {ExplorerIndexAsset} explorerIndexAsset The asset response + * @returns {{circulating, total, deleted, decimals, name, verified, txid, fullName, id, url, timestamp, txns}} + */ +const toExplorerAsset = (data) => { + const { + index: id, + 'creation-txid': txid, + 'asset-tx-counter': txns, + deleted, + verification, + params + } = data.asset + const { + decimals, + url, + name: fullName, + 'unit-name': name, + total, + 'circulating-supply': circulating + } = params + + let res = { + id, + deleted, + txid, + decimals, + name: name || fullName || null, + txns, + fullName, + circulating, + verified: typeof verification !== 'undefined', + url: url || null, + total + } + + if (typeof verification !== 'undefined') { + console.log(verification) + res.verified_info = verification + } + return res +} + +export async function fetchExplorerSearchv1(search) { + const { data } = await axios.get(`${ALGO_EXPLORER_V1_API}/v1/search/${search}`) + return data +} + +/** + * Fetch Asset Info + * + * Uses Algodex Explorer V1 Asset Info API + * + * @param id + * @returns {Promise<{circulating, total, decimals, name, verified, txid, fullName, id, url, timestamp, txns}>} + */ +export async function fetchExplorerAssetInfo(id) { + //console.debug(`Fetching ${id || 'Nothing'}`) + if (typeof id === 'undefined') { + throw new Error('Must have ID') + } + // console.debug(`${ALGO_EXPLORER_V2_API}/v2/asset/${id}/info`) + const { data } = await axios.get(`${ALGO_EXPLORER_V2_API}/v2/assets/${id}`) + //console.debug(`Fetched ${id} with ${data.txCount} transactions`) + return toExplorerAsset(data) +} + +/** + * Map Explorer Index V2 + * + * DO NOT USE! Not accurate, see fetchAssetInfo instead + * + * @param asset + * @returns {{score: *, circulating, total, deleted, decimals, name, verified: *, fullName, id, txns}} + */ +const toExplorerAssetV2 = (asset) => { + return { + ...indexerAssetMap({ asset }), + txns: asset['asset-tx-counter'], + score: asset.params.score, + circulating: asset.params['circulating-supply'], + verified: asset.params.verified + } +} +/** + * Get Asset Info v2 + * + * DO NOT USE! Not accurate see fetchAssetInfo + * + * Retrieve Asset Info from the Algorand Indexer + * It is experimental and not accurate state. + * + * @see https://indexer.testnet.algoexplorerapi.io/v2/assets/408947?include-all=true + * @param id + * @returns {Promise<{total, deleted, decimals, name, fullName, id}>} + */ +export async function fetchAssetInfoV2(id) { + if (typeof id === 'undefined') { + throw new Error('Must have ID') + } + const { + data: { asset } + } = await axios.get(`${EXPLORER_INDEXER_API}/v2/assets/${id}?include-all=true`) + + return toExplorerAssetV2(asset) +} + +/** + * Get Algorand price + * + * Retrieve price of Algorand from the Algorand Indexer + * + * @see https://price.algoexplorerapi.io/price/algo-usd + */ +export async function fetchAlgorandPrice() { + const { data } = await axios.get(`${EXPLORER_ALGORAND_PRICE}`) + return { algoPrice: data.price } +} + +/** + * Fetch Current Asset Prices and AlgorandPrice + * @see https://testnet.analytics.tinyman.org/api/v1/current-asset-prices + * @see https://mainnet.analytics.tinyman.org/api/v1/current-asset-prices + */ +export async function fetchCurrentAssetPrices(assetId) { + console.log('Env: ', process.env) + console.log('AssetId: ', assetId) + // console.debug(`fetchCurrentAssetPrices(): ${EXPLORER_CURRENT_ASSET_PRICES}`) + const { data } = await axios.get(`${EXPLORER_CURRENT_ASSET_PRICES}`) + console.debug(`fetchCurrentAssetPrices(): `, data?.[assetId]?.['price'] ?? 1) + + return { + usdPrice: data?.[assetId]?.['price'] ?? 0.99 + } +}