From 8d1782a800d03a194268671b03c39c21446f37ac Mon Sep 17 00:00:00 2001 From: mihaisc Date: Mon, 14 Feb 2022 18:27:36 +0200 Subject: [PATCH] Restore order (#1068) * minor refactors * minor refactors * fixes * buy dt * consumePrice + estimation * various fixes * cleanup * fix build * fix ssh issue * feedback * build fix * ssh fix * remove console.log * suggested fixes * other fixes * switch to decimal * more fixes * more fixes * fix * some fee refactors * more fee refactoring * lib update, fre rename Signed-off-by: mihaisc * minor refactors Signed-off-by: mihaisc * build fixes Signed-off-by: mihaisc * update + more refactoring * calc price * fix build * restore accountId in effect * fix order * fix build and update lib * fix order index * fix comments * pool fix * remove console.log * fix order fixed rate exchange * fixed free order and messaging * add comment * minor type fix * more type fixes --- .env.example | 9 + app.config.js | 19 ++ next.config.js | 23 +- package-lock.json | 17 +- package.json | 5 +- src/@context/Asset.tsx | 1 - src/@context/Pool/index.tsx | 11 +- src/@hooks/useConsume.ts | 159 ------------ src/@hooks/usePricing.ts | 244 ------------------ src/@hooks/useSiteMetadata/index.ts | 10 +- src/@hooks/useSiteMetadata/types.ts | 6 + src/@types/Price.d.ts | 50 +++- src/@utils/accessDetailsAndPricing.ts | 139 +++++++--- src/@utils/feedback.ts | 98 +------ src/@utils/fixedRateExchange.ts | 34 +++ src/@utils/markdownPages.tsx | 4 +- src/@utils/order.ts | 96 +++++++ src/@utils/pool.ts | 75 ++++++ src/@utils/provider.ts | 23 +- src/@utils/siteConfig.ts | 12 + src/@utils/web3.ts | 10 + src/components/@shared/AssetList/index.tsx | 10 +- src/components/@shared/ButtonBuy/index.tsx | 2 + src/components/@shared/Price/index.tsx | 5 +- .../AlgorithmDatasetsListForCompute.tsx | 17 +- .../Compute/FormComputeDataset.tsx | 5 +- .../AssetActions/Compute/PriceOutput.tsx | 5 +- .../Asset/AssetActions/Compute/index.tsx | 16 +- src/components/Asset/AssetActions/Consume.tsx | 154 ----------- ...Consume.module.css => Download.module.css} | 0 .../Asset/AssetActions/Download.tsx | 175 +++++++++++++ src/components/Asset/AssetActions/index.tsx | 6 +- src/components/Publish/Preview/index.tsx | 29 +-- src/components/Publish/Pricing/index.tsx | 9 +- src/components/Publish/_types.ts | 1 + src/components/Publish/_utils.ts | 31 ++- src/components/Publish/index.tsx | 3 - 37 files changed, 743 insertions(+), 770 deletions(-) delete mode 100644 src/@hooks/useConsume.ts delete mode 100644 src/@hooks/usePricing.ts create mode 100644 src/@utils/fixedRateExchange.ts create mode 100644 src/@utils/order.ts create mode 100644 src/@utils/pool.ts create mode 100644 src/@utils/siteConfig.ts delete mode 100644 src/components/Asset/AssetActions/Consume.tsx rename src/components/Asset/AssetActions/{Consume.module.css => Download.module.css} (100%) create mode 100644 src/components/Asset/AssetActions/Download.tsx diff --git a/.env.example b/.env.example index a670f9944..60a85927b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,15 @@ #NEXT_PUBLIC_INFURA_PROJECT_ID="xxx" #NEXT_PUBLIC_MARKET_FEE_ADDRESS="0xxx" +#NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE="1" +#NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE="1" +#NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE="1" +#NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE="1" +#NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE="1" +#NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE="1" + + + #NEXT_PUBLIC_PORTIS_ID="xxx" diff --git a/app.config.js b/app.config.js index f71710e34..d11c07c60 100644 --- a/app.config.js +++ b/app.config.js @@ -22,6 +22,25 @@ module.exports = { marketFeeAddress: process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS || '0x9984b2453eC7D99a73A5B3a46Da81f197B753C8d', + // publisher market fee that is taken upon ordering an asset, it is an absolute value, it is declared on erc20 creation + publisherMarketOrderFee: + process.env.NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE || '0', + // fee recieved by the publisher market when a dt is swaped from a pool, percent + publisherMarketPoolSwapFee: + process.env.NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE || '0', + // fee recieved by the publisher market when a dt is bought from a fixed rate exchange, percent + publisherMarketFixedSwapFee: + process.env.NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE || '0', + + // consume market fee that is taken upon ordering an asset, it is an absolute value, it is specified on order + consumeMarketOrderFee: + process.env.NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE || '0', + // fee recieved by the consume market when a dt is swaped from a pool, percent + consumeMarketPoolSwapFee: + process.env.NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE || '0', + // fee recieved by the consume market when a dt is bought from a fixed rate exchange, percent + consumeMarketFixedSwapFee: + process.env.NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE || '0', // Used for conversion display, can be whatever coingecko API supports // see: https://api.coingecko.com/api/v3/simple/supported_vs_currencies diff --git a/next.config.js b/next.config.js index a1d6da1ed..63e8b59e6 100644 --- a/next.config.js +++ b/next.config.js @@ -17,24 +17,35 @@ module.exports = (phase, { defaultConfig }) => { type: 'asset/resource' } ) - // for old ocean.js, most likely can be removed later on config.plugins.push( new options.webpack.IgnorePlugin({ resourceRegExp: /^electron$/ }) ) - - config.resolve.fallback = { + const fallback = config.resolve.fallback || {} + Object.assign(fallback, { + // crypto: require.resolve('crypto-browserify'), + // stream: require.resolve('stream-browserify'), + // assert: require.resolve('assert'), + // os: require.resolve('os-browserify'), + // url: require.resolve('url'), + http: require.resolve('stream-http'), + https: require.resolve('https-browserify'), fs: false, crypto: false, os: false, stream: false, - http: false, - https: false, assert: false - } + }) + config.resolve.fallback = fallback + config.plugins = (config.plugins || []).concat([ + new options.webpack.ProvidePlugin({ + process: 'process/browser', + Buffer: ['buffer', 'Buffer'] + }) + ]) return typeof defaultConfig.webpack === 'function' ? defaultConfig.webpack(config, options) : config diff --git a/package-lock.json b/package-lock.json index 8687a8cdd..1824fdf15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@coingecko/cryptoformat": "^0.4.4", "@loadable/component": "^5.15.2", "@oceanprotocol/art": "^3.2.0", - "@oceanprotocol/lib": "^1.0.0-next.14", + "@oceanprotocol/lib": "^1.0.0-next.17", "@oceanprotocol/typographies": "^0.1.0", "@portis/web3": "^4.0.6", "@tippyjs/react": "^4.2.6", @@ -86,10 +86,13 @@ "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "file-loader": "^6.2.0", + "https-browserify": "^1.0.0", "husky": "^7.0.4", "prettier": "^2.5.1", "pretty-quick": "^3.1.3", + "process": "^0.11.10", "serve": "^13.0.2", + "stream-http": "^2.8.3", "typescript": "^4.5.4" }, "engines": { @@ -3453,9 +3456,9 @@ } }, "node_modules/@oceanprotocol/lib": { - "version": "1.0.0-next.14", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.14.tgz", - "integrity": "sha512-qz41c6LlvfELvz9oTJncIsZ/cZudEzq2gjCgI4E3iQfo71qB7jzwPd6oqiqIYHmRHl8J/F6QXe9fQsp81R4TIg==", + "version": "1.0.0-next.17", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.17.tgz", + "integrity": "sha512-qZQ2nWe/qNN/2M9YZEacwHSMyStwFoN6gXKDCw17G9E8QLwl3v5z54WjAqLlGOpNnGKMIvkrj4gUpUAMtjLSxw==", "dependencies": { "@oceanprotocol/contracts": "1.0.0-alpha.18", "bignumber.js": "^9.0.2", @@ -30132,9 +30135,9 @@ } }, "@oceanprotocol/lib": { - "version": "1.0.0-next.14", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.14.tgz", - "integrity": "sha512-qz41c6LlvfELvz9oTJncIsZ/cZudEzq2gjCgI4E3iQfo71qB7jzwPd6oqiqIYHmRHl8J/F6QXe9fQsp81R4TIg==", + "version": "1.0.0-next.17", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.17.tgz", + "integrity": "sha512-qZQ2nWe/qNN/2M9YZEacwHSMyStwFoN6gXKDCw17G9E8QLwl3v5z54WjAqLlGOpNnGKMIvkrj4gUpUAMtjLSxw==", "requires": { "@oceanprotocol/contracts": "1.0.0-alpha.18", "bignumber.js": "^9.0.2", diff --git a/package.json b/package.json index 17e3fc6ef..f8078cf80 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@coingecko/cryptoformat": "^0.4.4", "@loadable/component": "^5.15.2", "@oceanprotocol/art": "^3.2.0", - "@oceanprotocol/lib": "^1.0.0-next.14", + "@oceanprotocol/lib": "^1.0.0-next.17", "@oceanprotocol/typographies": "^0.1.0", "@portis/web3": "^4.0.6", "@tippyjs/react": "^4.2.6", @@ -94,10 +94,13 @@ "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "file-loader": "^6.2.0", + "https-browserify": "^1.0.0", "husky": "^7.0.4", "prettier": "^2.5.1", "pretty-quick": "^3.1.3", + "process": "^0.11.10", "serve": "^13.0.2", + "stream-http": "^2.8.3", "typescript": "^4.5.4" }, "repository": { diff --git a/src/@context/Asset.tsx b/src/@context/Asset.tsx index fd0b83f66..c13769525 100644 --- a/src/@context/Asset.tsx +++ b/src/@context/Asset.tsx @@ -95,7 +95,6 @@ function AssetProvider({ // ----------------------------------- const fetchAccessDetails = useCallback(async (): Promise => { if (!asset?.chainId || !asset?.services) return - const accessDetails = await getAccessDetails( asset.chainId, asset.services[0].datatokenAddress, diff --git a/src/@context/Pool/index.tsx b/src/@context/Pool/index.tsx index 33c08b744..2451bb18a 100644 --- a/src/@context/Pool/index.tsx +++ b/src/@context/Pool/index.tsx @@ -30,7 +30,8 @@ const initialPoolInfo: Partial = { } const initialPoolInfoUser: Partial = { - liquidity: new Decimal(0) + liquidity: new Decimal(0), + poolShares: '0' } const initialPoolInfoCreator: Partial = initialPoolInfoUser @@ -68,7 +69,7 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement { setPoolData(response.poolData) setPoolInfoUser((prevState) => ({ ...prevState, - poolShares: response.poolDataUser?.shares[0]?.shares + poolShares: response.poolDataUser?.shares[0]?.shares || '0' })) setPoolSnapshots(response.poolSnapshots) LoggerInstance.log('[pool] Fetched pool data:', response.poolData) @@ -193,10 +194,10 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement { !poolData || !poolInfo?.totalPoolTokens || !asset?.chainId || - !accountId + !accountId || + !poolInfoUser ) return - // Staking bot receives half the pool shares so for display purposes // we can multiply by 2 as we have a hardcoded 50/50 pool weight. const userPoolShares = new Decimal(poolInfoUser.poolShares || 0) @@ -237,6 +238,8 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement { poolShares: userPoolShares, ...newPoolInfoUser }) + // poolInfoUser was not added on purpose, we use setPoolInfoUser so it will just loop + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ poolData, poolInfoUser?.poolShares, diff --git a/src/@hooks/useConsume.ts b/src/@hooks/useConsume.ts deleted file mode 100644 index 10977daf8..000000000 --- a/src/@hooks/useConsume.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { useState } from 'react' -import { consumeFeedback } from '@utils/feedback' -import { - approve, - Datatoken, - FreOrderParams, - LoggerInstance, - OrderParams, - Pool, - ProviderFees, - ProviderInstance, - signHash, - ZERO_ADDRESS -} from '@oceanprotocol/lib' -import { useWeb3 } from '@context/Web3' -import { getOceanConfig } from '@utils/ocean' - -interface UseConsume { - consume: ( - did: string, - dataTokenAddress: string, - serviceType: string, - marketFeeAddress: string, - orderId?: string - ) => Promise - consumeStep?: number - consumeStepText?: string - consumeError?: string - isLoading: boolean -} - -function useConsume(): UseConsume { - const { accountId, web3, chainId } = useWeb3() - const [isLoading, setIsLoading] = useState(false) - const [consumeStep, setConsumeStep] = useState() - const [consumeStepText, setConsumeStepText] = useState() - const [consumeError, setConsumeError] = useState() - - function setStep(index: number) { - setConsumeStep(index) - setConsumeStepText(consumeFeedback[index]) - } - - // TODO: this will be done in another PR - async function consume( - did: string, - datatokenAddress: string, - serviceType = 'access', - marketFeeAddress: string, - orderId?: string - ): Promise { - if (!accountId) return - - setIsLoading(true) - setConsumeError(undefined) - - try { - setStep(0) - if (!orderId) { - const datatoken = new Datatoken(web3) - // if we don't have a previous valid order, get one - const userOwnedTokens = await datatoken.balance( - datatokenAddress, - accountId - ) - - setStep(1) - try { - const config = getOceanConfig(chainId) - // const txApprove = await approve( - // web3, - // accountId, - // config.oceanTokenAddress, - // accountId, - // '1', - // false - // ) - // console.log('approve tx', txApprove) - - // const txApprove1 = await approve( - // web3, - // accountId, - // config.oceanTokenAddress, - // datatokenAddress, - // '1', - // false - // ) - // console.log('approve tx', txApprove1) - - // diference between timeout and validUntil? - const initializeData = await ProviderInstance.initialize( - did, - 'fca052c239a62523be30ab8ee70c4046867f6cd89f228185fe2996ded3d23c3c', - 0, - accountId, - 'https://providerv4.rinkeby.oceanprotocol.com' - ) - const orderParams = { - consumer: accountId, - serviceIndex: 1, - _providerFees: initializeData.providerFee - } as OrderParams - const freParams = { - exchangeContract: config.fixedRateExchangeAddress, - exchangeId: - '0x7ac824fef114255e5e3521a161ef692ec32003916fb6f3fe985cb74790d053ca', - maxBaseTokenAmount: web3.utils.toWei('2'), - swapMarketFee: web3.utils.toWei('0'), - marketFeeAddress: ZERO_ADDRESS - } as FreOrderParams - - const esttx = await datatoken.estGasBuyFromFreAndOrder( - datatokenAddress, - accountId, - orderParams, - freParams - ) - const tx = await datatoken.buyFromFreAndOrder( - datatokenAddress, - accountId, - orderParams, - freParams - ) - - LoggerInstance.log('ordercreated', orderId) - setStep(2) - } catch (error) { - setConsumeError(error.message) - return error.message - } - } - - setStep(3) - if (orderId) - // await ocean.assets.download( - // did as string, - // orderId, - // dataTokenAddress, - // account, - // '' - // ) - setStep(4) - } catch (error) { - setConsumeError(error.message) - LoggerInstance.error(error) - return error.message - } finally { - setConsumeStep(undefined) - setConsumeStepText(undefined) - setIsLoading(false) - } - return undefined - } - - return { consume, consumeStep, consumeStepText, consumeError, isLoading } -} - -export { useConsume } -export default useConsume diff --git a/src/@hooks/usePricing.ts b/src/@hooks/usePricing.ts deleted file mode 100644 index b5bba9940..000000000 --- a/src/@hooks/usePricing.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { Asset, Config, LoggerInstance } from '@oceanprotocol/lib' -import { useEffect, useState } from 'react' -import { TransactionReceipt } from 'web3-core' -import { Decimal } from 'decimal.js' -import { - getCreatePricingPoolFeedback, - getCreatePricingExchangeFeedback, - getBuyDTFeedback, - getCreateFreePricingFeedback, - getDispenseFeedback -} from '@utils/feedback' -import { useWeb3 } from '@context/Web3' -import { getOceanConfig } from '@utils/ocean' - -interface UsePricing { - getDTSymbol: (ddo: Asset) => Promise - getDTName: (ddo: Asset) => Promise - mint: (tokensToMint: string, ddo: Asset) => Promise - buyDT: ( - amountDataToken: number | string, - accessDetails: AccessDetails, - ddo: Asset - ) => Promise - pricingStep?: number - pricingStepText?: string - pricingError?: string - pricingIsLoading: boolean -} - -function usePricing(): UsePricing { - const { accountId, networkId } = useWeb3() - const [pricingIsLoading, setPricingIsLoading] = useState(false) - const [pricingStep, setPricingStep] = useState() - const [pricingStepText, setPricingStepText] = useState() - const [pricingError, setPricingError] = useState() - const [oceanConfig, setOceanConfig] = useState() - - // Grab ocen config based on passed networkId - useEffect(() => { - if (!networkId) return - - const oceanConfig = getOceanConfig(networkId) - setOceanConfig(oceanConfig) - }, [networkId]) - - async function getDTSymbol(ddo: Asset): Promise { - if (!accountId) return - - const { datatokens } = ddo - return datatokens[0].symbol - // return dataTokenInfo - // ? dataTokenInfo.symbol - // : await ocean?.datatokens.getSymbol(dataTokenInfo.address) - } - - async function getDTName(ddo: Asset): Promise { - if (!accountId) return - const { datatokens } = ddo - return datatokens[0].name - // return dataTokenInfo - // ? dataTokenInfo.name - // : await ocean?.datatokens.getName(dataTokenInfo.address) - } - - // Helper for setting steps & feedback for all flows - async function setStep( - index: number, - type: 'pool' | 'exchange' | 'free' | 'buy' | 'dispense', - ddo: Asset - ) { - const dtSymbol = await getDTSymbol(ddo) - setPricingStep(index) - if (!dtSymbol) return - - let messages - - switch (type) { - case 'pool': - messages = getCreatePricingPoolFeedback(dtSymbol) - break - case 'exchange': - messages = getCreatePricingExchangeFeedback(dtSymbol) - break - case 'free': - messages = getCreateFreePricingFeedback(dtSymbol) - break - case 'buy': - messages = getBuyDTFeedback(dtSymbol) - break - case 'dispense': - messages = getDispenseFeedback(dtSymbol) - break - } - - setPricingStepText(messages[index]) - } - - async function mint( - tokensToMint: string, - ddo: Asset - ): Promise { - const { datatokens } = ddo - LoggerInstance.log('mint function', datatokens[0].address, accountId) - // const balance = new Decimal( - // await ocean.datatokens.balance(dataTokenInfo.address, accountId) - // ) - // const tokens = new Decimal(tokensToMint) - // if (tokens.greaterThan(balance)) { - // const mintAmount = tokens.minus(balance) - // const tx = await ocean.datatokens.mint( - // dataTokenInfo.address, - // accountId, - // mintAmount.toString() - // ) - // return tx - // } - } - - async function buyDT( - amountDataToken: number | string, - accessDetails: AccessDetails, - ddo: Asset - ): Promise { - if (!accountId) return - - let tx - - try { - setPricingIsLoading(true) - setPricingError(undefined) - setStep(1, 'buy', ddo) - - LoggerInstance.log('Price found for buying', accessDetails) - Decimal.set({ precision: 18 }) - - switch (accessDetails?.type) { - case 'dynamic': { - const oceanAmmount = new Decimal(accessDetails.price) - .times(1.05) - .toString() - const maxPrice = new Decimal(accessDetails.price).times(2).toString() - - setStep(2, 'buy', ddo) - LoggerInstance.log( - 'Buying token from pool', - accessDetails, - accountId, - oceanAmmount, - maxPrice - ) - // tx = await ocean.pool.buyDT( - // accountId, - // price.address, - // String(amountDataToken), - // oceanAmmount, - // maxPrice - // ) - setStep(3, 'buy', ddo) - LoggerInstance.log('DT buy response', tx) - break - } - case 'fixed': { - if (!oceanConfig.oceanTokenAddress) { - LoggerInstance.error(`'oceanTokenAddress' not set in oceanConfig`) - return - } - if (!oceanConfig.fixedRateExchangeAddress) { - LoggerInstance.error( - `'fixedRateExchangeAddress' not set in oceanConfig` - ) - return - } - LoggerInstance.log( - 'Buying token from exchange', - accessDetails, - accountId - ) - // await ocean.datatokens.approve( - // oceanConfig.oceanTokenAddress, - // oceanConfig.fixedRateExchangeAddress, - // `${price.value}`, - // accountId - // ) - setStep(2, 'buy', ddo) - // tx = await ocean.fixedRateExchange.buyDT( - // price.address, - // `${amountDataToken}`, - // accountId - // ) - setStep(3, 'buy', ddo) - LoggerInstance.log('DT exchange buy response', tx) - break - } - case 'free': { - setStep(1, 'dispense', ddo) - // const isDispensable = await ocean.OceanDispenser.isDispensable( - // ddo?.services[0].datatokenAddress, - // accountId, - // '1' - // ) - - // if (!isDispensable) { - // LoggerInstance.error( - // `Dispenser for ${ddo?.services[0].datatokenAddress} failed to dispense` - // ) - // return - // } - - // tx = await ocean.OceanDispenser.dispense( - // ddo?.services[0].datatokenAddress, - // accountId, - // '1' - // ) - setStep(2, 'dispense', ddo) - LoggerInstance.log('DT dispense response', tx) - break - } - } - } catch (error) { - setPricingError(error.message) - LoggerInstance.error(error) - } finally { - setStep(0, 'buy', ddo) - setPricingStepText(undefined) - setPricingIsLoading(false) - } - - return tx - } - - return { - getDTSymbol, - getDTName, - buyDT, - mint, - pricingStep, - pricingStepText, - pricingIsLoading, - pricingError - } -} - -export { usePricing } -export default usePricing diff --git a/src/@hooks/useSiteMetadata/index.ts b/src/@hooks/useSiteMetadata/index.ts index 40f9d3259..5692cf987 100644 --- a/src/@hooks/useSiteMetadata/index.ts +++ b/src/@hooks/useSiteMetadata/index.ts @@ -1,12 +1,6 @@ import { UseSiteMetadata } from './types' -import siteContent from '../../../content/site.json' -import appConfig from '../../../app.config' +import { getSiteMetadata } from '@utils/siteConfig' export function useSiteMetadata(): UseSiteMetadata { - const siteMeta: UseSiteMetadata = { - ...siteContent, - appConfig - } - - return siteMeta + return getSiteMetadata() } diff --git a/src/@hooks/useSiteMetadata/types.ts b/src/@hooks/useSiteMetadata/types.ts index c8e40c8d6..fce4f1141 100644 --- a/src/@hooks/useSiteMetadata/types.ts +++ b/src/@hooks/useSiteMetadata/types.ts @@ -23,6 +23,12 @@ export interface UseSiteMetadata { chainIds: number[] chainIdsSupported: number[] marketFeeAddress: string + publisherMarketOrderFee: string + publisherMarketPoolSwapFee: string + publisherMarketFixedSwapFee: string + consumeMarketOrderFee: string + consumeMarketPoolSwapFee: string + consumeMarketFixedSwapFee: string currencies: string[] portisId: string allowFixedPricing: string diff --git a/src/@types/Price.d.ts b/src/@types/Price.d.ts index 665d9cfd2..e733eacc2 100644 --- a/src/@types/Price.d.ts +++ b/src/@types/Price.d.ts @@ -1,14 +1,54 @@ +import { ProviderFees } from '@oceanprotocol/lib' + +/** + * @interface OrderPriceAndFee + * @prop {string} price total price including fees + * @prop {string} publisherMarketOrderFee fee received by the market where the asset was published. It is set on erc20 creation. It is a absolute value + * @prop {string} publisherMarketPoolSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage + * @prop {string} publisherMarketFixedSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage + * @prop {string} consumeMarketOrderFee fee received by the market where the asset is ordered. It is set on erc20 creation. It is a absolute value + * @prop {string} consumeMarketPoolSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage + * @prop {string} consumeMarketFixedSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage + * @prop {string} liquidityProviderSwapFee fee received by the liquidity providers of the pool. It is a percentage ( ex 50% means liquidityProviderSwapFee=0.5) + * @prop {ProviderFees} providerFee received from provider + * @prop {string} opcFee ocean protocol community fee, Absolute value based on the configured percentage + */ +interface OrderPriceAndFees { + price: string + publisherMarketOrderFee: string + publisherMarketPoolSwapFee: string + publisherMarketFixedSwapFee: string + consumeMarketOrderFee: string + consumeMarketPoolSwapFee: string + consumeMarketFixedSwapFee: string + liquidityProviderSwapFee: string + providerFee: ProviderFees + opcFee: string +} + +/** + * @interface AccessDetails + * @prop {'dynamic' | 'fixed' | 'free' | ''} type + * @prop {string} price can be either spotPrice/rate + * @prop {string} addressOrId if type is dynamic this is the pool address, for fixed/free this is an id. + * @prop {TokenInfo} baseToken + * @prop {TokenInfo} datatoken + * @prop {bool} isPurchasable checks if you can buy a datatoken from fixed rate exchange/pool/dispenser. For pool it also checks if there is enough dt liquidity + * @prop {bool} isOwned checks if there are valid orders for this, it also takes in consideration timeout + * @prop {string} validOrderTx the latest valid order tx, it also takes in consideration timeout + * @prop {string} publisherMarketOrderFee this is here just because it's more efficient, it's allready in the query + * @prop {FeeInfo} feeInfo values of the relevant fees + */ interface AccessDetails { type: 'dynamic' | 'fixed' | 'free' | '' - price: number - // if type is dynamic this is the pool address, for fixed/free this is an id. + price: string addressOrId: string baseToken: TokenInfo datatoken: TokenInfo - isConsumable?: boolean - // if there are valid orders for this - owned: bool + isPurchasable?: boolean + isOwned: bool validOrderTx: string + publisherMarketOrderFee: string } interface PriceOptions { diff --git a/src/@utils/accessDetailsAndPricing.ts b/src/@utils/accessDetailsAndPricing.ts index 518bea5ae..5ead8259f 100644 --- a/src/@utils/accessDetailsAndPricing.ts +++ b/src/@utils/accessDetailsAndPricing.ts @@ -8,8 +8,13 @@ import { TokensPriceQuery, TokensPriceQuery_tokens as TokensPrice } from '../@types/subgraph/TokensPriceQuery' -import { Asset } from '@oceanprotocol/lib' +import { Asset, ProviderInstance } from '@oceanprotocol/lib' import { AssetExtended } from 'src/@types/AssetExtended' +import { calculateBuyPrice } from './pool' +import { getFixedBuyPrice } from './fixedRateExchange' +import { getSiteMetadata } from './siteConfig' +import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price' +import Decimal from 'decimal.js' const TokensPriceQuery = gql` query TokensPriceQuery($datatokenIds: [ID!], $account: String) { @@ -132,25 +137,33 @@ const TokenPriceQuery = gql` } ` +// TODO: fill in fees after subgraph update function getAccessDetailsFromTokenPrice( tokenPrice: TokenPrice | TokensPrice, timeout?: number ): AccessDetails { const accessDetails = {} as AccessDetails - - if (!timeout && !tokenPrice.orders && tokenPrice.orders.length > 0) { + if ( + tokenPrice && + timeout && + tokenPrice.orders && + tokenPrice.orders.length > 0 + ) { const order = tokenPrice.orders[0] - accessDetails.owned = Date.now() / 1000 - order.createdTimestamp < timeout + accessDetails.isOwned = Date.now() / 1000 - order.createdTimestamp < timeout accessDetails.validOrderTx = order.tx } + // TODO: fetch order fee from sub query + accessDetails.publisherMarketOrderFee = '0' + // free is always the best price if (tokenPrice.dispensers && tokenPrice.dispensers.length > 0) { const dispenser = tokenPrice.dispensers[0] accessDetails.type = 'free' accessDetails.addressOrId = dispenser.id - accessDetails.price = 0 - accessDetails.isConsumable = dispenser.active + accessDetails.price = '0' + accessDetails.isPurchasable = dispenser.active accessDetails.datatoken = { address: dispenser.token.id, name: dispenser.token.name, @@ -164,21 +177,21 @@ function getAccessDetailsFromTokenPrice( tokenPrice.fixedRateExchanges && tokenPrice.fixedRateExchanges.length > 0 ) { - const fre = tokenPrice.fixedRateExchanges[0] + const fixed = tokenPrice.fixedRateExchanges[0] accessDetails.type = 'fixed' - accessDetails.addressOrId = fre.id - accessDetails.price = fre.price + accessDetails.addressOrId = fixed.id + accessDetails.price = fixed.price // in theory we should check dt balance here, we can skip this because in the market we always create fre with minting capabilities. - accessDetails.isConsumable = fre.active + accessDetails.isPurchasable = fixed.active accessDetails.baseToken = { - address: fre.baseToken.address, - name: fre.baseToken.name, - symbol: fre.baseToken.symbol + address: fixed.baseToken.address, + name: fixed.baseToken.name, + symbol: fixed.baseToken.symbol } accessDetails.datatoken = { - address: fre.datatoken.address, - name: fre.datatoken.name, - symbol: fre.datatoken.symbol + address: fixed.datatoken.address, + name: fixed.datatoken.name, + symbol: fixed.datatoken.symbol } return accessDetails } @@ -188,10 +201,10 @@ function getAccessDetailsFromTokenPrice( const pool = tokenPrice.pools[0] accessDetails.type = 'dynamic' accessDetails.addressOrId = pool.id - // TODO: this needs to be consumePrice accessDetails.price = pool.spotPrice // TODO: pool.datatokenLiquidity > 3 is kinda random here, we shouldn't run into this anymore now , needs more thinking/testing - accessDetails.isConsumable = pool.isFinalized && pool.datatokenLiquidity > 3 + accessDetails.isPurchasable = + pool.isFinalized && pool.datatokenLiquidity > 3 accessDetails.baseToken = { address: pool.baseToken.address, name: pool.baseToken.name, @@ -208,20 +221,81 @@ function getAccessDetailsFromTokenPrice( } /** - * returns various consume details for the desired datatoken - * @param chain chain on which the datatoken is preset - * @param datatokenAddress address of the datatoken - * @param timeout timeout of the service, only needed if you want order details like owned and validOrderId - * @param account account that wants to consume, only needed if you want order details like owned and validOrderId - * @returns AccessDetails + * This will be used to get price including feed before ordering + * @param {AssetExtended} asset + * @return {Promise} + */ +export async function getOrderPriceAndFees( + asset: AssetExtended, + accountId?: string +): Promise { + const orderPriceAndFee = { + price: '0', + publisherMarketOrderFee: '0', + publisherMarketPoolSwapFee: '0', + publisherMarketFixedSwapFee: '0', + consumeMarketOrderFee: '0', + consumeMarketPoolSwapFee: '0', + consumeMarketFixedSwapFee: '0', + providerFee: {}, + opcFee: '0' + } as OrderPriceAndFees + const { accessDetails } = asset + const { appConfig } = getSiteMetadata() + + // fetch publish market order fee + orderPriceAndFee.publisherMarketOrderFee = + asset.accessDetails.publisherMarketOrderFee + // fetch consume market order fee + orderPriceAndFee.consumeMarketOrderFee = appConfig.consumeMarketOrderFee + // fetch provider fee + const initializeData = await ProviderInstance.initialize( + asset.id, + asset.services[0].id, + 0, + accountId, + asset.services[0].serviceEndpoint + ) + orderPriceAndFee.providerFee = initializeData.providerFee + + // fetch price and swap fees + switch (accessDetails.type) { + case 'dynamic': { + const poolPrice = await calculateBuyPrice(accessDetails, asset.chainId) + orderPriceAndFee.price = poolPrice + break + } + case 'fixed': { + const fixed = await getFixedBuyPrice(accessDetails, asset.chainId) + orderPriceAndFee.price = fixed.baseTokenAmount + break + } + } + + // calculate full price, we assume that all the values are in ocean, otherwise this will be incorrect + orderPriceAndFee.price = new Decimal(orderPriceAndFee.price) + .add(new Decimal(orderPriceAndFee.consumeMarketOrderFee)) + .add(new Decimal(orderPriceAndFee.publisherMarketOrderFee)) + .add(new Decimal(orderPriceAndFee.providerFee.providerFeeAmount)) + .toString() + return orderPriceAndFee +} + +/** + * @param {number} chain + * @param {string} datatokenAddress + * @param {number=} timeout timout of the service, this is needed to return order details + * @param {string=} account account that wants to buy, is needed to return order details + * @param {bool=} includeOrderPriceAndFees if false price will be spot price (pool) and rate (fre), if true you will get the order price including fees !! fees not yet done + * @returns {Promise} */ export async function getAccessDetails( - chain: number, + chainId: number, datatokenAddress: string, timeout?: number, account = '' ): Promise { - const queryContext = getQueryContext(Number(chain)) + const queryContext = getQueryContext(Number(chainId)) const tokenQueryResult: OperationResult< TokenPriceQuery, { datatokenId: string; account: string } @@ -229,7 +303,7 @@ export async function getAccessDetails( TokenPriceQuery, { datatokenId: datatokenAddress.toLowerCase(), - account: account.toLowerCase() + account: account?.toLowerCase() }, queryContext ) @@ -247,7 +321,6 @@ export async function getAccessDetailsForAssets( const chainAssetLists: { [key: number]: string[] } = {} for (const asset of assets) { - // harcoded until we have chainId on assets if (chainAssetLists[asset.chainId]) { chainAssetLists[asset.chainId].push( asset?.services[0].datatokenAddress.toLowerCase() @@ -264,20 +337,24 @@ export async function getAccessDetailsForAssets( const queryContext = getQueryContext(Number(chainKey)) const tokenQueryResult: OperationResult< TokensPriceQuery, - { datatokenId: string; account: string } + { datatokenIds: [string]; account: string } > = await fetchData( TokensPriceQuery, { datatokenIds: chainAssetLists[chainKey], - account: account.toLowerCase() + account: account?.toLowerCase() }, queryContext ) tokenQueryResult.data?.tokens.forEach((token) => { - const accessDetails = getAccessDetailsFromTokenPrice(token) const currentAsset = assetsExtended.find( (asset) => asset.services[0].datatokenAddress.toLowerCase() === token.id ) + const accessDetails = getAccessDetailsFromTokenPrice( + token, + currentAsset?.services[0]?.timeout + ) + currentAsset.accessDetails = accessDetails }) } diff --git a/src/@utils/feedback.ts b/src/@utils/feedback.ts index ab440966e..2df15688b 100644 --- a/src/@utils/feedback.ts +++ b/src/@utils/feedback.ts @@ -1,89 +1,19 @@ -export const feedback: { [key in number]: string } = { - 99: 'Decrypting file URL...', - 0: '1/3 Looking for data token. Buying if none found...', - 1: '2/3 Transfering data token.', - 2: '3/3 Payment confirmed. Requesting access...' -} - -export const publishFeedback: { [key in number]: string } = { - 0: '1/5 Creating datatoken ...', - 2: '2/5 Encrypting files ...', - 4: '3/5 Storing ddo ...', - 6: '4/5 Minting tokens ...', - 8: '5/5 Asset published succesfully' -} - -// TODO: do something with this object, -// consumeStep should probably return one of those strings -// instead of just a number -export const consumeFeedback: { [key in number]: string } = { - ...feedback, - 3: '3/3 Access granted. Consuming file...' +// TODO: can be better +export function getOrderFeedback( + baseTokenSymbol: string, + datatokenSymbol: string +): { [key in number]: string } { + return { + 0: `Approving and buying one ${datatokenSymbol} from pool`, + 1: `Ordering asset`, + 2: `Approving ${baseTokenSymbol} and ordering asset`, + 3: 'Generating signature to access download url' + } } // TODO: customize for compute export const computeFeedback: { [key in number]: string } = { - 0: '1/3 Ordering asset...', - 1: '2/3 Transfering data token.', - 2: '3/3 Access granted. Starting job...' -} - -export function getCreatePricingPoolFeedback(dtSymbol: string): { - [key: number]: string -} { - return { - 99: `Minting ${dtSymbol} ...`, - 0: 'Creating pool ...', - 1: `Approving ${dtSymbol} ...`, - 2: 'Approving OCEAN ...', - 3: 'Setup pool ...', - 4: 'Pool created.' - } -} - -export function getCreatePricingExchangeFeedback(dtSymbol: string): { - [key: number]: string -} { - return { - 99: `Minting ${dtSymbol} ...`, - 0: 'Creating exchange ...', - 1: `Approving ${dtSymbol} ...`, - 2: 'Fixed exchange created.' - } -} - -export function getCreateFreePricingFeedback(dtSymbol: string): { - [key: number]: string -} { - return { - 99: `Creating ${dtSymbol} faucet...`, - 0: 'Setting faucet as minter ...', - 1: 'Approving minter...', - 2: 'Faucet created.' - } -} - -export function getBuyDTFeedback(dtSymbol: string): { [key: number]: string } { - return { - 1: '1/3 Approving OCEAN ...', - 2: `2/3 Buying ${dtSymbol} ...`, - 3: `3/3 ${dtSymbol} bought.` - } -} - -export function getSellDTFeedback(dtSymbol: string): { [key: number]: string } { - return { - 1: '1/3 Approving OCEAN ...', - 2: `2/3 Selling ${dtSymbol} ...`, - 3: `3/3 ${dtSymbol} sold.` - } -} - -export function getDispenseFeedback(dtSymbol: string): { - [key: number]: string -} { - return { - 1: `1/2 Requesting ${dtSymbol}...`, - 2: `2/2 Received ${dtSymbol}.` - } + 0: 'Ordering asset...', + 1: 'Transfering datatoken.', + 2: 'Access granted. Starting job...' } diff --git a/src/@utils/fixedRateExchange.ts b/src/@utils/fixedRateExchange.ts new file mode 100644 index 000000000..9b75dc79f --- /dev/null +++ b/src/@utils/fixedRateExchange.ts @@ -0,0 +1,34 @@ +import { FixedRateExchange, PriceAndFees } from '@oceanprotocol/lib' +import { AccessDetails } from 'src/@types/Price' +import Web3 from 'web3' +import { getOceanConfig } from './ocean' +import { getDummyWeb3 } from './web3' + +/** + * This is used to calculate the price to buy one datatoken from a fixed rate exchange. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created + * @param {AccessDetails} accessDetails + * @param {number} chainId + * @param {Web3?} web3 + * @return {Promise} + */ +export async function getFixedBuyPrice( + accessDetails: AccessDetails, + chainId?: number, + web3?: Web3 +): Promise { + if (!web3 && !chainId) + throw new Error("web3 and chainId can't be undefined at the same time!") + + if (!web3) { + web3 = await getDummyWeb3(chainId) + } + + const config = getOceanConfig(chainId) + + const fixed = new FixedRateExchange(web3, config.fixedRateExchangeAddress) + const estimatedPrice = await fixed.calcBaseInGivenOutDT( + accessDetails.addressOrId, + '1' + ) + return estimatedPrice +} diff --git a/src/@utils/markdownPages.tsx b/src/@utils/markdownPages.tsx index b2c145a3f..45b4d19d7 100644 --- a/src/@utils/markdownPages.tsx +++ b/src/@utils/markdownPages.tsx @@ -6,8 +6,8 @@ import matter from 'gray-matter' // Next.js specifics to be used in getStaticProps / getStaticPaths // to automatically generate pages from Markdown files in `src/pages/[slug].tsx`. // -const pagesDirectory = join(process.cwd(), 'content', 'pages') - +// const pagesDirectory = join(process.cwd(), 'content', 'pages') +const pagesDirectory = './content/pages' export interface PageData { slug: string frontmatter: { [key: string]: any } diff --git a/src/@utils/order.ts b/src/@utils/order.ts new file mode 100644 index 000000000..3c82f4322 --- /dev/null +++ b/src/@utils/order.ts @@ -0,0 +1,96 @@ +import { + approve, + Datatoken, + FreOrderParams, + OrderParams, + ProviderInstance +} from '@oceanprotocol/lib' +import { AssetExtended } from 'src/@types/AssetExtended' +import Web3 from 'web3' +import { getOceanConfig } from './ocean' +import { TransactionReceipt } from 'web3-eth' +import { getSiteMetadata } from './siteConfig' +import { OrderPriceAndFees } from 'src/@types/Price' + +/** + * For pool you need to buy the datatoken beforehand, this always assumes you want to order the first service + * @param web3 + * @param asset + * @param accountId + * @returns {TransactionReceipt} receipt of the order + */ +export async function order( + web3: Web3, + asset: AssetExtended, + orderPriceAndFees: OrderPriceAndFees, + accountId: string +): Promise { + const datatoken = new Datatoken(web3) + const config = getOceanConfig(asset.chainId) + const { appConfig } = getSiteMetadata() + + const initializeData = await ProviderInstance.initialize( + asset.id, + asset.services[0].id, + 0, + accountId, + asset.services[0].serviceEndpoint + ) + + const orderParams = { + consumer: accountId, + serviceIndex: 0, + _providerFees: initializeData.providerFee + } as OrderParams + + // TODO: we need to approve provider fee + switch (asset.accessDetails?.type) { + case 'fixed': { + // this assumes all fees are in ocean + const txApprove = await approve( + web3, + accountId, + asset.accessDetails.baseToken.address, + asset.accessDetails.datatoken.address, + orderPriceAndFees.price, + false + ) + + const freParams = { + exchangeContract: config.fixedRateExchangeAddress, + exchangeId: asset.accessDetails.addressOrId, + maxBaseTokenAmount: orderPriceAndFees.price, + swapMarketFee: appConfig.consumeMarketFixedSwapFee, + marketFeeAddress: appConfig.marketFeeAddress + } as FreOrderParams + const tx = await datatoken.buyFromFreAndOrder( + asset.accessDetails.datatoken.address, + accountId, + orderParams, + freParams + ) + + return tx + } + case 'dynamic': { + const tx = await datatoken.startOrder( + asset.accessDetails.datatoken.address, + accountId, + accountId, + 0, + initializeData.providerFee + ) + return tx + } + + case 'free': { + const tx = await datatoken.buyFromDispenserAndOrder( + asset.services[0].datatokenAddress, + accountId, + orderParams, + config.dispenserAddress + ) + return tx + } + } +} diff --git a/src/@utils/pool.ts b/src/@utils/pool.ts new file mode 100644 index 000000000..6ee8c26d2 --- /dev/null +++ b/src/@utils/pool.ts @@ -0,0 +1,75 @@ +import { approve, Pool } from '@oceanprotocol/lib' +import Web3 from 'web3' +import { getSiteMetadata } from './siteConfig' +import { getDummyWeb3 } from './web3' +import { TransactionReceipt } from 'web3-eth' +import Decimal from 'decimal.js' +import { AccessDetails } from 'src/@types/Price' + +/** + * This is used to calculate the price to buy one datatoken from a pool, that is different from spot price. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created + * @param {AccessDetails} accessDetails + * @param {Web3?} [web3] + * @param {number?} [chainId] + * @return {Promise} + */ +export async function calculateBuyPrice( + accessDetails: AccessDetails, + chainId?: number, + web3?: Web3 +): Promise { + if (!web3 && !chainId) + throw new Error("web3 and chainId can't be undefined at the same time!") + + if (!web3) { + web3 = await getDummyWeb3(chainId) + } + + const pool = new Pool(web3) + const { appConfig } = getSiteMetadata() + const estimatedPrice = await pool.getAmountInExactOut( + accessDetails.addressOrId, + accessDetails.baseToken.address, + accessDetails.datatoken.address, + '1', + appConfig.consumeMarketPoolSwapFee + ) + + return estimatedPrice +} + +export async function buyDtFromPool( + accessDetails: AccessDetails, + accountId: string, + web3: Web3 +): Promise { + const pool = new Pool(web3) + const { appConfig } = getSiteMetadata() + // we need to calculate the actual price to buy one datatoken + const dtPrice = await calculateBuyPrice(accessDetails, null, web3) + const approveTx = await approve( + web3, + accountId, + accessDetails.baseToken.address, + accessDetails.addressOrId, + dtPrice, + false + ) + const result = await pool.swapExactAmountOut( + accountId, + accessDetails.addressOrId, + { + marketFeeAddress: appConfig.marketFeeAddress, + tokenIn: accessDetails.baseToken.address, + tokenOut: accessDetails.datatoken.address + }, + { + // this is just to be safe + maxAmountIn: new Decimal(dtPrice).mul(10).toString(), + swapMarketFee: appConfig.consumeMarketPoolSwapFee, + tokenAmountOut: '1' + } + ) + + return result +} diff --git a/src/@utils/provider.ts b/src/@utils/provider.ts index 057194a3f..5ba257d25 100644 --- a/src/@utils/provider.ts +++ b/src/@utils/provider.ts @@ -1,10 +1,13 @@ import { + downloadFileBrowser, FileMetadata, LoggerInstance, ProviderInstance } from '@oceanprotocol/lib' +import { AssetExtended } from 'src/@types/AssetExtended' +import Web3 from 'web3' -// TODO: Why do we have these functions ?!?!?! +// TODO: Why do we have these one line functions ?!?!?! export async function getEncryptedFiles( files: FileMetadata[], providerUrl: string @@ -46,3 +49,21 @@ export async function getFileUrlInfo( LoggerInstance.error(error.message) } } + +export async function downloadFile( + web3: Web3, + asset: AssetExtended, + accountId: string, + validOrderTx?: string +) { + const downloadUrl = await ProviderInstance.getDownloadUrl( + asset.id, + accountId, + asset.services[0].id, + 0, + validOrderTx || asset.accessDetails.validOrderTx, + asset.services[0].serviceEndpoint, + web3 + ) + await downloadFileBrowser(downloadUrl) +} diff --git a/src/@utils/siteConfig.ts b/src/@utils/siteConfig.ts new file mode 100644 index 000000000..f0de32f54 --- /dev/null +++ b/src/@utils/siteConfig.ts @@ -0,0 +1,12 @@ +import { UseSiteMetadata } from '@hooks/useSiteMetadata/types' +import siteContent from '../../content/site.json' +import appConfig from '../../app.config' + +export function getSiteMetadata(): UseSiteMetadata { + const siteMeta: UseSiteMetadata = { + ...siteContent, + appConfig + } + + return siteMeta +} diff --git a/src/@utils/web3.ts b/src/@utils/web3.ts index b960d8ea3..0cc429b91 100644 --- a/src/@utils/web3.ts +++ b/src/@utils/web3.ts @@ -1,5 +1,6 @@ import { getNetworkDisplayName } from '@hooks/useNetworkMetadata' import { LoggerInstance } from '@oceanprotocol/lib' +import Web3 from 'web3' import { getOceanConfig } from './ocean' export function accountTruncate(account: string): string { @@ -8,6 +9,15 @@ export function accountTruncate(account: string): string { const truncated = account.replace(middle, '…') return truncated } +/** + * returns a dummy web3 instance, only usable to get info from the chain + * @param chainId + * @returns Web3 instance + */ +export async function getDummyWeb3(chainId: number): Promise { + const config = getOceanConfig(chainId) + return new Web3(config.nodeUri) +} export async function addCustomNetwork( web3Provider: any, diff --git a/src/components/@shared/AssetList/index.tsx b/src/components/@shared/AssetList/index.tsx index 9ce17b46d..b9e2b3c15 100644 --- a/src/components/@shared/AssetList/index.tsx +++ b/src/components/@shared/AssetList/index.tsx @@ -10,6 +10,7 @@ import { useIsMounted } from '@hooks/useIsMounted' import { AssetExtended } from 'src/@types/AssetExtended' import { Asset } from '@oceanprotocol/lib' import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing' +import { useWeb3 } from '@context/Web3' const cx = classNames.bind(styles) @@ -43,6 +44,7 @@ export default function AssetList({ noPublisher }: AssetListProps): ReactElement { const { chainIds } = useUserPreferences() + const { accountId } = useWeb3() const [assetsWithPrices, setAssetsWithPrices] = useState() const [loading, setLoading] = useState(isLoading) const isMounted = useIsMounted() @@ -51,14 +53,16 @@ export default function AssetList({ if (!assets) return setAssetsWithPrices(assets as AssetExtended[]) setLoading(false) - async function fetchPrices() { - const assetsWithPrices = await getAccessDetailsForAssets(assets) + const assetsWithPrices = await getAccessDetailsForAssets( + assets, + accountId || '' + ) if (!isMounted()) return setAssetsWithPrices([...assetsWithPrices]) } fetchPrices() - }, [assets, isMounted]) + }, [assets, isMounted, accountId]) // // This changes the page field inside the query function handlePageChange(selected: number) { diff --git a/src/components/@shared/ButtonBuy/index.tsx b/src/components/@shared/ButtonBuy/index.tsx index 8033b9f5b..e2db0e640 100644 --- a/src/components/@shared/ButtonBuy/index.tsx +++ b/src/components/@shared/ButtonBuy/index.tsx @@ -31,6 +31,8 @@ interface ButtonBuyProps { algorithmConsumableStatus?: number } +// TODO: we need to take a look at these messages + function getConsumeHelpText( dtBalance: string, dtSymbol: string, diff --git a/src/components/@shared/Price/index.tsx b/src/components/@shared/Price/index.tsx index ca2cfa6bd..9384929dd 100644 --- a/src/components/@shared/Price/index.tsx +++ b/src/components/@shared/Price/index.tsx @@ -3,21 +3,24 @@ import styles from './index.module.css' import Loader from '../atoms/Loader' import Tooltip from '../atoms/Tooltip' import PriceUnit from './PriceUnit' +import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price' export default function Price({ accessDetails, + orderPriceAndFees, className, small, conversion }: { accessDetails: AccessDetails + orderPriceAndFees?: OrderPriceAndFees className?: string small?: boolean conversion?: boolean }): ReactElement { return accessDetails?.price || accessDetails?.type === 'free' ? ( { - if (!ddo) return + if (!asset) return async function getDatasetsAllowedForCompute() { - const isCompute = Boolean(getServiceByName(ddo, 'compute')) + const isCompute = Boolean(getServiceByName(asset, 'compute')) const datasetComputeService = getServiceByName( - ddo, + asset, isCompute ? 'compute' : 'access' ) const datasets = await getAlgorithmDatasetsForCompute( algorithmDid, datasetComputeService?.serviceEndpoint, - ddo?.chainId, + asset?.chainId, newCancelToken() ) setDatasetsForCompute(datasets) } - ddo.metadata.type === 'algorithm' && getDatasetsAllowedForCompute() - }, [ddo?.metadata?.type]) + asset.metadata.type === 'algorithm' && getDatasetsAllowedForCompute() + }, [asset?.metadata?.type]) return (
diff --git a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx index 5c1bcf0e9..7cb5a84fd 100644 --- a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx +++ b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx @@ -10,6 +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' export default function FormStartCompute({ algorithms, @@ -106,7 +107,7 @@ export default function FormStartCompute({ ? 0 : Number(algorithmConsumeDetails.price) - setTotalPrice(priceDataset + priceAlgo) + setTotalPrice((priceDataset + priceAlgo).toString()) }, [ asset?.accessDetails, algorithmConsumeDetails, @@ -145,7 +146,7 @@ export default function FormStartCompute({ hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset} algorithmConsumeDetails={algorithmConsumeDetails} symbol={oceanSymbol} - totalPrice={totalPrice} + totalPrice={Number.parseFloat(totalPrice)} /> () @@ -181,13 +180,13 @@ export default function Compute({ useEffect(() => { if (!algorithmConsumeDetails) return - setIsAlgoConsumablePrice(algorithmConsumeDetails.isConsumable) + setIsAlgoConsumablePrice(algorithmConsumeDetails.isPurchasable) }, [algorithmConsumeDetails]) useEffect(() => { if (!accessDetails) return - setIsConsumablePrice(accessDetails.isConsumable) + setIsConsumablePrice(accessDetails.isPurchasable) }, [accessDetails]) // useEffect(() => { @@ -234,10 +233,10 @@ export default function Compute({ // Output errors in toast UI useEffect(() => { - const newError = error || pricingError + const newError = error if (!newError) return toast.error(newError) - }, [error, pricingError]) + }, [error]) // async function startJob(algorithmId: string) { // try { @@ -400,7 +399,7 @@ export default function Compute({ text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!" state="info" /> - + ) : ( () - const { isInPurgatory, isAssetNetwork } = useAsset() - const { buyDT, pricingStepText, pricingError, pricingIsLoading } = - usePricing() - const { consumeStepText, consume, consumeError, isLoading } = useConsume() - const [isDisabled, setIsDisabled] = useState(true) - const [hasDatatoken, setHasDatatoken] = useState(false) - const [isConsumablePrice, setIsConsumablePrice] = useState(true) - const [assetTimeout, setAssetTimeout] = useState('') - const isMounted = useIsMounted() - - useEffect(() => { - if (!ddo) return - - const { timeout } = ddo.services[0] - setAssetTimeout(`${timeout}`) - }, [ddo]) - - useEffect(() => { - if (!accessDetails) return - - setIsConsumablePrice(accessDetails.isConsumable) - setHasPreviousOrder(accessDetails.owned) - setPreviousOrderId(accessDetails.validOrderTx) - }, [accessDetails]) - - useEffect(() => { - setHasDatatoken(Number(dtBalance) >= 1) - }, [dtBalance]) - - useEffect(() => { - if (!accountId) return - setIsDisabled( - !isConsumable || - ((!isBalanceSufficient || - !isAssetNetwork || - typeof consumeStepText !== 'undefined' || - pricingIsLoading || - !isConsumablePrice) && - !hasPreviousOrder && - !hasDatatoken) - ) - }, [ - hasPreviousOrder, - isBalanceSufficient, - isAssetNetwork, - consumeStepText, - pricingIsLoading, - isConsumablePrice, - hasDatatoken, - isConsumable, - accountId - ]) - - async function handleConsume() { - // if (!hasPreviousOrder && !hasDatatoken) { - // const tx = await buyDT('1', price, ddo) - // if (tx === undefined) return - // } - const error = await consume( - ddo.id, - ddo.services[0].datatokenAddress, - 'access', - appConfig.marketFeeAddress, - previousOrderId - ) - error || setHasPreviousOrder(true) - } - - // Output errors in UI - useEffect(() => { - consumeError && toast.error(consumeError) - }, [consumeError]) - - useEffect(() => { - pricingError && toast.error(pricingError) - }, [pricingError]) - - const PurchaseButton = () => ( - - ) - - return ( - - ) -} diff --git a/src/components/Asset/AssetActions/Consume.module.css b/src/components/Asset/AssetActions/Download.module.css similarity index 100% rename from src/components/Asset/AssetActions/Consume.module.css rename to src/components/Asset/AssetActions/Download.module.css diff --git a/src/components/Asset/AssetActions/Download.tsx b/src/components/Asset/AssetActions/Download.tsx new file mode 100644 index 000000000..3f43f48f9 --- /dev/null +++ b/src/components/Asset/AssetActions/Download.tsx @@ -0,0 +1,175 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import FileIcon from '@shared/FileIcon' +import Price from '@shared/Price' +import { useAsset } from '@context/Asset' +import { useWeb3 } from '@context/Web3' +import ButtonBuy from '@shared/ButtonBuy' +import { secondsToString } from '@utils/ddo' +import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute' +import styles from './Download.module.css' +import { FileMetadata, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib' +import { order } from '@utils/order' +import { AssetExtended } from 'src/@types/AssetExtended' +import { buyDtFromPool, calculateBuyPrice } from '@utils/pool' +import { downloadFile } from '@utils/provider' +import { getOrderFeedback } from '@utils/feedback' +import { getOrderPriceAndFees } from '@utils/accessDetailsAndPricing' +import { OrderPriceAndFees } from 'src/@types/Price' +import { toast } from 'react-toastify' + +export default function Download({ + asset, + file, + isBalanceSufficient, + dtBalance, + fileIsLoading, + consumableFeedback +}: { + asset: AssetExtended + file: FileMetadata + isBalanceSufficient: boolean + dtBalance: string + fileIsLoading?: boolean + consumableFeedback?: string +}): ReactElement { + const { accountId, web3 } = useWeb3() + const { isInPurgatory, isAssetNetwork } = useAsset() + const [isDisabled, setIsDisabled] = useState(true) + const [hasDatatoken, setHasDatatoken] = useState(false) + const [statusText, setStatusText] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isOwned, setIsOwned] = useState(false) + const [validOrderTx, setValidOrderTx] = useState('') + const [orderPriceAndFees, setOrderPriceAndFees] = + useState() + useEffect(() => { + if (!asset?.accessDetails) return + + setIsOwned(asset?.accessDetails?.isOwned) + setValidOrderTx(asset?.accessDetails?.validOrderTx) + // get full price and fees + async function init() { + if (asset?.accessDetails?.addressOrId === ZERO_ADDRESS) return + setIsLoading(true) + setStatusText('Calculating price including fees.') + const orderPriceAndFees = await getOrderPriceAndFees(asset, ZERO_ADDRESS) + setOrderPriceAndFees(orderPriceAndFees) + + setIsLoading(false) + } + + init() + }, [asset, accountId]) + + useEffect(() => { + setHasDatatoken(Number(dtBalance) >= 1) + }, [dtBalance]) + + useEffect(() => { + if (!accountId || !asset?.accessDetails) return + setIsDisabled( + !asset?.accessDetails.isPurchasable || + ((!isBalanceSufficient || !isAssetNetwork) && !isOwned && !hasDatatoken) + ) + }, [ + asset?.accessDetails, + isBalanceSufficient, + isAssetNetwork, + hasDatatoken, + accountId, + isOwned + ]) + + async function handleOrderOrDownload() { + setIsLoading(true) + if (isOwned) { + setStatusText( + getOrderFeedback( + asset.accessDetails?.baseToken?.symbol, + asset.accessDetails?.datatoken?.symbol + )[3] + ) + await downloadFile(web3, asset, accountId, validOrderTx) + } else { + try { + if (!hasDatatoken && asset.accessDetails.type === 'dynamic') { + setStatusText( + getOrderFeedback( + asset.accessDetails.baseToken?.symbol, + asset.accessDetails.datatoken?.symbol + )[0] + ) + + const tx = await buyDtFromPool(asset.accessDetails, accountId, web3) + + if (!tx) { + toast.error('Failed to buy datatoken from pool!') + setIsLoading(false) + return + } + } + setStatusText( + getOrderFeedback( + asset.accessDetails.baseToken?.symbol, + asset.accessDetails.datatoken?.symbol + )[asset.accessDetails?.type === 'fixed' ? 2 : 1] + ) + const orderTx = await order(web3, asset, orderPriceAndFees, accountId) + + setIsOwned(true) + setValidOrderTx(orderTx.transactionHash) + } catch (ex) { + LoggerInstance.log(ex.message) + setIsLoading(false) + } + } + + setIsLoading(false) + } + + const PurchaseButton = () => ( + + ) + + return ( + + ) +} diff --git a/src/components/Asset/AssetActions/index.tsx b/src/components/Asset/AssetActions/index.tsx index 98433fd11..4687c7f8b 100644 --- a/src/components/Asset/AssetActions/index.tsx +++ b/src/components/Asset/AssetActions/index.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useState, useEffect } from 'react' import Compute from './Compute' -import Consume from './Consume' +import Consume from './Download' import { FileMetadata, LoggerInstance, Datatoken } from '@oceanprotocol/lib' import Tabs, { TabsItem } from '@shared/atoms/Tabs' import { compareAsBN } from '@utils/numbers' @@ -141,13 +141,11 @@ export default function AssetActions({ /> ) : ( ) diff --git a/src/components/Publish/Preview/index.tsx b/src/components/Publish/Preview/index.tsx index 2db5e6fd3..4bf74060f 100644 --- a/src/components/Publish/Preview/index.tsx +++ b/src/components/Publish/Preview/index.tsx @@ -4,37 +4,36 @@ import { FormPublishData } from '../_types' import { useFormikContext } from 'formik' import AssetContent from 'src/components/Asset/AssetContent' import { transformPublishFormToDdo } from '../_utils' -import { Asset } from '@oceanprotocol/lib' +import { AssetExtended } from 'src/@types/AssetExtended' +import { ZERO_ADDRESS } from '@oceanprotocol/lib' export default function Preview(): ReactElement { - const [asset, setAsset] = useState() - const [accessDetails, setAccessDetails] = useState() + const [asset, setAsset] = useState() const { values } = useFormikContext() useEffect(() => { async function makeDdo() { - const asset = await transformPublishFormToDdo(values) - setAsset(asset as Asset) - + const asset = (await transformPublishFormToDdo(values)) as AssetExtended // dummy BestPrice to trigger certain AssetActions - const accessDetails: AccessDetails = { + asset.accessDetails = { type: values.pricing.type, - addressOrId: '0x...', + addressOrId: ZERO_ADDRESS, price: values.pricing.price, baseToken: { - address: '0x..', - name: '', - symbol: '' + address: ZERO_ADDRESS, + name: 'OCEAN', + symbol: 'OCEAN' }, datatoken: { - address: '0x..', + address: ZERO_ADDRESS, name: '', symbol: '' }, - owned: false, + isPurchasable: true, + isOwned: false, validOrderTx: '' } - setAccessDetails(accessDetails) + setAsset(asset) } makeDdo() }, [values]) @@ -44,7 +43,7 @@ export default function Preview(): ReactElement {

Preview

{values.metadata.name}

- + {asset && }
) } diff --git a/src/components/Publish/Pricing/index.tsx b/src/components/Publish/Pricing/index.tsx index bf41ab357..95ee3a793 100644 --- a/src/components/Publish/Pricing/index.tsx +++ b/src/components/Publish/Pricing/index.tsx @@ -42,7 +42,14 @@ export default function PricingFields(): ReactElement { : 0 setFieldValue('pricing.amountDataToken', amountDataToken) - }, [price, amountOcean, weightOnOcean, weightOnDataToken, type]) + }, [ + price, + amountOcean, + weightOnOcean, + weightOnDataToken, + type, + setFieldValue + ]) const tabs = [ appConfig.allowFixedPricing === 'true' diff --git a/src/components/Publish/_types.ts b/src/components/Publish/_types.ts index 2e9267e71..a2acb06d2 100644 --- a/src/components/Publish/_types.ts +++ b/src/components/Publish/_types.ts @@ -1,6 +1,7 @@ import { ServiceComputeOptions } from '@oceanprotocol/lib' import { NftMetadata } from '@utils/nft' import { ReactElement } from 'react' +import { PriceOptions } from 'src/@types/Price' interface FileMetadata { url: string diff --git a/src/components/Publish/_utils.ts b/src/components/Publish/_utils.ts index d1eeb307e..925dbbbbf 100644 --- a/src/components/Publish/_utils.ts +++ b/src/components/Publish/_utils.ts @@ -18,6 +18,8 @@ import { import { mapTimeoutStringToSeconds } from '@utils/ddo' import { generateNftCreateData } from '@utils/nft' import { getEncryptedFiles } from '@utils/provider' +import { getSiteMetadata } from '@utils/siteConfig' +import Decimal from 'decimal.js' import slugify from 'slugify' import Web3 from 'web3' import { @@ -191,7 +193,6 @@ export async function transformPublishFormToDdo( export async function createTokensAndPricing( values: FormPublishData, accountId: string, - marketFeeAddress: string, config: Config, nftFactory: NftFactory, web3: Web3 @@ -199,19 +200,17 @@ export async function createTokensAndPricing( const nftCreateData: NftCreateData = generateNftCreateData( values.metadata.nft ) - + const { appConfig } = getSiteMetadata() LoggerInstance.log('[publish] Creating NFT with metadata', nftCreateData) // TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point - // fee is default 0 for now - // TODO: templateIndex is hardcoded for now but this is incorrect, in the future it should be something like 1 for pools, and 2 for fre and free const ercParams: Erc20CreateParams = { templateIndex: values.pricing.type === 'dynamic' ? 1 : 2, minter: accountId, feeManager: accountId, - mpFeeAddress: marketFeeAddress, + mpFeeAddress: appConfig.marketFeeAddress, feeToken: config.oceanTokenAddress, - feeAmount: `0`, + feeAmount: appConfig.publisherMarketOrderFee, cap: '1000', name: values.services[0].dataTokenOptions.name, symbol: values.services[0].dataTokenOptions.symbol @@ -226,21 +225,22 @@ export async function createTokensAndPricing( case 'dynamic': { // no vesting in market by default, maybe at a later time , vestingAmount and vestedBlocks are hardcoded // we use only ocean as basetoken - // TODO: discuss swapFeeLiquidityProvider, swapFeeMarketPlaceRunner + // swapFeeLiquidityProvider is the swap fee of the liquidity providers + // swapFeeMarketRunner is the swap fee of the market where the swap occurs const poolParams: PoolCreationParams = { ssContract: config.sideStakingAddress, baseTokenAddress: config.oceanTokenAddress, baseTokenSender: config.erc721FactoryAddress, publisherAddress: accountId, - marketFeeCollector: marketFeeAddress, + marketFeeCollector: appConfig.marketFeeAddress, poolTemplateAddress: config.poolTemplateAddress, - rate: values.pricing.price.toString(), + rate: new Decimal(1).div(values.pricing.price).toString(), baseTokenDecimals: 18, vestingAmount: '0', vestedBlocks: 2726000, initialBaseTokenLiquidity: values.pricing.amountOcean.toString(), - swapFeeLiquidityProvider: '0.1', - swapFeeMarketRunner: '0' + swapFeeLiquidityProvider: (values.pricing.swapFee / 100).toString(), + swapFeeMarketRunner: appConfig.publisherMarketPoolSwapFee } LoggerInstance.log( @@ -249,16 +249,15 @@ export async function createTokensAndPricing( ) // the spender in this case is the erc721Factory because we are delegating - const pool = new Pool(web3) const txApprove = await approve( web3, accountId, config.oceanTokenAddress, config.erc721FactoryAddress, - '200', + values.pricing.amountOcean.toString(), false ) - LoggerInstance.log('[publish] pool.approve tx', txApprove) + LoggerInstance.log('[publish] pool.approve tx', txApprove, nftFactory) const result = await nftFactory.createNftErc20WithPool( accountId, @@ -279,11 +278,11 @@ export async function createTokensAndPricing( fixedRateAddress: config.fixedRateExchangeAddress, baseTokenAddress: config.oceanTokenAddress, owner: accountId, - marketFeeCollector: marketFeeAddress, + marketFeeCollector: appConfig.marketFeeAddress, baseTokenDecimals: 18, datatokenDecimals: 18, fixedRate: values.pricing.price.toString(), - marketFee: '0', + marketFee: appConfig.publisherMarketFixedSwapFee, withMint: true } diff --git a/src/components/Publish/index.tsx b/src/components/Publish/index.tsx index 7687fb613..368627f5f 100644 --- a/src/components/Publish/index.tsx +++ b/src/components/Publish/index.tsx @@ -21,7 +21,6 @@ import { LoggerInstance, DDO } from '@oceanprotocol/lib' -import { useSiteMetadata } from '@hooks/useSiteMetadata' import { getOceanConfig } from '@utils/ocean' import { validationSchema } from './_validation' import { useAbortController } from '@hooks/useAbortController' @@ -38,7 +37,6 @@ export default function PublishPage({ const { accountId, web3, chainId } = useWeb3() const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId) const scrollToRef = useRef() - const { appConfig } = useSiteMetadata() const nftFactory = useNftFactory() const newAbortController = useAbortController() @@ -75,7 +73,6 @@ export default function PublishPage({ await createTokensAndPricing( values, accountId, - appConfig.marketFeeAddress, config, nftFactory, web3