1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

merged v4 into c2d

This commit is contained in:
Bogdan Fazakas 2022-02-23 15:58:20 +02:00
commit d4570aa1cb
87 changed files with 6801 additions and 17599 deletions

View File

@ -1,6 +1,15 @@
#NEXT_PUBLIC_INFURA_PROJECT_ID="xxx" #NEXT_PUBLIC_INFURA_PROJECT_ID="xxx"
#NEXT_PUBLIC_MARKET_FEE_ADDRESS="0xxx" #NEXT_PUBLIC_MARKET_FEE_ADDRESS="0xxx"
#NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE="1"
#NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE="1"
#NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE="1"
#NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE="1"
#NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE="1"
#NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE="1"
#NEXT_PUBLIC_PORTIS_ID="xxx" #NEXT_PUBLIC_PORTIS_ID="xxx"

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @mihaisc @kremalicious @claudiaHash @bogdanfazakas @KatunaNorbert @jamiehewitt15 @DimitarSD * @mihaisc @kremalicious @claudiaHash @bogdanfazakas @EnzoVezzaro

View File

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- v4
tags: tags:
- '**' - '**'
pull_request: pull_request:

View File

@ -13,10 +13,10 @@ name: 'CodeQL'
on: on:
push: push:
branches: [main] branches: [main, v4]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [main] branches: [main, v4]
schedule: schedule:
- cron: '21 8 * * 4' - cron: '21 8 * * 4'

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ coverage
repo-metadata.json repo-metadata.json
networks-metadata.json networks-metadata.json
src/@types/subgraph src/@types/subgraph
src/@types/apollo/
graphql.schema.json graphql.schema.json
src/@types/graph.types.ts src/@types/graph.types.ts
tsconfig.tsbuildinfo tsconfig.tsbuildinfo

View File

@ -7,7 +7,7 @@ module.exports = {
// return appConfig.metadataCacheUri // return appConfig.metadataCacheUri
metadataCacheUri: metadataCacheUri:
process.env.NEXT_PUBLIC_METADATACACHE_URI || process.env.NEXT_PUBLIC_METADATACACHE_URI ||
'https://aquariusv4.oceanprotocol.com', 'https://v4.aquarius.oceanprotocol.com',
// List of chainIds which metadata cache queries will return by default. // List of chainIds which metadata cache queries will return by default.
// This preselects the Chains user preferences. // This preselects the Chains user preferences.
@ -22,6 +22,25 @@ module.exports = {
marketFeeAddress: marketFeeAddress:
process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS || process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS ||
'0x9984b2453eC7D99a73A5B3a46Da81f197B753C8d', '0x9984b2453eC7D99a73A5B3a46Da81f197B753C8d',
// publisher market fee that is taken upon ordering an asset, it is an absolute value, it is declared on erc20 creation
publisherMarketOrderFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE || '0',
// fee recieved by the publisher market when a dt is swaped from a pool, percent
publisherMarketPoolSwapFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE || '0',
// fee recieved by the publisher market when a dt is bought from a fixed rate exchange, percent
publisherMarketFixedSwapFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE || '0',
// consume market fee that is taken upon ordering an asset, it is an absolute value, it is specified on order
consumeMarketOrderFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE || '0',
// fee recieved by the consume market when a dt is swaped from a pool, percent
consumeMarketPoolSwapFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE || '0',
// fee recieved by the consume market when a dt is bought from a fixed rate exchange, percent
consumeMarketFixedSwapFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE || '0',
// Used for conversion display, can be whatever coingecko API supports // Used for conversion display, can be whatever coingecko API supports
// see: https://api.coingecko.com/api/v3/simple/supported_vs_currencies // see: https://api.coingecko.com/api/v3/simple/supported_vs_currencies

View File

@ -28,7 +28,16 @@
}, },
"free": { "free": {
"title": "Free", "title": "Free",
"info": "Set your data set as free. The datatoken for this data set will be given for free via creating a faucet." "info": "Set your data set as free. The datatoken for this data set will be given for free via creating a faucet.",
"fields": [
{
"name": "freeAgreement",
"type": "checkbox",
"options": [
"I want this asset to be free. I understand network fees are still to be paid"
]
}
]
} }
}, },
"pool": { "pool": {

View File

@ -6,7 +6,7 @@
"name": "nft", "name": "nft",
"label": "Data NFT", "label": "Data NFT",
"type": "nft", "type": "nft",
"help": "All metadata is stored on-chain in a newly deployed ERC-721 contract representing this asset, created with this name & symbol.", "help": "All metadata is stored on-chain in a newly deployed ERC-721 contract representing this asset, created with this name, symbol, description and image.",
"required": true "required": true
}, },
{ {

View File

@ -17,24 +17,35 @@ module.exports = (phase, { defaultConfig }) => {
type: 'asset/resource' type: 'asset/resource'
} }
) )
// for old ocean.js, most likely can be removed later on // for old ocean.js, most likely can be removed later on
config.plugins.push( config.plugins.push(
new options.webpack.IgnorePlugin({ new options.webpack.IgnorePlugin({
resourceRegExp: /^electron$/ resourceRegExp: /^electron$/
}) })
) )
const fallback = config.resolve.fallback || {}
config.resolve.fallback = { Object.assign(fallback, {
// crypto: require.resolve('crypto-browserify'),
// stream: require.resolve('stream-browserify'),
// assert: require.resolve('assert'),
// os: require.resolve('os-browserify'),
// url: require.resolve('url'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
fs: false, fs: false,
crypto: false, crypto: false,
os: false, os: false,
stream: false, stream: false,
http: false,
https: false,
assert: false assert: false
} })
config.resolve.fallback = fallback
config.plugins = (config.plugins || []).concat([
new options.webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
})
])
return typeof defaultConfig.webpack === 'function' return typeof defaultConfig.webpack === 'function'
? defaultConfig.webpack(config, options) ? defaultConfig.webpack(config, options)
: config : config

