diff --git a/content/price.json b/content/price.json index f9e6450f3..b9b7f4c51 100644 --- a/content/price.json +++ b/content/price.json @@ -2,7 +2,7 @@ "create": { "fixed": { "title": "Fixed", - "info": "Set your price for accessing this data set. The datatoken for this data set will be worth the entered amount of OCEAN.", + "info": "Set your price for accessing this data set. The datatoken for this data set will be worth the entered amount of the selected base token.", "tooltips": { "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.", "marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community." diff --git a/src/@context/MarketMetadata/_queries.ts b/src/@context/MarketMetadata/_queries.ts index ba13dd606..651980dea 100644 --- a/src/@context/MarketMetadata/_queries.ts +++ b/src/@context/MarketMetadata/_queries.ts @@ -6,7 +6,10 @@ export const opcQuery = gql` swapOceanFee swapNonOceanFee approvedTokens { - id + address: id + symbol + name + decimals } id } diff --git a/src/@context/MarketMetadata/index.tsx b/src/@context/MarketMetadata/index.tsx index a9e2145b9..1117a269b 100644 --- a/src/@context/MarketMetadata/index.tsx +++ b/src/@context/MarketMetadata/index.tsx @@ -36,7 +36,9 @@ function MarketMetadataProvider({ opcData.push({ chainId: appConfig.chainIdsSupported[i], - approvedTokens: response.data?.opc.approvedTokens?.map((x) => x.id), + approvedTokens: response.data?.opc.approvedTokens.map( + (token) => token.address + ), swapApprovedFee: response.data?.opc.swapOceanFee, swapNotApprovedFee: response.data?.opc.swapNonOceanFee } as OpcFee) diff --git a/src/@context/Web3.tsx b/src/@context/Web3.tsx index f00aa8836..cc023c047 100644 --- a/src/@context/Web3.tsx +++ b/src/@context/Web3.tsx @@ -14,7 +14,6 @@ import WalletConnectProvider from '@walletconnect/web3-provider' import { LoggerInstance } from '@oceanprotocol/lib' import { isBrowser } from '@utils/index' import { getEnsName } from '@utils/ens' -import { getOceanBalance } from '@utils/ocean' import useNetworkMetadata, { getNetworkDataById, getNetworkDisplayName, @@ -22,9 +21,12 @@ import useNetworkMetadata, { NetworkType } from '../@hooks/useNetworkMetadata' import { useMarketMetadata } from './MarketMetadata' +import { getTokenBalance } from '@utils/web3' +import { getOpcsApprovedTokens } from '@utils/subgraph' interface Web3ProviderValue { web3: Web3 + // eslint-disable-next-line @typescript-eslint/no-explicit-any web3Provider: any web3Modal: Web3Modal web3ProviderInfo: IProviderInfo @@ -39,6 +41,7 @@ interface Web3ProviderValue { isTestnet: boolean web3Loading: boolean isSupportedOceanNetwork: boolean + approvedBaseTokens: TokenInfo[] connect: () => Promise logout: () => Promise } @@ -65,16 +68,6 @@ const providerOptions = isBrowser } } } - // torus: { - // package: require('@toruslabs/torus-embed') - // // options: { - // // networkParams: { - // // host: oceanConfig.url, // optional - // // chainId: 1337, // optional - // // networkId: 1337 // optional - // // } - // // } - // } } : {} @@ -93,7 +86,9 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { const { appConfig } = useMarketMetadata() const [web3, setWeb3] = useState() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [web3Provider, setWeb3Provider] = useState() + const [web3Modal, setWeb3Modal] = useState() const [web3ProviderInfo, setWeb3ProviderInfo] = useState() const [networkId, setNetworkId] = useState() @@ -106,10 +101,10 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { const [accountEns, setAccountEns] = useState() const [web3Loading, setWeb3Loading] = useState(true) const [balance, setBalance] = useState({ - eth: '0', - ocean: '0' + eth: '0' }) const [isSupportedOceanNetwork, setIsSupportedOceanNetwork] = useState(true) + const [approvedBaseTokens, setApprovedBaseTokens] = useState() // ----------------------------------- // Helper: connect to web3 @@ -148,6 +143,19 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { } }, [web3Modal]) + // ----------------------------------- + // Helper: Get approved base tokens list + // ----------------------------------- + const getApprovedBaseTokens = useCallback(async (chainId: number) => { + try { + const approvedTokensList = await getOpcsApprovedTokens(chainId) + setApprovedBaseTokens(approvedTokensList) + LoggerInstance.log('[web3] Approved baseTokens', approvedTokensList) + } catch (error) { + LoggerInstance.error('[web3] Error: ', error.message) + } + }, []) + // ----------------------------------- // Helper: Get user balance // ----------------------------------- @@ -155,16 +163,30 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { if (!accountId || !networkId || !web3) return try { - const balance = { - eth: web3.utils.fromWei(await web3.eth.getBalance(accountId, 'latest')), - ocean: await getOceanBalance(accountId, networkId, web3) + const balance: UserBalance = { + eth: web3.utils.fromWei(await web3.eth.getBalance(accountId, 'latest')) } + if (approvedBaseTokens?.length > 0) { + await Promise.all( + approvedBaseTokens.map(async (token) => { + const { address, decimals, symbol } = token + const tokenBalance = await getTokenBalance( + accountId, + decimals, + address, + web3 + ) + balance[symbol.toLocaleLowerCase()] = tokenBalance + }) + ) + } + setBalance(balance) LoggerInstance.log('[web3] Balance: ', balance) } catch (error) { LoggerInstance.error('[web3] Error: ', error.message) } - }, [accountId, networkId, web3]) + }, [accountId, approvedBaseTokens, networkId, web3]) // ----------------------------------- // Helper: Get user ENS name @@ -227,6 +249,14 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { connectCached() }, [connect, web3Modal]) + // ----------------------------------- + // Get and set approved base tokens list + // ----------------------------------- + useEffect(() => { + if (web3Loading) return + getApprovedBaseTokens(chainId || 1) + }, [chainId, getApprovedBaseTokens, web3Loading]) + // ----------------------------------- // Get and set user balance // ----------------------------------- @@ -303,9 +333,12 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { // Logout helper // ----------------------------------- async function logout() { + /* eslint-disable @typescript-eslint/no-explicit-any */ if (web3 && web3.currentProvider && (web3.currentProvider as any).close) { await (web3.currentProvider as any).close() } + /* eslint-enable @typescript-eslint/no-explicit-any */ + await web3Modal.clearCachedProvider() } // ----------------------------------- @@ -354,6 +387,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { web3Provider.removeListener('networkChanged', handleNetworkChanged) web3Provider.removeListener('accountsChanged', handleAccountsChanged) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [web3Provider, web3]) return ( @@ -374,6 +408,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement { isTestnet, web3Loading, isSupportedOceanNetwork, + approvedBaseTokens, connect, logout }} diff --git a/src/@types/Price.d.ts b/src/@types/Price.d.ts index ec9754f5a..4f47b8578 100644 --- a/src/@types/Price.d.ts +++ b/src/@types/Price.d.ts @@ -50,6 +50,7 @@ declare global { interface PricePublishOptions { price: number + baseToken: TokenInfo type: 'fixed' | 'free' freeAgreement: boolean } diff --git a/src/@types/TokenBalance.d.ts b/src/@types/TokenBalance.d.ts index 70ff69bdd..9d241c52d 100644 --- a/src/@types/TokenBalance.d.ts +++ b/src/@types/TokenBalance.d.ts @@ -1,4 +1,4 @@ interface UserBalance { eth: string - ocean: string + [key: string]: string } diff --git a/src/@utils/accessDetailsAndPricing.ts b/src/@utils/accessDetailsAndPricing.ts index f7e8a2175..20c861ae1 100644 --- a/src/@utils/accessDetailsAndPricing.ts +++ b/src/@utils/accessDetailsAndPricing.ts @@ -66,6 +66,7 @@ const tokensPriceQuery = gql` symbol name address + decimals } datatoken { symbol @@ -122,6 +123,7 @@ const tokenPriceQuery = gql` symbol name address + decimals } datatoken { symbol @@ -187,7 +189,8 @@ function getAccessDetailsFromTokenPrice( accessDetails.baseToken = { address: fixed.baseToken.address, name: fixed.baseToken.name, - symbol: fixed.baseToken.symbol + symbol: fixed.baseToken.symbol, + decimals: fixed.baseToken.decimals } accessDetails.datatoken = { address: fixed.datatoken.address, diff --git a/src/@utils/ocean.ts b/src/@utils/ocean.ts index e19cb25ce..3d903e3c6 100644 --- a/src/@utils/ocean.ts +++ b/src/@utils/ocean.ts @@ -1,7 +1,5 @@ -import { ConfigHelper, LoggerInstance, Config } from '@oceanprotocol/lib' +import { ConfigHelper, Config } from '@oceanprotocol/lib' // import contractAddresses from '@oceanprotocol/contracts/artifacts/address.json' -import { AbiItem } from 'web3-utils/types' -import Web3 from 'web3' export function getOceanConfig(network: string | number): Config { const config = new ConfigHelper().getConfig( @@ -30,45 +28,3 @@ export function getDevelopmentConfig(): Config { subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com' } as Config } - -export async function getOceanBalance( - accountId: string, - networkId: number, - web3: Web3 -): Promise { - const minABI = [ - { - constant: true, - inputs: [ - { - name: '_owner', - type: 'address' - } - ], - name: 'balanceOf', - outputs: [ - { - name: 'balance', - type: 'uint256' - } - ], - payable: false, - stateMutability: 'view', - type: 'function' - } - ] as AbiItem[] - - try { - const token = new web3.eth.Contract( - minABI, - getOceanConfig(networkId).oceanTokenAddress, - { from: accountId } - ) - const result = web3.utils.fromWei( - await token.methods.balanceOf(accountId).call() - ) - return result - } catch (e) { - LoggerInstance.error(`ERROR: Failed to get the balance: ${e.message}`) - } -} diff --git a/src/@utils/order.ts b/src/@utils/order.ts index 2c4070eba..a0bfde14b 100644 --- a/src/@utils/order.ts +++ b/src/@utils/order.ts @@ -57,7 +57,7 @@ export async function order( _consumeMarketFee: { consumeMarketFeeAddress: marketFeeAddress, consumeMarketFeeAmount: consumeMarketOrderFee, - consumeMarketFeeToken: config.oceanTokenAddress + consumeMarketFeeToken: asset.accessDetails.baseToken.address } } as OrderParams diff --git a/src/@utils/subgraph.ts b/src/@utils/subgraph.ts index dd7a3bff1..41ca897bf 100644 --- a/src/@utils/subgraph.ts +++ b/src/@utils/subgraph.ts @@ -66,6 +66,19 @@ const OpcFeesQuery = gql` } ` +const OpcsApprovedTokensQuery = gql` + query OpcsApprovedTokensQuery { + opcs { + approvedTokens { + address: id + symbol + name + decimals + } + } + } +` + export function getSubgraphUri(chainId: number): string { const config = getOceanConfig(chainId) return config.subgraphUri @@ -221,3 +234,17 @@ export async function getTopAssetsPublishers( return publishers.slice(0, nrItems) } + +export async function getOpcsApprovedTokens( + chainId: number +): Promise { + const context = getQueryContext(chainId) + + try { + const response = await fetchData(OpcsApprovedTokensQuery, null, context) + return response?.data?.opcs[0].approvedTokens + } catch (error) { + LoggerInstance.error('Error getOpcsApprovedTokens: ', error.message) + throw Error(error.message) + } +} diff --git a/src/@utils/web3.ts b/src/@utils/web3.ts index 0cc429b91..9a7ff7409 100644 --- a/src/@utils/web3.ts +++ b/src/@utils/web3.ts @@ -2,6 +2,7 @@ import { getNetworkDisplayName } from '@hooks/useNetworkMetadata' import { LoggerInstance } from '@oceanprotocol/lib' import Web3 from 'web3' import { getOceanConfig } from './ocean' +import { AbiItem } from 'web3-utils/types' export function accountTruncate(account: string): string { if (!account || account === '') return @@ -110,3 +111,52 @@ export async function addTokenToWallet( } ) } + +export async function getTokenBalance( + accountId: string, + decimals: number, + tokenAddress: string, + web3: Web3 +): Promise { + const minABI = [ + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } + ] as AbiItem[] + + try { + const token = new web3.eth.Contract(minABI, tokenAddress, { + from: accountId + }) + const balance = await token.methods.balanceOf(accountId).call() + const adjustedDecimalsBalance = `${balance}${'0'.repeat(18 - decimals)}` + return web3.utils.fromWei(adjustedDecimalsBalance) + } catch (e) { + LoggerInstance.error(`ERROR: Failed to get the balance: ${e.message}`) + } +} + +export function getTokenBalanceFromSymbol( + balance: UserBalance, + symbol: string +): string { + if (!symbol) return + const baseTokenBalance = balance?.[symbol.toLocaleLowerCase()] + return baseTokenBalance || '0' +} diff --git a/src/components/@shared/AddToken/index.module.css b/src/components/@shared/AddToken/index.module.css index a7fcdc78f..9311aba91 100644 --- a/src/components/@shared/AddToken/index.module.css +++ b/src/components/@shared/AddToken/index.module.css @@ -38,6 +38,10 @@ transition: 0.2s ease-out; } +.logo svg { + height: 100%; +} + .button:hover .logo, .button:focus .logo { border-color: var(--color-primary); diff --git a/src/components/@shared/AddToken/index.tsx b/src/components/@shared/AddToken/index.tsx index 908af5db1..c315e5018 100644 --- a/src/components/@shared/AddToken/index.tsx +++ b/src/components/@shared/AddToken/index.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames/bind' import { addTokenToWallet } from '@utils/web3' import { useWeb3 } from '@context/Web3' import Button from '@shared/atoms/Button' +import OceanLogo from '@images/logo.svg' import styles from './index.module.css' const cx = classNames.bind(styles) @@ -10,14 +11,12 @@ const cx = classNames.bind(styles) export default function AddToken({ address, symbol, - logo, text, className, minimal }: { address: string symbol: string - logo: string // needs to be a remote image text?: string className?: string minimal?: boolean @@ -33,7 +32,7 @@ export default function AddToken({ async function handleAddToken() { if (!web3Provider) return - await addTokenToWallet(web3Provider, address, symbol, logo) + await addTokenToWallet(web3Provider, address, symbol) } return ( @@ -44,7 +43,9 @@ export default function AddToken({ onClick={handleAddToken} > - +
+ +
{text || `Add ${symbol}`} diff --git a/src/components/@shared/ButtonBuy/index.tsx b/src/components/@shared/ButtonBuy/index.tsx index f8979bedb..9791f667f 100644 --- a/src/components/@shared/ButtonBuy/index.tsx +++ b/src/components/@shared/ButtonBuy/index.tsx @@ -8,6 +8,7 @@ interface ButtonBuyProps { disabled: boolean hasPreviousOrder: boolean hasDatatoken: boolean + btSymbol: string dtSymbol: string dtBalance: string assetType: string @@ -33,6 +34,7 @@ interface ButtonBuyProps { // TODO: we need to take a look at these messages function getConsumeHelpText( + btSymbol: string, dtBalance: string, dtSymbol: string, hasDatatoken: boolean, @@ -48,9 +50,9 @@ function getConsumeHelpText( : hasPreviousOrder ? `You bought this ${assetType} already allowing you to use it without paying again.` : hasDatatoken - ? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.` + ? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying ${btSymbol} again.` : isBalanceSufficient === false - ? 'You do not have enough OCEAN in your wallet to purchase this asset.' + ? `You do not have enough ${btSymbol} in your wallet to purchase this asset.` : `For using this ${assetType}, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher.` return text } @@ -58,6 +60,7 @@ function getConsumeHelpText( function getComputeAssetHelpText( hasPreviousOrder: boolean, hasDatatoken: boolean, + btSymbol: string, dtSymbol: string, dtBalance: string, isConsumable: boolean, @@ -73,6 +76,7 @@ function getComputeAssetHelpText( hasProviderFee?: boolean ) { const computeAssetHelpText = getConsumeHelpText( + btSymbol, dtBalance, dtSymbol, hasDatatoken, @@ -91,7 +95,7 @@ function getComputeAssetHelpText( : hasPreviousOrderSelectedComputeAsset ? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.` : hasDatatokenSelectedComputeAsset - ? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying OCEAN again.` + ? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying ${btSymbol} again.` : isBalanceSufficient === false ? '' : `Additionally, you will buy 1 ${dtSymbolSelectedComputeAsset} for the ${selectedComputeAssetType} and spend it back to its publisher.` @@ -107,6 +111,7 @@ export default function ButtonBuy({ disabled, hasPreviousOrder, hasDatatoken, + btSymbol, dtSymbol, dtBalance, assetType, @@ -161,6 +166,7 @@ export default function ButtonBuy({
{action === 'download' ? getConsumeHelpText( + btSymbol, dtBalance, dtSymbol, hasDatatoken, @@ -173,6 +179,7 @@ export default function ButtonBuy({ : getComputeAssetHelpText( hasPreviousOrder, hasDatatoken, + btSymbol, dtSymbol, dtBalance, isConsumable, diff --git a/src/components/@shared/Price/Conversion.tsx b/src/components/@shared/Price/Conversion.tsx index e672d87af..845acc389 100644 --- a/src/components/@shared/Price/Conversion.tsx +++ b/src/components/@shared/Price/Conversion.tsx @@ -59,7 +59,7 @@ export default function Conversion({ return ( {!hideApproximateSymbol && '≈ '} {' '} diff --git a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx index 6f144cf53..e9e8fa45b 100644 --- a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx +++ b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx @@ -15,6 +15,7 @@ import Decimal from 'decimal.js' import { MAX_DECIMALS } from '@utils/constants' import { useMarketMetadata } from '@context/MarketMetadata' import Alert from '@shared/atoms/Alert' +import { getTokenBalanceFromSymbol } from '@utils/web3' export default function FormStartCompute({ algorithms, @@ -160,12 +161,16 @@ export default function FormStartCompute({ ]) useEffect(() => { - if (!totalPrice || !balance?.ocean || !dtBalance) return - - setIsBalanceSufficient( - compareAsBN(balance.ocean, `${totalPrice}`) || Number(dtBalance) >= 1 + const baseTokenBalance = getTokenBalanceFromSymbol( + balance, + asset?.accessDetails?.baseToken?.symbol ) - }, [totalPrice, balance?.ocean, dtBalance]) + + if (!totalPrice || !baseTokenBalance || !dtBalance) return + setIsBalanceSufficient( + compareAsBN(baseTokenBalance, `${totalPrice}`) || Number(dtBalance) >= 1 + ) + }, [totalPrice, balance, dtBalance, asset?.accessDetails?.baseToken?.symbol]) return (
@@ -215,6 +220,7 @@ export default function FormStartCompute({ } hasPreviousOrder={hasPreviousOrder} hasDatatoken={hasDatatoken} + btSymbol={asset?.accessDetails?.baseToken?.symbol} dtSymbol={asset?.datatokens[0]?.symbol} dtBalance={dtBalance} assetTimeout={assetTimeout} diff --git a/src/components/Asset/AssetActions/Download.tsx b/src/components/Asset/AssetActions/Download.tsx index 7f4a05448..84efbaacc 100644 --- a/src/components/Asset/AssetActions/Download.tsx +++ b/src/components/Asset/AssetActions/Download.tsx @@ -158,6 +158,7 @@ export default function Download({ disabled={isDisabled} hasPreviousOrder={isOwned} hasDatatoken={hasDatatoken} + btSymbol={asset?.accessDetails?.baseToken?.symbol} dtSymbol={asset?.datatokens[0]?.symbol} dtBalance={dtBalance} onClick={handleOrderOrDownload} diff --git a/src/components/Asset/AssetActions/index.tsx b/src/components/Asset/AssetActions/index.tsx index b4118812c..3a83f1322 100644 --- a/src/components/Asset/AssetActions/index.tsx +++ b/src/components/Asset/AssetActions/index.tsx @@ -14,6 +14,7 @@ import { useIsMounted } from '@hooks/useIsMounted' import styles from './index.module.css' import { useFormikContext } from 'formik' import { FormPublishData } from 'src/components/Publish/_types' +import { getTokenBalanceFromSymbol } from '@utils/web3' import AssetStats from './AssetStats' export default function AssetActions({ @@ -104,14 +105,19 @@ export default function AssetActions({ if (asset?.accessDetails?.type === 'free') setIsBalanceSufficient(true) if ( !asset?.accessDetails?.price || + !asset?.accessDetails?.baseToken?.symbol || !accountId || - !balance?.ocean || + !balance || !dtBalance ) return + const baseTokenBalance = getTokenBalanceFromSymbol( + balance, + asset?.accessDetails?.baseToken?.symbol + ) setIsBalanceSufficient( - compareAsBN(balance.ocean, `${asset?.accessDetails.price}`) || + compareAsBN(baseTokenBalance, `${asset?.accessDetails.price}`) || Number(dtBalance) >= 1 ) diff --git a/src/components/Asset/AssetContent/MetaMain/MetaAsset.module.css b/src/components/Asset/AssetContent/MetaMain/MetaAsset.module.css index 005d09b61..45b4dd6a9 100644 --- a/src/components/Asset/AssetContent/MetaMain/MetaAsset.module.css +++ b/src/components/Asset/AssetContent/MetaMain/MetaAsset.module.css @@ -24,3 +24,7 @@ .add { font-size: var(--font-size-mini); } + +.add svg path { + fill: var(--brand-violet); +} diff --git a/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx b/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx index 6418bec11..94bf50b20 100644 --- a/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx +++ b/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx @@ -41,7 +41,6 @@ export default function MetaAsset({ (
  • - {key === 'eth' ? mainCurrency : oceanTokenMetadata?.symbol} + {key === 'eth' ? mainCurrency : key.toUpperCase()} {' '} {formatCurrency(Number(value), '', locale, false, { significantFigures: 4 @@ -67,7 +67,6 @@ export default function Details(): ReactElement { )} diff --git a/src/components/Publish/Preview/index.tsx b/src/components/Publish/Preview/index.tsx index 3749e5e1d..03448d5db 100644 --- a/src/components/Publish/Preview/index.tsx +++ b/src/components/Publish/Preview/index.tsx @@ -20,8 +20,8 @@ export default function Preview(): ReactElement { price: `${values.pricing.price}`, baseToken: { address: ZERO_ADDRESS, - name: 'OCEAN', - symbol: 'OCEAN' + name: values.pricing?.baseToken?.symbol || 'OCEAN', + symbol: values.pricing?.baseToken?.symbol || 'OCEAN' }, datatoken: { address: ZERO_ADDRESS, diff --git a/src/components/Publish/Pricing/CoinSelect.module.css b/src/components/Publish/Pricing/CoinSelect.module.css new file mode 100644 index 000000000..0f7b838b4 --- /dev/null +++ b/src/components/Publish/Pricing/CoinSelect.module.css @@ -0,0 +1,22 @@ +.coinSelect { + composes: select from '../../@shared/FormInput/InputElement.module.css'; + font-size: var(--font-size-small); + font-weight: var(--font-weight-base); + border: none; + margin-right: -0.5rem; + background-color: var(--background-highlight); + width: auto; + padding: 0 1.75rem 0 0.25rem; + height: 41px; + text-align: center; + font-weight: var(--font-weight-bold); + + /* custom arrow, without the divider line */ + background-image: linear-gradient( + 45deg, + transparent 50%, + var(--font-color-text) 50% + ), + linear-gradient(135deg, var(--font-color-text) 50%, transparent 50%); + background-position: calc(100% - 14px) 1.2rem, calc(100% - 9px) 1.2rem, 100% 0; +} diff --git a/src/components/Publish/Pricing/CoinSelect.tsx b/src/components/Publish/Pricing/CoinSelect.tsx new file mode 100644 index 000000000..0b1137894 --- /dev/null +++ b/src/components/Publish/Pricing/CoinSelect.tsx @@ -0,0 +1,34 @@ +import Input from '@shared/FormInput' +import InputElement from '@shared/FormInput/InputElement' +import { useFormikContext } from 'formik' +import React, { ChangeEvent, ReactElement } from 'react' +import { FormPublishData } from '../_types' +import styles from './CoinSelect.module.css' + +export default function CoinSelect({ + approvedBaseTokens +}: { + approvedBaseTokens: TokenInfo[] +}): ReactElement { + const { values, setFieldValue } = useFormikContext() + + const handleBaseTokenSelection = (e: ChangeEvent) => { + const selectedBaseToken = approvedBaseTokens.find( + (token) => token.symbol === e.target.value + ) + setFieldValue('pricing.baseToken', selectedBaseToken) + } + + return ( + approvedBaseTokens?.length > 0 && ( + token.symbol)} + value={values.pricing?.baseToken?.symbol} + onChange={handleBaseTokenSelection} + /> + ) + ) +} diff --git a/src/components/Publish/Pricing/Fixed.module.css b/src/components/Publish/Pricing/Fixed.module.css new file mode 100644 index 000000000..f2a1b3973 --- /dev/null +++ b/src/components/Publish/Pricing/Fixed.module.css @@ -0,0 +1,32 @@ +.title { + font-size: var(--font-size-base); + margin-top: var(--spacer); + margin-bottom: 0; + padding-bottom: calc(var(--spacer) / 4); +} + +.tokens { + display: grid; + border: 1px solid var(--border-color); + background: var(--background-highlight); + border-radius: var(--border-radius); +} + +@media screen and (min-width: 40rem) { + .tokens { + grid-template-columns: 1fr 1fr; + } +} + +.subtitle { + display: inline-block; + color: var(--color-secondary); + margin-left: calc(var(--spacer) / 4); + font-family: var(--font-family-base); + font-weight: var(--font-weight-base); + font-size: var(--font-size-small); +} + +.alertArea { + padding: 0 calc(var(--spacer) / 2); +} diff --git a/src/components/Publish/Pricing/Fixed.tsx b/src/components/Publish/Pricing/Fixed.tsx index f77f5f5de..2d6de926b 100644 --- a/src/components/Publish/Pricing/Fixed.tsx +++ b/src/components/Publish/Pricing/Fixed.tsx @@ -2,16 +2,22 @@ import React, { ReactElement } from 'react' import FormHelp from '@shared/FormInput/Help' import Price from './Price' import Fees from './Fees' -import styles from './index.module.css' +import styles from './Fixed.module.css' -export default function Fixed({ content }: { content: any }): ReactElement { +export default function Fixed({ + approvedBaseTokens, + content +}: { + approvedBaseTokens: TokenInfo[] + content: any +}): ReactElement { return ( <> {content.info}

    Price

    - + ) diff --git a/src/components/Publish/Pricing/Price.module.css b/src/components/Publish/Pricing/Price.module.css index 7e5849c51..f703671ea 100644 --- a/src/components/Publish/Pricing/Price.module.css +++ b/src/components/Publish/Pricing/Price.module.css @@ -1,34 +1,43 @@ +.form { + position: relative; +} + +.form *, +.form label { + margin-bottom: 0; +} + .price { background: var(--background-highlight); border: 1px solid var(--border-color); border-radius: var(--border-radius); - padding: calc(var(--spacer) / 1.5) calc(var(--spacer) / 2); } -.form { - display: flex; - justify-content: center; +.grid { + display: grid; + gap: calc(var(--spacer) / 2); + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + padding: calc(var(--spacer) / 2); + max-width: 30rem; + margin-left: auto; + margin-right: auto; align-items: center; } -.inputWrap { - position: relative; - max-width: 15rem; -} - -.inputWrap > div { - margin-bottom: 0; -} - .fixed label { display: none; } .datatoken { + color: var(--color-secondary); + font-size: var(--font-size-small); + font-weight: var(--font-weight-bold); +} + +.datatoken h4 { font-size: var(--font-size-base); color: var(--color-secondary); margin: 0; - margin-left: calc(var(--spacer) / 2); } .datatoken strong { @@ -41,10 +50,10 @@ .free { text-align: left; + margin: calc(var(--spacer) / 2); font-size: var(--font-size-base); } -.free [class*='FormInput_field'], -.free [class*='InputRadio_radioGroup'] { +.free [class*='FormInput_field'] { margin: 0; } diff --git a/src/components/Publish/Pricing/Price.tsx b/src/components/Publish/Pricing/Price.tsx index 73d0b45d6..0ed7eb297 100644 --- a/src/components/Publish/Pricing/Price.tsx +++ b/src/components/Publish/Pricing/Price.tsx @@ -6,43 +6,57 @@ import Error from '@shared/FormInput/Error' import styles from './Price.module.css' import { FormPublishData } from '../_types' import { getFieldContent } from '@utils/form' +import CoinSelect from './CoinSelect' -export default function Price({ content }: { content?: any }): ReactElement { +export default function Price({ + approvedBaseTokens, + content +}: { + approvedBaseTokens?: TokenInfo[] + content?: any +}): ReactElement { const [field, meta] = useField('pricing.price') const { values } = useFormikContext() const { dataTokenOptions } = values.services[0] - const classNames = `${styles.price} ${ - values.pricing.type === 'free' ? styles.free : styles.fixed - }` - return ( -
    +
    {values.pricing.type === 'free' ? ( - - ) : ( -
    -
    - - -
    - -

    - = 1 {dataTokenOptions.symbol}{' '} - -

    +
    +
    + ) : ( + <> +
    +
    + 1 ? ( + + ) : ( + values.pricing?.baseToken?.symbol + ) + } + {...field} + /> + +
    +
    +

    + = 1 {dataTokenOptions.symbol}{' '} + +

    +
    +
    + )}
    ) diff --git a/src/components/Publish/Pricing/index.tsx b/src/components/Publish/Pricing/index.tsx index 1c485da52..1f8678d21 100644 --- a/src/components/Publish/Pricing/index.tsx +++ b/src/components/Publish/Pricing/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect } from 'react' +import React, { ReactElement, useCallback, useEffect, useState } from 'react' import { useFormikContext } from 'formik' import Tabs from '@shared/atoms/Tabs' import { FormPublishData } from '../_types' @@ -7,14 +7,37 @@ import Free from './Free' import content from '../../../../content/price.json' import styles from './index.module.css' import { useMarketMetadata } from '@context/MarketMetadata' +import { useWeb3 } from '@context/Web3' export default function PricingFields(): ReactElement { const { appConfig } = useMarketMetadata() - + const { approvedBaseTokens, chainId } = useWeb3() // Connect with main publish form const { values, setFieldValue } = useFormikContext() const { pricing } = values - const { price, type } = pricing + const { type } = pricing + + const defaultBaseToken = + approvedBaseTokens?.find((token) => + token.name.toLowerCase().includes('ocean') + ) || approvedBaseTokens?.[0] + + const isBaseTokenSet = !!approvedBaseTokens?.find( + (token) => token?.address === values?.pricing?.baseToken?.address + ) + + useEffect(() => { + if (!approvedBaseTokens?.length) return + if (isBaseTokenSet) return + setFieldValue('pricing.baseToken', defaultBaseToken) + }, [ + approvedBaseTokens, + chainId, + defaultBaseToken, + isBaseTokenSet, + setFieldValue, + values.pricing.baseToken + ]) // Switch type value upon tab change function handleTabChange(tabName: string) { @@ -22,28 +45,42 @@ export default function PricingFields(): ReactElement { setFieldValue('pricing.type', type) setFieldValue('pricing.price', 0) setFieldValue('pricing.freeAgreement', false) + setFieldValue('pricing.baseToken', defaultBaseToken) + type !== 'free' && setFieldValue('pricing.amountDataToken', 1000) } - // Update price when price is changed - useEffect(() => { - setFieldValue('pricing.price', price) - setFieldValue('pricing.type', type) - }, [price, setFieldValue, type]) + const updateTabs = useCallback(() => { + return [ + appConfig.allowFixedPricing === 'true' + ? { + title: content.create.fixed.title, + content: ( + + ) + } + : undefined, - const tabs = [ - appConfig.allowFixedPricing === 'true' - ? { - title: content.create.fixed.title, - content: - } - : undefined, - appConfig.allowFreePricing === 'true' - ? { - title: content.create.free.title, - content: - } - : undefined - ].filter((tab) => tab !== undefined) + appConfig.allowFreePricing === 'true' + ? { + title: content.create.free.title, + content: + } + : undefined + ].filter((tab) => tab !== undefined) + }, [ + appConfig.allowFixedPricing, + appConfig.allowFreePricing, + approvedBaseTokens + ]) + + const [tabs, setTabs] = useState(updateTabs()) + + useEffect(() => { + setTabs(updateTabs()) + }, [updateTabs]) return ( { + if (!chainId) return + + setFieldValue('pricing.baseToken', null) + }, [chainId, setFieldValue]) + // auto-sync publish feedback into form data values useEffect(() => { setFieldValue('feedback', feedback) diff --git a/src/components/Publish/_constants.tsx b/src/components/Publish/_constants.tsx index 65f7549b9..19eba3f69 100644 --- a/src/components/Publish/_constants.tsx +++ b/src/components/Publish/_constants.tsx @@ -86,6 +86,7 @@ export const initialValues: FormPublishData = { } ], pricing: { + baseToken: { address: '', name: '', symbol: 'OCEAN', decimals: 18 }, price: 0, type: allowFixedPricing === 'true' ? 'fixed' : 'free', freeAgreement: false diff --git a/src/components/Publish/_utils.ts b/src/components/Publish/_utils.ts index 6f0a0c649..ccc643df2 100644 --- a/src/components/Publish/_utils.ts +++ b/src/components/Publish/_utils.ts @@ -207,7 +207,7 @@ export async function createTokensAndPricing( minter: accountId, paymentCollector: accountId, mpFeeAddress: marketFeeAddress, - feeToken: config.oceanTokenAddress, + feeToken: values.pricing.baseToken.address, feeAmount: publisherMarketOrderFee, // max number cap: '115792089237316195423570985008687907853269984665640564039457', @@ -223,10 +223,10 @@ export async function createTokensAndPricing( case 'fixed': { const freParams: FreCreationParams = { fixedRateAddress: config.fixedRateExchangeAddress, - baseTokenAddress: config.oceanTokenAddress, + baseTokenAddress: values.pricing.baseToken.address, owner: accountId, marketFeeCollector: marketFeeAddress, - baseTokenDecimals: 18, + baseTokenDecimals: values.pricing.baseToken.decimals, datatokenDecimals: 18, fixedRate: values.pricing.price.toString(), marketFee: publisherMarketFixedSwapFee,