diff --git a/src/@context/Pool/index.tsx b/src/@context/Pool/index.tsx index 34f28e5bb..de6ff8b68 100644 --- a/src/@context/Pool/index.tsx +++ b/src/@context/Pool/index.tsx @@ -56,7 +56,13 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement { // const [fetchInterval, setFetchInterval] = useState() const fetchAllData = useCallback(async () => { - if (!asset?.chainId || !asset?.accessDetails?.addressOrId || !owner) return + if ( + !accountId || + !asset?.chainId || + !asset?.accessDetails?.addressOrId || + !owner + ) + return const response = await getPoolData( asset.chainId, @@ -64,6 +70,7 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement { owner, accountId || '' ) + if (!response) return setPoolData(response.poolData) diff --git a/src/@utils/constants.ts b/src/@utils/constants.ts index cf39b726d..ef3558975 100644 --- a/src/@utils/constants.ts +++ b/src/@utils/constants.ts @@ -1 +1 @@ -export const MAX_DECIMALS = 5 +export const MAX_DECIMALS = 6 diff --git a/src/components/@shared/FormFields/FilesInput/index.tsx b/src/components/@shared/FormFields/FilesInput/index.tsx index 73d5057c4..6f0c97c56 100644 --- a/src/components/@shared/FormFields/FilesInput/index.tsx +++ b/src/components/@shared/FormFields/FilesInput/index.tsx @@ -65,7 +65,7 @@ export default function FilesInput(props: InputProps): ReactElement { return ( <> - {field.value[0].valid !== undefined ? ( + {field?.value && field?.value[0]?.valid !== undefined ? ( ) : ( item.pool.datatoken.id))] if (dtList.length === 0) { setTransactions([]) setIsLoading(false) return } - const ddoList = await getAssetsFromDtList(dtList, chainIds, cancelToken) - + const ddoList = !minimal + ? await getAssetsFromDtList(dtList, chainIds, cancelToken) + : [] for (let i = 0; i < data.length; i++) { poolTransactions.push({ ...data[i], - networkId: getAsset(ddoList, data[i].datatoken.address).chainId, - asset: getAsset(ddoList, data[i].datatoken.address) + networkId: !minimal + ? getAsset(ddoList, data[i].pool.datatoken.id).chainId + : poolChainId, + asset: !minimal ? getAsset(ddoList, data[i].pool.datatoken.id) : null }) } const sortedTransactions = poolTransactions.sort( diff --git a/src/components/@shared/atoms/Button.module.css b/src/components/@shared/atoms/Button.module.css index d63783b26..e187b43cf 100644 --- a/src/components/@shared/atoms/Button.module.css +++ b/src/components/@shared/atoms/Button.module.css @@ -26,6 +26,7 @@ .button:last-child { margin-right: 0; + min-width: auto; } .button:hover, @@ -33,8 +34,8 @@ color: var(--brand-white); background: var(--brand-grey-light); text-decoration: none; - transform: translate3d(0, -0.05rem, 0); box-shadow: 0 12px 30px 0 rgba(0, 0, 0, 0.1); + transform: translate3d(0, -0.05rem, 0); } .button:active { diff --git a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx index d11fd09de..e991f02bb 100644 --- a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx +++ b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx @@ -10,7 +10,7 @@ import { useAsset } from '@context/Asset' import { useWeb3 } from '@context/Web3' import content from '../../../../../content/pages/startComputeDataset.json' import { Asset } from '@oceanprotocol/lib' -import { AccessDetails } from 'src/@types/Price' +import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price' import { getAccessDetailsForAssets, getAccessDetails @@ -40,7 +40,9 @@ export default function FormStartCompute({ selectedComputeAssetTimeout, stepText, isConsumable, - consumableFeedback + consumableFeedback, + datasetOrderPriceAndFees, + algoOrderPriceAndFees }: { algorithms: AssetSelectionAsset[] ddoListAlgorithms: Asset[] @@ -65,11 +67,19 @@ export default function FormStartCompute({ stepText: string isConsumable: boolean consumableFeedback: string + datasetOrderPriceAndFees?: OrderPriceAndFees + algoOrderPriceAndFees?: OrderPriceAndFees }): ReactElement { const { isValid, values }: FormikContextType<{ algorithm: string }> = useFormikContext() const { asset, isAssetNetwork } = useAsset() const [totalPrice, setTotalPrice] = useState(asset?.accessDetails?.price) + const [datasetOrderPrice, setDatasetOrderPrice] = useState( + asset?.accessDetails?.price + ) + const [algoOrderPrice, setAlgoOrderPrice] = useState( + selectedAlgorithmAsset?.accessDetails?.price + ) const [isBalanceSufficient, setIsBalanceSufficient] = useState(false) const { accountId, balance } = useWeb3() @@ -106,12 +116,24 @@ export default function FormStartCompute({ useEffect(() => { if (!asset?.accessDetails || !selectedAlgorithmAsset?.accessDetails) return + setDatasetOrderPrice( + datasetOrderPriceAndFees?.price || asset.accessDetails.price + ) + setAlgoOrderPrice( + algoOrderPriceAndFees?.price || + selectedAlgorithmAsset?.accessDetails.price + ) const priceDataset = - hasPreviousOrder || hasDatatoken ? 0 : Number(asset.accessDetails.price) + hasPreviousOrder || hasDatatoken + ? 0 + : Number(datasetOrderPriceAndFees?.price || asset.accessDetails.price) const priceAlgo = hasPreviousOrderSelectedComputeAsset || hasDatatokenSelectedComputeAsset ? 0 - : Number(selectedAlgorithmAsset?.accessDetails.price) + : Number( + algoOrderPriceAndFees?.price || + selectedAlgorithmAsset?.accessDetails.price + ) setTotalPrice((priceDataset + priceAlgo).toString()) }, [ @@ -120,7 +142,9 @@ export default function FormStartCompute({ hasPreviousOrder, hasDatatoken, hasPreviousOrderSelectedComputeAsset, - hasDatatokenSelectedComputeAsset + hasDatatokenSelectedComputeAsset, + datasetOrderPriceAndFees, + algoOrderPriceAndFees ]) useEffect(() => { @@ -153,6 +177,8 @@ export default function FormStartCompute({ algorithmConsumeDetails={selectedAlgorithmAsset?.accessDetails} symbol={oceanSymbol} totalPrice={Number.parseFloat(totalPrice)} + datasetOrderPrice={datasetOrderPrice} + algoOrderPrice={algoOrderPrice} /> () + const [isRequestingDataseOrderPrice, setIsRequestingDataseOrderPrice] = + useState(false) + const [algoOrderPriceAndFees, setAlgoOrderPriceAndFees] = + useState() + const [isRequestingAlgoOrderPrice, setIsRequestingAlgoOrderPrice] = + useState(false) const isComputeButtonDisabled = isJobStarting === true || file === null || (!validOrderTx && !hasDatatoken && !isConsumablePrice) || (!validAlgorithmOrderTx && !hasAlgoAssetDatatoken && !isAlgoConsumablePrice) - async function checkAssetDTBalance(asset: DDO) { + async function checkAssetDTBalance(asset: DDO): Promise { if (!asset?.services[0].datatokenAddress) return const datatokenInstance = new Datatoken(web3) const dtBalance = await datatokenInstance.balance( @@ -97,7 +105,9 @@ export default function Compute({ accountId ) setAlgorithmDTBalance(new Decimal(dtBalance).toString()) - setHasAlgoAssetDatatoken(Number(dtBalance) >= 1) + const hasAlgoDt = Number(dtBalance) >= 1 + setHasAlgoAssetDatatoken(hasAlgoDt) + return hasAlgoDt } useEffect(() => { @@ -106,16 +116,56 @@ export default function Compute({ setIsConsumablePrice(asset?.accessDetails?.isPurchasable) setIsOwned(asset?.accessDetails?.isOwned) setValidOrderTx(asset?.accessDetails?.validOrderTx) + + async function initDatasetPriceAndFees() { + if ( + asset?.accessDetails?.addressOrId === ZERO_ADDRESS || + asset?.accessDetails?.type === 'free' + ) + return + + setIsRequestingDataseOrderPrice(true) + setComputeStatusText('Calculating price including fees.') + const orderPriceAndFees = await getOrderPriceAndFees(asset, accountId) + setDatasetOrderPriceAndFees(orderPriceAndFees) + setIsRequestingDataseOrderPrice(false) + } + + initDatasetPriceAndFees() }, [asset?.accessDetails]) useEffect(() => { if (!selectedAlgorithmAsset?.accessDetails || !accountId) return - checkAssetDTBalance(selectedAlgorithmAsset) + setIsConsumablePrice(selectedAlgorithmAsset?.accessDetails?.isPurchasable) setIsAlgorithmOwned(selectedAlgorithmAsset?.accessDetails?.isOwned) setValidAlgorithmOrderTx( selectedAlgorithmAsset?.accessDetails?.validOrderTx ) + + async function initAlgoPriceAndFees() { + if ( + selectedAlgorithmAsset?.accessDetails?.addressOrId === ZERO_ADDRESS || + selectedAlgorithmAsset?.accessDetails?.type === 'free' + ) + return + + setIsRequestingAlgoOrderPrice(true) + setComputeStatusText('Calculating price including fees.') + const orderPriceAndFees = await getOrderPriceAndFees( + selectedAlgorithmAsset, + accountId + ) + setAlgoOrderPriceAndFees(orderPriceAndFees) + setIsRequestingAlgoOrderPrice(false) + } + + async function initSelectedAlgo() { + const hasAlgoDt = await checkAssetDTBalance(selectedAlgorithmAsset) + !hasAlgoDt && (await initAlgoPriceAndFees()) + } + + initSelectedAlgo() }, [selectedAlgorithmAsset]) useEffect(() => { @@ -406,7 +456,11 @@ export default function Compute({ ddoListAlgorithms={ddoAlgorithmList} selectedAlgorithmAsset={selectedAlgorithmAsset} setSelectedAlgorithm={setSelectedAlgorithmAsset} - isLoading={isJobStarting} + isLoading={ + isJobStarting || + isRequestingDataseOrderPrice || + isRequestingAlgoOrderPrice + } isComputeButtonDisabled={isComputeButtonDisabled} hasPreviousOrder={validOrderTx !== undefined} hasDatatoken={hasDatatoken} @@ -436,6 +490,8 @@ export default function Compute({ stepText={computeStatusText} isConsumable={isConsumable} consumableFeedback={consumableFeedback} + datasetOrderPriceAndFees={datasetOrderPriceAndFees} + algoOrderPriceAndFees={algoOrderPriceAndFees} /> )} diff --git a/src/components/Asset/AssetActions/Pool/Graph/_utils.ts b/src/components/Asset/AssetActions/Pool/Graph/_utils.ts index eb36b1e77..6d5ec2451 100644 --- a/src/components/Asset/AssetActions/Pool/Graph/_utils.ts +++ b/src/components/Asset/AssetActions/Pool/Graph/_utils.ts @@ -25,7 +25,7 @@ export function getOptions( borderColor: isDarkMode ? `#41474e` : `#e2e2e2`, callbacks: { label: (tooltipItem: TooltipItem) => - `${formatPrice(`${tooltipItem.formattedValue}`, locale)} ${symbol}` + `${tooltipItem.formattedValue} ${symbol}` } } }, diff --git a/src/components/Asset/AssetActions/Pool/Remove/index.module.css b/src/components/Asset/AssetActions/Pool/Remove/index.module.css index 0bdc13d89..f34cd2dbb 100644 --- a/src/components/Asset/AssetActions/Pool/Remove/index.module.css +++ b/src/components/Asset/AssetActions/Pool/Remove/index.module.css @@ -31,13 +31,17 @@ .maximum { position: absolute; - right: -2rem; - bottom: 2rem; + right: 0; + bottom: 2.5rem; font-size: var(--font-size-mini); min-width: 5rem; text-align: center; } +.maximum:hover { + transform: none; +} + .toggle { margin-top: calc(var(--spacer) / 2); margin-bottom: 0; diff --git a/src/components/Asset/AssetActions/Pool/Remove/index.tsx b/src/components/Asset/AssetActions/Pool/Remove/index.tsx index 07691f391..538c1c858 100644 --- a/src/components/Asset/AssetActions/Pool/Remove/index.tsx +++ b/src/components/Asset/AssetActions/Pool/Remove/index.tsx @@ -67,11 +67,16 @@ export default function Remove({ minOceanAmount ) setTxId(result?.transactionHash) + // fetch new data fetchAllData() } catch (error) { LoggerInstance.error(error.message) toast.error(error.message) } finally { + // reset slider after transaction + setAmountPercent('0') + setAmountOcean('0') + setMinOceanAmount('0') setIsLoading(false) } } @@ -80,8 +85,10 @@ export default function Remove({ if (!accountId || !poolTokens) return async function getMax() { + const poolTokensAmount = + !poolTokens || poolTokens === '0' ? '1' : poolTokens const maxTokensToRemoveFromPool = calcMaxExactOut(totalPoolTokens) - const poolTokensDecimal = new Decimal(poolTokens) + const poolTokensDecimal = new Decimal(poolTokensAmount) const maxTokensToRemoveForUser = maxTokensToRemoveFromPool.greaterThan( poolTokensDecimal ) @@ -105,6 +112,7 @@ export default function Remove({ tokenOutAddress, newAmountPoolShares ) + setAmountOcean(newAmountOcean) }, 150) ) @@ -116,6 +124,11 @@ export default function Remove({ }, [amountPoolShares, accountId, poolTokens, poolAddress, totalPoolTokens]) useEffect(() => { + if (!amountOcean || amountPercent === '0') { + setMinOceanAmount('0') + return + } + const minOceanAmount = new Decimal(amountOcean) .mul(new Decimal(100).minus(new Decimal(slippage))) .dividedBy(100) @@ -220,7 +233,12 @@ export default function Remove({ actionName={content.pool.remove.action} action={handleRemoveLiquidity} successMessage="Successfully removed liquidity." - isDisabled={!isAssetNetwork || amountOcean === '0'} + isDisabled={ + !isAssetNetwork || + amountPercent === '0' || + amountOcean === '0' || + poolTokens === '0' + } txId={txId} tokenAddress={tokenOutAddress} tokenSymbol={tokenOutSymbol} diff --git a/src/components/Asset/AssetActions/Pool/index.tsx b/src/components/Asset/AssetActions/Pool/index.tsx index 26eebff59..2b9a94bfe 100644 --- a/src/components/Asset/AssetActions/Pool/index.tsx +++ b/src/components/Asset/AssetActions/Pool/index.tsx @@ -208,7 +208,7 @@ export default function Pool(): ReactElement { diff --git a/src/components/Publish/Pricing/Price.tsx b/src/components/Publish/Pricing/Price.tsx index b18b80292..c55b92544 100644 --- a/src/components/Publish/Pricing/Price.tsx +++ b/src/components/Publish/Pricing/Price.tsx @@ -34,7 +34,13 @@ export default function Price({ <>
- +
diff --git a/src/components/Publish/_validation.ts b/src/components/Publish/_validation.ts index 7e4e50195..bbae3841f 100644 --- a/src/components/Publish/_validation.ts +++ b/src/components/Publish/_validation.ts @@ -1,8 +1,10 @@ +import { MAX_DECIMALS } from '@utils/constants' import * as Yup from 'yup' // TODO: conditional validation // e.g. when algo is selected, Docker image is required // hint, hint: https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema + const validationMetadata = { type: Yup.string() .matches(/dataset|algorithm/g, { excludeEmptyString: true }) @@ -54,25 +56,53 @@ const validationService = { }) } +const maxDecimalsValidation = new RegExp( + '^\\d+(\\.\\d{1,' + MAX_DECIMALS + '})?$' +) + const validationPricing = { type: Yup.string() .matches(/fixed|dynamic|free/g, { excludeEmptyString: true }) .required('Required'), // https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema + price: Yup.number() .min(1, (param: { min: number }) => `Must be more or equal to ${param.min}`) + .max( + 1000000, + (param: { max: number }) => `Must be less than or equal to ${param.max}` + ) + .test( + 'maxDigitsAfterDecimal', + `Must have maximum ${MAX_DECIMALS} decimal digits`, + (param) => maxDecimalsValidation.test(param?.toString()) + ) .required('Required'), amountDataToken: Yup.number() .min(50, (param) => `Must be more or equal to ${param.min}`) .required('Required'), amountOcean: Yup.number() .min(50, (param) => `Must be more or equal to ${param.min}`) + .max( + 1000000, + (param: { max: number }) => `Must be less than or equal to ${param.max}` + ) + .test( + 'maxDigitsAfterDecimal', + `Must have maximum ${MAX_DECIMALS} decimal digits`, + (param) => maxDecimalsValidation.test(param?.toString()) + ) .required('Required'), weightOnDataToken: Yup.string().required('Required'), weightOnOcean: Yup.string().required('Required'), swapFee: Yup.number() .min(0.1, (param) => `Must be more or equal to ${param.min}`) .max(10, 'Maximum is 10%') + .test( + 'maxDigitsAfterDecimal', + `Must have maximum ${MAX_DECIMALS} decimal digits`, + (param) => maxDecimalsValidation.test(param?.toString()) + ) .required('Required') }