diff --git a/package-lock.json b/package-lock.json index 59c6631..e7087bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.3.0-beta.2", "license": "Apache-2.0", "dependencies": { - "@oceanprotocol/lib": "3.1.1", + "@oceanprotocol/lib": "^3.1.1", "@urql/exchange-refocus": "^1.0.0", "axios": "^1.3.4", "decimal.js": "^10.4.3", diff --git a/src/@types/Nautilus.ts b/src/@types/Nautilus.ts index b9fd750..b2ecec4 100644 --- a/src/@types/Nautilus.ts +++ b/src/@types/Nautilus.ts @@ -1,4 +1,4 @@ -import { Metadata, PublisherTrustedAlgorithm } from '@oceanprotocol/lib' +import { Asset, Metadata, PublisherTrustedAlgorithm } from '@oceanprotocol/lib' import { NautilusConsumerParameter } from '../Nautilus/Asset/ConsumerParameters' import { NautilusAsset } from '../Nautilus/Asset/NautilusAsset' import { @@ -17,6 +17,16 @@ export interface NautilusOptions { skipDefaultConfig: boolean } +export type ServiceBuilderConfig = + | { + serviceType: ServiceTypes + fileType: FileTypes + } + | { + aquariusAsset: Asset + serviceId: string + } + export interface IBuilder { build: () => T reset: () => void @@ -27,6 +37,15 @@ export enum CredentialListTypes { DENY = 'deny' } +export enum LifecycleStates { + ACTIVE = 0, + END_OF_LIFE = 1, + DEPRECATED = 2, + REVOKED_BY_PUBLISHER = 3, + ORDERING_DISABLED_TEMPORARILY = 4, + ASSET_UNLISTED = 5 +} + export interface IAssetBuilder extends IBuilder { setType: (type: Metadata['type']) => IAssetBuilder setName: (name: Metadata['name']) => IAssetBuilder @@ -64,8 +83,8 @@ export interface IServiceBuilder parameter: NautilusConsumerParameter ) => IServiceBuilder addTrustedAlgorithmPublisher: (publisher: string) => IServiceBuilder - addTrustedAlgorithm: ( - algorithm: PublisherTrustedAlgorithm + addTrustedAlgorithms: ( + algorithms: PublisherTrustedAlgorithm[] ) => IServiceBuilder allowRawAlgorithms: (allow?: boolean) => IServiceBuilder allowAlgorithmNetworkAccess: (allow?: boolean) => IServiceBuilder diff --git a/src/@types/Publish.ts b/src/@types/Publish.ts index 6ccbf05..fd7bc61 100644 --- a/src/@types/Publish.ts +++ b/src/@types/Publish.ts @@ -12,6 +12,7 @@ import { import { Signer, providers } from 'ethers' import { FileTypes, + NautilusAsset, NautilusService, PricingConfigWithoutOwner, ServiceTypes @@ -93,6 +94,7 @@ export interface PublishDDOConfig { chainConfig: Config signer: Signer ddo: DDO + asset?: NautilusAsset } export interface PublishResponse { @@ -105,3 +107,8 @@ export interface PublishResponse { ddo: DDO setMetadataTxReceipt: providers.TransactionReceipt } + +export type TrustedAlgorithmAsset = { + did: string + serviceIds?: string[] +} diff --git a/src/Nautilus/Asset/AssetBuilder.ts b/src/Nautilus/Asset/AssetBuilder.ts index da1cedb..cb0a1c3 100644 --- a/src/Nautilus/Asset/AssetBuilder.ts +++ b/src/Nautilus/Asset/AssetBuilder.ts @@ -1,4 +1,9 @@ -import { CredentialListTypes, IAssetBuilder } from '../../@types/Nautilus' +import { Asset } from '@oceanprotocol/lib' +import { + CredentialListTypes, + IAssetBuilder, + LifecycleStates +} from '../../@types/Nautilus' import { MetadataConfig, NftCreateDataWithoutOwner } from '../../@types/Publish' import { combineArrays } from '../../utils' import { NautilusAsset } from './NautilusAsset' @@ -7,9 +12,21 @@ import { NautilusService, ServiceTypes } from './Service/NautilusService' +import { NautilusDDO } from './NautilusDDO' export class AssetBuilder implements IAssetBuilder { - private asset: NautilusAsset = new NautilusAsset() + private asset: NautilusAsset + + constructor(aquariusAsset?: Asset) { + if (aquariusAsset) { + const nautilusDDO = NautilusDDO.createFromAquariusAsset(aquariusAsset) + this.asset = new NautilusAsset(nautilusDDO) + this.asset.owner = aquariusAsset.nft.owner + this.asset.lifecycleState = aquariusAsset.nft.state + } else { + this.asset = new NautilusAsset() + } + } reset() { this.asset = new NautilusAsset() @@ -57,12 +74,24 @@ export class AssetBuilder implements IAssetBuilder { return this } + removeService(serviceId: string) { + this.asset.ddo.removeServices.push(serviceId) + + return this + } + setNftData(tokenData: NftCreateDataWithoutOwner) { this.asset.nftCreateData = tokenData return this } + setLifecycleState(state: LifecycleStates) { + this.asset.lifecycleState = state + + return this + } + setOwner(owner: string) { this.asset.owner = owner @@ -121,22 +150,23 @@ export class AssetBuilder implements IAssetBuilder { addCredentialAddresses(list: CredentialListTypes, addresses: string[]) { // first get the index of the address credential list - const addressCredentialIndex = this.asset.credentials[list].findIndex( + const addressCredentialIndex = this.asset.ddo.credentials[list].findIndex( (credential) => credential.type === 'address' ) // get addresses already added to the credential values const oldAddresses = - this.asset.credentials[list][addressCredentialIndex]?.values || [] + this.asset.ddo.credentials[list][addressCredentialIndex]?.values || [] // add new values and remove duplicates const newAddresses = combineArrays(oldAddresses, addresses) // update the existing credential or add a new one for type address if (addressCredentialIndex > -1) - this.asset.credentials[list][addressCredentialIndex].values = newAddresses + this.asset.ddo.credentials[list][addressCredentialIndex].values = + newAddresses else - this.asset.credentials[list].push({ + this.asset.ddo.credentials[list].push({ type: 'address', values: newAddresses }) @@ -144,6 +174,32 @@ export class AssetBuilder implements IAssetBuilder { return this } + removeCredentialAddresses(list: CredentialListTypes, addresses: string[]) { + // first get the index of the address credential list + const addressCredentialIndex = this.asset.ddo.credentials[list].findIndex( + (credential) => credential.type === 'address' + ) + + if (addressCredentialIndex === -1) return this + + // get addresses already added to the credential values + const oldAddresses = + this.asset.ddo.credentials[list][addressCredentialIndex]?.values + + const newAddresses = oldAddresses.filter( + (address) => !addresses.includes(address) + ) + + if (newAddresses.length > 0) { + this.asset.ddo.credentials[list][addressCredentialIndex].values = + newAddresses + } else { + this.asset.ddo.credentials[list].splice(addressCredentialIndex, 1) + } + + return this + } + build() { // TODO: look for errors / missing input return this.asset diff --git a/src/Nautilus/Asset/NautilusAsset.ts b/src/Nautilus/Asset/NautilusAsset.ts index cd1aad7..27bc140 100644 --- a/src/Nautilus/Asset/NautilusAsset.ts +++ b/src/Nautilus/Asset/NautilusAsset.ts @@ -1,7 +1,8 @@ -import { Credentials, NftCreateData } from '@oceanprotocol/lib' +import { NftCreateData } from '@oceanprotocol/lib' import { NftCreateDataWithoutOwner, PricingConfig } from '../../@types/Publish' import { NautilusDDO } from './NautilusDDO' import { nftInitialCreateData } from './constants/nft.constants' +import { LifecycleStates } from '../../@types' export type PricingConfigWithoutOwner = { type: PricingConfig['type'] @@ -12,15 +13,18 @@ export type PricingConfigWithoutOwner = { * @internal */ export class NautilusAsset { - ddo: NautilusDDO = new NautilusDDO() + ddo: NautilusDDO nftCreateData: NftCreateDataWithoutOwner owner: string - credentials: Credentials = { - allow: [], - deny: [] - } + lifecycleState: LifecycleStates + + constructor(ddo?: NautilusDDO) { + if (ddo) { + this.ddo = ddo + } else { + this.ddo = new NautilusDDO() + } - constructor() { this.initNftData() } diff --git a/src/Nautilus/Asset/NautilusDDO.ts b/src/Nautilus/Asset/NautilusDDO.ts index d3f14e6..6dd65d0 100644 --- a/src/Nautilus/Asset/NautilusDDO.ts +++ b/src/Nautilus/Asset/NautilusDDO.ts @@ -1,15 +1,23 @@ -import { DDO, Service, generateDid } from '@oceanprotocol/lib' +import { + Asset, + Credentials, + DDO, + Service, + generateDid +} from '@oceanprotocol/lib' import { MetadataConfig } from '../../@types' import { dateToStringNoMS, getAllPromisesOnArray, - combineArraysAndReplaceItems + combineArraysAndReplaceItems, + removeDuplicatesFromArray } from '../../utils' import { FileTypes, NautilusService, ServiceTypes } from './Service/NautilusService' +import { transformAquariusAssetToDDO } from '../../utils/aquarius' export class NautilusDDO { id: string @@ -19,8 +27,19 @@ export class NautilusDDO { version: string = '4.1.0' metadata: Partial = {} services: NautilusService[] = [] + removeServices: string[] = [] private ddo: DDO + credentials: Credentials = { + allow: [], + deny: [] + } + + static createFromAquariusAsset(aquariusAsset: Asset): NautilusDDO { + const ddo = transformAquariusAssetToDDO(aquariusAsset) + + return this.createFromDDO(ddo) + } static createFromDDO(ddo: DDO): NautilusDDO { const nautilusDDO = new NautilusDDO() @@ -32,9 +51,18 @@ export class NautilusDDO { nautilusDDO.chainId = ddo.chainId nautilusDDO.version = ddo.version + if (ddo.credentials?.allow) + nautilusDDO.credentials.allow = ddo.credentials.allow + if (ddo.credentials?.deny) + nautilusDDO.credentials.deny = ddo.credentials.deny + return nautilusDDO } + getOriginalDDO() { + return this.ddo + } + private async buildDDOServices(): Promise { if (this.services.length < 1) throw new Error('At least one service needs to be defined.') @@ -68,20 +96,38 @@ export class NautilusDDO { // take ddo.services const existingServices: Service[] = this.ddo?.services || [] - // we simply return ddo.services, if nothing new was added - if (this.services.length < 1) return existingServices + // remove service from existing services if id changes to prevent old service after edit + for (const service of this.services) { + const isFilesObjectChanged = service.checkIfFilesObjectChanged() - // build new services if needed - const newServices = await this.buildDDOServices() + if (service.id && isFilesObjectChanged) { + this.removeServices.push(service.id) + } + } - // replace all existing services with new ones, based on the servie.id - const replacedServices = combineArraysAndReplaceItems( - existingServices, - newServices, - NautilusDDO.replaceServiceBasedOnId + let newServices: Service[] + if (this.services.length > 0) { + // build new services if needed + newServices = await this.buildDDOServices() + } + + this.removeServices = removeDuplicatesFromArray(this.removeServices) + + const reducedExistingServices = existingServices.filter( + (service) => !this.removeServices.includes(service.id) ) - return replacedServices + // replace all existing services with new ones, based on the servie.id + let replacedServices: Service[] | PromiseLike + if (this.services.length > 0) { + replacedServices = combineArraysAndReplaceItems( + reducedExistingServices, + newServices, + NautilusDDO.replaceServiceBasedOnId + ) + } + + return replacedServices || reducedExistingServices } private async buildDDO(create: boolean): Promise { @@ -110,7 +156,8 @@ export class NautilusDDO { chainId: this.chainId, version: this.version, metadata: newMetadata, - services: newServices + services: newServices, + credentials: this.credentials } return this.ddo diff --git a/src/Nautilus/Asset/Service/NautilusService.ts b/src/Nautilus/Asset/Service/NautilusService.ts index 7ee0835..ddf605f 100644 --- a/src/Nautilus/Asset/Service/NautilusService.ts +++ b/src/Nautilus/Asset/Service/NautilusService.ts @@ -8,7 +8,10 @@ import { UrlFile, getHash } from '@oceanprotocol/lib' -import { DatatokenCreateParamsWithoutOwner } from '../../../@types/Publish' +import { + DatatokenCreateParamsWithoutOwner, + TrustedAlgorithmAsset +} from '../../../@types/Publish' import { getEncryptedFiles, getFileInfo, @@ -61,9 +64,14 @@ export class NautilusService< serviceEndpoint: string timeout: number files: ServiceFileType[] = [] + existingEncryptedFiles: string pricing: PricingConfigWithoutOwner + newPrice: string datatokenCreateParams: DatatokenCreateParamsWithoutOwner + editExistingService: boolean + filesEdited: boolean + serviceEndpointEdited: boolean name?: string description?: string @@ -75,6 +83,8 @@ export class NautilusService< publisherTrustedAlgorithms: [] } + addedPublisherTrustedAlgorithms: TrustedAlgorithmAsset[] = [] + consumerParameters?: NautilusConsumerParameter[] = [] additionalInformation?: { [key: string]: any } @@ -82,15 +92,9 @@ export class NautilusService< datatokenAddress?: string constructor() { + this.editExistingService = false + this.filesEdited = false this.initDatatokenData() - this.initPricing() - } - - // TODO: refactor to not assume free pricing, but rather expect user to set this - private initPricing() { - this.pricing = { - type: 'free' - } } private initDatatokenData() { @@ -111,26 +115,36 @@ export class NautilusService< const datatokenAddress = dtAddress || this.datatokenAddress if (!datatokenAddress) throw new Error('datatokenAddress is required') - const assetURL = { - datatokenAddress, - nftAddress, - files: this.files - } + const isFilesObjectChanged = this.checkIfFilesObjectChanged() - const encryptedFiles = await getEncryptedFiles( - assetURL, - chainId, - this.serviceEndpoint - ) + let encryptedFiles: string + + if (isFilesObjectChanged) { + if (this.files.length < 1) { + throw new Error('Can not encrypt files. No files defined!') + } + + const assetURL = { + datatokenAddress, + nftAddress, + files: this.files + } + + encryptedFiles = await getEncryptedFiles( + assetURL, + chainId, + this.serviceEndpoint + ) + } // required attributes const oceanService: Service = { - id: this.id || getHash(encryptedFiles), + id: this.id && !isFilesObjectChanged ? this.id : getHash(encryptedFiles), datatokenAddress, type: this.type, serviceEndpoint: this.serviceEndpoint, timeout: this.timeout, - files: encryptedFiles + files: isFilesObjectChanged ? encryptedFiles : this.existingEncryptedFiles } // add optional attributes if they are defined @@ -164,4 +178,12 @@ export class NautilusService< return true } + + checkIfFilesObjectChanged(): boolean { + return ( + (this.editExistingService && + (this.filesEdited || this.serviceEndpointEdited)) || + !!this.pricing + ) + } } diff --git a/src/Nautilus/Asset/Service/ServiceBuilder.ts b/src/Nautilus/Asset/Service/ServiceBuilder.ts index 372f321..546bd2c 100644 --- a/src/Nautilus/Asset/Service/ServiceBuilder.ts +++ b/src/Nautilus/Asset/Service/ServiceBuilder.ts @@ -1,13 +1,20 @@ -import { PublisherTrustedAlgorithm } from '@oceanprotocol/lib' -import { IServiceBuilder } from '../../../@types/Nautilus' -import { NautilusConsumerParameter } from '../ConsumerParameters' +import { Service } from '@oceanprotocol/lib' +import { IServiceBuilder, ServiceBuilderConfig } from '../../../@types/Nautilus' +import { + ConsumerParameterBuilder, + NautilusConsumerParameter +} from '../ConsumerParameters' import { FileTypes, NautilusService, ServiceFileType, ServiceTypes } from './NautilusService' -import { DatatokenCreateParamsWithoutOwner } from '../../../@types/Publish' +import { + ConsumerParameterSelectOption, + DatatokenCreateParamsWithoutOwner, + TrustedAlgorithmAsset +} from '../../../@types/Publish' import { PricingConfigWithoutOwner } from '../NautilusAsset' export class ServiceBuilder< @@ -17,21 +24,92 @@ export class ServiceBuilder< { private service = new NautilusService() - constructor(serviceType: ServiceType, fileType = FileTypes.URL) { - this.service.type = serviceType + constructor(config: ServiceBuilderConfig) { + if ('serviceType' in config) { + this.service.type = config.serviceType as ServiceType + + if (this.service.type === ServiceTypes.COMPUTE) { + this.service.compute = { + allowNetworkAccess: false, + allowRawAlgorithm: false, + publisherTrustedAlgorithmPublishers: [], + publisherTrustedAlgorithms: [] + } + } + } else { + const { aquariusAsset, serviceId } = config - if (this.service.type === 'compute') { - this.service.compute = { - allowNetworkAccess: false, - allowRawAlgorithm: false, - publisherTrustedAlgorithmPublishers: [], - publisherTrustedAlgorithms: [] + if (!aquariusAsset || !serviceId) { + throw new Error('Missing parameter(s) in serviceBuilder config.') + } + const service: Service = aquariusAsset.services.find( + (service) => service.id === serviceId + ) + if (!service) { + throw new Error('No service with matching id found in provided DDO.') + } + + // mark service as existing service + this.service.editExistingService = true + + this.service.id = service.id + this.service.type = ServiceTypes[service.type.toUpperCase()] + this.service.datatokenAddress = service.datatokenAddress + this.service.serviceEndpoint = service.serviceEndpoint + this.service.timeout = service.timeout + + // aquariusAsset must be used since datatoken NAME and SYMBOL are not included in the service object of the DDO + const datatokenObj = aquariusAsset.datatokens.find( + (datatoken) => datatoken.address === service.datatokenAddress + ) + this.service.datatokenCreateParams = { + ...this.service.datatokenCreateParams, + name: datatokenObj.name, + symbol: datatokenObj.symbol + } + + this.service.existingEncryptedFiles = service.files + + // required for compute assets + if (service.compute) this.service.compute = service.compute + + // optional + if (service.name) this.service.name = service.name + if (service.description) this.service.description = service.description + if (service.additionalInformation) + this.service.additionalInformation = service.additionalInformation + + if (service.consumerParameters && service.consumerParameters.length > 0) { + for (const ddoParameter of service.consumerParameters) { + const builder = new ConsumerParameterBuilder() + + builder + .setType(ddoParameter.type) + .setName(ddoParameter.name) + .setLabel(ddoParameter.label) + .setDescription(ddoParameter.description) + .setDefault(ddoParameter.default) + .setRequired(ddoParameter.required) + + if (ddoParameter.options) { + const parameterOptions: ConsumerParameterSelectOption[] = + JSON.parse(ddoParameter.options) + for (const option of parameterOptions) { + builder.addOption(option) + } + } + + const parameter = builder.build() + + this.addConsumerParameter(parameter) + } } } } addFile(file: ServiceFileType) { this.service.files.push(file) + this.service.filesEdited = true return this } @@ -44,6 +122,7 @@ export class ServiceBuilder< setServiceEndpoint(endpoint: string) { this.service.serviceEndpoint = endpoint + this.service.serviceEndpointEdited = true return this } @@ -66,9 +145,19 @@ export class ServiceBuilder< return this } + addAdditionalInformation(additionalInformation: { [key: string]: any }) { + this.service.additionalInformation = { + ...this.service.additionalInformation, + ...additionalInformation + } + + return this + } + // #region compute allowRawAlgorithms(allow = true) { - if (this.service.type !== 'compute') return + if (this.service.type !== 'compute') + throw new Error('Illegal operation, asset is not a compute asset!') this.service.compute.allowRawAlgorithm = allow @@ -76,25 +165,120 @@ export class ServiceBuilder< } allowAlgorithmNetworkAccess(allow = true) { - if (this.service.type !== 'compute') return + if (this.service.type !== 'compute') + throw new Error('Illegal operation, asset is not a compute asset!') this.service.compute.allowNetworkAccess = allow return this } - addTrustedAlgorithm(algorithm: PublisherTrustedAlgorithm) { - if (this.service.type !== 'compute') return + addTrustedAlgorithms(trustedAlgorithmAssets: TrustedAlgorithmAsset[]) { + if (this.service.type !== 'compute') { + throw new Error('Illegal operation, asset is not a compute asset!') + } + + if (!trustedAlgorithmAssets || trustedAlgorithmAssets.length === 0) { + throw new Error('No TrustedAlgorithmAssets provided.') + } + + trustedAlgorithmAssets.forEach((trustedAlgorithmAsset) => { + const existingIndex = + this.service.addedPublisherTrustedAlgorithms.findIndex( + (existingAsset) => existingAsset.did === trustedAlgorithmAsset.did + ) + + if (existingIndex > -1) { + // Merge serviceIds + this.service.addedPublisherTrustedAlgorithms[existingIndex].serviceIds = + Array.from( + new Set([ + ...(this.service.addedPublisherTrustedAlgorithms[existingIndex] + .serviceIds || []), + ...(trustedAlgorithmAsset.serviceIds || []) + ]) + ) + } else { + // Add new trusted algorithm asset + this.service.addedPublisherTrustedAlgorithms.push(trustedAlgorithmAsset) + } + }) + + return this + } + + removeTrustedAlgorithm(did: string) { + if (this.service.type !== 'compute') + throw new Error('Illegal operation, asset is not a compute asset!') + + this.service.compute.publisherTrustedAlgorithms = + this.service.compute.publisherTrustedAlgorithms.filter( + (algorithm) => algorithm.did !== did + ) + + return this + } + + setAllAlgorithmsTrusted() { + this.service.compute.publisherTrustedAlgorithms = null - this.service.compute.publisherTrustedAlgorithms.push(algorithm) + return this + } + + setAllAlgorithmsUntrusted() { + this.service.compute.publisherTrustedAlgorithms = [] return this } - addTrustedAlgorithmPublisher(publisher: string) { - if (this.service.type !== 'compute') return + addTrustedAlgorithmPublisher(publisherAddress: string) { + if (this.service.type !== 'compute') { + throw new Error('Illegal operation, asset is not a compute asset!') + } - this.service.compute.publisherTrustedAlgorithmPublishers.push(publisher) + if (!this.service.compute.publisherTrustedAlgorithmPublishers) { + this.service.compute.publisherTrustedAlgorithmPublishers = [ + publisherAddress + ] + return this + } + + if ( + !this.service.compute.publisherTrustedAlgorithmPublishers.includes( + publisherAddress + ) + ) { + this.service.compute.publisherTrustedAlgorithmPublishers.push( + publisherAddress + ) + } + + return this + } + + removeTrustedAlgorithmPublisher(publisherAddress: string) { + if (this.service.type !== 'compute') + throw new Error('Illegal operation, asset is not a compute asset!') + + const lowerCasePublisherAddress = publisherAddress.toLowerCase() + + // Remove all occurrences of publisherAddress + this.service.compute.publisherTrustedAlgorithmPublishers = + this.service.compute.publisherTrustedAlgorithmPublishers.filter( + (address) => address.toLowerCase() !== lowerCasePublisherAddress + ) + + return this + } + + setAllAlgorithmPublishersTrusted() { + this.service.compute.publisherTrustedAlgorithmPublishers = null + + return this + } + + setAllAlgorithmPublishersUntrusted() { + this.service.compute.publisherTrustedAlgorithmPublishers = [] return this } @@ -116,6 +300,10 @@ export class ServiceBuilder< } setPricing(pricing: PricingConfigWithoutOwner) { + if (this.service.editExistingService) + throw new Error( + 'Can not set new pricing configs for existing services using the builder. Use nautilus.setServicePrice() method instead.' + ) this.service.pricing = pricing return this @@ -127,6 +315,13 @@ export class ServiceBuilder< } build() { + if ( + this.service.pricing === undefined && + !this.service.editExistingService + ) { + throw new Error(`Missing pricing config.`) + } + return this.service } } diff --git a/src/Nautilus/Nautilus.ts b/src/Nautilus/Nautilus.ts index b728604..74b2b71 100644 --- a/src/Nautilus/Nautilus.ts +++ b/src/Nautilus/Nautilus.ts @@ -1,8 +1,10 @@ import { + Asset, Config, ConfigHelper, LogLevel, - LoggerInstance + LoggerInstance, + Nft } from '@oceanprotocol/lib' import { Signer, utils as ethersUtils } from 'ethers' import { @@ -11,13 +13,23 @@ import { ComputeResultConfig, ComputeStatusConfig, CreateAssetConfig, + LifecycleStates, PublishResponse } from '../@types' -import { createAsset, createDatatokenAndPricing, publishDDO } from '../publish' +import { + createAsset, + createServiceWithDatatokenAndPricing, + publishDDO +} from '../publish' import { getAllPromisesOnArray } from '../utils' import { NautilusAsset } from './Asset/NautilusAsset' import { access } from '../access' import { compute, getStatus, retrieveResult } from '../compute' +import { FileTypes, NautilusService, ServiceTypes } from './Asset' +import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { resolvePublisherTrustedAlgorithms } from '../utils/helpers/trusted-algorithms' +import { getAsset, getAssets } from '../utils/aquarius' +import { editPrice } from '../utils/contracts' export { LogLevel } from '@oceanprotocol/lib' @@ -134,37 +146,32 @@ export class Nautilus { } // -------------------------------------------------- - // 2. Create Datatokens and Pricing for new Services + // 2. resolve publisher trusted algorithm checksums + // -------------------------------------------------- + + await resolvePublisherTrustedAlgorithms( + asset.ddo.services, + chainConfig.metadataCacheUri + ) + + // -------------------------------------------------- + // 3. Create Datatokens and Pricing for new Services // -------------------------------------------------- const services = await getAllPromisesOnArray( asset.ddo.services, async (service) => { - const { datatokenAddress, tx } = await createDatatokenAndPricing({ + return createServiceWithDatatokenAndPricing( + service, signer, chainConfig, nftAddress, - pricing: { - ...service.pricing, - freCreationParams: { - ...service.pricing.freCreationParams, - owner: asset.owner - } - }, - datatokenParams: { - ...service.datatokenCreateParams, - minter: asset.owner, - paymentCollector: asset.owner - } - }) - - service.datatokenAddress = datatokenAddress - - return { service, datatokenAddress, tx } + asset.owner + ) } ) // -------------------------------------------------- - // 3. Create the DDO and publish it on NFT + // 4. Create the DDO and publish it on NFT // -------------------------------------------------- const ddo = await asset.ddo.getDDO({ create: true, @@ -175,7 +182,65 @@ export class Nautilus { const setMetadataTxReceipt = await publishDDO({ signer, chainConfig, - ddo + ddo, + asset + }) + + return { + nftAddress, + services, + ddo, + setMetadataTxReceipt + } + } + + async edit(asset: NautilusAsset): Promise { + const { signer, chainConfig } = this.getChainConfig() + const { nftAddress, services: nautilusDDOServices } = asset.ddo + + let services: { + service: NautilusService + datatokenAddress: string + tx: TransactionReceipt + }[] + + await resolvePublisherTrustedAlgorithms( + nautilusDDOServices, + chainConfig.metadataCacheUri + ) + + // This includes fresh published services + const changedPriceServices = nautilusDDOServices.filter( + (nautilusService) => nautilusService.pricing + ) + + // TODO check if service prices can be changed via datatoken replacement (currently buggy could be a caching problem) + if (changedPriceServices.length > 0) { + services = await getAllPromisesOnArray( + changedPriceServices, + async (service) => { + return createServiceWithDatatokenAndPricing( + service, + signer, + chainConfig, + nftAddress, + asset.owner + ) + } + ) + } + + const ddo = await asset.ddo.getDDO({ + create: false, + chainId: chainConfig.chainId, + nftAddress + }) + + const setMetadataTxReceipt = await publishDDO({ + signer, + chainConfig, + ddo, + asset }) return { @@ -186,6 +251,66 @@ export class Nautilus { } } + async setServicePrice( + aquaAsset: Asset, + serviceId: string, + newPrice: string + ): Promise { + if (typeof newPrice !== 'string' || isNaN(parseFloat(newPrice))) { + throw new Error('newPrice must be a numeric string') + } + + return await editPrice( + aquaAsset, + serviceId, + newPrice, + this.config, + this.signer + ) + } + + async getAquariusAssets(dids: string[]): Promise<{ [did: string]: Asset }> { + try { + return await getAssets(this.config.metadataCacheUri, dids) + } catch (error) { + throw new Error(`getAquariusAssets failed: ${error}`) + } + } + + async getAquariusAsset(did: string): Promise { + try { + return await getAsset(this.config.metadataCacheUri, did) + } catch (error) { + throw new Error(`getAquariusAsset failed: ${error}`) + } + } + + async setAssetLifecycleState(aquariusAsset: Asset, state: LifecycleStates) { + const { signer } = this.getChainConfig() + const address = await signer.getAddress() + const nft = new Nft(signer) + const existingNftState = aquariusAsset.nft.state + + if (existingNftState === state) { + LoggerInstance.warn( + `[lifecycle] Asset lifecycle state is already ${state} (${LifecycleStates[state]}), action aborted` + ) + return + } + LoggerInstance.debug( + `[lifecycle] Change asset lifecycle state from ${existingNftState} (${LifecycleStates[existingNftState]}) to ${state} (${LifecycleStates[state]}) ` + ) + + const stateTxReceipt = await nft.setMetadataState( + aquariusAsset.nft.address, + address, + state + ) + const stateTx = await stateTxReceipt.wait() + + return stateTx + } + /** * @param accessConfig configuration object */ diff --git a/src/compute/index.ts b/src/compute/index.ts index c035824..323bb12 100644 --- a/src/compute/index.ts +++ b/src/compute/index.ts @@ -31,7 +31,7 @@ import { export async function compute(computeConfig: ComputeConfig) { const { - dataset: datasetConfig, // TODO consider syncing naming with type to prevent renaming + dataset: datasetConfig, // TODO consider syncing naming to prevent renaming algorithm: algorithmConfig, signer, chainConfig, diff --git a/src/publish/index.ts b/src/publish/index.ts index 66b24d3..5c724ab 100644 --- a/src/publish/index.ts +++ b/src/publish/index.ts @@ -1,5 +1,6 @@ import { Aquarius, + Config, Datatoken, DispenserParams, LoggerInstance, @@ -12,7 +13,10 @@ import { CreateDatatokenConfig, PublishDDOConfig } from '../@types/Publish' -import { utils as ethersUtils, providers } from 'ethers' +import { Signer, utils as ethersUtils, providers } from 'ethers' +import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { LifecycleStates } from '../@types' +import { FileTypes, NautilusService, ServiceTypes } from '../Nautilus' export async function createAsset(assetConfig: CreateAssetConfig) { LoggerInstance.debug('[publish] Publishing new asset NFT...') @@ -20,7 +24,6 @@ export async function createAsset(assetConfig: CreateAssetConfig) { // 1. Create NFT with NftFactory // -------------------------------------------------- const { signer, chainConfig, nftParams } = assetConfig - const publisherAccount = await signer?.getAddress() const nftFactory = new NftFactory( chainConfig.nftFactoryAddress, signer, @@ -35,7 +38,7 @@ export async function createAsset(assetConfig: CreateAssetConfig) { return { nftAddress } } -export async function createDatatokenAndPricing(config: CreateDatatokenConfig) { +async function createDatatokenAndPricing(config: CreateDatatokenConfig) { // -------------------------------------------------- // 1. Create Datatoken // -------------------------------------------------- @@ -122,7 +125,7 @@ export async function createDatatokenAndPricing(config: CreateDatatokenConfig) { } export async function publishDDO(config: PublishDDOConfig) { - const { chainConfig, signer, ddo } = config + const { chainConfig, signer, ddo, asset } = config const publisherAccount = await signer?.getAddress() // -------------------------------------------------- @@ -134,7 +137,8 @@ export async function publishDDO(config: PublishDDOConfig) { const aquarius = new Aquarius(chainConfig.metadataCacheUri) const validateResult = await aquarius.validate(ddo) - if (!validateResult.valid) throw new Error('Validating Metadata failed') + if (!validateResult.valid) + throw new Error(`Validating Metadata failed: ${validateResult?.errors}`) // -------------------------------------------------- // 2. Encrypt DDO @@ -153,15 +157,16 @@ export async function publishDDO(config: PublishDDOConfig) { // -------------------------------------------------- const nft = new Nft(signer, chainConfig.network, chainConfig) - // TODO: let user set state - const LIFECYCLE_STATE_ACTIVE = 0 + const lifecycleState = asset?.lifecycleState || LifecycleStates.ACTIVE const FLAGS = '0x02' // market sets '0x02' insteadconst validateResult = await aquariusInstance.validate(ddo) of '0x2', theoretically used by aquarius or provider, not implemented yet, will remain hardcoded + LoggerInstance.debug(`[publish] Asset lifecycleState: ${lifecycleState}`) + LoggerInstance.debug('[publish] Set Metadata...') const transactionReceipt = await nft.setMetadata( ddo.nftAddress, publisherAccount, - LIFECYCLE_STATE_ACTIVE, + lifecycleState, chainConfig.providerUri, '', FLAGS, @@ -173,12 +178,46 @@ export async function publishDDO(config: PublishDDOConfig) { LoggerInstance.debug(`[publish] Published metadata on NFT.`, { ddo, - tx: tx.transactionHash + tx }) return tx } +export async function createServiceWithDatatokenAndPricing( + service: NautilusService, + signer: Signer, + chainConfig: Config, + nftAddress: string, + assetOwner: string +): Promise<{ + service: NautilusService + datatokenAddress: string + tx: TransactionReceipt +}> { + const { datatokenAddress, tx } = await createDatatokenAndPricing({ + signer, + chainConfig, + nftAddress, + pricing: { + ...service.pricing, + freCreationParams: { + ...service.pricing.freCreationParams, + owner: assetOwner + } + }, + datatokenParams: { + ...service.datatokenCreateParams, + minter: assetOwner, + paymentCollector: assetOwner + } + }) + + service.datatokenAddress = datatokenAddress + + return { service, datatokenAddress, tx } +} + // TODO evaluate if we need these (1 transaction for multiple actions) // async function createTokensAndPricing( // assetConfig: Pick< diff --git a/src/utils/aquarius.ts b/src/utils/aquarius.ts index 4fe1ddb..af52d3f 100644 --- a/src/utils/aquarius.ts +++ b/src/utils/aquarius.ts @@ -1,5 +1,6 @@ -import { Asset, LoggerInstance } from '@oceanprotocol/lib' +import { Asset, DDO, LoggerInstance } from '@oceanprotocol/lib' import axios, { AxiosResponse } from 'axios' +import { AQUARIUS_ASSET_EXTENDED_DDO_PROPS } from './constants' export async function getAsset( metadataCacheUri: string, @@ -11,10 +12,13 @@ export async function getAsset( ) if (!did || !metadataCacheUri) return try { - const response: AxiosResponse = await axios.get( - `${metadataCacheUri}/api/aquarius/assets/ddo/${did}`, - { signal } - ) + const apiPath = `/api/aquarius/assets/ddo/${did}` + + const fullAquariusUrl = new URL(apiPath, metadataCacheUri).href + + const response: AxiosResponse = await axios.get(fullAquariusUrl, { + signal + }) if (!response || response.status !== 200 || !response.data) return const data = { ...response.data } @@ -25,5 +29,72 @@ export async function getAsset( } else { LoggerInstance.error(error.message) } + throw error + } +} + +export async function getAssets( + metadataCacheUri: string, + dids: string[] +): Promise<{ [did: string]: Asset }> { + const apiPath = '/api/aquarius/assets/query' + + if (!metadataCacheUri) { + throw new Error('[aquarius] No metadata cache URI provided') + } + + if (!dids?.length) { + throw new Error('[aquarius] The DIDs array is empty') + } + + const lowerCaseDids = dids.map((did) => did.toLowerCase()) + + const queryPayload = { + query: { + bool: { + filter: [ + { + ids: { + values: lowerCaseDids + } + } + ] + } + } + } + + const assets: { [did: string]: Asset } = {} + + try { + const fullAquariusUrl = new URL(apiPath, metadataCacheUri).href + const response: AxiosResponse = await axios.post( + fullAquariusUrl, + queryPayload + ) + + LoggerInstance.debug(`[aquarius] Query status: ${response.status}`) + + if (response?.status === 200 && response?.data?.hits) { + for (const hit of response.data.hits.hits) { + const asset: Asset = hit._source + if (asset && asset.id) { + assets[asset.id.toLowerCase()] = asset + } + } + } + + return assets + } catch (error) { + LoggerInstance.error(`Error in retrieving assets: ${error.message}`) + throw error + } +} + +export function transformAquariusAssetToDDO(aquariusAsset: Asset): DDO { + const ddo = structuredClone(aquariusAsset) + + for (const attribute of AQUARIUS_ASSET_EXTENDED_DDO_PROPS) { + delete ddo[attribute] } + return ddo as DDO } diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..41fd5a8 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,13 @@ +import { Asset, DDO } from '@oceanprotocol/lib' + +// TODO look into more advanced type solutions +export type AssetSpecificProps = Exclude + +// these are the props which extend the DDO interface to be an aquarius Asset +// this const is needed to strip these away from an Asset to convert it into an DDO +export const AQUARIUS_ASSET_EXTENDED_DDO_PROPS: AssetSpecificProps[] = [ + 'nft', + 'datatokens', + 'stats', + 'purgatory' +] diff --git a/src/utils/contracts.ts b/src/utils/contracts.ts new file mode 100644 index 0000000..827d27f --- /dev/null +++ b/src/utils/contracts.ts @@ -0,0 +1,73 @@ +import { + Asset, + Config, + FixedRateExchange, + LoggerInstance, + Service +} from '@oceanprotocol/lib' +import { Signer } from 'ethers' +import { getAccessDetails } from './helpers/access-details' +import { TransactionReceipt } from '@ethersproject/abstract-provider' + +export async function editPrice( + aquariusAsset: Asset, + serviceId: string, + newPrice: string, + chainConfig: Config, + signer: Signer +): Promise { + if (!aquariusAsset) { + throw new Error('[editPrice] Aquarius asset is undefined') + } + + if (!aquariusAsset.services || aquariusAsset.services.length === 0) { + throw new Error('[editPrice] Aquarius asset has no services') + } + + // Find the specific service by its id + const service: Service | undefined = aquariusAsset.services.find( + (service) => service.id === serviceId + ) + + if (!service) { + throw new Error( + '[editPrice] No matching service found for provided serviceId' + ) + } + + const fixedRateInstance = new FixedRateExchange( + chainConfig.fixedRateExchangeAddress, + signer + ) + + let accessDetails + try { + accessDetails = await getAccessDetails( + chainConfig.subgraphUri, + service.datatokenAddress + ) + } catch (error) { + LoggerInstance.error( + `[editPrice] Error fetching access details: ${error.message}` + ) + throw error + } + + let txReceipt: TransactionReceipt + try { + const tx = await fixedRateInstance.setRate( + accessDetails.addressOrId, + newPrice + ) + + // Wait for the transaction to be confirmed + txReceipt = await tx.wait() + } catch (error) { + LoggerInstance.error( + `[editPrice] Error setting new price (setRate): ${error.message}` + ) + throw error + } + + return txReceipt +} diff --git a/src/utils/helpers/trusted-algorithms.ts b/src/utils/helpers/trusted-algorithms.ts new file mode 100644 index 0000000..73d877f --- /dev/null +++ b/src/utils/helpers/trusted-algorithms.ts @@ -0,0 +1,115 @@ +import { + Asset, + FileInfo, + ProviderInstance, + PublisherTrustedAlgorithm, + getHash +} from '@oceanprotocol/lib' +import { getAsset } from '../aquarius' +import { FileTypes, NautilusService, ServiceTypes } from '../../Nautilus' + +// TODO replace hardcoded service index 0 with service id once supported by the stack +async function getPublisherTrustedAlgorithms( + dids: string[], + metadataCacheUri: string +): Promise { + const trustedAlgorithms: PublisherTrustedAlgorithm[] = [] + + const assetPromises = dids.map((did) => getAsset(metadataCacheUri, did)) + let assets: Asset[] = [] + + try { + assets = await Promise.all(assetPromises) + } catch (error) { + throw new Error( + `Failed to fetch assets for PublisherTrustedAlgorithms: ${error}` + ) + } + for (const asset of assets) { + if (asset.metadata.type !== 'algorithm') + throw new Error(`Asset ${asset.id} is not of type algorithm`) + if (!asset.services?.[0]) throw new Error(`No service in ${asset.id}`) + + const filesChecksum = await getFileDidInfo( + asset?.id, + asset?.services?.[0]?.id, + asset?.services?.[0]?.serviceEndpoint + ) + if (!filesChecksum?.[0]?.checksum) + throw new Error(`Unable to get fileChecksum for asset ${asset.id}`) + + const containerChecksum = + asset.metadata.algorithm.container.entrypoint + + asset.metadata.algorithm.container.checksum + + // Trusted Algorithms Docs https://docs.oceanprotocol.com/developers/compute-to-data/compute-options#trusted-algorithms + const trustedAlgorithm = { + did: asset.id, + containerSectionChecksum: getHash(containerChecksum), + filesChecksum: filesChecksum?.[0]?.checksum + } + trustedAlgorithms.push(trustedAlgorithm) + } + return trustedAlgorithms +} + +async function getFileDidInfo( + did: string, + serviceId: string, + providerUrl: string +): Promise { + try { + const response = await ProviderInstance.checkDidFiles( + did, + serviceId, + providerUrl, + true + ) + return response + } catch (error) { + throw new Error(`[Initialize check file did] Error:' ${error}`) + } +} + +export async function resolvePublisherTrustedAlgorithms( + nautilusDDOServices: NautilusService[], + metadataCacheUri: string +) { + for (const service of nautilusDDOServices) { + if (service.addedPublisherTrustedAlgorithms?.length) continue + + const dids = service.addedPublisherTrustedAlgorithms.map( + (asset) => asset.did + ) + const newPublisherTrustedAlgorithms = await getPublisherTrustedAlgorithms( + dids, + metadataCacheUri + ) + + if (service.compute?.publisherTrustedAlgorithms?.length === 0) { + service.compute.publisherTrustedAlgorithms = newPublisherTrustedAlgorithms + return + } + + newPublisherTrustedAlgorithms.forEach((algorithm) => { + const index = service.compute.publisherTrustedAlgorithms.findIndex( + (existingAlgorithm) => existingAlgorithm.did === algorithm.did + ) + + if (index === -1) { + // Algorithm with the same DID doesn't exist, add it + service.compute.publisherTrustedAlgorithms.push(algorithm) + } else { + // If either checksum is different, replace the existing algorithm + const existing = service.compute.publisherTrustedAlgorithms[index] + if ( + existing.containerSectionChecksum !== + algorithm.containerSectionChecksum || + existing.filesChecksum !== algorithm.filesChecksum + ) { + service.compute.publisherTrustedAlgorithms[index] = algorithm + } + } + }) + } +} diff --git a/test/fixtures/Config.ts b/test/fixtures/Config.ts index d384c8e..0b9d65c 100644 --- a/test/fixtures/Config.ts +++ b/test/fixtures/Config.ts @@ -8,32 +8,40 @@ import { homedir } from 'os' export const getTestConfig = async (signer: Signer): Promise => { const config = new ConfigHelper().getConfig(await signer.getChainId()) - const addresses = getAddresses() + return { + ...config, + providerUri: process.env.PROVIDER_URI_TEST || config.providerUri, + metadataCacheUri: + process.env.METADATA_CACHE_URI_TEST || config.metadataCacheUri + } + + // TODO: look into fixing development test env + // const addresses = getAddresses() - const isDevelopment = config.network === 'development' + // const isDevelopment = config.network === 'development' - return isDevelopment - ? { - ...config, - metadataCacheUri: 'http://127.0.0.1:5000', // if running on macOS - // metadataCacheUri: 'http://172.15.0.5:5000', - providerUri: 'http://127.0.0.1:8030', // if running on macOS - // providerUri: 'http://172.15.0.4:8030', - providerAddress: '0xe08A1dAe983BC701D05E492DB80e0144f8f4b909', // barge - subgraphUri: 'http://127.0.0.1:9000', // if running on macOS - // subgraphUri: 'https://172.15.0.15:8000', - oceanTokenAddress: addresses.Ocean, - nftFactoryAddress: addresses.ERC721Factory, - dispenserAddress: addresses.Dispenser, - opfCommunityFeeCollector: addresses.OPFCommunityFeeCollector, - fixedRateExchangeAddress: addresses.FixedPrice - } - : { - ...config, - providerUri: process.env.PROVIDER_URI_TEST || config.providerUri, - metadataCacheUri: - process.env.METADATA_CACHE_URI_TEST || config.metadataCacheUri - } + // return isDevelopment + // ? { + // ...config, + // metadataCacheUri: 'http://127.0.0.1:5000', // if running on macOS + // // metadataCacheUri: 'http://172.15.0.5:5000', + // providerUri: 'http://127.0.0.1:8030', // if running on macOS + // // providerUri: 'http://172.15.0.4:8030', + // providerAddress: '0xe08A1dAe983BC701D05E492DB80e0144f8f4b909', // barge + // subgraphUri: 'http://127.0.0.1:9000', // if running on macOS + // // subgraphUri: 'https://172.15.0.15:8000', + // oceanTokenAddress: addresses.Ocean, + // nftFactoryAddress: addresses.ERC721Factory, + // dispenserAddress: addresses.Dispenser, + // opfCommunityFeeCollector: addresses.OPFCommunityFeeCollector, + // fixedRateExchangeAddress: addresses.FixedPrice + // } + // : { + // ...config, + // providerUri: process.env.PROVIDER_URI_TEST || config.providerUri, + // metadataCacheUri: + // process.env.METADATA_CACHE_URI_TEST || config.metadataCacheUri + // } } export const getAddresses = (): { diff --git a/test/integration/access-flow.test.ts b/test/integration/access-flow.test.ts index 97231e2..c115259 100644 --- a/test/integration/access-flow.test.ts +++ b/test/integration/access-flow.test.ts @@ -35,10 +35,10 @@ describe('Access Flow Integration', function () { const { providerUri } = nautilus.getOceanConfig() serviceEndpoint = providerUri - const serviceBuilder = new ServiceBuilder( - ServiceTypes.ACCESS, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(serviceEndpoint) .setTimeout(algorithmService.timeout) diff --git a/test/integration/compute-flow.test.ts b/test/integration/compute-flow.test.ts index abd7777..93cff5b 100644 --- a/test/integration/compute-flow.test.ts +++ b/test/integration/compute-flow.test.ts @@ -65,10 +65,10 @@ describe('Compute Flow Integration', async function () { const { providerUri } = nautilusAlgoPublisher.getOceanConfig() // Create the "compute" service - const serviceBuilder = new ServiceBuilder( - ServiceTypes.COMPUTE, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(providerUri) .setTimeout(algorithmService.timeout) @@ -103,10 +103,10 @@ describe('Compute Flow Integration', async function () { const { providerUri } = nautilusDatasetPublisher.getOceanConfig() // Create the "compute" service - const serviceBuilder = new ServiceBuilder( - ServiceTypes.COMPUTE, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(providerUri) .setTimeout(datasetService.timeout) diff --git a/test/integration/edit.test.ts b/test/integration/edit.test.ts new file mode 100644 index 0000000..03febf9 --- /dev/null +++ b/test/integration/edit.test.ts @@ -0,0 +1,686 @@ +import assert from 'assert' +import { Signer } from 'ethers' +import { + AssetBuilder, + ConsumerParameterBuilder, + CredentialListTypes, + FileTypes, + LifecycleStates, + LogLevel, + Nautilus, + ServiceBuilder, + ServiceTypes +} from '../../src' +import { + algorithmMetadata, + datasetService, + getPricing +} from '../fixtures/AssetConfig' +import { MUMBAI_NODE_URI, getSigner } from '../fixtures/Ethers' +import { Aquarius, Config, DDO } from '@oceanprotocol/lib' +import { getTestConfig } from '../fixtures/Config' + +const nodeUri = MUMBAI_NODE_URI + +describe('Edit Integration tests', function () { + // set timeout for this describe block + this.timeout(100000) + + let signer: Signer + let signerAddress: string + let nautilus: Nautilus + let providerUri: string + let aquarius: Aquarius + let config: Config + + // test assets + let fixedPricedAlgoWithCredentials: DDO + let fixedPriceComputeDataset: DDO + + before(async () => { + Nautilus.setLogLevel(LogLevel.Verbose) + signer = getSigner(1, nodeUri) + signerAddress = await signer.getAddress() + config = await getTestConfig(signer) + + console.log('Testing with signer:', signerAddress) + + nautilus = await Nautilus.create(signer, { + metadataCacheUri: + process.env.METADATA_CACHE_URI_TEST || config?.metadataCacheUri + }) + + providerUri = + process.env.PROVIDER_URI_TEST || nautilus.getOceanConfig().providerUri + + console.log('Testing with signer:', signerAddress) + + aquarius = new Aquarius( + process.env.METADATA_CACHE_URI_TEST || config?.metadataCacheUri + ) + }) + + it('publishes an algorithm with fixed price and credentials', async () => { + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) + const service = serviceBuilder + .setServiceEndpoint(providerUri) + .setTimeout(datasetService.timeout) + .addFile(datasetService.files[0]) + .setPricing(await getPricing(signer, 'fixed')) + .build() + + const assetBuilder = new AssetBuilder() + const asset = assetBuilder + .setAuthor('testAuthor') + .setDescription('A dataset publishing test') + .setLicense('MIT') + .setName('Test Publish Dataset Fixed') + .setOwner(signerAddress) + .setType('algorithm') + .setAlgorithm({ + ...algorithmMetadata.algorithm + }) + .addService(service) + .addCredentialAddresses(CredentialListTypes.ALLOW, [signerAddress]) + .addCredentialAddresses(CredentialListTypes.DENY, [signerAddress]) + .build() + + const result = await nautilus.publish(asset) + fixedPricedAlgoWithCredentials = result?.ddo + await aquarius.waitForAqua(fixedPricedAlgoWithCredentials?.id) + + assert(result) + }) + + it('publishes a compute type dataset', async () => { + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + }) + + const testServiceOne = serviceBuilder + .setName('Test service 1') + .setServiceEndpoint(providerUri) + .setTimeout(datasetService.timeout) + .setPricing(await getPricing(signer, 'fixed')) + .addFile(datasetService.files[0]) + .build() + + const assetBuilder = new AssetBuilder() + const asset = assetBuilder + .setAuthor('testAuthor') + .setDescription('A dataset publishing test') + .setLicense('MIT') + .setName('Test Publish Dataset Fixed') + .setOwner(signerAddress) + .setType('dataset') + .addService(testServiceOne) + .build() + + const result = await nautilus.publish(asset) + fixedPriceComputeDataset = result?.ddo + await aquarius.waitForAqua(fixedPricedAlgoWithCredentials?.id) + + assert(result) + }) + + it('edit asset metadata fields', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id // use algo for algo metadata + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder + .setAuthor('Company Name') + .setDescription( + `# Nautilus-Example Description \n\nThis asset has been published using the [nautilus-examples](https://github.com/deltaDAO/nautilus-examples) repository.` + ) + .setLicense('Edited License') + .setName('Nautilus edit Example: New name') + .setCopyrightHolder('TheHolder') + .addLinks(['https://docs.oceanprotocol.com/']) + .setContentLanguage('EN') + .addTags(['edit', 'test']) + .addCategories(['test']) + .addAdditionalInformation({ + toplevel: 'random', + nesting: { nested: 'in the deep' } + }) + .setAlgorithm({ + ...algorithmMetadata.algorithm + }) + .build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit credentials - add address ALLOW', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder + .addCredentialAddresses(CredentialListTypes.ALLOW, [ + '0x0000000000000000000000000000000000000000' + ]) + .build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit credentials - add address DENY', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder + .addCredentialAddresses(CredentialListTypes.DENY, [ + '0x0000000000000000000000000000000000000000' + ]) + .build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit credentials - remove address ALLOW', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder + .removeCredentialAddresses(CredentialListTypes.ALLOW, [signerAddress]) + .build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit credentials - remove address DENY', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder + .removeCredentialAddresses(CredentialListTypes.DENY, [signerAddress]) + .build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit lifecycleState static function', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + // TODO decide if we want to support both routes to set lifecycle state + // static function is required to reactivate revoked assets where aquarius id providing not enough data to use builder route + const tx = await nautilus.setAssetLifecycleState( + aquariusAsset, + LifecycleStates.ASSET_UNLISTED + ) + + assert(tx) + }) + + it('edit lifecycleState AssetBuilder', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.setLifecycleState(LifecycleStates.ACTIVE).build() + const result = await nautilus.edit(asset) + + assert(result) + }) + + // TODO this is experimental, pretty buggy regarding caching, maybe not even possible with this stack + it.skip('edit services - change price replacing service', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + 'did:op:f92be296bfd36e99f0e7ce7583dcb8a3846f10f0b71a40e5dcac8ab6624a2548' + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: + '1accb051fdb757cf5fac7c88a724af82e841bbb3c02fc16e53fb91d45e85e09d' + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + + // create new service with new datatoken and replace the old one + const service = serviceBuilder + .addFile(datasetService.files[0]) // a new datatoken requires a new encrypted files field since it's includes in the encrypted data + .setPricing({ + type: 'fixed', + freCreationParams: { + fixedRateAddress: '0x25e1926E3d57eC0651e89C654AB0FA182C6D5CF7', + baseTokenAddress: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8', + baseTokenDecimals: 18, + datatokenDecimals: 18, + fixedRate: '4', + marketFee: '0', + marketFeeCollector: '0x0000000000000000000000000000000000000000' + } + }) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit service - editPrice static function', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const serviceId = fixedPricedAlgoWithCredentials?.services?.[0]?.id + const newPrice = '0.1' + + const txReceipt = await nautilus.setServicePrice( + aquariusAsset, + serviceId, + newPrice + ) + console.log(txReceipt) + + assert(txReceipt) + }) + + it('edit services - name, description, timeout', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPricedAlgoWithCredentials?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + + const service = serviceBuilder + .setName('TestServiceName') + .setDescription('TestServiceDescription') + .setTimeout(1000) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - files', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPricedAlgoWithCredentials?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + + const service = serviceBuilder + .addFile(datasetService.files[0]) // TODO should be named replaceFile() for edit function, future UX improvement + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - add consumerParameter', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPricedAlgoWithCredentials?.services?.[0]?.id + } + + const consumerParameterBuilder = new ConsumerParameterBuilder() + const numberParameter = consumerParameterBuilder + .setType('number') + .setName('numberParameter2') + .setLabel('Number Parameter2') + .setDescription('A cool description for a test number parameter') + .setDefault('12') + .setRequired(false) + .build() + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder.addConsumerParameter(numberParameter).build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - serviceEndpoint', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPricedAlgoWithCredentials?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPricedAlgoWithCredentials?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .setServiceEndpoint('https://v4.provider.oceanprotocol.com/') + .addFile(datasetService.files[0]) // we have to add files since serviceEndpoint is in encrypted files + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute add trusted publishers', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .addTrustedAlgorithmPublisher(signerAddress) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute add trusted algos', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .addTrustedAlgorithms([ + { + did: fixedPricedAlgoWithCredentials?.id + }, + { + did: fixedPricedAlgoWithCredentials?.id + } + ]) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute allow algorithm network access', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder.allowAlgorithmNetworkAccess().build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute allow raw algorithm', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder.allowRawAlgorithms().build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute trust all publishers and algos', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .setAllAlgorithmsTrusted() + .setAllAlgorithmPublishersTrusted() + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute remove publishers and algos', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .removeTrustedAlgorithm(fixedPricedAlgoWithCredentials?.id) + .removeTrustedAlgorithmPublisher(signerAddress) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute untrust publishers and algos', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .setAllAlgorithmsUntrusted() + .setAllAlgorithmPublishersUntrusted() + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - compute do not allow', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .allowAlgorithmNetworkAccess(false) + .allowRawAlgorithms(false) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - add additionalInfo', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + aquariusAsset, + serviceId: fixedPriceComputeDataset?.services?.[0]?.id + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .addAdditionalInformation({ + test: 'SuperInf2', + number: 6, + nested: { name: 'nested2' } + }) + .addAdditionalInformation({ + additional: undefined, + nested: { name: 'overwritten3' } + }) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + it('edit services - add another service', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceBuilderConfig = { + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + } + + const serviceBuilder = new ServiceBuilder(serviceBuilderConfig) + const service = serviceBuilder + .setServiceEndpoint('https://v4.provider.oceanprotocol.com/') + .addFile(datasetService.files[0]) + .setTimeout(datasetService.timeout) + .setPricing({ type: 'free' }) + .build() + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.addService(service).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) + + // TODO: reinstate test after fixing publishing with multiple services + it.skip('edit services - remove service', async () => { + const aquariusAsset = await nautilus.getAquariusAsset( + fixedPriceComputeDataset?.id + ) + + const serviceId = fixedPriceComputeDataset?.services?.[1]?.id + + const assetBuilder = new AssetBuilder(aquariusAsset) + const asset = assetBuilder.removeService(serviceId).build() + + const result = await nautilus.edit(asset) + + assert(result) + }) +}) diff --git a/test/integration/publish.test.ts b/test/integration/publish.test.ts index 8d8e6b1..ad7fbbf 100644 --- a/test/integration/publish.test.ts +++ b/test/integration/publish.test.ts @@ -48,10 +48,10 @@ describe('Publish Integration tests', function () { }) it('publishes a free access asset', async () => { - const serviceBuilder = new ServiceBuilder( - ServiceTypes.ACCESS, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(providerUri) .setTimeout(datasetService.timeout) @@ -76,10 +76,10 @@ describe('Publish Integration tests', function () { }) it('publishes a fixed price access asset', async () => { - const serviceBuilder = new ServiceBuilder( - ServiceTypes.ACCESS, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(providerUri) .setTimeout(datasetService.timeout) @@ -104,10 +104,10 @@ describe('Publish Integration tests', function () { }) it('publishes an asset with credentials', async () => { - const serviceBuilder = new ServiceBuilder( - ServiceTypes.ACCESS, - FileTypes.URL - ) + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) const service = serviceBuilder .setServiceEndpoint(providerUri) .setTimeout(datasetService.timeout) @@ -132,11 +132,11 @@ describe('Publish Integration tests', function () { assert(result) }) - it('publishes an asset with service consumerParamters', async () => { - const serviceBuilder = new ServiceBuilder( - ServiceTypes.ACCESS, - FileTypes.URL - ) + it('publishes an asset with service consumerParameters', async () => { + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.ACCESS, + fileType: FileTypes.URL + }) const { textParameter, numberParameter, @@ -171,11 +171,11 @@ describe('Publish Integration tests', function () { assert(result) }) - it('publishes an asset with algorithm metadata consumerParamters', async () => { - const serviceBuilder = new ServiceBuilder( - ServiceTypes.COMPUTE, - FileTypes.URL - ) + it('publishes an asset with algorithm metadata consumerParameters', async () => { + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + }) const { textParameter, numberParameter, @@ -214,6 +214,40 @@ describe('Publish Integration tests', function () { assert(result) }) + + // TODO use published algo did for addTrustedAlgorithms + it('publishes a fixed price compute dataset with trusted algorithm', async () => { + const serviceBuilder = new ServiceBuilder({ + serviceType: ServiceTypes.COMPUTE, + fileType: FileTypes.URL + }) + const service = serviceBuilder + .setServiceEndpoint(providerUri) + .setTimeout(datasetService.timeout) + .addFile(datasetService.files[0]) + .setPricing(await getPricing(signer, 'fixed')) + .addTrustedAlgorithms([ + { + did: 'did:op:02961b8c52b0273bac94f776a88ed13833cbc50bc2bc666ab7495751941546dc' + } + ]) + .build() + + const assetBuilder = new AssetBuilder() + const asset = assetBuilder + .setAuthor('testAuthor') + .setDescription('A dataset publishing test') + .setLicense('MIT') + .setName('Test Publish Dataset Fixed') + .setOwner(signerAddress) + .setType('dataset') + .addService(service) + .build() + + const result = await nautilus.publish(asset) + + assert(result) + }) }) function getConsumerParameters(): { [key: string]: NautilusConsumerParameter } {