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

Restore order (#1068)

* minor refactors

* minor refactors

* fixes

* buy dt

* consumePrice + estimation

* various fixes

* cleanup

* fix build

* fix ssh issue

* feedback

* build fix

* ssh fix

* remove console.log

* suggested fixes

* other fixes

* switch to decimal

* more fixes

* more fixes

* fix

* some fee refactors

* more fee refactoring

* lib update, fre rename

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* minor refactors

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* build fixes

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* update + more refactoring

* calc price

* fix build

* restore accountId in effect

* fix order

* fix build and update lib

* fix order index

* fix comments

* pool fix

* remove console.log

* fix order fixed rate exchange

* fixed free order and messaging

* add comment

* minor type fix

* more type fixes
This commit is contained in:
mihaisc 2022-02-14 18:27:36 +02:00 committed by GitHub
parent 4576151e0c
commit 8d1782a800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 743 additions and 770 deletions

View File

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

View File

@ -22,6 +22,25 @@ module.exports = {
marketFeeAddress:
process.env.NEXT_PUBLIC_MARKET_FEE_ADDRESS ||
'0x9984b2453eC7D99a73A5B3a46Da81f197B753C8d',
// publisher market fee that is taken upon ordering an asset, it is an absolute value, it is declared on erc20 creation
publisherMarketOrderFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_ORDER_FEE || '0',
// fee recieved by the publisher market when a dt is swaped from a pool, percent
publisherMarketPoolSwapFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_POOL_SWAP_FEE || '0',
// fee recieved by the publisher market when a dt is bought from a fixed rate exchange, percent
publisherMarketFixedSwapFee:
process.env.NEXT_PUBLIC_PUBLISHER_MARKET_FIXED_SWAP_FEE || '0',
// consume market fee that is taken upon ordering an asset, it is an absolute value, it is specified on order
consumeMarketOrderFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_ORDER_FEE || '0',
// fee recieved by the consume market when a dt is swaped from a pool, percent
consumeMarketPoolSwapFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_POOL_SWAP_FEE || '0',
// fee recieved by the consume market when a dt is bought from a fixed rate exchange, percent
consumeMarketFixedSwapFee:
process.env.NEXT_PUBLIC_CONSUME_MARKET_FIXED_SWAP_FEE || '0',
// Used for conversion display, can be whatever coingecko API supports
// see: https://api.coingecko.com/api/v3/simple/supported_vs_currencies

View File

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

17
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@coingecko/cryptoformat": "^0.4.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^1.0.0-next.14",
"@oceanprotocol/lib": "^1.0.0-next.17",
"@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^4.0.6",
"@tippyjs/react": "^4.2.6",
@ -86,10 +86,13 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"https-browserify": "^1.0.0",
"husky": "^7.0.4",
"prettier": "^2.5.1",
"pretty-quick": "^3.1.3",
"process": "^0.11.10",
"serve": "^13.0.2",
"stream-http": "^2.8.3",
"typescript": "^4.5.4"
},
"engines": {
@ -3453,9 +3456,9 @@
}
},
"node_modules/@oceanprotocol/lib": {
"version": "1.0.0-next.14",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.14.tgz",
"integrity": "sha512-qz41c6LlvfELvz9oTJncIsZ/cZudEzq2gjCgI4E3iQfo71qB7jzwPd6oqiqIYHmRHl8J/F6QXe9fQsp81R4TIg==",
"version": "1.0.0-next.17",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.17.tgz",
"integrity": "sha512-qZQ2nWe/qNN/2M9YZEacwHSMyStwFoN6gXKDCw17G9E8QLwl3v5z54WjAqLlGOpNnGKMIvkrj4gUpUAMtjLSxw==",
"dependencies": {
"@oceanprotocol/contracts": "1.0.0-alpha.18",
"bignumber.js": "^9.0.2",
@ -30132,9 +30135,9 @@
}
},
"@oceanprotocol/lib": {
"version": "1.0.0-next.14",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.14.tgz",
"integrity": "sha512-qz41c6LlvfELvz9oTJncIsZ/cZudEzq2gjCgI4E3iQfo71qB7jzwPd6oqiqIYHmRHl8J/F6QXe9fQsp81R4TIg==",
"version": "1.0.0-next.17",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-1.0.0-next.17.tgz",
"integrity": "sha512-qZQ2nWe/qNN/2M9YZEacwHSMyStwFoN6gXKDCw17G9E8QLwl3v5z54WjAqLlGOpNnGKMIvkrj4gUpUAMtjLSxw==",
"requires": {
"@oceanprotocol/contracts": "1.0.0-alpha.18",
"bignumber.js": "^9.0.2",

View File

@ -21,7 +21,7 @@
"@coingecko/cryptoformat": "^0.4.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^1.0.0-next.14",
"@oceanprotocol/lib": "^1.0.0-next.17",
"@oceanprotocol/typographies": "^0.1.0",
"@portis/web3": "^4.0.6",
"@tippyjs/react": "^4.2.6",
@ -94,10 +94,13 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"https-browserify": "^1.0.0",
"husky": "^7.0.4",
"prettier": "^2.5.1",
"pretty-quick": "^3.1.3",
"process": "^0.11.10",
"serve": "^13.0.2",
"stream-http": "^2.8.3",
"typescript": "^4.5.4"
},
"repository": {

View File

@ -95,7 +95,6 @@ function AssetProvider({
// -----------------------------------
const fetchAccessDetails = useCallback(async (): Promise<void> => {
if (!asset?.chainId || !asset?.services) return
const accessDetails = await getAccessDetails(
asset.chainId,
asset.services[0].datatokenAddress,

View File

@ -30,7 +30,8 @@ const initialPoolInfo: Partial<PoolInfo> = {
}
const initialPoolInfoUser: Partial<PoolInfoUser> = {
liquidity: new Decimal(0)
liquidity: new Decimal(0),
poolShares: '0'
}
const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser
@ -68,7 +69,7 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
setPoolData(response.poolData)
setPoolInfoUser((prevState) => ({
...prevState,
poolShares: response.poolDataUser?.shares[0]?.shares
poolShares: response.poolDataUser?.shares[0]?.shares || '0'
}))
setPoolSnapshots(response.poolSnapshots)
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
@ -193,10 +194,10 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
!poolData ||
!poolInfo?.totalPoolTokens ||
!asset?.chainId ||
!accountId
!accountId ||
!poolInfoUser
)
return
// Staking bot receives half the pool shares so for display purposes
// we can multiply by 2 as we have a hardcoded 50/50 pool weight.
const userPoolShares = new Decimal(poolInfoUser.poolShares || 0)
@ -237,6 +238,8 @@ function PoolProvider({ children }: { children: ReactNode }): ReactElement {
poolShares: userPoolShares,
...newPoolInfoUser
})
// poolInfoUser was not added on purpose, we use setPoolInfoUser so it will just loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
poolData,
poolInfoUser?.poolShares,

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 siteContent from '../../../content/site.json'
import appConfig from '../../../app.config'
import { getSiteMetadata } from '@utils/siteConfig'
export function useSiteMetadata(): UseSiteMetadata {
const siteMeta: UseSiteMetadata = {
...siteContent,
appConfig
}
return siteMeta
return getSiteMetadata()
}

View File

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

50
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 {
type: 'dynamic' | 'fixed' | 'free' | ''
price: number
// if type is dynamic this is the pool address, for fixed/free this is an id.
price: string
addressOrId: string
baseToken: TokenInfo
datatoken: TokenInfo
isConsumable?: boolean
// if there are valid orders for this
owned: bool
isPurchasable?: boolean
isOwned: bool
validOrderTx: string
publisherMarketOrderFee: string
}
interface PriceOptions {

View File

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

View File

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

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
// to automatically generate pages from Markdown files in `src/pages/[slug].tsx`.
//
const pagesDirectory = join(process.cwd(), 'content', 'pages')
// const pagesDirectory = join(process.cwd(), 'content', 'pages')
const pagesDirectory = './content/pages'
export interface PageData {
slug: string
frontmatter: { [key: string]: any }

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

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

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

@ -0,0 +1,75 @@
import { approve, Pool } from '@oceanprotocol/lib'
import Web3 from 'web3'
import { getSiteMetadata } from './siteConfig'
import { getDummyWeb3 } from './web3'
import { TransactionReceipt } from 'web3-eth'
import Decimal from 'decimal.js'
import { AccessDetails } from 'src/@types/Price'
/**
* This is used to calculate the price to buy one datatoken from a pool, that is different from spot price. You need to pass either a web3 object or a chainId. If you pass a chainId a dummy web3 object will be created
* @param {AccessDetails} accessDetails
* @param {Web3?} [web3]
* @param {number?} [chainId]
* @return {Promise<PriceAndEstimation>}
*/
export async function calculateBuyPrice(
accessDetails: AccessDetails,
chainId?: number,
web3?: Web3
): Promise<string> {
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,
false
)
const result = await pool.swapExactAmountOut(
accountId,
accessDetails.addressOrId,
{
marketFeeAddress: appConfig.marketFeeAddress,
tokenIn: accessDetails.baseToken.address,
tokenOut: accessDetails.datatoken.address
},
{
// this is just to be safe
maxAmountIn: new Decimal(dtPrice).mul(10).toString(),
swapMarketFee: appConfig.consumeMarketPoolSwapFee,
tokenAmountOut: '1'
}
)
return result
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import FileIcon from '@shared/FileIcon'
import Alert from '@shared/atoms/Alert'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { useWeb3 } from '@context/Web3'
import { usePricing } from '@hooks/usePricing'
import {
generateBaseQuery,
getFilterTerm,
@ -37,6 +36,7 @@ import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import { SortTermOptions } from '../../../../@types/aquarius/SearchQuery'
import { getAccessDetails } from '@utils/accessDetailsAndPricing'
import { AccessDetails } from 'src/@types/Price'
export default function Compute({
ddo,
@ -57,7 +57,6 @@ export default function Compute({
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { accountId } = useWeb3()
const { buyDT, pricingError, pricingStepText } = usePricing()
const [isJobStarting, setIsJobStarting] = useState(false)
const [error, setError] = useState<string>()
@ -181,13 +180,13 @@ export default function Compute({
useEffect(() => {
if (!algorithmConsumeDetails) return
setIsAlgoConsumablePrice(algorithmConsumeDetails.isConsumable)
setIsAlgoConsumablePrice(algorithmConsumeDetails.isPurchasable)
}, [algorithmConsumeDetails])
useEffect(() => {
if (!accessDetails) return
setIsConsumablePrice(accessDetails.isConsumable)
setIsConsumablePrice(accessDetails.isPurchasable)
}, [accessDetails])
// useEffect(() => {
@ -234,10 +233,10 @@ export default function Compute({
// Output errors in toast UI
useEffect(() => {
const newError = error || pricingError
const newError = error
if (!newError) return
toast.error(newError)
}, [error, pricingError])
}, [error])
// async function startJob(algorithmId: string) {
// try {
@ -400,7 +399,7 @@ export default function Compute({
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} ddo={ddo} />
<AlgorithmDatasetsListForCompute algorithmDid={ddo.id} asset={ddo} />
</>
) : (
<Formik
@ -433,7 +432,8 @@ export default function Compute({
selectedComputeAssetLowPoolLiquidity={!isAlgoConsumablePrice}
selectedComputeAssetType="algorithm"
selectedComputeAssetTimeout={algorithmTimeout}
stepText={pricingStepText || 'Starting Compute Job...'}
// lazy comment when removing pricingStepText
stepText={'pricingStepText' || 'Starting Compute Job...'}
algorithmConsumeDetails={algorithmConsumeDetails}
isConsumable={isConsumable}
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

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

View File

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

View File

@ -42,7 +42,14 @@ export default function PricingFields(): ReactElement {
: 0
setFieldValue('pricing.amountDataToken', amountDataToken)
}, [price, amountOcean, weightOnOcean, weightOnDataToken, type])
}, [
price,
amountOcean,
weightOnOcean,
weightOnDataToken,
type,
setFieldValue
])
const tabs = [
appConfig.allowFixedPricing === 'true'

View File

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

View File

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

View File

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