1
0
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:
Luca Milanese 2022-08-03 14:48:57 +02:00 committed by GitHub
parent eb29c4ce3b
commit 52ad877b13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 430 additions and 160 deletions

View File

@ -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."

View File

@ -6,7 +6,10 @@ export const opcQuery = gql`
swapOceanFee
swapNonOceanFee
approvedTokens {
id
address: id
symbol
name
decimals
}
id
}

View File

@ -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)

View File

@ -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
}}

View File

@ -50,6 +50,7 @@ declare global {
interface PricePublishOptions {
price: number
baseToken: TokenInfo
type: 'fixed' | 'free'
freeAgreement: boolean
}

View File

@ -1,4 +1,4 @@
interface UserBalance {
eth: string
ocean: string
[key: string]: string
}

View File

@ -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,

View File

@ -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}`)
}
}

View File

@ -57,7 +57,7 @@ export async function order(
_consumeMarketFee: {
consumeMarketFeeAddress: marketFeeAddress,
consumeMarketFeeAmount: consumeMarketOrderFee,
consumeMarketFeeToken: config.oceanTokenAddress
consumeMarketFeeToken: asset.accessDetails.baseToken.address
}
} as OrderParams

View File

@ -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)
}
}

View File

@ -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'
}

View File

@ -38,6 +38,10 @@
transition: 0.2s ease-out;
}
.logo svg {
height: 100%;
}
.button:hover .logo,
.button:focus .logo {
border-color: var(--color-primary);

View File

@ -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>

View File

@ -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,

View File

@ -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 }} />{' '}

View File

@ -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}

View File

@ -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}

View File

@ -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
)

View File

@ -24,3 +24,7 @@
.add {
font-size: var(--font-size-mini);
}
.add svg path {
fill: var(--brand-violet);
}

View File

@ -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

View File

@ -88,3 +88,7 @@
.addToken {
margin-left: 0.3rem;
}
.addToken svg path {
fill: var(--brand-violet);
}

View File

@ -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}
/>
)}

View File

@ -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,

View 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;
}

View 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}
/>
)
)
}

View 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);
}

View File

@ -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} />
</>
)

View File

@ -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;
}

View File

@ -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>
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,