mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Dynamic token list support for publishing (#1516)
* feat: add approved tokens list query to subgraph * feat: add base token selector * feat: add placeholder tooltip message for base token * feat: use user selected base token for publish * fix: publish constants * feat: update base token query to include digits and symbol * feat: display correct token name and symbol in publish pricing tab * fix: publish preview token name * fix: query type * feat: add balance fetch for all approved tokens * fix: balance check for dynamic price with alternative base tokens * feat: update balance to show baseToken instead of ocean * fix: default baseToken in publish form * feat: update text content for pricing publish step * chore: update ocean.js * add decimals to token Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * fix dt decimals Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * update ocean.js Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * fix: show correct basetoken symbol under button buy * refactor: move baseToken selector to input label [WIP] * refactor: preserve baseToken value value when switching tabs * remove basetoken tooltip from content json * fix: price props * refactor: remove BaseToken component * fix: baseToken name on first load * fix: baseToken display name in dynamic price * fix: conversion tooltip text * fix: error box overlapping in Coin component * feat: add token logo component * feat: add basetoken logo to asset actions pool * fix: token images size * fix: add default appproved token list when disconnected or chainId not supported * fix: datatoken logo on asset details meta * refactor: balance fetch + move approved base tokens list in web3 provider * feat: update all datatokens to display ocean logo in violet * fix: show correct logos on polygon * fix wallet, remove dynamic * fix build * fix: reset baseToken on chainId change during publish * fix: price tabs selection indicator * feat: set the ocean token as default in pricing * add baseToken * fix price * remove firstPrice * cleanup, more affordance for token dropdown Co-authored-by: mihaisc <mihai.scarlat@smartcontrol.ro> Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
parent
eb29c4ce3b
commit
52ad877b13
@ -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."
|
||||
|
@ -6,7 +6,10 @@ export const opcQuery = gql`
|
||||
swapOceanFee
|
||||
swapNonOceanFee
|
||||
approvedTokens {
|
||||
id
|
||||
address: id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
id
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<void>
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
@ -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<Web3>()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [web3Provider, setWeb3Provider] = useState<any>()
|
||||
|
||||
const [web3Modal, setWeb3Modal] = useState<Web3Modal>()
|
||||
const [web3ProviderInfo, setWeb3ProviderInfo] = useState<IProviderInfo>()
|
||||
const [networkId, setNetworkId] = useState<number>()
|
||||
@ -106,10 +101,10 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
||||
const [balance, setBalance] = useState<UserBalance>({
|
||||
eth: '0',
|
||||
ocean: '0'
|
||||
eth: '0'
|
||||
})
|
||||
const [isSupportedOceanNetwork, setIsSupportedOceanNetwork] = useState(true)
|
||||
const [approvedBaseTokens, setApprovedBaseTokens] = useState<TokenInfo[]>()
|
||||
|
||||
// -----------------------------------
|
||||
// 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
|
||||
}}
|
||||
|
1
src/@types/Price.d.ts
vendored
1
src/@types/Price.d.ts
vendored
@ -50,6 +50,7 @@ declare global {
|
||||
|
||||
interface PricePublishOptions {
|
||||
price: number
|
||||
baseToken: TokenInfo
|
||||
type: 'fixed' | 'free'
|
||||
freeAgreement: boolean
|
||||
}
|
||||
|
2
src/@types/TokenBalance.d.ts
vendored
2
src/@types/TokenBalance.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
interface UserBalance {
|
||||
eth: string
|
||||
ocean: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<string> {
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export async function order(
|
||||
_consumeMarketFee: {
|
||||
consumeMarketFeeAddress: marketFeeAddress,
|
||||
consumeMarketFeeAmount: consumeMarketOrderFee,
|
||||
consumeMarketFeeToken: config.oceanTokenAddress
|
||||
consumeMarketFeeToken: asset.accessDetails.baseToken.address
|
||||
}
|
||||
} as OrderParams
|
||||
|
||||
|
@ -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<TokenInfo[]> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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<string> {
|
||||
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'
|
||||
}
|
||||
|
@ -38,6 +38,10 @@
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button:hover .logo,
|
||||
.button:focus .logo {
|
||||
border-color: var(--color-primary);
|
||||
|
@ -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}
|
||||
>
|
||||
<span className={styles.logoWrap}>
|
||||
<img src={logo} className={styles.logo} width="16" height="16" />
|
||||
<div className={styles.logo}>
|
||||
<OceanLogo />
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span className={styles.text}>{text || `Add ${symbol}`}</span>
|
||||
|
@ -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({
|
||||
<div className={styles.help}>
|
||||
{action === 'download'
|
||||
? getConsumeHelpText(
|
||||
btSymbol,
|
||||
dtBalance,
|
||||
dtSymbol,
|
||||
hasDatatoken,
|
||||
@ -173,6 +179,7 @@ export default function ButtonBuy({
|
||||
: getComputeAssetHelpText(
|
||||
hasPreviousOrder,
|
||||
hasDatatoken,
|
||||
btSymbol,
|
||||
dtSymbol,
|
||||
dtBalance,
|
||||
isConsumable,
|
||||
|
@ -59,7 +59,7 @@ export default function Conversion({
|
||||
return (
|
||||
<span
|
||||
className={styleClasses}
|
||||
title="Approximation based on current OCEAN spot price on Coingecko"
|
||||
title="Approximation based on the current selected base token spot price on Coingecko"
|
||||
>
|
||||
{!hideApproximateSymbol && '≈ '}
|
||||
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
|
||||
|
@ -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 (
|
||||
<Form className={styles.form}>
|
||||
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -24,3 +24,7 @@
|
||||
.add {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.add svg path {
|
||||
fill: var(--brand-violet);
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ export default function MetaAsset({
|
||||
<AddToken
|
||||
address={asset?.services[0].datatokenAddress}
|
||||
symbol={(asset as Asset)?.datatokens[0]?.symbol}
|
||||
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/datatoken.png"
|
||||
text={`Add ${(asset as Asset)?.datatokens[0]?.symbol} to wallet`}
|
||||
className={styles.add}
|
||||
minimal
|
||||
|
@ -88,3 +88,7 @@
|
||||
.addToken {
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.addToken svg path {
|
||||
fill: var(--brand-violet);
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export default function Details(): ReactElement {
|
||||
{Object.entries(balance).map(([key, value]) => (
|
||||
<li className={styles.balance} key={key}>
|
||||
<span className={styles.symbol}>
|
||||
{key === 'eth' ? mainCurrency : oceanTokenMetadata?.symbol}
|
||||
{key === 'eth' ? mainCurrency : key.toUpperCase()}
|
||||
</span>{' '}
|
||||
{formatCurrency(Number(value), '', locale, false, {
|
||||
significantFigures: 4
|
||||
@ -67,7 +67,6 @@ export default function Details(): ReactElement {
|
||||
<AddToken
|
||||
address={oceanTokenMetadata?.address}
|
||||
symbol={oceanTokenMetadata?.symbol}
|
||||
logo="https://raw.githubusercontent.com/oceanprotocol/art/main/logo/token.png"
|
||||
className={styles.addToken}
|
||||
/>
|
||||
)}
|
||||
|
@ -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,
|
||||
|
22
src/components/Publish/Pricing/CoinSelect.module.css
Normal file
22
src/components/Publish/Pricing/CoinSelect.module.css
Normal file
@ -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;
|
||||
}
|
34
src/components/Publish/Pricing/CoinSelect.tsx
Normal file
34
src/components/Publish/Pricing/CoinSelect.tsx
Normal file
@ -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<FormPublishData>()
|
||||
|
||||
const handleBaseTokenSelection = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedBaseToken = approvedBaseTokens.find(
|
||||
(token) => token.symbol === e.target.value
|
||||
)
|
||||
setFieldValue('pricing.baseToken', selectedBaseToken)
|
||||
}
|
||||
|
||||
return (
|
||||
approvedBaseTokens?.length > 0 && (
|
||||
<InputElement
|
||||
name="coinselect"
|
||||
className={styles.coinSelect}
|
||||
type="select"
|
||||
options={approvedBaseTokens?.map((token) => token.symbol)}
|
||||
value={values.pricing?.baseToken?.symbol}
|
||||
onChange={handleBaseTokenSelection}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
32
src/components/Publish/Pricing/Fixed.module.css
Normal file
32
src/components/Publish/Pricing/Fixed.module.css
Normal file
@ -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);
|
||||
}
|
@ -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 (
|
||||
<>
|
||||
<FormHelp>{content.info}</FormHelp>
|
||||
|
||||
<h4 className={styles.title}>Price</h4>
|
||||
|
||||
<Price />
|
||||
<Price approvedBaseTokens={approvedBaseTokens} />
|
||||
<Fees tooltips={content.tooltips} />
|
||||
</>
|
||||
)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<FormPublishData>()
|
||||
const { dataTokenOptions } = values.services[0]
|
||||
|
||||
const classNames = `${styles.price} ${
|
||||
values.pricing.type === 'free' ? styles.free : styles.fixed
|
||||
}`
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={styles.price}>
|
||||
{values.pricing.type === 'free' ? (
|
||||
<div className={styles.free}>
|
||||
<Field
|
||||
{...getFieldContent('freeAgreement', content.fields)}
|
||||
component={Input}
|
||||
name="pricing.freeAgreement"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.inputWrap}>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="0"
|
||||
prefix="OCEAN"
|
||||
prefix={
|
||||
approvedBaseTokens?.length > 1 ? (
|
||||
<CoinSelect approvedBaseTokens={approvedBaseTokens} />
|
||||
) : (
|
||||
values.pricing?.baseToken?.symbol
|
||||
)
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
<Error meta={meta} />
|
||||
</div>
|
||||
|
||||
<h4 className={styles.datatoken}>
|
||||
<div className={styles.datatoken}>
|
||||
<h4>
|
||||
= <strong>1</strong> {dataTokenOptions.symbol}{' '}
|
||||
<Conversion price={field.value} className={styles.conversion} />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
@ -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<FormPublishData>()
|
||||
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,21 +45,24 @@ 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 tabs = [
|
||||
const updateTabs = useCallback(() => {
|
||||
return [
|
||||
appConfig.allowFixedPricing === 'true'
|
||||
? {
|
||||
title: content.create.fixed.title,
|
||||
content: <Fixed content={content.create.fixed} />
|
||||
content: (
|
||||
<Fixed
|
||||
approvedBaseTokens={approvedBaseTokens}
|
||||
content={content.create.fixed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
: undefined,
|
||||
|
||||
appConfig.allowFreePricing === 'true'
|
||||
? {
|
||||
title: content.create.free.title,
|
||||
@ -44,6 +70,17 @@ export default function PricingFields(): ReactElement {
|
||||
}
|
||||
: undefined
|
||||
].filter((tab) => tab !== undefined)
|
||||
}, [
|
||||
appConfig.allowFixedPricing,
|
||||
appConfig.allowFreePricing,
|
||||
approvedBaseTokens
|
||||
])
|
||||
|
||||
const [tabs, setTabs] = useState(updateTabs())
|
||||
|
||||
useEffect(() => {
|
||||
setTabs(updateTabs())
|
||||
}, [updateTabs])
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
|
@ -22,6 +22,13 @@ export function Steps({
|
||||
setFieldValue('user.accountId', accountId)
|
||||
}, [chainId, accountId, setFieldValue])
|
||||
|
||||
// Reset the selected baseToken on chainId change
|
||||
useEffect(() => {
|
||||
if (!chainId) return
|
||||
|
||||
setFieldValue('pricing.baseToken', null)
|
||||
}, [chainId, setFieldValue])
|
||||
|
||||
// auto-sync publish feedback into form data values
|
||||
useEffect(() => {
|
||||
setFieldValue('feedback', feedback)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user