21513
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,13 +15,13 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh", "deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install", "postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://subgraphv4.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/" "codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/"
}, },
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.4.4", "@coingecko/cryptoformat": "^0.4.4",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^1.0.0-next.11", "@oceanprotocol/lib": "^1.0.0-next.21",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^4.0.6", "@portis/web3": "^4.0.6",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
@ -30,7 +30,6 @@
"bignumber.js": "^9.0.2", "bignumber.js": "^9.0.2",
"chart.js": "^3.7.0", "chart.js": "^3.7.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"d3": "^7.3.0",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"decimal.js": "^10.3.1", "decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2", "dom-confetti": "^0.2.2",
@ -44,7 +43,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"next": "^12.0.9", "next": "^12.1.0",
"query-string": "^7.1.0", "query-string": "^7.1.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-chartjs-2": "^4.0.1", "react-chartjs-2": "^4.0.1",
@ -95,10 +94,13 @@
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"https-browserify": "^1.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"process": "^0.11.10",
"serve": "^13.0.2", "serve": "^13.0.2",
"stream-http": "^2.8.3",
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"repository": { "repository": {

View File

@ -69,7 +69,8 @@ function AssetProvider({
if (!asset) { if (!asset) {
setError( setError(
`[asset] The asset for ${did} was not found in MetadataCache. If you just published a new data set, wait some seconds and refresh this page.` `\`${did}\`` +
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
) )
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset) LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
} else { } else {
@ -95,7 +96,6 @@ function AssetProvider({
// ----------------------------------- // -----------------------------------
const fetchAccessDetails = useCallback(async (): Promise<void> => { const fetchAccessDetails = useCallback(async (): Promise<void> => {
if (!asset?.chainId || !asset?.services) return if (!asset?.chainId || !asset?.services) return
const accessDetails = await getAccessDetails( const accessDetails = await getAccessDetails(
asset.chainId, asset.chainId,
asset.services[0].datatokenAddress, asset.services[0].datatokenAddress,

View File

@ -10,9 +10,9 @@ export const poolDataQuery = gql`
poolData: pool(id: $pool) { poolData: pool(id: $pool) {
id id
totalShares totalShares
poolFee liquidityProviderFee
opfFee opcFee
marketFee marketSwapFee
spotPrice spotPrice
baseToken { baseToken {
address address

View File

@ -30,7 +30,8 @@ const initialPoolInfo: Partial<PoolInfo> = {
} }
const initialPoolInfoUser: Partial<PoolInfoUser> = { const initialPoolInfoUser: Partial<PoolInfoUser> = {
liquidity: new Decimal(0) liquidity: new Decimal(0),
poolShares: '0'
} }
const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser
@ -68,7 +69,7 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
setPoolData(response.poolData) setPoolData(response.poolData)
setPoolInfoUser((prevState) => ({ setPoolInfoUser((prevState) => ({
...prevState, ...prevState,
poolShares: response.poolDataUser?.shares[0]?.shares poolShares: response.poolDataUser?.shares[0]?.shares || '0'
})) }))
setPoolSnapshots(response.poolSnapshots) setPoolSnapshots(response.poolSnapshots)
LoggerInstance.log('[pool] Fetched pool data:', response.poolData) LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
@ -77,8 +78,6 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
}, [asset?.chainId, asset?.accessDetails?.addressOrId, owner, accountId]) }, [asset?.chainId, asset?.accessDetails?.addressOrId, owner, accountId])
// Helper: start interval fetching // Helper: start interval fetching
// Having `accountId` as dependency is important for interval to
// change after user account switch.
const initFetchInterval = useCallback(() => { const initFetchInterval = useCallback(() => {
if (fetchInterval) return if (fetchInterval) return
@ -89,6 +88,10 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
) )
}, refreshInterval) }, refreshInterval)
setFetchInterval(newInterval) setFetchInterval(newInterval)
// Having `accountId` as dependency is important for interval to
// change after user account switch.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchInterval, fetchAllData, accountId]) }, [fetchInterval, fetchAllData, accountId])
useEffect(() => { useEffect(() => {
@ -115,10 +118,10 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
useEffect(() => { useEffect(() => {
if (!poolData) return if (!poolData) return
// Fees // Fees - this will be renamed again in subgraph
const poolFee = getFee(poolData.poolFee) const poolFee = getFee(poolData.liquidityProviderFee)
const marketFee = getFee(poolData.marketFee) const marketFee = getFee(poolData.marketSwapFee)
const opfFee = getFee(poolData.opfFee) const opfFee = getFee(poolData.opcFee)
// Total Liquidity // Total Liquidity
const totalLiquidityInOcean = isValidNumber(poolData.spotPrice) const totalLiquidityInOcean = isValidNumber(poolData.spotPrice)
@ -191,13 +194,13 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
!poolData || !poolData ||
!poolInfo?.totalPoolTokens || !poolInfo?.totalPoolTokens ||
!asset?.chainId || !asset?.chainId ||
!accountId !accountId ||
!poolInfoUser
) )
return return
// Staking bot receives half the pool shares so for display purposes // Staking bot receives half the pool shares so for display purposes
// we can multiply by 2 as we have a hardcoded 50/50 pool weight. // we can multiply by 2 as we have a hardcoded 50/50 pool weight.
const userPoolShares = new Decimal(poolInfoUser.poolShares) const userPoolShares = new Decimal(poolInfoUser.poolShares || 0)
.mul(2) .mul(2)
.toString() .toString()
@ -235,6 +238,8 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
poolShares: userPoolShares, poolShares: userPoolShares,
...newPoolInfoUser ...newPoolInfoUser
}) })
// poolInfoUser was not added on purpose, we use setPoolInfoUser so it will just loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
poolData, poolData,
poolInfoUser?.poolShares, poolInfoUser?.poolShares,

View File

@ -220,18 +220,14 @@ function ProfileProvider({
async (cancelToken: CancelToken) => { async (cancelToken: CancelToken) => {
if (!accountId || !chainIds) return if (!accountId || !chainIds) return
const didList: string[] = [] const dtList: string[] = []
const tokenOrders = await getUserTokenOrders(accountId, chainIds) const tokenOrders = await getUserTokenOrders(accountId, chainIds)
for (let i = 0; i < tokenOrders?.length; i++) { for (let i = 0; i < tokenOrders?.length; i++) {
const did = web3.utils dtList.push(tokenOrders[i].datatoken.address)
.toChecksumAddress(tokenOrders[i].datatoken.address)
.replace('0x', 'did:op:')
didList.push(did)
} }
const downloads = await getDownloadAssets( const downloads = await getDownloadAssets(
didList, dtList,
tokenOrders, tokenOrders,
chainIds, chainIds,
cancelToken cancelToken

View File

@ -1,159 +0,0 @@
import { useState } from 'react'
import { consumeFeedback } from '@utils/feedback'
import {
approve,
Datatoken,
FreOrderParams,
LoggerInstance,
OrderParams,
Pool,
ProviderFees,
ProviderInstance,
signHash,
ZERO_ADDRESS
} from '@oceanprotocol/lib'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
interface UseConsume {
consume: (
did: string,
dataTokenAddress: string,
serviceType: string,
marketFeeAddress: string,
orderId?: string
) => Promise<string>
consumeStep?: number
consumeStepText?: string
consumeError?: string
isLoading: boolean
}
function useConsume(): UseConsume {
const { accountId, web3, chainId } = useWeb3()
const [isLoading, setIsLoading] = useState(false)
const [consumeStep, setConsumeStep] = useState<number | undefined>()
const [consumeStepText, setConsumeStepText] = useState<string | undefined>()
const [consumeError, setConsumeError] = useState<string | undefined>()
function setStep(index: number) {
setConsumeStep(index)
setConsumeStepText(consumeFeedback[index])
}
// TODO: this will be done in another PR
async function consume(
did: string,
datatokenAddress: string,
serviceType = 'access',
marketFeeAddress: string,
orderId?: string
): Promise<string> {
if (!accountId) return
setIsLoading(true)
setConsumeError(undefined)
try {
setStep(0)
if (!orderId) {
const datatoken = new Datatoken(web3)
// if we don't have a previous valid order, get one
const userOwnedTokens = await datatoken.balance(
datatokenAddress,
accountId
)
setStep(1)
try {
const config = getOceanConfig(chainId)
// const txApprove = await approve(
// web3,
// accountId,
// config.oceanTokenAddress,
// accountId,
// '1',
// false
// )
// console.log('approve tx', txApprove)
// const txApprove1 = await approve(
// web3,
// accountId,
// config.oceanTokenAddress,
// datatokenAddress,
// '1',
// false
// )
// console.log('approve tx', txApprove1)
// diference between timeout and validUntil?
const initializeData = await ProviderInstance.initialize(
did,
'fca052c239a62523be30ab8ee70c4046867f6cd89f228185fe2996ded3d23c3c',
0,
accountId,
'https://providerv4.rinkeby.oceanprotocol.com'
)
const orderParams = {
consumer: accountId,
serviceIndex: 1,
_providerFees: initializeData.providerFee
} as OrderParams
const freParams = {
exchangeContract: config.fixedRateExchangeAddress,
exchangeId:
'0x7ac824fef114255e5e3521a161ef692ec32003916fb6f3fe985cb74790d053ca',
maxBaseTokenAmount: web3.utils.toWei('2'),
swapMarketFee: web3.utils.toWei('0'),
marketFeeAddress: ZERO_ADDRESS
} as FreOrderParams
const esttx = await datatoken.estGasBuyFromFreAndOrder(
datatokenAddress,
accountId,
orderParams,
freParams
)
const tx = await datatoken.buyFromFreAndOrder(
datatokenAddress,
accountId,
orderParams,
freParams
)
LoggerInstance.log('ordercreated', orderId)
setStep(2)
} catch (error) {
setConsumeError(error.message)
return error.message
}
}
setStep(3)
if (orderId)
// await ocean.assets.download(
// did as string,
// orderId,
// dataTokenAddress,
// account,
// ''
// )
setStep(4)
} catch (error) {
setConsumeError(error.message)
LoggerInstance.error(error)
return error.message
} finally {
setConsumeStep(undefined)
setConsumeStepText(undefined)
setIsLoading(false)
}
return undefined
}
return { consume, consumeStep, consumeStepText, consumeError, isLoading }
}
export { useConsume }
export default useConsume

View File

@ -1,244 +0,0 @@
import { Asset, Config, LoggerInstance } from '@oceanprotocol/lib'
import { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-core'
import { Decimal } from 'decimal.js'
import {
getCreatePricingPoolFeedback,
getCreatePricingExchangeFeedback,
getBuyDTFeedback,
getCreateFreePricingFeedback,
getDispenseFeedback
} from '@utils/feedback'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
interface UsePricing {
getDTSymbol: (ddo: Asset) => Promise<string>
getDTName: (ddo: Asset) => Promise<string>
mint: (tokensToMint: string, ddo: Asset) => Promise<TransactionReceipt | void>
buyDT: (
amountDataToken: number | string,
accessDetails: AccessDetails,
ddo: Asset
) => Promise<TransactionReceipt | void>
pricingStep?: number
pricingStepText?: string
pricingError?: string
pricingIsLoading: boolean
}
function usePricing(): UsePricing {
const { accountId, networkId } = useWeb3()
const [pricingIsLoading, setPricingIsLoading] = useState(false)
const [pricingStep, setPricingStep] = useState<number>()
const [pricingStepText, setPricingStepText] = useState<string>()
const [pricingError, setPricingError] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<Config>()
// Grab ocen config based on passed networkId
useEffect(() => {
if (!networkId) return
const oceanConfig = getOceanConfig(networkId)
setOceanConfig(oceanConfig)
}, [networkId])
async function getDTSymbol(ddo: Asset): Promise<string> {
if (!accountId) return
const { datatokens } = ddo
return datatokens[0].symbol
// return dataTokenInfo
// ? dataTokenInfo.symbol
// : await ocean?.datatokens.getSymbol(dataTokenInfo.address)
}
async function getDTName(ddo: Asset): Promise<string> {
if (!accountId) return
const { datatokens } = ddo
return datatokens[0].name
// return dataTokenInfo
// ? dataTokenInfo.name
// : await ocean?.datatokens.getName(dataTokenInfo.address)
}
// Helper for setting steps & feedback for all flows
async function setStep(
index: number,
type: 'pool' | 'exchange' | 'free' | 'buy' | 'dispense',
ddo: Asset
) {
const dtSymbol = await getDTSymbol(ddo)
setPricingStep(index)
if (!dtSymbol) return
let messages
switch (type) {
case 'pool':
messages = getCreatePricingPoolFeedback(dtSymbol)
break
case 'exchange':
messages = getCreatePricingExchangeFeedback(dtSymbol)
break
case 'free':
messages = getCreateFreePricingFeedback(dtSymbol)
break
case 'buy':
messages = getBuyDTFeedback(dtSymbol)
break
case 'dispense':
messages = getDispenseFeedback(dtSymbol)
break
}
setPricingStepText(messages[index])
}
async function mint(
tokensToMint: string,
ddo: Asset
): Promise<TransactionReceipt | void> {
const { datatokens } = ddo
LoggerInstance.log('mint function', datatokens[0].address, accountId)
// const balance = new Decimal(
// await ocean.datatokens.balance(dataTokenInfo.address, accountId)
// )
// const tokens = new Decimal(tokensToMint)
// if (tokens.greaterThan(balance)) {
// const mintAmount = tokens.minus(balance)
// const tx = await ocean.datatokens.mint(
// dataTokenInfo.address,
// accountId,
// mintAmount.toString()
// )
// return tx
// }
}
async function buyDT(
amountDataToken: number | string,
accessDetails: AccessDetails,
ddo: Asset
): Promise<TransactionReceipt | void> {
if (!accountId) return
let tx
try {
setPricingIsLoading(true)
setPricingError(undefined)
setStep(1, 'buy', ddo)
LoggerInstance.log('Price found for buying', accessDetails)
Decimal.set({ precision: 18 })
switch (accessDetails?.type) {
case 'dynamic': {
const oceanAmmount = new Decimal(accessDetails.price)
.times(1.05)
.toString()
const maxPrice = new Decimal(accessDetails.price).times(2).toString()
setStep(2, 'buy', ddo)
LoggerInstance.log(
'Buying token from pool',
accessDetails,
accountId,
oceanAmmount,
maxPrice
)
// tx = await ocean.pool.buyDT(
// accountId,
// price.address,
// String(amountDataToken),
// oceanAmmount,
// maxPrice
// )
setStep(3, 'buy', ddo)
LoggerInstance.log('DT buy response', tx)
break
}
case 'fixed': {
if (!oceanConfig.oceanTokenAddress) {
LoggerInstance.error(`'oceanTokenAddress' not set in oceanConfig`)
return
}
if (!oceanConfig.fixedRateExchangeAddress) {
LoggerInstance.error(
`'fixedRateExchangeAddress' not set in oceanConfig`
)
return
}
LoggerInstance.log(
'Buying token from exchange',
accessDetails,
accountId
)
// await ocean.datatokens.approve(
// oceanConfig.oceanTokenAddress,
// oceanConfig.fixedRateExchangeAddress,
// `${price.value}`,
// accountId
// )
setStep(2, 'buy', ddo)
// tx = await ocean.fixedRateExchange.buyDT(
// price.address,
// `${amountDataToken}`,
// accountId
// )
setStep(3, 'buy', ddo)
LoggerInstance.log('DT exchange buy response', tx)
break
}
case 'free': {
setStep(1, 'dispense', ddo)
// const isDispensable = await ocean.OceanDispenser.isDispensable(
// ddo?.services[0].datatokenAddress,
// accountId,
// '1'
// )
// if (!isDispensable) {
// LoggerInstance.error(
// `Dispenser for ${ddo?.services[0].datatokenAddress} failed to dispense`
// )
// return
// }
// tx = await ocean.OceanDispenser.dispense(
// ddo?.services[0].datatokenAddress,
// accountId,
// '1'
// )
setStep(2, 'dispense', ddo)
LoggerInstance.log('DT dispense response', tx)
break
}
}
} catch (error) {
setPricingError(error.message)
LoggerInstance.error(error)
} finally {
setStep(0, 'buy', ddo)
setPricingStepText(undefined)
setPricingIsLoading(false)
}
return tx
}
return {
getDTSymbol,
getDTName,
buyDT,
mint,
pricingStep,
pricingStepText,
pricingIsLoading,
pricingError
}
}
export { usePricing }
export default usePricing

View File

@ -1,12 +1,6 @@
import { UseSiteMetadata } from './types' import { UseSiteMetadata } from './types'
import siteContent from '../../../content/site.json' import { getSiteMetadata } from '@utils/siteConfig'
import appConfig from '../../../app.config'
export function useSiteMetadata(): UseSiteMetadata { export function useSiteMetadata(): UseSiteMetadata {
const siteMeta: UseSiteMetadata = { return getSiteMetadata()
...siteContent,
appConfig
}
return siteMeta
} }

View File

@ -23,6 +23,12 @@ export interface UseSiteMetadata {
chainIds: number[] chainIds: number[]
chainIdsSupported: number[] chainIdsSupported: number[]
marketFeeAddress: string marketFeeAddress: string
publisherMarketOrderFee: string
publisherMarketPoolSwapFee: string
publisherMarketFixedSwapFee: string
consumeMarketOrderFee: string
consumeMarketPoolSwapFee: string
consumeMarketFixedSwapFee: string
currencies: string[] currencies: string[]
portisId: string portisId: string
allowFixedPricing: string allowFixedPricing: string

51
src/@types/Price.d.ts vendored
View File

@ -1,14 +1,54 @@
import { ProviderFees } from '@oceanprotocol/lib'
/**
* @interface OrderPriceAndFee
* @prop {string} price total price including fees
* @prop {string} publisherMarketOrderFee fee received by the market where the asset was published. It is set on erc20 creation. It is a absolute value
* @prop {string} publisherMarketPoolSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage
* @prop {string} publisherMarketFixedSwapFee fee received by the market where the asset was published on any swap (pool or fre). Absolute value based on the configured percentage
* @prop {string} consumeMarketOrderFee fee received by the market where the asset is ordered. It is set on erc20 creation. It is a absolute value
* @prop {string} consumeMarketPoolSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage
* @prop {string} consumeMarketFixedSwapFee fee received by the market where the asset is ordered on any swap (pool or fre). Absolute value based on the configured percentage
* @prop {string} liquidityProviderSwapFee fee received by the liquidity providers of the pool. It is a percentage ( ex 50% means liquidityProviderSwapFee=0.5)
* @prop {ProviderFees} providerFee received from provider
* @prop {string} opcFee ocean protocol community fee, Absolute value based on the configured percentage
*/
interface OrderPriceAndFees {
price: string
publisherMarketOrderFee: string
publisherMarketPoolSwapFee: string
publisherMarketFixedSwapFee: string
consumeMarketOrderFee: string
consumeMarketPoolSwapFee: string
consumeMarketFixedSwapFee: string
liquidityProviderSwapFee: string
providerFee: ProviderFees
opcFee: string
}
/**
* @interface AccessDetails
* @prop {'dynamic' | 'fixed' | 'free' | ''} type
* @prop {string} price can be either spotPrice/rate
* @prop {string} addressOrId if type is dynamic this is the pool address, for fixed/free this is an id.
* @prop {TokenInfo} baseToken
* @prop {TokenInfo} datatoken
* @prop {bool} isPurchasable checks if you can buy a datatoken from fixed rate exchange/pool/dispenser. For pool it also checks if there is enough dt liquidity
* @prop {bool} isOwned checks if there are valid orders for this, it also takes in consideration timeout
* @prop {string} validOrderTx the latest valid order tx, it also takes in consideration timeout
* @prop {string} publisherMarketOrderFee this is here just because it's more efficient, it's allready in the query
* @prop {FeeInfo} feeInfo values of the relevant fees
*/
interface AccessDetails { interface AccessDetails {
type: 'dynamic' | 'fixed' | 'free' | '' type: 'dynamic' | 'fixed' | 'free' | ''
price: number price: string
// if type is dynamic this is the pool address, for fixed/free this is an id.
addressOrId: string addressOrId: string
baseToken: TokenInfo baseToken: TokenInfo
datatoken: TokenInfo datatoken: TokenInfo
isConsumable?: boolean isPurchasable?: boolean
// if there are valid orders for this isOwned: bool
owned: bool
validOrderTx: string validOrderTx: string
publisherMarketOrderFee: string
} }
interface PriceOptions { interface PriceOptions {
@ -20,4 +60,5 @@ interface PriceOptions {
weightOnOcean: string weightOnOcean: string
// easier to keep this as number for Yup input validation // easier to keep this as number for Yup input validation
swapFee: number swapFee: number
freeAgreement: boolean
} }

View File

@ -20,7 +20,10 @@ interface SearchResponse {
_scroll_id?: string | undefined _scroll_id?: string | undefined
_shards: ShardsResponse _shards: ShardsResponse
hits: { hits: {
total: number total: {
relation: string
value: number
}
max_score: number max_score: number
hits: Array<{ hits: Array<{
_index: string _index: string

191
src/@utils/SvgWaves.ts Normal file
View File

@ -0,0 +1,191 @@
import { computeControlPoints } from './bezier-spline'
import { randomIntFromInterval } from './numbers'
export interface WaveProperties {
width?: number
height?: number
color?: string
fill?: boolean
layerCount?: number
pointsPerLayer?: number
variance?: number
maxOpacity?: number
opacitySteps?: number
}
class Point {
x: number
y: number
constructor(x: number, y: number) {
this.x = Math.floor(x)
this.y = Math.floor(y)
}
}
enum WaveColors {
Violet = '#e000cf',
Pink = '#ff4092',
Grey = '#8b98a9'
}
export class SvgWaves {
properties: WaveProperties
layers: Point[][]
static xmlns = 'http://www.w3.org/2000/svg'
/**
* Helper function to get randomly generated WaveProperties
* These are generated with a focus on small file size for the svg
* meaning low character count.
* - width & height: default is 2 digits max (99)
* - color pink is selected per default
* - set coloring to fill or stroke (fill is selected per default)
* - randomly decide if fill or stroke coloring should be used
* - create 4 layers with 4 - 5 points per layers
* -> results in random looking, yet small enough svgs
* - variance between 0.5 and 0.7 returns smooth & different results
*
* @returns new randomly generated WaveProperties
*/
static getProps(): WaveProperties {
return {
width: 99,
height: 99,
color: WaveColors.Pink,
fill: true,
layerCount: 4,
pointsPerLayer: randomIntFromInterval(3, 4),
variance: Math.random() * 0.2 + 0.5, // 0.5 - 0.7
maxOpacity: 255, // 0xff
opacitySteps: 68 // 0x44
}
}
static generatePoint(
x: number,
y: number,
moveX: number,
moveY: number,
width: number
): Point {
const varianceY = y - moveY / 2 + Math.random() * moveY
const varianceX =
x === 0 || x === width ? x : x - moveX / 2 + Math.random() * moveX
return new Point(varianceX, varianceY)
}
constructor() {
this.properties = SvgWaves.getProps()
this.layers = this.generateLayers()
}
generateLayers(): Point[][] {
const { width, height, layerCount, pointsPerLayer, variance } =
this.properties
const cellWidth = width / pointsPerLayer
const cellHeight = height / layerCount
// define movement constraints for point generation
// lower smoothness results in steeper curves in waves
const horizontalSmoothness = 0.5
const moveX = cellWidth * variance * (1 - horizontalSmoothness)
const moveY = cellHeight * variance
const layers = []
for (let y = cellHeight; y < height; y += cellHeight) {
const points = []
for (let x = 0; x <= width; x += cellWidth) {
points.push(SvgWaves.generatePoint(x, y, moveX, moveY, width))
}
layers.push(points)
}
return layers
}
generateSvg(): Element {
const svg = document.createElementNS(SvgWaves.xmlns, 'svg')
svg.setAttribute('width', this.properties.width.toString())
svg.setAttribute('height', this.properties.height.toString())
svg.setAttribute('fill', this.properties.fill ? undefined : 'transparent')
svg.setAttribute('xmlns', SvgWaves.xmlns)
for (let i = 0; i < this.layers.length; i++) {
const path = this.generatePath(
this.layers[i],
this.getOpacityForLayer(i + 1)
)
svg.appendChild(path)
}
return svg
}
generatePath(points: Point[], opacity = 'ff'): Element {
const xPoints = points.map((p) => Math.floor(p.x))
const yPoints = points.map((p) => Math.floor(p.y))
// get bezier control points
const xControlPoints = computeControlPoints(xPoints)
const yControlPoints = computeControlPoints(yPoints)
// Should the path be closed & filled or stroke only
const closed = this.properties.fill
// For closed paths define bottom corners as start & end point
const bottomLeftPoint = new Point(0, this.properties.height)
const bottomRightPoint = new Point(
this.properties.width,
this.properties.height
)
const startPoint = closed ? bottomLeftPoint : points[0]
const endPoint = closed ? bottomRightPoint : points[points.length - 1]
// start constructing the 'd' attribute for the <path>
let path = `M${startPoint.x},${startPoint.y}`
// if starting in bottom corner, move to start of wave first
if (closed) path += `L${xPoints[0]},${yPoints[0]}`
// add path curves
for (let i = 0; i < xPoints.length - 1; i++) {
path +=
`C${xControlPoints.p1[i]},${yControlPoints.p1[i]} ` +
`${xControlPoints.p2[i]},${yControlPoints.p2[i]} ` +
`${xPoints[i + 1]},${yPoints[i + 1]}`
}
path = closed
? `${path}L${endPoint.x},${endPoint.y}Z` // if closing in bottom corners move there and close
: `${path}AZ` // else just close the path
// create the path element
const svgPath = document.createElementNS(SvgWaves.xmlns, 'path')
const colorStyle = closed ? 'fill' : 'stroke'
svgPath.setAttributeNS(
null,
colorStyle,
`${this.properties.color}${opacity}`
)
svgPath.setAttributeNS(null, 'd', path)
return svgPath
}
getOpacityForLayer(layer: number): string {
const { layerCount, maxOpacity, opacitySteps } = this.properties
const minOpacity = maxOpacity - (layerCount - 1) * opacitySteps
// calculate decimal opacity value for layer
const opacityDec = minOpacity + layer * opacitySteps
// translate to hex string
return opacityDec.toString(16)
}
setProps(properties: WaveProperties): void {
this.properties = { ...SvgWaves.getProps(), ...properties }
this.layers = this.generateLayers()
}
}

View File

@ -8,8 +8,13 @@ import {
TokensPriceQuery, TokensPriceQuery,
TokensPriceQuery_tokens as TokensPrice TokensPriceQuery_tokens as TokensPrice
} from '../@types/subgraph/TokensPriceQuery' } from '../@types/subgraph/TokensPriceQuery'
import { Asset } from '@oceanprotocol/lib' import { Asset, ProviderInstance } from '@oceanprotocol/lib'
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
import { calculateBuyPrice } from './pool'
import { getFixedBuyPrice } from './fixedRateExchange'
import { getSiteMetadata } from './siteConfig'
import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price'
import Decimal from 'decimal.js'
const TokensPriceQuery = gql` const TokensPriceQuery = gql`
query TokensPriceQuery($datatokenIds: [ID!], $account: String) { query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
@ -39,6 +44,7 @@ const TokensPriceQuery = gql`
} }
fixedRateExchanges { fixedRateExchanges {
id id
exchangeId
price price
baseToken { baseToken {
symbol symbol
@ -99,6 +105,7 @@ const TokenPriceQuery = gql`
} }
fixedRateExchanges { fixedRateExchanges {
id id
exchangeId
price price
baseToken { baseToken {
symbol symbol
@ -132,25 +139,33 @@ const TokenPriceQuery = gql`
} }
` `
// TODO: fill in fees after subgraph update
function getAccessDetailsFromTokenPrice( function getAccessDetailsFromTokenPrice(
tokenPrice: TokenPrice | TokensPrice, tokenPrice: TokenPrice | TokensPrice,
timeout?: number timeout?: number
): AccessDetails { ): AccessDetails {
const accessDetails = {} as AccessDetails const accessDetails = {} as AccessDetails
if (
if (!timeout && !tokenPrice.orders && tokenPrice.orders.length > 0) { tokenPrice &&
timeout &&
tokenPrice.orders &&
tokenPrice.orders.length > 0
) {
const order = tokenPrice.orders[0] const order = tokenPrice.orders[0]
accessDetails.owned = Date.now() / 1000 - order.createdTimestamp < timeout accessDetails.isOwned = Date.now() / 1000 - order.createdTimestamp < timeout
accessDetails.validOrderTx = order.tx accessDetails.validOrderTx = order.tx
} }
// TODO: fetch order fee from sub query
accessDetails.publisherMarketOrderFee = '0'
// free is always the best price // free is always the best price
if (tokenPrice.dispensers && tokenPrice.dispensers.length > 0) { if (tokenPrice.dispensers && tokenPrice.dispensers.length > 0) {
const dispenser = tokenPrice.dispensers[0] const dispenser = tokenPrice.dispensers[0]
accessDetails.type = 'free' accessDetails.type = 'free'
accessDetails.addressOrId = dispenser.id accessDetails.addressOrId = dispenser.token.id
accessDetails.price = 0 accessDetails.price = '0'
accessDetails.isConsumable = dispenser.active accessDetails.isPurchasable = dispenser.active
accessDetails.datatoken = { accessDetails.datatoken = {
address: dispenser.token.id, address: dispenser.token.id,
name: dispenser.token.name, name: dispenser.token.name,
@ -164,21 +179,21 @@ function getAccessDetailsFromTokenPrice(
tokenPrice.fixedRateExchanges && tokenPrice.fixedRateExchanges &&
tokenPrice.fixedRateExchanges.length > 0 tokenPrice.fixedRateExchanges.length > 0
) { ) {
const fre = tokenPrice.fixedRateExchanges[0] const fixed = tokenPrice.fixedRateExchanges[0]
accessDetails.type = 'fixed' accessDetails.type = 'fixed'
accessDetails.addressOrId = fre.id accessDetails.addressOrId = fixed.exchangeId
accessDetails.price = fre.price accessDetails.price = fixed.price
// in theory we should check dt balance here, we can skip this because in the market we always create fre with minting capabilities. // in theory we should check dt balance here, we can skip this because in the market we always create fre with minting capabilities.
accessDetails.isConsumable = fre.active accessDetails.isPurchasable = fixed.active
accessDetails.baseToken = { accessDetails.baseToken = {
address: fre.baseToken.address, address: fixed.baseToken.address,
name: fre.baseToken.name, name: fixed.baseToken.name,
symbol: fre.baseToken.symbol symbol: fixed.baseToken.symbol
} }
accessDetails.datatoken = { accessDetails.datatoken = {
address: fre.datatoken.address, address: fixed.datatoken.address,
name: fre.datatoken.name, name: fixed.datatoken.name,
symbol: fre.datatoken.symbol symbol: fixed.datatoken.symbol
} }
return accessDetails return accessDetails
} }
@ -188,10 +203,10 @@ function getAccessDetailsFromTokenPrice(
const pool = tokenPrice.pools[0] const pool = tokenPrice.pools[0]
accessDetails.type = 'dynamic' accessDetails.type = 'dynamic'
accessDetails.addressOrId = pool.id accessDetails.addressOrId = pool.id
// TODO: this needs to be consumePrice
accessDetails.price = pool.spotPrice accessDetails.price = pool.spotPrice
// TODO: pool.datatokenLiquidity > 3 is kinda random here, we shouldn't run into this anymore now , needs more thinking/testing // TODO: pool.datatokenLiquidity > 3 is kinda random here, we shouldn't run into this anymore now , needs more thinking/testing
accessDetails.isConsumable = pool.isFinalized && pool.datatokenLiquidity > 3 accessDetails.isPurchasable =
pool.isFinalized && pool.datatokenLiquidity > 3
accessDetails.baseToken = { accessDetails.baseToken = {
address: pool.baseToken.address, address: pool.baseToken.address,
name: pool.baseToken.name, name: pool.baseToken.name,
@ -208,20 +223,92 @@ function getAccessDetailsFromTokenPrice(
} }
/** /**
* returns various consume details for the desired datatoken * This will be used to get price including feed before ordering
* @param chain chain on which the datatoken is preset * @param {AssetExtended} asset
* @param datatokenAddress address of the datatoken * @return {Promise<OrdePriceAndFee>}
* @param timeout timeout of the service, only needed if you want order details like owned and validOrderId */
* @param account account that wants to consume, only needed if you want order details like owned and validOrderId export async function getOrderPriceAndFees(
* @returns AccessDetails asset: AssetExtended,
accountId?: string
): Promise<OrderPriceAndFees> {
const orderPriceAndFee = {
price: '0',
publisherMarketOrderFee: '0',
publisherMarketPoolSwapFee: '0',
publisherMarketFixedSwapFee: '0',
consumeMarketOrderFee: '0',
consumeMarketPoolSwapFee: '0',
consumeMarketFixedSwapFee: '0',
providerFee: {},
opcFee: '0'
} as OrderPriceAndFees
const { accessDetails } = asset
const { appConfig } = getSiteMetadata()
// fetch publish market order fee
orderPriceAndFee.publisherMarketOrderFee =
asset.accessDetails.publisherMarketOrderFee
// fetch consume market order fee
orderPriceAndFee.consumeMarketOrderFee = appConfig.consumeMarketOrderFee
// fetch provider fee
const initializeData = await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
)
orderPriceAndFee.providerFee = initializeData.providerFee
// fetch price and swap fees
switch (accessDetails.type) {
case 'dynamic': {
const poolPrice = await calculateBuyPrice(accessDetails, asset.chainId)
orderPriceAndFee.price = poolPrice.tokenAmount
orderPriceAndFee.liquidityProviderSwapFee =
poolPrice.liquidityProviderSwapFeeAmount
orderPriceAndFee.publisherMarketPoolSwapFee =
poolPrice.publishMarketSwapFeeAmount
orderPriceAndFee.consumeMarketPoolSwapFee =
poolPrice.consumeMarketSwapFeeAmount
break
}
case 'fixed': {
const fixed = await getFixedBuyPrice(accessDetails, asset.chainId)
orderPriceAndFee.price = fixed.baseTokenAmount
orderPriceAndFee.opcFee = fixed.oceanFeeAmount
orderPriceAndFee.publisherMarketFixedSwapFee = fixed.marketFeeAmount
// hack because we don't have it in contracts
orderPriceAndFee.consumeMarketFixedSwapFee = fixed.consumeMarketFeeAmount
break
}
}
// calculate full price, we assume that all the values are in ocean, otherwise this will be incorrect
orderPriceAndFee.price = new Decimal(orderPriceAndFee.price)
.add(new Decimal(orderPriceAndFee.consumeMarketOrderFee))
.add(new Decimal(orderPriceAndFee.publisherMarketOrderFee))
.add(new Decimal(orderPriceAndFee.providerFee.providerFeeAmount))
.toString()
return orderPriceAndFee
}
/**
* @param {number} chain
* @param {string} datatokenAddress
* @param {number=} timeout timout of the service, this is needed to return order details
* @param {string=} account account that wants to buy, is needed to return order details
* @param {bool=} includeOrderPriceAndFees if false price will be spot price (pool) and rate (fre), if true you will get the order price including fees !! fees not yet done
* @returns {Promise<AccessDetails>}
*/ */
export async function getAccessDetails( export async function getAccessDetails(
chain: number, chainId: number,
datatokenAddress: string, datatokenAddress: string,
timeout?: number, timeout?: number,
account = '' account = ''
): Promise<AccessDetails> { ): Promise<AccessDetails> {
const queryContext = getQueryContext(Number(chain)) const queryContext = getQueryContext(Number(chainId))
const tokenQueryResult: OperationResult< const tokenQueryResult: OperationResult<
TokenPriceQuery, TokenPriceQuery,
{ datatokenId: string; account: string } { datatokenId: string; account: string }
@ -229,7 +316,7 @@ export async function getAccessDetails(
TokenPriceQuery, TokenPriceQuery,
{ {
datatokenId: datatokenAddress.toLowerCase(), datatokenId: datatokenAddress.toLowerCase(),
account: account.toLowerCase() account: account?.toLowerCase()
}, },
queryContext queryContext
) )
@ -247,15 +334,14 @@ export async function getAccessDetailsForAssets(
const chainAssetLists: { [key: number]: string[] } = {} const chainAssetLists: { [key: number]: string[] } = {}
for (const asset of assets) { for (const asset of assets) {
// harcoded until we have chainId on assets
if (chainAssetLists[asset.chainId]) { if (chainAssetLists[asset.chainId]) {
chainAssetLists[asset.chainId].push( chainAssetLists[asset.chainId].push(
asset?.services[0].datatokenAddress.toLowerCase() asset.services[0].datatokenAddress.toLowerCase()
) )
} else { } else {
chainAssetLists[asset.chainId] = [] chainAssetLists[asset.chainId] = []
chainAssetLists[asset.chainId].push( chainAssetLists[asset.chainId].push(
asset?.services[0].datatokenAddress.toLowerCase() asset.services[0].datatokenAddress.toLowerCase()
) )
} }
} }
@ -264,20 +350,24 @@ export async function getAccessDetailsForAssets(
const queryContext = getQueryContext(Number(chainKey)) const queryContext = getQueryContext(Number(chainKey))
const tokenQueryResult: OperationResult< const tokenQueryResult: OperationResult<
TokensPriceQuery, TokensPriceQuery,
{ datatokenId: string; account: string } { datatokenIds: [string]; account: string }
> = await fetchData( > = await fetchData(
TokensPriceQuery, TokensPriceQuery,
{ {
datatokenIds: chainAssetLists[chainKey], datatokenIds: chainAssetLists[chainKey],
account: account.toLowerCase() account: account?.toLowerCase()
}, },
queryContext queryContext
) )
tokenQueryResult.data?.tokens.forEach((token) => { tokenQueryResult.data?.tokens.forEach((token) => {
const accessDetails = getAccessDetailsFromTokenPrice(token)
const currentAsset = assetsExtended.find( const currentAsset = assetsExtended.find(
(asset) => asset.services[0].datatokenAddress.toLowerCase() === token.id (asset) => asset.services[0].datatokenAddress.toLowerCase() === token.id
) )
const accessDetails = getAccessDetailsFromTokenPrice(
token,
currentAsset?.services[0]?.timeout
)
currentAsset.accessDetails = accessDetails currentAsset.accessDetails = accessDetails
}) })
} }

View File

@ -79,7 +79,7 @@ export function transformQueryResult(
result.results = (queryResult.hits.hits || []).map( result.results = (queryResult.hits.hits || []).map(
(hit) => hit._source as Asset (hit) => hit._source as Asset
) )
result.totalResults = queryResult.hits.total result.totalResults = queryResult.hits.total.value
result.totalPages = result.totalPages =
result.totalResults / size < 1 result.totalResults / size < 1
? Math.floor(result.totalResults / size) ? Math.floor(result.totalResults / size)
@ -163,7 +163,7 @@ export async function getAssetsFromDidList(
const baseParams = { const baseParams = {
chainIds: chainIds, chainIds: chainIds,
filters: [getFilterTerm('id', didList)], filters: [getFilterTerm('_id', didList)],
ignorePurgatory: true ignorePurgatory: true
} as BaseQueryParams } as BaseQueryParams
const query = generateBaseQuery(baseParams) const query = generateBaseQuery(baseParams)
@ -180,19 +180,21 @@ export async function retrieveDDOListByDIDs(
chainIds: number[], chainIds: number[],
cancelToken: CancelToken cancelToken: CancelToken
): Promise<Asset[]> { ): Promise<Asset[]> {
if (didList?.length === 0 || chainIds?.length === 0) return []
try { try {
if (didList?.length === 0 || chainIds?.length === 0) return []
const orderedDDOListByDIDList: Asset[] = [] const orderedDDOListByDIDList: Asset[] = []
const baseQueryparams = { const baseQueryparams = {
chainIds, chainIds,
filters: [getFilterTerm('id', didList)], filters: [getFilterTerm('_id', didList)],
ignorePurgatory: true ignorePurgatory: true
} as BaseQueryParams } as BaseQueryParams
const query = generateBaseQuery(baseQueryparams) const query = generateBaseQuery(baseQueryparams)
const result = await queryMetadata(query, cancelToken) const result = await queryMetadata(query, cancelToken)
didList.forEach((did: string) => { didList.forEach((did: string) => {
const ddo = result.results.find((ddo: Asset) => ddo.id === did) const ddo = result.results.find((ddo: Asset) => ddo.id === did)
orderedDDOListByDIDList.push(ddo) if (ddo) orderedDDOListByDIDList.push(ddo)
}) })
return orderedDDOListByDIDList return orderedDDOListByDIDList
} catch (error) { } catch (error) {
@ -330,22 +332,21 @@ export async function getPublishedAssets(
} }
export async function getDownloadAssets( export async function getDownloadAssets(
didList: string[], dtList: string[],
tokenOrders: OrdersData[], tokenOrders: OrdersData[],
chainIds: number[], chainIds: number[],
cancelToken: CancelToken cancelToken: CancelToken
): Promise<DownloadedAsset[]> { ): Promise<DownloadedAsset[]> {
const baseQueryparams = {
chainIds,
filters: [
getFilterTerm('services.datatokenAddress', dtList),
getFilterTerm('services.type', 'access')
]
} as BaseQueryParams
const query = generateBaseQuery(baseQueryparams)
try { try {
const baseQueryparams = {
chainIds,
filters: [
getFilterTerm('id', didList),
getFilterTerm('service.type', 'access')
]
} as BaseQueryParams
const query = generateBaseQuery(baseQueryparams)
const result = await queryMetadata(query, cancelToken) const result = await queryMetadata(query, cancelToken)
const downloadedAssets: DownloadedAsset[] = result.results const downloadedAssets: DownloadedAsset[] = result.results
.map((asset) => { .map((asset) => {
const order = tokenOrders.find( const order = tokenOrders.find(
@ -365,6 +366,10 @@ export async function getDownloadAssets(
return downloadedAssets return downloadedAssets
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) if (axios.isCancel(error)) {
LoggerInstance.log(error.message)
} else {
LoggerInstance.error(error.message)
}
} }
} }

View File

@ -0,0 +1,63 @@
/* bezier-spline.js
*
* computes cubic bezier coefficients to generate a smooth
* line through specified points. couples with SVG graphics
* for interactive processing.
*
* For more info see:
* http://www.particleincell.com/2012/bezier-splines/
*
* Lubos Brieda, Particle In Cell Consulting LLC, 2012
* you may freely use this algorithm in your codes however where feasible
* please include a link/reference to the source article
*/
/* computes control points given knots K, this is the brain of the operation */
export function computeControlPoints(K: number[]) {
const p1 = []
const p2 = []
const n = K.length - 1
/* rhs vector */
const a = []
const b = []
const c = []
const r = []
/* left most segment */
a[0] = 0
b[0] = 2
c[0] = 1
r[0] = K[0] + 2 * K[1]
/* internal segments */
for (let i = 1; i < n - 1; i++) {
a[i] = 1
b[i] = 4
c[i] = 1
r[i] = 4 * K[i] + 2 * K[i + 1]
}
/* right segment */
a[n - 1] = 2
b[n - 1] = 7
c[n - 1] = 0
r[n - 1] = 8 * K[n - 1] + K[n]
/* solves Ax=b with the Thomas algorithm (from Wikipedia) */
for (let i = 1; i < n; i++) {
const m: number = a[i] / b[i - 1]
b[i] = b[i] - m * c[i - 1]
r[i] = r[i] - m * r[i - 1]
}
p1[n - 1] = r[n - 1] / b[n - 1]
for (let i = n - 2; i >= 0; --i) p1[i] = (r[i] - c[i] * p1[i + 1]) / b[i]
/* we have p1, now compute p2 */
for (let i = 0; i < n - 1; i++) p2[i] = 2 * K[i + 1] - p1[i + 1]
p2[n - 1] = 0.5 * (K[n] + p1[n - 1])
return { p1: p1.map((p) => Math.floor(p)), p2: p2.map((p) => Math.floor(p)) }
}

View File

@ -1,89 +1,19 @@
export const feedback: { [key in number]: string } = { // TODO: can be better
99: 'Decrypting file URL...', export function getOrderFeedback(
0: '1/3 Looking for data token. Buying if none found...', baseTokenSymbol: string,
1: '2/3 Transfering data token.', datatokenSymbol: string
2: '3/3 Payment confirmed. Requesting access...' ): { [key in number]: string } {
} return {
0: `Approving and buying one ${datatokenSymbol} from pool`,
export const publishFeedback: { [key in number]: string } = { 1: `Ordering asset`,
0: '1/5 Creating datatoken ...', 2: `Approving ${baseTokenSymbol} and ordering asset`,
2: '2/5 Encrypting files ...', 3: 'Generating signature to access download url'
4: '3/5 Storing ddo ...', }
6: '4/5 Minting tokens ...',
8: '5/5 Asset published succesfully'
}
// TODO: do something with this object,
// consumeStep should probably return one of those strings
// instead of just a number
export const consumeFeedback: { [key in number]: string } = {
...feedback,
3: '3/3 Access granted. Consuming file...'
} }
// TODO: customize for compute // TODO: customize for compute
export const computeFeedback: { [key in number]: string } = { export const computeFeedback: { [key in number]: string } = {
0: '1/3 Ordering asset...', 0: 'Ordering asset...',
1: '2/3 Transfering data token.', 1: 'Transfering datatoken.',
2: '3/3 Access granted. Starting job...' 2: 'Access granted. Starting job...'
}
export function getCreatePricingPoolFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
99: `Minting ${dtSymbol} ...`,
0: 'Creating pool ...',
1: `Approving ${dtSymbol} ...`,
2: 'Approving OCEAN ...',
3: 'Setup pool ...',
4: 'Pool created.'
}
}
export function getCreatePricingExchangeFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
99: `Minting ${dtSymbol} ...`,
0: 'Creating exchange ...',
1: `Approving ${dtSymbol} ...`,
2: 'Fixed exchange created.'
}
}
export function getCreateFreePricingFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
99: `Creating ${dtSymbol} faucet...`,
0: 'Setting faucet as minter ...',
1: 'Approving minter...',
2: 'Faucet created.'
}
}
export function getBuyDTFeedback(dtSymbol: string): { [key: number]: string } {
return {
1: '1/3 Approving OCEAN ...',
2: `2/3 Buying ${dtSymbol} ...`,
3: `3/3 ${dtSymbol} bought.`
}
}
export function getSellDTFeedback(dtSymbol: string): { [key: number]: string } {
return {
1: '1/3 Approving OCEAN ...',
2: `2/3 Selling ${dtSymbol} ...`,
3: `3/3 ${dtSymbol} sold.`
}
}
export function getDispenseFeedback(dtSymbol: string): {
[key: number]: string
} {
return {
1: `1/2 Requesting ${dtSymbol}...`,
2: `2/2 Received ${dtSymbol}.`
}
} }

View File

@ -0,0 +1,34 @@
import { FixedRateExchange, PriceAndFees } from '@oceanprotocol/lib'
import { AccessDetails } from 'src/@types/Price'
import Web3 from 'web3'
import { getOceanConfig } from './ocean'
import { getDummyWeb3 } from './web3'
/**
* This is used to calculate the price to buy one datatoken from a fixed rate exchange. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created
* @param {AccessDetails} accessDetails
* @param {number} chainId
* @param {Web3?} web3
* @return {Promise<PriceAndFees>}
*/
export async function getFixedBuyPrice(
accessDetails: AccessDetails,
chainId?: number,
web3?: Web3
): Promise<PriceAndFees> {
if (!web3 && !chainId)
throw new Error("web3 and chainId can't be undefined at the same time!")
if (!web3) {
web3 = await getDummyWeb3(chainId)
}
const config = getOceanConfig(chainId)
const fixed = new FixedRateExchange(web3, config.fixedRateExchangeAddress)
const estimatedPrice = await fixed.calcBaseInGivenOutDT(
accessDetails.addressOrId,
'1'
)
return estimatedPrice
}

View File

@ -6,8 +6,8 @@ import matter from 'gray-matter'
// Next.js specifics to be used in getStaticProps / getStaticPaths // Next.js specifics to be used in getStaticProps / getStaticPaths
// to automatically generate pages from Markdown files in `src/pages/[slug].tsx`. // to automatically generate pages from Markdown files in `src/pages/[slug].tsx`.
// //
const pagesDirectory = join(process.cwd(), 'content', 'pages') // const pagesDirectory = join(process.cwd(), 'content', 'pages')
const pagesDirectory = './content/pages'
export interface PageData { export interface PageData {
slug: string slug: string
frontmatter: { [key: string]: any } frontmatter: { [key: string]: any }

View File

@ -1,4 +1,4 @@
import { renderStaticWaves } from './oceanWaves' import { SvgWaves } from './SvgWaves'
// https://docs.opensea.io/docs/metadata-standards // https://docs.opensea.io/docs/metadata-standards
export interface NftMetadata { export interface NftMetadata {
@ -21,6 +21,7 @@ function encodeSvg(svgString: string): string {
? '<svg' ? '<svg'
: '<svg xmlns="http://www.w3.org/2000/svg"' : '<svg xmlns="http://www.w3.org/2000/svg"'
) )
.replace('></path>', '/>')
.replace(/"/g, "'") .replace(/"/g, "'")
.replace(/%/g, '%25') .replace(/%/g, '%25')
.replace(/#/g, '%23') .replace(/#/g, '%23')
@ -32,41 +33,40 @@ function encodeSvg(svgString: string): string {
} }
export function generateNftMetadata(): NftMetadata { export function generateNftMetadata(): NftMetadata {
// TODO: crop image properly in the end as generated SVG waves are a super-wide image, const waves = new SvgWaves()
// and add a filled background deciding on either black or white. const svg = waves.generateSvg()
const image = renderStaticWaves()
// const image = new XMLSerializer().serializeToString(waves) // TODO: figure out if also image URI needs base64 encoding
// const image = `<svg><path d="M0 10.4304L16.3396 10.4304L8.88727 17.6833L10.2401 19L20 9.5L10.2401 0L8.88727 1.31491L16.3396 8.56959L0 8.56959V10.4304Z" /></svg>` // e.g. 'data:image/svg+xml;base64,'
// generated SVG embedded as 'data:image/svg+xml' and encoded characters
const imageData = `data:image/svg+xml,${encodeSvg(svg.outerHTML)}`
const newNft: NftMetadata = { const newNft: NftMetadata = {
name: 'Ocean Asset v4 NFT', name: 'Ocean Asset NFT',
symbol: 'OCEAN-NFT', symbol: 'OCEAN-NFT',
description: `This NFT represents an asset in the Ocean Protocol v4 ecosystem.`, description: `This NFT represents an asset in the Ocean Protocol v4 ecosystem.`,
// TODO: ideally this includes the final DID // TODO: ideally this includes the final DID
external_url: 'https://market.oceanprotocol.com', external_url: 'https://market.oceanprotocol.com',
background_color: '141414', // dark background background_color: '141414', // dark background
// TODO: figure out if also image URI needs base64 encoding image_data: imageData
// generated SVG embedded as 'data:image/svg+xml' and encoded characters
image_data: `data:image/svg+xml,${encodeSvg(image)}`
// generated SVG embedded as 'data:image/svg+xml;base64'
// image: `data:image/svg+xml;base64,${window?.btoa(image)}`
// image: `data:image/svg+xml;base64,${Buffer.from(image).toString('base64')}`
} }
return newNft return newNft
} }
export function generateNftCreateData(nftMetadata: NftMetadata): any { export function generateNftCreateData(nftMetadata: NftMetadata): any {
// TODO: figure out if Buffer.from method is working in browser in final build
// as BTOA is deprecated.
// tokenURI: window?.btoa(JSON.stringify(nftMetadata))
const encodedMetadata = Buffer.from(JSON.stringify(nftMetadata)).toString(
'base64'
)
const nftCreateData = { const nftCreateData = {
name: nftMetadata.name, name: nftMetadata.name,
symbol: nftMetadata.symbol, symbol: nftMetadata.symbol,
templateIndex: 1, templateIndex: 1,
// Gas estimation fails if we add our huge tokenURI tokenURI: `data:application/json;base64,${encodedMetadata}`
tokenURI: '{url:"https://coolImage.com, name: "just for test"}'
// TODO: figure out if Buffer.from method is working in browser in final build
// as BTOA is deprecated.
// tokenURI: window?.btoa(JSON.stringify(nftMetadata))
// tokenURI: Buffer.from(JSON.stringify(nftMetadata)).toString('base64') // should end up as data:application/json;base64
} }
return nftCreateData return nftCreateData

View File

@ -24,3 +24,9 @@ export function compareAsBN(balance: string, price: string): boolean {
return false return false
} }
} }
// Random number from interval
export function randomIntFromInterval(min: number, max: number): number {
// min and max are included
return Math.floor(Math.random() * (max - min + 1) + min)
}

View File

@ -1,49 +0,0 @@
import * as d3 from 'd3'
/*
* Ocean Protocol D3 waves
* https://oceanprotocol.com/art
* Based off of Bostock's Circle Wave
* https://bl.ocks.org/mbostock/2d466ec3417722e3568cd83fc35338e3
*/
export function renderStaticWaves(): string {
const svg = d3.create('svg')
const width = 1000
const height = 250
const x = d3.scaleLinear().range([0, width])
const angles = d3.range(Math.random(), 4 * Math.PI, Math.PI / 20)
const path = svg
// .append('rect')
// .attr('fill', '#fff')
// .attr('width', '100%')
// .attr('height', '100%')
.append('g')
.attr('transform', `translate(${width / -4}, ${height / 2})`)
.attr('fill', 'none')
.attr('stroke-width', 2)
.selectAll('path')
.data(['#FF4092', '#E000CF', '#8B98A9', '#E2E2E2'])
.enter()
.append('path')
.attr('stroke', (d) => d)
.style('mix-blend-mode', 'darken')
.datum((d, i) => {
return d3
.line()
.curve(d3.curveBasisOpen)
.x((angles: any) => x(angles / 4))
.y((angles: any) => {
const t = d3.now() / 3000
return (
Math.cos(angles * 8 - (i * 2 * Math.PI) / 10 + t) *
Math.pow((2 + Math.cos(angles - t)) / 2, 4) *
15
)
})
})
path.attr('d', (d) => d(angles as any))
return `<svg>${svg.node().innerHTML}</svg>`
}

101
src/@utils/order.ts Normal file
View File

@ -0,0 +1,101 @@
import {
approve,
Datatoken,
FreOrderParams,
OrderParams,
ProviderInstance
} from '@oceanprotocol/lib'
import { AssetExtended } from 'src/@types/AssetExtended'
import Web3 from 'web3'
import { getOceanConfig } from './ocean'
import { TransactionReceipt } from 'web3-eth'
import { getSiteMetadata } from './siteConfig'
import { OrderPriceAndFees } from 'src/@types/Price'
/**
* For pool you need to buy the datatoken beforehand, this always assumes you want to order the first service
* @param web3
* @param asset
* @param accountId
* @returns {TransactionReceipt} receipt of the order
*/
export async function order(
web3: Web3,
asset: AssetExtended,
orderPriceAndFees: OrderPriceAndFees,
accountId: string
): Promise<TransactionReceipt> {
const datatoken = new Datatoken(web3)
const config = getOceanConfig(asset.chainId)
const { appConfig } = getSiteMetadata()
const initializeData = await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
)
const orderParams = {
consumer: accountId,
serviceIndex: 0,
_providerFee: initializeData.providerFee,
_consumeMarketFee: {
consumeMarketFeeAddress: appConfig.marketFeeAddress,
consumeMarketFeeAmount: appConfig.consumeMarketOrderFee,
consumeMarketFeeToken: config.oceanTokenAddress
}
} as OrderParams
// TODO: we need to approve provider fee
switch (asset.accessDetails?.type) {
case 'fixed': {
// this assumes all fees are in ocean
const txApprove = await approve(
web3,
accountId,
asset.accessDetails.baseToken.address,
asset.accessDetails.datatoken.address,
orderPriceAndFees.price,
false
)
const freParams = {
exchangeContract: config.fixedRateExchangeAddress,
exchangeId: asset.accessDetails.addressOrId,
maxBaseTokenAmount: orderPriceAndFees.price,
swapMarketFee: appConfig.consumeMarketFixedSwapFee,
marketFeeAddress: appConfig.marketFeeAddress
} as FreOrderParams
const tx = await datatoken.buyFromFreAndOrder(
asset.accessDetails.datatoken.address,
accountId,
orderParams,
freParams
)
return tx
}
case 'dynamic': {
const tx = await datatoken.startOrder(
asset.accessDetails.datatoken.address,
accountId,
accountId,
0,
initializeData.providerFee
)
return tx
}
case 'free': {
const tx = await datatoken.buyFromDispenserAndOrder(
asset.services[0].datatokenAddress,
accountId,
orderParams,
config.dispenserAddress
)
return tx
}
}
}

75
src/@utils/pool.ts Normal file
View File

@ -0,0 +1,75 @@
import { approve, Pool, PoolPriceAndFees } from '@oceanprotocol/lib'
import Web3 from 'web3'
import { getSiteMetadata } from './siteConfig'
import { getDummyWeb3 } from './web3'
import { TransactionReceipt } from 'web3-eth'
import Decimal from 'decimal.js'
import { AccessDetails } from 'src/@types/Price'
/**
* This is used to calculate the price to buy one datatoken from a pool, that is different from spot price. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created
* @param {AccessDetails} accessDetails
* @param {Web3?} [web3]
* @param {number?} [chainId]
* @return {Promise<PoolPriceAndFees>}
*/
export async function calculateBuyPrice(
accessDetails: AccessDetails,
chainId?: number,
web3?: Web3
): Promise<PoolPriceAndFees> {
if (!web3 && !chainId)
throw new Error("web3 and chainId can't be undefined at the same time!")
if (!web3) {
web3 = await getDummyWeb3(chainId)
}
const pool = new Pool(web3)
const { appConfig } = getSiteMetadata()
const estimatedPrice = await pool.getAmountInExactOut(
accessDetails.addressOrId,
accessDetails.baseToken.address,
accessDetails.datatoken.address,
'1',
appConfig.consumeMarketPoolSwapFee
)
return estimatedPrice
}
export async function buyDtFromPool(
accessDetails: AccessDetails,
accountId: string,
web3: Web3
): Promise<TransactionReceipt> {
const pool = new Pool(web3)
const { appConfig } = getSiteMetadata()
// we need to calculate the actual price to buy one datatoken
const dtPrice = await calculateBuyPrice(accessDetails, null, web3)
const approveTx = await approve(
web3,
accountId,
accessDetails.baseToken.address,
accessDetails.addressOrId,
dtPrice.tokenAmount,
false
)
const result = await pool.swapExactAmountOut(
accountId,
accessDetails.addressOrId,
{
marketFeeAddress: appConfig.marketFeeAddress,
tokenIn: accessDetails.baseToken.address,
tokenOut: accessDetails.datatoken.address
},
{
// this is just to be safe
maxAmountIn: new Decimal(dtPrice.tokenAmount).mul(10).toString(),
swapMarketFee: appConfig.consumeMarketPoolSwapFee,
tokenAmountOut: '1'
}
)
return result
}

View File

@ -1,10 +1,13 @@
import { import {
downloadFileBrowser,
FileMetadata, FileMetadata,
LoggerInstance, LoggerInstance,
ProviderInstance ProviderInstance
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { AssetExtended } from 'src/@types/AssetExtended'
import Web3 from 'web3'
// TODO: Why do we have these functions ?!?!?! // TODO: Why do we have these one line functions ?!?!?!
export async function getEncryptedFiles( export async function getEncryptedFiles(
files: FileMetadata[], files: FileMetadata[],
providerUrl: string providerUrl: string
@ -46,3 +49,21 @@ export async function getFileUrlInfo(
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
} }
} }
export async function downloadFile(
web3: Web3,
asset: AssetExtended,
accountId: string,
validOrderTx?: string
) {
const downloadUrl = await ProviderInstance.getDownloadUrl(
asset.id,
accountId,
asset.services[0].id,
0,
validOrderTx || asset.accessDetails.validOrderTx,
asset.services[0].serviceEndpoint,
web3
)
await downloadFileBrowser(downloadUrl)
}

12
src/@utils/siteConfig.ts Normal file
View File

@ -0,0 +1,12 @@
import { UseSiteMetadata } from '@hooks/useSiteMetadata/types'
import siteContent from '../../content/site.json'
import appConfig from '../../app.config'
export function getSiteMetadata(): UseSiteMetadata {
const siteMeta: UseSiteMetadata = {
...siteContent,
appConfig
}
return siteMeta
}

View File

@ -12,7 +12,10 @@ import {
PoolShares as PoolSharesList, PoolShares as PoolSharesList,
PoolShares_poolShares as PoolShare PoolShares_poolShares as PoolShare
} from '../@types/subgraph/PoolShares' } from '../@types/subgraph/PoolShares'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import {
OrdersData_orders as OrdersData,
OrdersData_orders_datatoken as OrdersDatatoken
} from '../@types/subgraph/OrdersData'
import { UserSalesQuery as UsersSalesList } from '../@types/subgraph/UserSalesQuery' import { UserSalesQuery as UsersSalesList } from '../@types/subgraph/UserSalesQuery'
export interface UserLiquidity { export interface UserLiquidity {
@ -71,6 +74,7 @@ const HighestLiquidityAssets = gql`
symbol symbol
} }
baseTokenLiquidity baseTokenLiquidity
datatokenLiquidity
} }
} }
` `
@ -140,7 +144,11 @@ const UserTokenOrders = gql`
orderDirection: desc orderDirection: desc
where: { consumer: $user } where: { consumer: $user }
) { ) {
consumer {
id
}
datatoken { datatoken {
id
address address
symbol symbol
} }
@ -154,14 +162,11 @@ const UserTokenOrders = gql`
} }
` `
// TODO: counting orders might be enough here to get sales for a user
const UserSalesQuery = gql` const UserSalesQuery = gql`
query UserSalesQuery($userSalesId: ID) { query UserSalesQuery($user: String!) {
users(where: { id: $userSalesId }) { users(where: { id: $user }) {
id id
orders(first: 10000) { totalSales
id
}
} }
} }
` `
@ -294,6 +299,7 @@ export async function getHighestLiquidityDatatokens(
highestLiquidityAssets.sort( highestLiquidityAssets.sort(
(a, b) => b.baseTokenLiquidity - a.baseTokenLiquidity (a, b) => b.baseTokenLiquidity - a.baseTokenLiquidity
) )
for (let i = 0; i < highestLiquidityAssets.length; i++) { for (let i = 0; i < highestLiquidityAssets.length; i++) {
if (!highestLiquidityAssets[i]?.datatoken?.address) continue if (!highestLiquidityAssets[i]?.datatoken?.address) continue
dtList.push(highestLiquidityAssets[i].datatoken.address) dtList.push(highestLiquidityAssets[i].datatoken.address)
@ -376,9 +382,8 @@ export async function getUserTokenOrders(
variables, variables,
chainIds chainIds
) )
for (let i = 0; i < tokenOrders?.length; i++) { for (let i = 0; i < tokenOrders?.length; i++) {
tokenOrders[i].tokenOrders.forEach((tokenOrder: OrdersData) => { tokenOrders[i].orders.forEach((tokenOrder: OrdersData) => {
data.push(tokenOrder) data.push(tokenOrder)
}) })
} }
@ -393,7 +398,7 @@ export async function getUserSales(
accountId: string, accountId: string,
chainIds: number[] chainIds: number[]
): Promise<number> { ): Promise<number> {
const variables = { userSalesId: accountId?.toLowerCase() } const variables = { user: accountId?.toLowerCase() }
try { try {
const userSales = await fetchDataForMultipleChains( const userSales = await fetchDataForMultipleChains(
UserSalesQuery, UserSalesQuery,
@ -403,7 +408,7 @@ export async function getUserSales(
let salesSum = 0 let salesSum = 0
for (let i = 0; i < userSales.length; i++) { for (let i = 0; i < userSales.length; i++) {
if (userSales[i].users.length > 0) { if (userSales[i].users.length > 0) {
salesSum += userSales[i].users[0].nrSales salesSum += parseInt(userSales[i].users[0].totalSales)
} }
} }
return salesSum return salesSum
@ -432,7 +437,7 @@ export async function getTopAssetsPublishers(
if (publishersIndex === -1) { if (publishersIndex === -1) {
const publisher: AccountTeaserVM = { const publisher: AccountTeaserVM = {
address: fetchedUsers.data.users[i].id, address: fetchedUsers.data.users[i].id,
nrSales: fetchedUsers.data.users[i].orders.length nrSales: fetchedUsers.data.users[i].totalSales
} }
publisherSales.push(publisher) publisherSales.push(publisher)
} else { } else {

View File

@ -1,5 +1,6 @@
import { getNetworkDisplayName } from '@hooks/useNetworkMetadata' import { getNetworkDisplayName } from '@hooks/useNetworkMetadata'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import Web3 from 'web3'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
export function accountTruncate(account: string): string { export function accountTruncate(account: string): string {
@ -8,6 +9,15 @@ export function accountTruncate(account: string): string {
const truncated = account.replace(middle, '…') const truncated = account.replace(middle, '…')
return truncated return truncated
} }
/**
* returns a dummy web3 instance, only usable to get info from the chain
* @param chainId
* @returns Web3 instance
*/
export async function getDummyWeb3(chainId: number): Promise<Web3> {
const config = getOceanConfig(chainId)
return new Web3(config.nodeUri)
}
export async function addCustomNetwork( export async function addCustomNetwork(
web3Provider: any, web3Provider: any,

View File

@ -10,6 +10,7 @@ import { useIsMounted } from '@hooks/useIsMounted'
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing' import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -43,6 +44,7 @@ export default function AssetList({
noPublisher noPublisher
}: AssetListProps): ReactElement { }: AssetListProps): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>() const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>()
const [loading, setLoading] = useState<boolean>(isLoading) const [loading, setLoading] = useState<boolean>(isLoading)
const isMounted = useIsMounted() const isMounted = useIsMounted()
@ -51,14 +53,16 @@ export default function AssetList({
if (!assets) return if (!assets) return
setAssetsWithPrices(assets as AssetExtended[]) setAssetsWithPrices(assets as AssetExtended[])
setLoading(false) setLoading(false)
async function fetchPrices() { async function fetchPrices() {
const assetsWithPrices = await getAccessDetailsForAssets(assets) const assetsWithPrices = await getAccessDetailsForAssets(
assets,
accountId || ''
)
if (!isMounted()) return if (!isMounted()) return
setAssetsWithPrices([...assetsWithPrices]) setAssetsWithPrices([...assetsWithPrices])
} }
fetchPrices() fetchPrices()
}, [assets, isMounted]) }, [assets, isMounted, accountId])
// // This changes the page field inside the query // // This changes the page field inside the query
function handlePageChange(selected: number) { function handlePageChange(selected: number) {

View File

@ -31,6 +31,8 @@ interface ButtonBuyProps {
algorithmConsumableStatus?: number algorithmConsumableStatus?: number
} }
// TODO: we need to take a look at these messages
function getConsumeHelpText( function getConsumeHelpText(
dtBalance: string, dtBalance: string,
dtSymbol: string, dtSymbol: string,

View File

@ -1,7 +1,8 @@
.datatoken { .datatoken {
display: grid; display: grid;
gap: var(--spacer); gap: var(--spacer);
grid-template-columns: 1fr 11fr; grid-template-columns: none;
margin-top: calc(var(--spacer) / 2);
margin-bottom: var(--spacer); margin-bottom: var(--spacer);
align-items: center; align-items: center;
} }
@ -20,9 +21,16 @@
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
fill: var(--brand-violet); fill: var(--brand-violet);
border-radius: 50%; border-radius: 50%;
margin: 0;
} }
.image svg { .image svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (min-width: 40rem) {
.datatoken {
grid-template-columns: 1fr 11fr;
}
}

View File

@ -0,0 +1,67 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { usePrices } from '@context/Prices'
import { useWeb3 } from '@context/Web3'
import useNftFactory from '@hooks/contracts/useNftFactory'
import { NftFactory } from '@oceanprotocol/lib'
import Conversion from '@shared/Price/Conversion'
import { generateNftCreateData, NftMetadata } from '@utils/nft'
const getEstGasFee = async (
address: string,
nftFactory: NftFactory,
nftMetadata: NftMetadata,
ethToOceanConversionRate: number
): Promise<string> => {
if (!address || !nftFactory || !nftMetadata || !ethToOceanConversionRate)
return
const { web3 } = nftFactory
const nft = generateNftCreateData(nftMetadata)
const gasPrice = await web3.eth.getGasPrice()
const gasLimit = await nftFactory?.estGasCreateNFT(address, nft)
const gasFeeEth = web3.utils.fromWei(
(+gasPrice * +gasLimit).toString(),
'ether'
)
const gasFeeOcean = (+gasFeeEth / +ethToOceanConversionRate).toString()
return gasFeeOcean
}
export default function TxFee({
nftMetadata
}: {
nftMetadata: NftMetadata
}): ReactElement {
const { accountId } = useWeb3()
const { prices } = usePrices()
const nftFactory = useNftFactory()
const [gasFee, setGasFee] = useState('')
useEffect(() => {
const calculateGasFee = async () =>
setGasFee(
await getEstGasFee(
accountId,
nftFactory,
nftMetadata,
(prices as any)?.eth
)
)
calculateGasFee()
}, [accountId, nftFactory, nftMetadata, prices])
return gasFee ? (
<p>
Gas fee estimation for this artwork
<Conversion price={gasFee} />
</p>
) : accountId ? (
<p>
An error occurred while estimating the gas fee for this artwork, please
try again.
</p>
) : (
<p>Please connect your wallet to get a gas fee estimate for this artwork</p>
)
}

View File

@ -1,7 +1,8 @@
.nft { .nft {
display: grid; display: grid;
gap: var(--spacer); gap: var(--spacer);
grid-template-columns: 1fr 11fr; grid-template-columns: none;
margin-top: calc(var(--spacer) / 2);
margin-bottom: var(--spacer); margin-bottom: var(--spacer);
align-items: center; align-items: center;
} }
@ -19,16 +20,32 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
position: relative; position: relative;
margin: 0;
} }
.refresh { .actions {
position: absolute; position: absolute;
right: calc(var(--spacer) / 4); left: 0;
bottom: calc(var(--spacer) / 4); bottom: 0;
padding: 0 calc(var(--spacer) / 4);
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.actions svg {
margin: 0;
fill: var(--font-color-text);
} }
.refresh svg { .refresh svg {
fill: var(--brand-pink); transform: scale(0.9);
width: var(--font-size-mini); transform-origin: center;
height: var(--font-size-mini); }
@media (min-width: 40rem) {
.nft {
grid-template-columns: 1fr 11fr;
}
} }

View File

@ -5,35 +5,43 @@ import { useField } from 'formik'
import React, { ReactElement, useEffect } from 'react' import React, { ReactElement, useEffect } from 'react'
import Refresh from '@images/refresh.svg' import Refresh from '@images/refresh.svg'
import styles from './index.module.css' import styles from './index.module.css'
import Tooltip from '@shared/atoms/Tooltip'
import TxFee from './TxFee'
export default function Nft(props: InputProps): ReactElement { export default function Nft(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name) const [field, meta, helpers] = useField(props.name)
const refreshNftMetadata = () => {
const nftMetadata = generateNftMetadata()
helpers.setValue({ ...nftMetadata })
}
// Generate on first mount // Generate on first mount
useEffect(() => { useEffect(() => {
if (field.value?.name !== '') return if (field.value?.name !== '') return
const nftOptions = generateNftMetadata() refreshNftMetadata()
helpers.setValue({ ...nftOptions })
}, [field.value?.name]) }, [field.value?.name])
return ( return (
<div className={styles.nft}> <div className={styles.nft}>
<figure className={styles.image}> <figure className={styles.image}>
<img src={field?.value?.image_data} width="128" height="128" /> <img src={field?.value?.image_data} width="128" height="128" />
<Button <div className={styles.actions}>
style="text" <Tooltip content={<TxFee nftMetadata={field.value} />} />
size="small" <Button
className={styles.refresh} style="text"
title="Generate new image" size="small"
onClick={(e) => { className={styles.refresh}
e.preventDefault() title="Generate new image"
const nftMetadata = generateNftMetadata() onClick={(e) => {
helpers.setValue({ ...nftMetadata }) e.preventDefault()
}} refreshNftMetadata()
> }}
<Refresh /> >
</Button> <Refresh />
</Button>
</div>
</figure> </figure>
<div className={styles.token}> <div className={styles.token}>

View File

@ -96,7 +96,6 @@
padding: 0; padding: 0;
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
font-size: var(--font-size-small); font-size: var(--font-size-small);
color: var(--font-color-text);
padding-left: 0.5rem; padding-left: 0.5rem;
} }

View File

@ -3,21 +3,24 @@ import styles from './index.module.css'
import Loader from '../atoms/Loader' import Loader from '../atoms/Loader'
import Tooltip from '../atoms/Tooltip' import Tooltip from '../atoms/Tooltip'
import PriceUnit from './PriceUnit' import PriceUnit from './PriceUnit'
import { AccessDetails, OrderPriceAndFees } from 'src/@types/Price'
export default function Price({ export default function Price({
accessDetails, accessDetails,
orderPriceAndFees,
className, className,
small, small,
conversion conversion
}: { }: {
accessDetails: AccessDetails accessDetails: AccessDetails
orderPriceAndFees?: OrderPriceAndFees
className?: string className?: string
small?: boolean small?: boolean
conversion?: boolean conversion?: boolean
}): ReactElement { }): ReactElement {
return accessDetails?.price || accessDetails?.type === 'free' ? ( return accessDetails?.price || accessDetails?.type === 'free' ? (
<PriceUnit <PriceUnit
price={`${accessDetails.price}`} price={`${orderPriceAndFees?.price || accessDetails?.price}`}
symbol={accessDetails.baseToken?.symbol} symbol={accessDetails.baseToken?.symbol}
className={className} className={className}
small={small} small={small}

View File

@ -45,6 +45,13 @@
margin-bottom: 0; margin-bottom: 0;
} }
.text code {
font-size: 0.8rem;
color: inherit;
padding-left: 0;
padding-right: 0;
}
.action, .action,
button.action { button.action {
margin-top: calc(var(--spacer) / 2); margin-top: calc(var(--spacer) / 2);

View File

@ -2,6 +2,8 @@
text-align: center; text-align: center;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: calc(var(--spacer) / 2); padding: calc(var(--spacer) / 2);
display: flex;
justify-content: center;
} }
.tab { .tab {
@ -15,7 +17,7 @@
background-color: var(--background-body); background-color: var(--background-body);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
margin-right: -1px; margin-right: -1px;
min-width: 100px; min-width: 90px;
} }
.tab:first-child { .tab:first-child {
@ -38,6 +40,10 @@
cursor: not-allowed; cursor: not-allowed;
} }
.tab > div {
margin: 0;
}
.tabContent { .tabContent {
padding: calc(var(--spacer) / 2); padding: calc(var(--spacer) / 2);
} }

View File

@ -1,5 +1,6 @@
import React, { ReactElement, ReactNode } from 'react' import React, { ReactElement, ReactNode } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs' import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import InputElement from '@shared/FormInput/InputElement'
import styles from './Tabs.module.css' import styles from './Tabs.module.css'
export interface TabsItem { export interface TabsItem {
@ -12,12 +13,14 @@ export default function Tabs({
items, items,
className, className,
handleTabChange, handleTabChange,
defaultIndex defaultIndex,
showRadio
}: { }: {
items: TabsItem[] items: TabsItem[]
className?: string className?: string
handleTabChange?: (tabName: string) => void handleTabChange?: (tabName: string) => void
defaultIndex?: number defaultIndex?: number
showRadio?: boolean
}): ReactElement { }): ReactElement {
return ( return (
<ReactTabs <ReactTabs
@ -25,14 +28,24 @@ export default function Tabs({
defaultIndex={defaultIndex} defaultIndex={defaultIndex}
> >
<TabList className={styles.tabList}> <TabList className={styles.tabList}>
{items.map((item) => ( {items.map((item, index) => (
<Tab <Tab
className={styles.tab} className={styles.tab}
key={item.title} key={item.title}
onClick={handleTabChange ? () => handleTabChange(item.title) : null} onClick={handleTabChange ? () => handleTabChange(item.title) : null}
disabled={item.disabled} disabled={item.disabled}
> >
{item.title} {showRadio ? (
<InputElement
name={item.title}
type="radio"
checked={defaultIndex === index}
options={[item.title]}
readOnly
/>
) : (
item.title
)}
</Tab> </Tab>
))} ))}
</TabList> </TabList>

View File

@ -15,7 +15,9 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
return noLinks ? ( return noLinks ? (
<span className={styles.tag}>{tag}</span> <span className={styles.tag}>{tag}</span>
) : ( ) : (
<Link href={`/search?tags=${urlEncodedTag}&sort=created&sortOrder=desc`}> <Link
href={`/search?tags=${urlEncodedTag}&sort=metadata.created&sortOrder=desc`}
>
<a className={styles.tag} title={tag}> <a className={styles.tag} title={tag}>
{tag} {tag}
</a> </a>

View File

@ -6,12 +6,13 @@ import AssetComputeList from '@shared/AssetList/AssetComputeList'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
import { AssetExtended } from 'src/@types/AssetExtended'
export default function AlgorithmDatasetsListForCompute({ export default function AlgorithmDatasetsListForCompute({
ddo, asset,
algorithmDid algorithmDid
}: { }: {
ddo: Asset asset: AssetExtended
algorithmDid: string algorithmDid: string
}): ReactElement { }): ReactElement {
const [datasetsForCompute, setDatasetsForCompute] = const [datasetsForCompute, setDatasetsForCompute] =
@ -19,24 +20,24 @@ export default function AlgorithmDatasetsListForCompute({
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
useEffect(() => { useEffect(() => {
if (!ddo) return if (!asset) return
async function getDatasetsAllowedForCompute() { async function getDatasetsAllowedForCompute() {
const isCompute = Boolean(getServiceByName(ddo, 'compute')) const isCompute = Boolean(getServiceByName(asset, 'compute'))
const datasetComputeService = getServiceByName( const datasetComputeService = getServiceByName(
ddo, asset,
isCompute ? 'compute' : 'access' isCompute ? 'compute' : 'access'
) )
const datasets = await getAlgorithmDatasetsForCompute( const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid, algorithmDid,
datasetComputeService?.serviceEndpoint, datasetComputeService?.serviceEndpoint,
ddo?.chainId, asset?.chainId,
newCancelToken() newCancelToken()
) )
setDatasetsForCompute(datasets) setDatasetsForCompute(datasets)
} }
ddo.metadata.type === 'algorithm' && getDatasetsAllowedForCompute() asset.metadata.type === 'algorithm' && getDatasetsAllowedForCompute()
}, [ddo?.metadata?.type]) }, [asset?.metadata?.type])
return ( return (
<div className={styles.datasetsContainer}> <div className={styles.datasetsContainer}>

View File

@ -11,6 +11,7 @@ import { useWeb3 } from '@context/Web3'
import { checkIfConsumable } from '@utils/ddo' import { checkIfConsumable } from '@utils/ddo'
import content from '../../../../../content/pages/startComputeDataset.json' import content from '../../../../../content/pages/startComputeDataset.json'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
import { AccessDetails } from 'src/@types/Price'
export default function FormStartCompute({ export default function FormStartCompute({
algorithms, algorithms,
@ -104,7 +105,7 @@ export default function FormStartCompute({
? 0 ? 0
: Number(algorithmConsumeDetails.price) : Number(algorithmConsumeDetails.price)
setTotalPrice(priceDataset + priceAlgo) setTotalPrice((priceDataset + priceAlgo).toString())
}, [ }, [
asset?.accessDetails, asset?.accessDetails,
algorithmConsumeDetails, algorithmConsumeDetails,
@ -143,7 +144,7 @@ export default function FormStartCompute({
hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset} hasDatatokenSelectedComputeAsset={hasDatatokenSelectedComputeAsset}
algorithmConsumeDetails={algorithmConsumeDetails} algorithmConsumeDetails={algorithmConsumeDetails}
symbol={oceanSymbol} symbol={oceanSymbol}
totalPrice={totalPrice} totalPrice={Number.parseFloat(totalPrice)}
/> />
<ButtonBuy <ButtonBuy

View File

@ -3,6 +3,7 @@ import { useAsset } from '@context/Asset'
import PriceUnit from '@shared/Price/PriceUnit' import PriceUnit from '@shared/Price/PriceUnit'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import styles from './PriceOutput.module.css' import styles from './PriceOutput.module.css'
import { AccessDetails } from 'src/@types/Price'
interface PriceOutputProps { interface PriceOutputProps {
totalPrice: number totalPrice: number
@ -74,14 +75,14 @@ export default function PriceOutput({
<Row <Row
hasPreviousOrder={hasPreviousOrder} hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken} hasDatatoken={hasDatatoken}
price={asset?.accessDetails?.price} price={Number.parseFloat(asset?.accessDetails?.price)}
timeout={assetTimeout} timeout={assetTimeout}
symbol={symbol} symbol={symbol}
/> />
<Row <Row
hasPreviousOrder={hasPreviousOrderSelectedComputeAsset} hasPreviousOrder={hasPreviousOrderSelectedComputeAsset}
hasDatatoken={hasDatatokenSelectedComputeAsset} hasDatatoken={hasDatatokenSelectedComputeAsset}
price={algorithmConsumeDetails?.price} price={Number.parseFloat(algorithmConsumeDetails?.price)}
timeout={selectedComputeAssetTimeout} timeout={selectedComputeAssetTimeout}
symbol={symbol} symbol={symbol}
sign="+" sign="+"

View File

@ -26,7 +26,6 @@ import FileIcon from '@shared/FileIcon'
import Alert from '@shared/atoms/Alert' import Alert from '@shared/atoms/Alert'
import { useSiteMetadata } from '@hooks/useSiteMetadata' import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { usePricing } from '@hooks/usePricing'
import { import {
generateBaseQuery, generateBaseQuery,
getFilterTerm, getFilterTerm,
@ -59,6 +58,7 @@ import { Decimal } from 'decimal.js'
import { TransactionReceipt } from 'web3-core' import { TransactionReceipt } from 'web3-core'
import { useAbortController } from '@hooks/useAbortController' import { useAbortController } from '@hooks/useAbortController'
import { getAccessDetails } from '@utils/accessDetailsAndPricing' import { getAccessDetails } from '@utils/accessDetailsAndPricing'
import { AccessDetails } from 'src/@types/Price'
export default function Compute({ export default function Compute({
asset, asset,
@ -78,8 +78,7 @@ export default function Compute({
consumableFeedback?: string consumableFeedback?: string
}): ReactElement { }): ReactElement {
const { appConfig } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const { accountId, web3 } = useWeb3() const { accountId } = useWeb3()
const { pricingError, pricingStepText } = usePricing()
const [isJobStarting, setIsJobStarting] = useState(false) const [isJobStarting, setIsJobStarting] = useState(false)
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const newAbortController = useAbortController() const newAbortController = useAbortController()
@ -156,13 +155,13 @@ export default function Compute({
useEffect(() => { useEffect(() => {
if (!algorithmConsumeDetails) return if (!algorithmConsumeDetails) return
setIsAlgoConsumablePrice(algorithmConsumeDetails.isConsumable) setIsAlgoConsumablePrice(algorithmConsumeDetails.isPurchasable)
}, [algorithmConsumeDetails]) }, [algorithmConsumeDetails])
useEffect(() => { useEffect(() => {
if (!accessDetails) return if (!accessDetails) return
setIsConsumablePrice(accessDetails.isConsumable) setIsConsumablePrice(accessDetails.isPurchasable)
}, [accessDetails]) }, [accessDetails])
// useEffect(() => { // useEffect(() => {
@ -217,10 +216,10 @@ export default function Compute({
// Output errors in toast UI // Output errors in toast UI
useEffect(() => { useEffect(() => {
const newError = error || pricingError const newError = error
if (!newError) return if (!newError) return
toast.error(newError) toast.error(newError)
}, [error, pricingError]) }, [error])
async function startJob(algorithmId: string): Promise<string> { async function startJob(algorithmId: string): Promise<string> {
try { try {
@ -646,7 +645,7 @@ export default function Compute({
/> />
<AlgorithmDatasetsListForCompute <AlgorithmDatasetsListForCompute
algorithmDid={asset.id} algorithmDid={asset.id}
ddo={asset} asset={asset}
/> />
</> </>
) : ( ) : (
@ -680,7 +679,8 @@ export default function Compute({
selectedComputeAssetLowPoolLiquidity={!isAlgoConsumablePrice} selectedComputeAssetLowPoolLiquidity={!isAlgoConsumablePrice}
selectedComputeAssetType="algorithm" selectedComputeAssetType="algorithm"
selectedComputeAssetTimeout={algorithmTimeout} selectedComputeAssetTimeout={algorithmTimeout}
stepText={pricingStepText || 'Starting Compute Job...'} // lazy comment when removing pricingStepText
stepText={'pricingStepText' || 'Starting Compute Job...'}
algorithmConsumeDetails={algorithmConsumeDetails} algorithmConsumeDetails={algorithmConsumeDetails}
isConsumable={isConsumable} isConsumable={isConsumable}
consumableFeedback={consumableFeedback} consumableFeedback={consumableFeedback}

View File

@ -1,154 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import FileIcon from '@shared/FileIcon'
import Price from '@shared/Price'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3'
import { usePricing } from '@hooks/usePricing'
import { useConsume } from '@hooks/useConsume'
import ButtonBuy from '@shared/ButtonBuy'
import { secondsToString } from '@utils/ddo'
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
import styles from './Consume.module.css'
import { useIsMounted } from '@hooks/useIsMounted'
import { Asset, FileMetadata } from '@oceanprotocol/lib'
export default function Consume({
ddo,
accessDetails,
file,
isBalanceSufficient,
dtBalance,
fileIsLoading,
isConsumable,
consumableFeedback
}: {
ddo: Asset
accessDetails: AccessDetails
file: FileMetadata
isBalanceSufficient: boolean
dtBalance: string
fileIsLoading?: boolean
isConsumable?: boolean
consumableFeedback?: string
}): ReactElement {
const { accountId } = useWeb3()
const { appConfig } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, isAssetNetwork } = useAsset()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } =
usePricing()
const { consumeStepText, consume, consumeError, isLoading } = useConsume()
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumablePrice, setIsConsumablePrice] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
const isMounted = useIsMounted()
useEffect(() => {
if (!ddo) return
const { timeout } = ddo.services[0]
setAssetTimeout(`${timeout}`)
}, [ddo])
useEffect(() => {
if (!accessDetails) return
setIsConsumablePrice(accessDetails.isConsumable)
setHasPreviousOrder(accessDetails.owned)
setPreviousOrderId(accessDetails.validOrderTx)
}, [accessDetails])
useEffect(() => {
setHasDatatoken(Number(dtBalance) >= 1)
}, [dtBalance])
useEffect(() => {
if (!accountId) return
setIsDisabled(
!isConsumable ||
((!isBalanceSufficient ||
!isAssetNetwork ||
typeof consumeStepText !== 'undefined' ||
pricingIsLoading ||
!isConsumablePrice) &&
!hasPreviousOrder &&
!hasDatatoken)
)
}, [
hasPreviousOrder,
isBalanceSufficient,
isAssetNetwork,
consumeStepText,
pricingIsLoading,
isConsumablePrice,
hasDatatoken,
isConsumable,
accountId
])
async function handleConsume() {
// if (!hasPreviousOrder && !hasDatatoken) {
// const tx = await buyDT('1', price, ddo)
// if (tx === undefined) return
// }
const error = await consume(
ddo.id,
ddo.services[0].datatokenAddress,
'access',
appConfig.marketFeeAddress,
previousOrderId
)
error || setHasPreviousOrder(true)
}
// Output errors in UI
useEffect(() => {
consumeError && toast.error(consumeError)
}, [consumeError])
useEffect(() => {
pricingError && toast.error(pricingError)
}, [pricingError])
const PurchaseButton = () => (
<ButtonBuy
action="download"
disabled={isDisabled}
hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}
dtSymbol={ddo?.datatokens[0]?.symbol}
dtBalance={dtBalance}
datasetLowPoolLiquidity={!isConsumablePrice}
onClick={handleConsume}
assetTimeout={secondsToString(parseInt(assetTimeout))}
assetType={ddo?.metadata?.type}
stepText={consumeStepText || pricingStepText}
isLoading={pricingIsLoading || isLoading}
priceType={accessDetails?.type}
isConsumable={isConsumable}
isBalanceSufficient={isBalanceSufficient}
consumableFeedback={consumableFeedback}
/>
)
return (
<aside className={styles.consume}>
<div className={styles.info}>
<div className={styles.filewrapper}>
<FileIcon file={file} isLoading={fileIsLoading} />
</div>
<div className={styles.pricewrapper}>
<Price accessDetails={accessDetails} conversion />
{!isInPurgatory && <PurchaseButton />}
</div>
</div>
{ddo?.metadata?.type === 'algorithm' && (
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} ddo={ddo} />
)}
</aside>
)
}

View File

@ -0,0 +1,175 @@
import React, { ReactElement, useEffect, useState } from 'react'
import FileIcon from '@shared/FileIcon'
import Price from '@shared/Price'
import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3'
import ButtonBuy from '@shared/ButtonBuy'
import { secondsToString } from '@utils/ddo'
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
import styles from './Download.module.css'
import { FileMetadata, LoggerInstance, ZERO_ADDRESS } from '@oceanprotocol/lib'
import { order } from '@utils/order'
import { AssetExtended } from 'src/@types/AssetExtended'
import { buyDtFromPool, calculateBuyPrice } from '@utils/pool'
import { downloadFile } from '@utils/provider'
import { getOrderFeedback } from '@utils/feedback'
import { getOrderPriceAndFees } from '@utils/accessDetailsAndPricing'
import { OrderPriceAndFees } from 'src/@types/Price'
import { toast } from 'react-toastify'
export default function Download({
asset,
file,
isBalanceSufficient,
dtBalance,
fileIsLoading,
consumableFeedback
}: {
asset: AssetExtended
file: FileMetadata
isBalanceSufficient: boolean
dtBalance: string
fileIsLoading?: boolean
consumableFeedback?: string
}): ReactElement {
const { accountId, web3 } = useWeb3()
const { isInPurgatory, isAssetNetwork } = useAsset()
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [statusText, setStatusText] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isOwned, setIsOwned] = useState(false)
const [validOrderTx, setValidOrderTx] = useState('')
const [orderPriceAndFees, setOrderPriceAndFees] =
useState<OrderPriceAndFees>()
useEffect(() => {
if (!asset?.accessDetails) return
setIsOwned(asset?.accessDetails?.isOwned)
setValidOrderTx(asset?.accessDetails?.validOrderTx)
// get full price and fees
async function init() {
if (asset?.accessDetails?.addressOrId === ZERO_ADDRESS) return
setIsLoading(true)
setStatusText('Calculating price including fees.')
const orderPriceAndFees = await getOrderPriceAndFees(asset, ZERO_ADDRESS)
setOrderPriceAndFees(orderPriceAndFees)
setIsLoading(false)
}
init()
}, [asset, accountId])
useEffect(() => {
setHasDatatoken(Number(dtBalance) >= 1)
}, [dtBalance])
useEffect(() => {
if (!accountId || !asset?.accessDetails) return
setIsDisabled(
!asset?.accessDetails.isPurchasable ||
((!isBalanceSufficient || !isAssetNetwork) && !isOwned && !hasDatatoken)
)
}, [
asset?.accessDetails,
isBalanceSufficient,
isAssetNetwork,
hasDatatoken,
accountId,
isOwned
])
async function handleOrderOrDownload() {
setIsLoading(true)
if (isOwned) {
setStatusText(
getOrderFeedback(
asset.accessDetails?.baseToken?.symbol,
asset.accessDetails?.datatoken?.symbol
)[3]
)
await downloadFile(web3, asset, accountId, validOrderTx)
} else {
try {
if (!hasDatatoken && asset.accessDetails.type === 'dynamic') {
setStatusText(
getOrderFeedback(
asset.accessDetails.baseToken?.symbol,
asset.accessDetails.datatoken?.symbol
)[0]
)
const tx = await buyDtFromPool(asset.accessDetails, accountId, web3)
if (!tx) {
toast.error('Failed to buy datatoken from pool!')
setIsLoading(false)
return
}
}
setStatusText(
getOrderFeedback(
asset.accessDetails.baseToken?.symbol,
asset.accessDetails.datatoken?.symbol
)[asset.accessDetails?.type === 'fixed' ? 2 : 1]
)
const orderTx = await order(web3, asset, orderPriceAndFees, accountId)
setIsOwned(true)
setValidOrderTx(orderTx.transactionHash)
} catch (ex) {
LoggerInstance.log(ex.message)
setIsLoading(false)
}
}
setIsLoading(false)
}
const PurchaseButton = () => (
<ButtonBuy
action="download"
disabled={isDisabled}
hasPreviousOrder={isOwned}
hasDatatoken={hasDatatoken}
dtSymbol={asset?.datatokens[0]?.symbol}
dtBalance={dtBalance}
datasetLowPoolLiquidity={!asset.accessDetails?.isPurchasable}
onClick={handleOrderOrDownload}
assetTimeout={secondsToString(asset.services[0].timeout)}
assetType={asset?.metadata?.type}
stepText={statusText}
// isLoading={pricingIsLoading || isLoading}
isLoading={isLoading}
priceType={asset.accessDetails?.type}
isConsumable={asset.accessDetails?.isPurchasable}
isBalanceSufficient={isBalanceSufficient}
consumableFeedback={consumableFeedback}
/>
)
return (
<aside className={styles.consume}>
<div className={styles.info}>
<div className={styles.filewrapper}>
<FileIcon file={file} isLoading={fileIsLoading} />
</div>
<div className={styles.pricewrapper}>
<Price
accessDetails={asset.accessDetails}
orderPriceAndFees={orderPriceAndFees}
conversion
/>
{!isInPurgatory && <PurchaseButton />}
</div>
</div>
{asset?.metadata?.type === 'algorithm' && (
<AlgorithmDatasetsListForCompute
algorithmDid={asset.id}
asset={asset}
/>
)}
</aside>
)
}

View File

@ -106,7 +106,6 @@ export default function Add({
const result = await poolInstance.joinswapExternAmountIn( const result = await poolInstance.joinswapExternAmountIn(
accountId, accountId,
poolAddress, poolAddress,
tokenInAddress,
amount, amount,
minPoolAmountOut minPoolAmountOut
) )

View File

@ -64,7 +64,6 @@ export default function Remove({
const result = await poolInstance.exitswapPoolAmountIn( const result = await poolInstance.exitswapPoolAmountIn(
accountId, accountId,
poolAddress, poolAddress,
tokenOutAddress,
amountPoolShares, amountPoolShares,
minOceanAmount minOceanAmount
) )

View File

@ -1,6 +1,6 @@
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Consume' import Consume from './Download'
import { FileMetadata, LoggerInstance, Datatoken } from '@oceanprotocol/lib' import { FileMetadata, LoggerInstance, Datatoken } from '@oceanprotocol/lib'
import Tabs, { TabsItem } from '@shared/atoms/Tabs' import Tabs, { TabsItem } from '@shared/atoms/Tabs'
import { compareAsBN } from '@utils/numbers' import { compareAsBN } from '@utils/numbers'
@ -141,13 +141,11 @@ export default function AssetActions({
/> />
) : ( ) : (
<Consume <Consume
ddo={asset} asset={asset}
accessDetails={asset?.accessDetails}
dtBalance={dtBalance} dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
file={fileMetadata} file={fileMetadata}
fileIsLoading={fileIsLoading} fileIsLoading={fileIsLoading}
isConsumable={isConsumable}
consumableFeedback={consumableFeedback} consumableFeedback={consumableFeedback}
/> />
) )

View File

@ -1,199 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { gql, OperationContext } from 'urql'
import Conversion from '@shared/Price/Conversion'
import PriceUnit from '@shared/Price/PriceUnit'
import Tooltip from '@shared/atoms/Tooltip'
import NetworkName from '@shared/NetworkName'
import { fetchData, getSubgraphUri } from '@utils/subgraph'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import useNetworkMetadata, {
filterNetworksByType
} from '@hooks/useNetworkMetadata'
import { LoggerInstance } from '@oceanprotocol/lib'
import styles from './MarketStats.module.css'
import { FooterStatsValues_globalStats_totalLiquidity_token as LiquidityToken } from 'src/@types/subgraph/FooterStatsValues'
const getGlobalStatsValues = gql`
query FooterStatsValues {
globalStats {
poolCount
nftCount
datatokenCount
orderCount
totalLiquidity {
token {
id
name
symbol
}
value
}
}
}
`
interface Value {
[chainId: number]: string
}
function MarketNetworkStats({
totalValueLocked,
poolCount,
totalOceanLiquidity
}: {
totalValueLocked: string
poolCount: string
totalOceanLiquidity: string
}): ReactElement {
return (
<>
<Conversion price={totalValueLocked} hideApproximateSymbol />{' '}
<abbr title="Total Value Locked">TVL</abbr> across{' '}
<strong>{poolCount}</strong> asset pools that contain{' '}
<PriceUnit price={totalOceanLiquidity} small className={styles.total} />,
plus datatokens for each pool.
</>
)
}
function MarketNetworkStatsTooltip({
totalValueLocked,
poolCount,
totalOceanLiquidity,
mainChainIds
}: {
totalValueLocked: Value
poolCount: Value
totalOceanLiquidity: Value
mainChainIds: number[]
}): ReactElement {
return (
<>
<ul className={styles.statsList}>
{totalValueLocked &&
totalOceanLiquidity &&
poolCount &&
mainChainIds?.map((chainId, key) => (
<li className={styles.tooltipStats} key={key}>
<NetworkName networkId={chainId} className={styles.network} />
<br />
<Conversion
price={totalValueLocked[chainId] || '0'}
hideApproximateSymbol
/>{' '}
<abbr title="Total Value Locked">TVL</abbr>
{' | '}
<strong>{poolCount[chainId] || '0'}</strong> pools
{' | '}
<PriceUnit price={totalOceanLiquidity[chainId] || '0'} small />
</li>
))}
</ul>
<p className={styles.note}>
Counted on-chain from our pool factory. Does not filter out assets in{' '}
<a href="https://github.com/oceanprotocol/list-purgatory">
list-purgatory
</a>
</p>
</>
)
}
export default function MarketStats(): ReactElement {
const [totalValueLocked, setTotalValueLocked] = useState<Value>()
const [totalOceanLiquidity, setTotalOceanLiquidity] = useState<Value>()
const [poolCount, setPoolCount] = useState<Value>()
const [totalValueLockedSum, setTotalValueLockedSum] = useState<string>()
const [totalOceanLiquiditySum, setTotalOceanLiquiditySum] = useState<string>()
const [poolCountSum, setPoolCountSum] = useState<string>()
const [mainChainIds, setMainChainIds] = useState<number[]>()
const { appConfig } = useSiteMetadata()
const { networksList } = useNetworkMetadata()
async function getMarketStats() {
const mainChainIdsList = await filterNetworksByType(
'mainnet',
appConfig.chainIdsSupported,
networksList
)
setMainChainIds(mainChainIdsList)
let newTotalValueLockedSum = 0
const newTotalOceanLiquiditySum = 0
let newPoolCountSum = 0
for (const chainId of mainChainIdsList) {
const context: OperationContext = {
url: `${getSubgraphUri(
chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only'
}
try {
const response = await fetchData(getGlobalStatsValues, null, context)
if (!response) continue
const {
poolCount,
nftCount,
datatokenCount,
orderCount,
totalLiquidity
} = response?.data?.globalStats[0]
await setTotalValueLocked((prevState) => ({
...prevState,
[chainId]: totalLiquidity.value
}))
// TODO: how to get total OCEAN liquidity? Does this work?
await setTotalOceanLiquidity((prevState) => ({
...prevState,
[chainId]: totalLiquidity.filter(
(token: LiquidityToken) => token.symbol === 'OCEAN'
)[0]
}))
await setPoolCount((prevState) => ({
...prevState,
[chainId]: poolCount
}))
newTotalValueLockedSum += parseInt(totalLiquidity.value)
// newTotalOceanLiquiditySum += parseInt(totalOceanLiquidity.value)
newPoolCountSum += parseInt(poolCount)
} catch (error) {
LoggerInstance.error('Error fetchData: ', error.message)
}
}
setTotalValueLockedSum(`${newTotalValueLockedSum}`)
setTotalOceanLiquiditySum(`${newTotalOceanLiquiditySum}`)
setPoolCountSum(`${newPoolCountSum}`)
}
useEffect(() => {
getMarketStats()
}, [])
return (
<div className={styles.stats}>
<>
<MarketNetworkStats
totalValueLocked={totalValueLockedSum || '0'}
totalOceanLiquidity={totalOceanLiquiditySum || '0'}
poolCount={poolCountSum || '0'}
/>{' '}
<Tooltip
className={styles.info}
content={
<MarketNetworkStatsTooltip
totalValueLocked={totalValueLocked}
poolCount={poolCount}
totalOceanLiquidity={totalOceanLiquidity}
mainChainIds={mainChainIds}
/>
}
/>
</>
</div>
)
}

View File

@ -1,28 +1,12 @@
.stats {
margin-bottom: calc(var(--spacer) * 2);
}
/* specificity sledgehammer override without !important */
.stats,
.stats *,
.statsList * {
font-size: var(--font-size-small);
color: var(--color-secondary);
margin-left: 0;
}
.tooltipStats { .tooltipStats {
margin-bottom: calc(var(--spacer) / 3); margin-bottom: calc(var(--spacer) / 3);
padding-bottom: calc(var(--spacer) / 3); padding-bottom: calc(var(--spacer) / 3);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.network { .statsList {
font-weight: var(--font-weight-bold); composes: statsList from './index.module.css';
} padding-bottom: 0;
.info {
width: 0.85rem;
} }
.statsList, .statsList,
@ -30,13 +14,13 @@
padding: calc(var(--spacer) / 4); padding: calc(var(--spacer) / 4);
} }
.statsList {
padding-bottom: 0;
}
.note { .note {
margin-bottom: 0; margin-bottom: 0;
padding-top: 0; padding-top: 0;
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
color: var(--color-secondary); color: var(--color-secondary);
} }
.network {
font-weight: var(--font-weight-bold);
}

View File

@ -0,0 +1,51 @@
import React, { ReactElement } from 'react'
import Conversion from '@shared/Price/Conversion'
import PriceUnit from '@shared/Price/PriceUnit'
import NetworkName from '@shared/NetworkName'
import styles from './Tooltip.module.css'
import { StatsValue } from './_types'
export default function MarketStatsTooltip({
totalValueLockedInOcean,
poolCount,
totalOceanLiquidity,
mainChainIds
}: {
totalValueLockedInOcean: StatsValue
poolCount: StatsValue
totalOceanLiquidity: StatsValue
mainChainIds: number[]
}): ReactElement {
return (
<>
<ul className={styles.statsList}>
{mainChainIds?.map((chainId, key) => (
<li className={styles.tooltipStats} key={key}>
<NetworkName networkId={chainId} className={styles.network} />
<br />
<Conversion
price={totalValueLockedInOcean?.[chainId] || '0'}
hideApproximateSymbol
/>{' '}
<abbr title="Total Value Locked">TVL</abbr>
{' | '}
<strong>{poolCount?.[chainId] || '0'}</strong> pools
{' | '}
<PriceUnit
price={totalOceanLiquidity?.[chainId] || '0'}
symbol="OCEAN"
small
/>
</li>
))}
</ul>
<p className={styles.note}>
Counted on-chain from our NFT and pool factories. Does not filter out
assets in{' '}
<a href="https://github.com/oceanprotocol/list-purgatory">
list-purgatory
</a>
</p>
</>
)
}

View File

@ -0,0 +1,28 @@
import React, { ReactElement } from 'react'
import Conversion from '@shared/Price/Conversion'
import PriceUnit from '@shared/Price/PriceUnit'
import { StatsTotal } from './_types'
export default function MarketStatsTotal({
total
}: {
total: StatsTotal
}): ReactElement {
return (
<>
<p>
<strong>{total.orders}</strong> orders across{' '}
<strong>{total.nfts}</strong> assets with{' '}
<strong>{total.datatokens}</strong> different datatokens.
</p>
<Conversion
price={`${total.totalValueLockedInOcean}`}
hideApproximateSymbol
/>{' '}
<abbr title="Total Value Locked">TVL</abbr> across{' '}
<strong>{total.pools}</strong> asset pools that contain{' '}
<PriceUnit price={`${total.totalOceanLiquidity}`} symbol="OCEAN" small />,
plus datatokens for each pool.
</>
)
}

View File

@ -0,0 +1,20 @@
import { gql } from 'urql'
export const queryGlobalStatistics = gql`
query FooterStatsValues {
globalStatistics {
poolCount
nftCount
datatokenCount
orderCount
totalLiquidity {
value
token {
address
name
symbol
}
}
}
}
`

View File

@ -0,0 +1,12 @@
export interface StatsValue {
[chainId: number]: string
}
export interface StatsTotal {
totalValueLockedInOcean: number
totalOceanLiquidity: number
pools: number
nfts: number
datatokens: number
orders: number
}

View File

@ -0,0 +1,21 @@
.stats {
margin-bottom: calc(var(--spacer) * 2);
}
/* specificity sledgehammer override without !important */
.stats,
.stats *,
.statsList * {
font-size: var(--font-size-small);
color: var(--color-secondary);
margin-left: 0;
}
.info {
width: 0.85rem;
}
.stats p {
display: block;
margin-bottom: 0;
}

View File

@ -0,0 +1,166 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import { OperationContext } from 'urql'
import Tooltip from '@shared/atoms/Tooltip'
import { fetchData, getSubgraphUri } from '@utils/subgraph'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import useNetworkMetadata, {
filterNetworksByType
} from '@hooks/useNetworkMetadata'
import { LoggerInstance } from '@oceanprotocol/lib'
import styles from './index.module.css'
import { FooterStatsValues_globalStatistics as FooterStatsValuesGlobalStatistics } from 'src/@types/subgraph/FooterStatsValues'
import MarketStatsTooltip from './Tooltip'
import MarketStatsTotal from './Total'
import { queryGlobalStatistics } from './_queries'
import { usePrices } from '@context/Prices'
import { useUserPreferences } from '@context/UserPreferences'
import Decimal from 'decimal.js'
import { StatsTotal, StatsValue } from './_types'
const initialTotal: StatsTotal = {
totalValueLockedInOcean: 0,
totalOceanLiquidity: 0,
pools: 0,
nfts: 0,
datatokens: 0,
orders: 0
}
export default function MarketStats(): ReactElement {
const { appConfig } = useSiteMetadata()
const { networksList } = useNetworkMetadata()
const { currency } = useUserPreferences()
const { prices } = usePrices()
const [mainChainIds, setMainChainIds] = useState<number[]>()
const [data, setData] =
useState<{ [chainId: number]: FooterStatsValuesGlobalStatistics }>()
const [totalValueLockedInOcean, setTotalValueLockedInOcean] =
useState<StatsValue>()
const [totalOceanLiquidity, setTotalOceanLiquidity] = useState<StatsValue>()
const [poolCount, setPoolCount] = useState<StatsValue>()
const [total, setTotal] = useState(initialTotal)
//
// Set the main chain ids we want to display stats for
//
useEffect(() => {
if (!networksList) return
const mainChainIdsList = filterNetworksByType(
'mainnet',
appConfig.chainIdsSupported,
networksList
)
setMainChainIds(mainChainIdsList)
}, [appConfig.chainIdsSupported, networksList])
//
// Helper: fetch data from subgraph
//
const getMarketStats = useCallback(async () => {
if (!mainChainIds?.length) return
for (const chainId of mainChainIds) {
const context: OperationContext = {
url: `${getSubgraphUri(
chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only'
}
try {
const response = await fetchData(queryGlobalStatistics, null, context)
if (!response?.data?.globalStatistics) return
setData((prevState) => ({
...prevState,
[chainId]: response.data.globalStatistics[0]
}))
} catch (error) {
LoggerInstance.error('Error fetching global stats: ', error.message)
}
}
}, [mainChainIds])
//
// 1. Fetch Data
//
useEffect(() => {
getMarketStats()
}, [getMarketStats])
//
// 2. Data Manipulation
//
useEffect(() => {
if (!data || !mainChainIds?.length) return
const newTotal: StatsTotal = {
...initialTotal // always start calculating beginning from initial 0 values
}
for (const chainId of mainChainIds) {
const baseTokenValue = data[chainId]?.totalLiquidity[0]?.value
try {
const totalValueLockedInOcean = baseTokenValue
? new Decimal(baseTokenValue).mul(2)
: new Decimal(0)
setTotalValueLockedInOcean((prevState) => ({
...prevState,
[chainId]: `${totalValueLockedInOcean}`
}))
const totalOceanLiquidity = Number(baseTokenValue) || 0
setTotalOceanLiquidity((prevState) => ({
...prevState,
[chainId]: `${totalOceanLiquidity}`
}))
const poolCount = data[chainId]?.poolCount || 0
setPoolCount((prevState) => ({
...prevState,
[chainId]: `${poolCount}`
}))
const nftCount = data[chainId]?.nftCount || 0
const datatokenCount = data[chainId]?.datatokenCount || 0
const orderCount = data[chainId]?.orderCount || 0
newTotal.totalValueLockedInOcean += totalValueLockedInOcean.toNumber()
newTotal.totalOceanLiquidity += totalOceanLiquidity
newTotal.pools += poolCount
newTotal.nfts += nftCount
newTotal.datatokens += datatokenCount
newTotal.orders += orderCount
} catch (error) {
LoggerInstance.error('Error data manipulation: ', error.message)
}
}
setTotal(newTotal)
}, [data, mainChainIds, prices, currency])
return (
<div className={styles.stats}>
<>
<MarketStatsTotal total={total} />{' '}
<Tooltip
className={styles.info}
content={
<MarketStatsTooltip
totalValueLockedInOcean={totalValueLockedInOcean}
poolCount={poolCount}
totalOceanLiquidity={totalOceanLiquidity}
mainChainIds={mainChainIds}
/>
}
/>
</>
</div>
)
}

View File

@ -1,12 +1,11 @@
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import React, { ReactElement, useEffect, useState, useCallback } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import Table from '@shared/atoms/Table' import Table from '@shared/atoms/Table'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import Price from '@shared/Price' import Price from '@shared/Price'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import AssetTitle from '@shared/AssetList/AssetListTitle' import AssetTitle from '@shared/AssetList/AssetListTitle'
import { retrieveDDOListByDIDs } from '@utils/aquarius' import { retrieveDDOListByDIDs } from '@utils/aquarius'
import { CancelToken } from 'axios'
import { useSiteMetadata } from '@hooks/useSiteMetadata' import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { AssetExtended } from 'src/@types/AssetExtended' import { AssetExtended } from 'src/@types/AssetExtended'
@ -53,26 +52,6 @@ export default function Bookmarks(): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const newCancelToken = useCancelToken() const newCancelToken = useCancelToken()
const getAssetsBookmarked = useCallback(
async (
bookmarks: string[],
chainIds: number[],
cancelToken: CancelToken
) => {
try {
const result = await retrieveDDOListByDIDs(
bookmarks,
chainIds,
cancelToken
)
return result
} catch (error) {
LoggerInstance.error(error.message)
}
},
[]
)
useEffect(() => { useEffect(() => {
if (!appConfig?.metadataCacheUri || bookmarks === []) return if (!appConfig?.metadataCacheUri || bookmarks === []) return
@ -85,21 +64,23 @@ export default function Bookmarks(): ReactElement {
setIsLoading(true) setIsLoading(true)
try { try {
const resultPinned = await getAssetsBookmarked( const result = await retrieveDDOListByDIDs(
bookmarks, bookmarks,
chainIds, chainIds,
newCancelToken() newCancelToken()
) )
if (!result?.length) return
const pinnedAssets: AssetExtended[] = await getAccessDetailsForAssets( const pinnedAssets: AssetExtended[] = await getAccessDetailsForAssets(
resultPinned, result,
accountId accountId
) )
setPinned(pinnedAssets) setPinned(pinnedAssets)
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(`Bookmarks error:`, error.message)
} finally {
setIsLoading(false)
} }
setIsLoading(false)
} }
init() init()
}, [ }, [
@ -107,7 +88,6 @@ export default function Bookmarks(): ReactElement {
bookmarks, bookmarks,
chainIds, chainIds,
accountId, accountId,
getAssetsBookmarked,
newCancelToken newCancelToken
]) ])

View File

@ -24,10 +24,9 @@ async function getQueryHighest(
esPaginationOptions: { esPaginationOptions: {
size: dtList.length > 0 ? dtList.length : 1 size: dtList.length > 0 ? dtList.length : 1
}, },
filters: [getFilterTerm('dataToken', dtList)] filters: [getFilterTerm('services.datatokenAddress', dtList)]
} as BaseQueryParams } as BaseQueryParams
const queryHighest = generateBaseQuery(baseQueryParams) const queryHighest = generateBaseQuery(baseQueryParams)
return [queryHighest, dtList] return [queryHighest, dtList]
} }

View File

@ -17,7 +17,6 @@ async function getPoolSharesLiquidity(
poolShares: PoolShare[] poolShares: PoolShare[]
): Promise<number> { ): Promise<number> {
let totalLiquidity = 0 let totalLiquidity = 0
for (const poolShare of poolShares) { for (const poolShare of poolShares) {
const poolLiquidity = calculateUserLiquidity(poolShare) const poolLiquidity = calculateUserLiquidity(poolShare)
totalLiquidity += poolLiquidity totalLiquidity += poolLiquidity

View File

@ -4,37 +4,36 @@ import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik' import { useFormikContext } from 'formik'
import AssetContent from 'src/components/Asset/AssetContent' import AssetContent from 'src/components/Asset/AssetContent'
import { transformPublishFormToDdo } from '../_utils' import { transformPublishFormToDdo } from '../_utils'
import { Asset } from '@oceanprotocol/lib' import { AssetExtended } from 'src/@types/AssetExtended'
import { ZERO_ADDRESS } from '@oceanprotocol/lib'
export default function Preview(): ReactElement { export default function Preview(): ReactElement {
const [asset, setAsset] = useState<Asset>() const [asset, setAsset] = useState<AssetExtended>()
const [accessDetails, setAccessDetails] = useState<AccessDetails>()
const { values } = useFormikContext<FormPublishData>() const { values } = useFormikContext<FormPublishData>()
useEffect(() => { useEffect(() => {
async function makeDdo() { async function makeDdo() {
const asset = await transformPublishFormToDdo(values) const asset = (await transformPublishFormToDdo(values)) as AssetExtended
setAsset(asset as Asset)
// dummy BestPrice to trigger certain AssetActions // dummy BestPrice to trigger certain AssetActions
const accessDetails: AccessDetails = { asset.accessDetails = {
type: values.pricing.type, type: values.pricing.type,
addressOrId: '0x...', addressOrId: ZERO_ADDRESS,
price: values.pricing.price, price: values.pricing.price,
baseToken: { baseToken: {
address: '0x..', address: ZERO_ADDRESS,
name: '', name: 'OCEAN',
symbol: '' symbol: 'OCEAN'
}, },
datatoken: { datatoken: {
address: '0x..', address: ZERO_ADDRESS,
name: '', name: '',
symbol: '' symbol: ''
}, },
owned: false, isPurchasable: true,
isOwned: false,
validOrderTx: '' validOrderTx: ''
} }
setAccessDetails(accessDetails) setAsset(asset)
} }
makeDdo() makeDdo()
}, [values]) }, [values])
@ -44,7 +43,7 @@ export default function Preview(): ReactElement {
<h2 className={styles.previewTitle}>Preview</h2> <h2 className={styles.previewTitle}>Preview</h2>
<h3 className={styles.assetTitle}>{values.metadata.name}</h3> <h3 className={styles.assetTitle}>{values.metadata.name}</h3>
<AssetContent asset={asset} /> {asset && <AssetContent asset={asset} />}
</div> </div>
) )
} }

View File

@ -10,7 +10,7 @@
.token { .token {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
margin-bottom: calc(var(--spacer) / 2); margin-bottom: calc(var(--spacer) / 2);
} }
@ -60,8 +60,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
max-width: 12rem; margin: auto 0;
margin: auto;
} }
.max { .max {
@ -73,3 +72,13 @@
.weight strong { .weight strong {
color: var(--font-color-text); color: var(--font-color-text);
} }
@media screen and (min-width: 41rem) {
.token {
justify-content: center;
}
.data {
margin: auto;
max-width: 12rem;
}
}

View File

@ -1,14 +1,14 @@
.fees { .fees {
display: grid; display: grid;
gap: var(--spacer); gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
padding: var(--spacer); padding: var(--spacer) 0;
text-align: center; text-align: center;
} }
.fees > div { .fees > div {
margin-bottom: 0; margin-bottom: 0;
justify-self: center; justify-self: start;
} }
.fees label { .fees label {
@ -18,3 +18,10 @@
.fees input { .fees input {
max-width: 5rem; max-width: 5rem;
} }
@media screen and (min-width: 25rem) {
.fees {
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
padding: var(--spacer);
}
}

View File

@ -1,14 +1,31 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useEffect } from 'react'
import { useFormikContext } from 'formik'
import { FormPublishData } from '../_types'
import FormHelp from '@shared/FormInput/Help' import FormHelp from '@shared/FormInput/Help'
import Price from './Price' import Price from './Price'
import styles from './Dynamic.module.css' import styles from './Dynamic.module.css'
export default function Free({ content }: { content: any }): ReactElement { export default function Free({ content }: { content: any }): ReactElement {
// connect with Form state, use for conditional field rendering
const { values, setFieldValue } = useFormikContext<FormPublishData>()
useEffect(() => {
// if the user has agreed, then set pricing to continue
if (values.pricing.freeAgreement) {
setFieldValue('pricing.price', 1)
setFieldValue('pricing.amountDataToken', 1000)
} else {
// disabled continue button if the user hasn't agree to the "free agreement"
setFieldValue('pricing.price', 0)
setFieldValue('pricing.amountDataToken', 0)
}
}, [setFieldValue, values])
return ( return (
<> <>
<FormHelp>{content.info}</FormHelp> <FormHelp>{content.info}</FormHelp>
<h4 className={styles.title}>Price</h4> <h4 className={styles.title}>Price</h4>
<Price /> <Price content={content} />
</> </>
) )
} }

View File

@ -62,7 +62,11 @@
} }
.free { .free {
text-align: center; text-align: left;
margin: calc(var(--spacer) / 2) 0; margin: calc(var(--spacer) / 2);
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
.free [class*='FormInput_field'] {
margin: 0;
}

View File

@ -1,16 +1,19 @@
import Conversion from '@shared/Price/Conversion' import Conversion from '@shared/Price/Conversion'
import { useField, useFormikContext } from 'formik' import { Field, useField, useFormikContext } from 'formik'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Input from '@shared/FormInput' import Input from '@shared/FormInput'
import Error from './Error' import Error from './Error'
import PriceUnit from '@shared/Price/PriceUnit' import PriceUnit from '@shared/Price/PriceUnit'
import styles from './Price.module.css' import styles from './Price.module.css'
import { FormPublishData } from '../_types' import { FormPublishData } from '../_types'
import { getFieldContent } from '../_utils'
export default function Price({ export default function Price({
firstPrice firstPrice,
content
}: { }: {
firstPrice?: string firstPrice?: string
content?: any
}): ReactElement { }): ReactElement {
const [field, meta] = useField('pricing.price') const [field, meta] = useField('pricing.price')
@ -20,7 +23,13 @@ export default function Price({
return ( return (
<div className={styles.price}> <div className={styles.price}>
{values.pricing.type === 'free' ? ( {values.pricing.type === 'free' ? (
<h4 className={styles.free}>Free</h4> <div className={styles.free}>
<Field
{...getFieldContent('freeAgreement', content.fields)}
component={Input}
name="pricing.freeAgreement"
/>
</div>
) : ( ) : (
<> <>
<div className={styles.grid}> <div className={styles.grid}>

View File

@ -2,6 +2,10 @@
text-align: center; text-align: center;
} }
.pricing ul > li[class*='Tabs_tab'] {
padding: calc(var(--spacer) / 4) var(--spacer);
}
.pricing [class*='Tabs_tabContent'] { .pricing [class*='Tabs_tabContent'] {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;

View File

@ -23,7 +23,9 @@ export default function PricingFields(): ReactElement {
function handleTabChange(tabName: string) { function handleTabChange(tabName: string) {
const type = tabName.toLowerCase() const type = tabName.toLowerCase()
setFieldValue('pricing.type', type) setFieldValue('pricing.type', type)
type === 'dynamic' && setFieldValue('pricing.amountDataToken', 1000) setFieldValue('pricing.price', 0)
setFieldValue('pricing.freeAgreement', false)
type !== 'free' && setFieldValue('pricing.amountDataToken', 1000)
} }
// Always update everything when price value changes // Always update everything when price value changes
@ -42,7 +44,14 @@ export default function PricingFields(): ReactElement {
: 0 : 0
setFieldValue('pricing.amountDataToken', amountDataToken) setFieldValue('pricing.amountDataToken', amountDataToken)
}, [price, amountOcean, weightOnOcean, weightOnDataToken, type]) }, [
price,
amountOcean,
weightOnOcean,
weightOnDataToken,
type,
setFieldValue
])
const tabs = [ const tabs = [
appConfig.allowFixedPricing === 'true' appConfig.allowFixedPricing === 'true'
@ -71,6 +80,7 @@ export default function PricingFields(): ReactElement {
handleTabChange={handleTabChange} handleTabChange={handleTabChange}
defaultIndex={type === 'dynamic' ? 1 : type === 'free' ? 2 : 0} defaultIndex={type === 'dynamic' ? 1 : type === 'free' ? 2 : 0}
className={styles.pricing} className={styles.pricing}
showRadio
/> />
) )
} }

View File

@ -58,7 +58,15 @@
.title { .title {
font-size: var(--font-size-large); font-size: var(--font-size-large);
display: inline-block; display: inline-block;
margin-bottom: 0;
}
.txs {
display: block;
margin-left: 1.5rem;
margin-bottom: calc(var(--spacer) / 4); margin-bottom: calc(var(--spacer) / 4);
white-space: nowrap;
width: 100%;
} }
.description { .description {
@ -78,3 +86,18 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@media screen and (min-width: 30rem) {
.txs {
display: inline;
}
}
@media screen and (min-width: 40rem) {
.title {
margin-bottom: calc(var(--spacer) / 2);
}
.txs {
margin-left: calc(var(--spacer) / 4);
}
}

View File

@ -9,8 +9,8 @@ export function Feedback(): ReactElement {
const items = Object.entries(values.feedback).map(([key, value], index) => ( const items = Object.entries(values.feedback).map(([key, value], index) => (
<li key={index} className={styles[value.status]}> <li key={index} className={styles[value.status]}>
<h3 className={styles.title}> <h3 className={styles.title}>{value.name}</h3>
{value.name} <div className={styles.txs}>
{value.txCount > 0 && ( {value.txCount > 0 && (
<TransactionCount <TransactionCount
txCount={value.txCount} txCount={value.txCount}
@ -18,7 +18,7 @@ export function Feedback(): ReactElement {
txHash={value.txHash} txHash={value.txHash}
/> />
)} )}
</h3> </div>
<p className={styles.description}>{value.description}</p> <p className={styles.description}>{value.description}</p>
{value.errorMessage && ( {value.errorMessage && (
<span className={styles.errorMessage}>{value.errorMessage}</span> <span className={styles.errorMessage}>{value.errorMessage}</span>

View File

@ -1,7 +1,6 @@
.txHash { .txHash {
display: inline-block; display: inline-block;
margin-left: calc(var(--spacer) / 2); margin: calc(var(--spacer) / 4) calc(var(--spacer) / 4);
margin-top: calc(var(--spacer) / 2);
font-size: var(--font-size-small); font-size: var(--font-size-small);
font-family: var(--font-family-base); font-family: var(--font-family-base);
font-weight: var(--font-weight-base); font-weight: var(--font-weight-base);

View File

@ -96,7 +96,8 @@ export const initialValues: FormPublishData = {
amountOcean: 50, amountOcean: 50,
weightOnOcean: '5', // 50% on OCEAN weightOnOcean: '5', // 50% on OCEAN
weightOnDataToken: '5', // 50% on datatoken weightOnDataToken: '5', // 50% on datatoken
swapFee: 0.1 // in % swapFee: 0.1, // in %
freeAgreement: false
} }
} }

View File

@ -1,6 +1,7 @@
import { ServiceComputeOptions } from '@oceanprotocol/lib' import { ServiceComputeOptions } from '@oceanprotocol/lib'
import { NftMetadata } from '@utils/nft' import { NftMetadata } from '@utils/nft'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { PriceOptions } from 'src/@types/Price'
interface FileMetadata { interface FileMetadata {
url: string url: string

View File

@ -18,6 +18,8 @@ import {
import { mapTimeoutStringToSeconds } from '@utils/ddo' import { mapTimeoutStringToSeconds } from '@utils/ddo'
import { generateNftCreateData } from '@utils/nft' import { generateNftCreateData } from '@utils/nft'
import { getEncryptedFiles } from '@utils/provider' import { getEncryptedFiles } from '@utils/provider'
import { getSiteMetadata } from '@utils/siteConfig'
import Decimal from 'decimal.js'
import slugify from 'slugify' import slugify from 'slugify'
import Web3 from 'web3' import Web3 from 'web3'
import { import {
@ -191,7 +193,6 @@ export async function transformPublishFormToDdo(
export async function createTokensAndPricing( export async function createTokensAndPricing(
values: FormPublishData, values: FormPublishData,
accountId: string, accountId: string,
marketFeeAddress: string,
config: Config, config: Config,
nftFactory: NftFactory, nftFactory: NftFactory,
web3: Web3 web3: Web3
@ -199,19 +200,17 @@ export async function createTokensAndPricing(
const nftCreateData: NftCreateData = generateNftCreateData( const nftCreateData: NftCreateData = generateNftCreateData(
values.metadata.nft values.metadata.nft
) )
const { appConfig } = getSiteMetadata()
LoggerInstance.log('[publish] Creating NFT with metadata', nftCreateData) LoggerInstance.log('[publish] Creating NFT with metadata', nftCreateData)
// TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point // TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point
// fee is default 0 for now
// TODO: templateIndex is hardcoded for now but this is incorrect, in the future it should be something like 1 for pools, and 2 for fre and free
const ercParams: Erc20CreateParams = { const ercParams: Erc20CreateParams = {
templateIndex: values.pricing.type === 'dynamic' ? 1 : 2, templateIndex: values.pricing.type === 'dynamic' ? 1 : 2,
minter: accountId, minter: accountId,
feeManager: accountId, feeManager: accountId,
mpFeeAddress: marketFeeAddress, mpFeeAddress: appConfig.marketFeeAddress,
feeToken: config.oceanTokenAddress, feeToken: config.oceanTokenAddress,
feeAmount: `0`, feeAmount: appConfig.publisherMarketOrderFee,
cap: '1000', cap: '1000',
name: values.services[0].dataTokenOptions.name, name: values.services[0].dataTokenOptions.name,
symbol: values.services[0].dataTokenOptions.symbol symbol: values.services[0].dataTokenOptions.symbol
@ -226,21 +225,22 @@ export async function createTokensAndPricing(
case 'dynamic': { case 'dynamic': {
// no vesting in market by default, maybe at a later time , vestingAmount and vestedBlocks are hardcoded // no vesting in market by default, maybe at a later time , vestingAmount and vestedBlocks are hardcoded
// we use only ocean as basetoken // we use only ocean as basetoken
// TODO: discuss swapFeeLiquidityProvider, swapFeeMarketPlaceRunner // swapFeeLiquidityProvider is the swap fee of the liquidity providers
// swapFeeMarketRunner is the swap fee of the market where the swap occurs
const poolParams: PoolCreationParams = { const poolParams: PoolCreationParams = {
ssContract: config.sideStakingAddress, ssContract: config.sideStakingAddress,
baseTokenAddress: config.oceanTokenAddress, baseTokenAddress: config.oceanTokenAddress,
baseTokenSender: config.erc721FactoryAddress, baseTokenSender: config.erc721FactoryAddress,
publisherAddress: accountId, publisherAddress: accountId,
marketFeeCollector: marketFeeAddress, marketFeeCollector: appConfig.marketFeeAddress,
poolTemplateAddress: config.poolTemplateAddress, poolTemplateAddress: config.poolTemplateAddress,
rate: values.pricing.price.toString(), rate: new Decimal(1).div(values.pricing.price).toString(),
baseTokenDecimals: 18, baseTokenDecimals: 18,
vestingAmount: '0', vestingAmount: '0',
vestedBlocks: 2726000, vestedBlocks: 2726000,
initialBaseTokenLiquidity: values.pricing.amountOcean.toString(), initialBaseTokenLiquidity: values.pricing.amountOcean.toString(),
swapFeeLiquidityProvider: 1e15, swapFeeLiquidityProvider: (values.pricing.swapFee / 100).toString(),
swapFeeMarketRunner: 1e15 swapFeeMarketRunner: appConfig.publisherMarketPoolSwapFee
} }
LoggerInstance.log( LoggerInstance.log(
@ -249,18 +249,17 @@ export async function createTokensAndPricing(
) )
// the spender in this case is the erc721Factory because we are delegating // the spender in this case is the erc721Factory because we are delegating
const pool = new Pool(web3)
const txApprove = await approve( const txApprove = await approve(
web3, web3,
accountId, accountId,
config.oceanTokenAddress, config.oceanTokenAddress,
config.erc721FactoryAddress, config.erc721FactoryAddress,
'200', values.pricing.amountOcean.toString(),
false false
) )
LoggerInstance.log('[publish] pool.approve tx', txApprove) LoggerInstance.log('[publish] pool.approve tx', txApprove, nftFactory)
const result = await nftFactory.createNftErcWithPool( const result = await nftFactory.createNftErc20WithPool(
accountId, accountId,
nftCreateData, nftCreateData,
ercParams, ercParams,
@ -279,11 +278,11 @@ export async function createTokensAndPricing(
fixedRateAddress: config.fixedRateExchangeAddress, fixedRateAddress: config.fixedRateExchangeAddress,
baseTokenAddress: config.oceanTokenAddress, baseTokenAddress: config.oceanTokenAddress,
owner: accountId, owner: accountId,
marketFeeCollector: marketFeeAddress, marketFeeCollector: appConfig.marketFeeAddress,
baseTokenDecimals: 18, baseTokenDecimals: 18,
datatokenDecimals: 18, datatokenDecimals: 18,
fixedRate: values.pricing.price.toString(), fixedRate: values.pricing.price.toString(),
marketFee: 1e15, marketFee: appConfig.publisherMarketFixedSwapFee,
withMint: true withMint: true
} }
@ -292,7 +291,7 @@ export async function createTokensAndPricing(
freParams freParams
) )
const result = await nftFactory.createNftErcWithFixedRate( const result = await nftFactory.createNftErc20WithFixedRate(
accountId, accountId,
nftCreateData, nftCreateData,
ercParams, ercParams,
@ -324,7 +323,7 @@ export async function createTokensAndPricing(
dispenserParams dispenserParams
) )
const result = await nftFactory.createNftErcWithDispenser( const result = await nftFactory.createNftErc20WithDispenser(
accountId, accountId,
nftCreateData, nftCreateData,
ercParams, ercParams,

View File

@ -21,7 +21,6 @@ import {
LoggerInstance, LoggerInstance,
DDO DDO
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
import { validationSchema } from './_validation' import { validationSchema } from './_validation'
import { useAbortController } from '@hooks/useAbortController' import { useAbortController } from '@hooks/useAbortController'
@ -38,7 +37,6 @@ export default function PublishPage({
const { accountId, web3, chainId } = useWeb3() const { accountId, web3, chainId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId) const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
const scrollToRef = useRef() const scrollToRef = useRef()
const { appConfig } = useSiteMetadata()
const nftFactory = useNftFactory() const nftFactory = useNftFactory()
const newAbortController = useAbortController() const newAbortController = useAbortController()
@ -75,7 +73,6 @@ export default function PublishPage({
await createTokensAndPricing( await createTokensAndPricing(
values, values,
accountId, accountId,
appConfig.marketFeeAddress,
config, config,
nftFactory, nftFactory,
web3 web3

View File

@ -61,8 +61,9 @@ export default function SearchPage({
setTotalResults(undefined) setTotalResults(undefined)
const queryResult = await getResults(parsed, chainIds, newCancelToken()) const queryResult = await getResults(parsed, chainIds, newCancelToken())
setQueryResult(queryResult) setQueryResult(queryResult)
setTotalResults(queryResult.totalResults)
setTotalPagesNumber(queryResult.totalPages) setTotalResults(queryResult?.totalResults || 0)
setTotalPagesNumber(queryResult?.totalPages || 0)
setLoading(false) setLoading(false)
}, },
[newCancelToken, setTotalPagesNumber, setTotalResults] [newCancelToken, setTotalPagesNumber, setTotalResults]

View File

@ -123,7 +123,7 @@ export function getSearchQuery(
const filters: FilterTerm[] = [] const filters: FilterTerm[] = []
accessType !== undefined && accessType !== undefined &&
filters.push(getFilterTerm('nft.type', accessType)) filters.push(getFilterTerm('services.type', accessType))
serviceType !== undefined && serviceType !== undefined &&
filters.push(getFilterTerm('metadata.type', serviceType)) filters.push(getFilterTerm('metadata.type', serviceType))