diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3b190a4..a664ed29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,7 @@ jobs: cd .. && ./scripts/waitforcontracts.sh - run: npm run test:integration:cover + - uses: actions/upload-artifact@v2 with: name: coverage diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6f9421e3..c72109de 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,8 @@ on: jobs: npm: runs-on: ubuntu-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 @@ -15,6 +17,11 @@ jobs: node-version: '16' registry-url: https://registry.npmjs.org/ - run: npm ci + + # pre-releases, triggered by `next` as part of git tag + - run: npm publish --tag next + if: ${{ contains(github.ref, 'next') }} + + # production releases - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + if: ${{ !contains(github.ref, 'next') }} diff --git a/README.md b/README.md index 8c974bd2..f394e4ba 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This is in alpha state and you can expect running into problems. If you run into - [🛳 Production](#-production) - [⬆️ Releases](#️-releases) - [Production](#production) + - [Pre-releases](#pre-releases) - [🏛 License](#-license) ## 📚 Prerequisites @@ -229,6 +230,16 @@ The task does the following: For the GitHub releases steps a GitHub personal access token, exported as `GITHUB_TOKEN` is required. [Setup](https://github.com/release-it/release-it#github-releases) +### Pre-Releases + +For pre-releases, this is required for the first one like `v0.18.0-next.0`: + +```bash +./node_modules/.bin/release-it major|minor|patch --preRelease=next +``` + +Further releases afterwards can be done with `npm run release` again and selecting the appropriate next version, in this case `v0.18.0-next.1` and so on. + ## 🏛 License ``` diff --git a/src/ddo/interfaces/Service.ts b/src/ddo/interfaces/Service.ts index af0b220c..712b3365 100644 --- a/src/ddo/interfaces/Service.ts +++ b/src/ddo/interfaces/Service.ts @@ -1,9 +1,23 @@ import { Metadata } from './Metadata' import { Status } from './Status' +export interface ServiceCustomParameter { + name: string + type: string + label: string + required: boolean + options?: any + description: string +} + +export interface ServiceCustomParametersRequired { + userCustomParameters?: ServiceCustomParameter[] + algoCustomParameters?: ServiceCustomParameter[] +} + export type ServiceType = 'authorization' | 'metadata' | 'access' | 'compute' -export interface ServiceCommonAttributes { +export interface ServiceCommonAttributes extends ServiceCustomParametersRequired { main: { [key: string]: any } additionalInformation?: { [key: string]: any } status?: Status @@ -33,9 +47,9 @@ export interface publisherTrustedAlgorithm { } export interface ServiceComputePrivacy { - allowRawAlgorithm: boolean - allowNetworkAccess: boolean - allowAllPublishedAlgorithms: boolean + allowRawAlgorithm?: boolean + allowNetworkAccess?: boolean + allowAllPublishedAlgorithms?: boolean publisherTrustedAlgorithms?: publisherTrustedAlgorithm[] } diff --git a/src/ocean/Assets.ts b/src/ocean/Assets.ts index 57de6edd..42bdeb76 100644 --- a/src/ocean/Assets.ts +++ b/src/ocean/Assets.ts @@ -1,6 +1,11 @@ import { DDO } from '../ddo/DDO' import { Metadata } from '../ddo/interfaces/Metadata' -import { Service, ServiceAccess } from '../ddo/interfaces/Service' +import { + Service, + ServiceAccess, + ServiceCustomParameter, + ServiceCustomParametersRequired +} from '../ddo/interfaces/Service' import { SearchQuery } from '../metadatacache/MetadataCache' import { EditableMetadata } from '../ddo/interfaces/EditableMetadata' import Account from './Account' @@ -9,15 +14,11 @@ import { SubscribablePromise, didNoZeroX, didPrefixed, assetResolve } from '../u import { Instantiable, InstantiableConfig } from '../Instantiable.abstract' import { WebServiceConnector } from './utils/WebServiceConnector' import BigNumber from 'bignumber.js' -import { Provider } from '../provider/Provider' +import { Provider, UserCustomParameters } from '../provider/Provider' import { isAddress } from 'web3-utils' import { MetadataMain } from '../ddo/interfaces' import { TransactionReceipt } from 'web3-core' -import { - CredentialType, - CredentialAction, - Credentials -} from '../ddo/interfaces/Credentials' +import { CredentialType } from '../ddo/interfaces/Credentials' import { updateCredentialDetail, removeCredentialDetail } from './AssetsCredential' import { Consumable } from '../ddo/interfaces/Consumable' @@ -71,7 +72,7 @@ export class Assets extends Instantiable { * @param {String} name Token name * @param {String} symbol Token symbol * @param {String} providerUri - * @return {Promise} + * @return {SubscribablePromise} */ public create( metadata: Metadata, @@ -443,17 +444,20 @@ export class Assets extends Instantiable { * @param {String} cost number of datatokens needed for this service * @param {String} datePublished * @param {Number} timeout - * @return {Promise} service + * @param {String} providerUri + * @param {ServiceCustomParametersRequired} requiredParameters + * @return {Promise} service */ public async createAccessServiceAttributes( creator: Account, cost: string, datePublished: string, - timeout = 0, - providerUri?: string + timeout: number = 0, + providerUri?: string, + requiredParameters?: ServiceCustomParametersRequired ): Promise { - return { + const service: ServiceAccess = { type: 'access', index: 2, serviceEndpoint: providerUri || this.ocean.provider.url, @@ -467,6 +471,11 @@ export class Assets extends Instantiable { } } } + if (requiredParameters?.userCustomParameters) + service.attributes.userCustomParameters = requiredParameters.userCustomParameters + if (requiredParameters?.algoCustomParameters) + service.attributes.algoCustomParameters = requiredParameters.algoCustomParameters + return service } /** @@ -477,14 +486,16 @@ export class Assets extends Instantiable { * @param {String} consumerAddress * @param {Number} serviceIndex * @param {String} serviceEndpoint + * @param {UserCustomParameters} userCustomParameters * @return {Promise} Order details */ public async initialize( asset: DDO | string, serviceType: string, consumerAddress: string, - serviceIndex = -1, - serviceEndpoint: string + serviceIndex: number = -1, + serviceEndpoint: string, + userCustomParameters?: UserCustomParameters ): Promise { const provider = await Provider.getInstance(this.instanceConfig) await provider.setBaseUrl(serviceEndpoint) @@ -492,7 +503,8 @@ export class Assets extends Instantiable { asset, serviceIndex, serviceType, - consumerAddress + consumerAddress, + userCustomParameters ) if (res === null) return null const providerData = JSON.parse(res) @@ -507,15 +519,17 @@ export class Assets extends Instantiable { * @param {Number} serviceIndex * @param {String} mpAddress Marketplace fee collector address * @param {String} consumerAddress Optionally, if the consumer is another address than payer + * @param {UserCustomParameters} userCustomParameters * @return {Promise} transactionHash of the payment */ public async order( asset: DDO | string, serviceType: string, payerAddress: string, - serviceIndex = -1, + serviceIndex: number = -1, mpAddress?: string, consumerAddress?: string, + userCustomParameters?: UserCustomParameters, searchPreviousOrders = true ): Promise { let service: Service @@ -533,13 +547,25 @@ export class Assets extends Instantiable { service = await this.getServiceByIndex(ddo, serviceIndex) serviceType = service.type } + // TODO validate userCustomParameters + if ( + !(await this.isUserCustomParametersValid( + service.attributes.userCustomParameters, + userCustomParameters + )) + ) { + throw new Error( + `Order asset failed, Missing required fiels in userCustomParameters` + ) + } try { const providerData = await this.initialize( ddo, serviceType, payerAddress, serviceIndex, - service.serviceEndpoint + service.serviceEndpoint, + userCustomParameters ) if (!providerData) throw new Error( @@ -732,4 +758,25 @@ export class Assets extends Instantiable { */ return { status, message, result } } + + /** + * Validate custom user parameters (user & algorithms) + * @param {ServiceCustomParameter[]} serviceCustomParameters + * @param {UserCustomParameters} userCustomParameters + * @return {Promise} + */ + public async isUserCustomParametersValid( + serviceCustomParameters: ServiceCustomParameter[], + userCustomParameters?: UserCustomParameters + ): Promise { + if (serviceCustomParameters) + for (const data of serviceCustomParameters) { + const keyname = data.name + if (!userCustomParameters || !userCustomParameters[keyname]) { + this.logger.error('Missing key: ' + keyname + ' from customData') + return false + } + } + return true + } } diff --git a/src/ocean/Compute.ts b/src/ocean/Compute.ts index 92c51720..1f4e3acb 100644 --- a/src/ocean/Compute.ts +++ b/src/ocean/Compute.ts @@ -3,7 +3,8 @@ import { Service, ServiceComputePrivacy, ServiceCompute, - publisherTrustedAlgorithm + publisherTrustedAlgorithm, + ServiceCustomParametersRequired } from '../ddo/interfaces/Service' import Account from './Account' import { SubscribablePromise, assetResolve, AssetResolved } from '../utils' @@ -14,7 +15,7 @@ import { ComputeInput, ComputeAlgorithm } from './interfaces/Compute' -import { Provider } from '../provider/Provider' +import { Provider, UserCustomParameters } from '../provider/Provider' import { SHA256 } from 'crypto-js' export enum OrderProgressStep { @@ -121,6 +122,18 @@ export class Compute extends Instantiable { const { did, ddo } = await assetResolve(asset, this.ocean) const service = ddo.findServiceByType('compute') const { serviceEndpoint } = service + if (algorithm.serviceIndex) { + const { ddo } = await assetResolve(algorithm.did, this.ocean) + const algoService: Service = ddo.findServiceById(algorithm.serviceIndex) + if ( + !(await this.ocean.assets.isUserCustomParametersValid( + algoService.attributes.algoCustomParameters, + algorithm.algoCustomParameters + )) + ) { + return null + } + } if (did && txId) { const provider = await Provider.getInstance(this.instanceConfig) await provider.setBaseUrl(serviceEndpoint) @@ -343,11 +356,12 @@ export class Compute extends Instantiable { providerAttributes: any, computePrivacy?: ServiceComputePrivacy, timeout?: number, - providerUri?: string + providerUri?: string, + requiredCustomParameters?: ServiceCustomParametersRequired ): ServiceCompute { const name = 'dataAssetComputingService' if (!timeout) timeout = 3600 - const service = { + const service: ServiceCompute = { type: 'compute', index: 3, serviceEndpoint: providerUri || this.ocean.provider.url, @@ -365,6 +379,12 @@ export class Compute extends Instantiable { } if (computePrivacy) service.attributes.main.privacy = computePrivacy + if (requiredCustomParameters?.userCustomParameters) + service.attributes.userCustomParameters = + requiredCustomParameters.userCustomParameters + if (requiredCustomParameters?.algoCustomParameters) + service.attributes.algoCustomParameters = + requiredCustomParameters.algoCustomParameters return service as ServiceCompute } @@ -508,7 +528,7 @@ export class Compute extends Instantiable { * @param {string} algorithmDid The DID of the algorithm asset (of type `algorithm`) to run on the asset. * @param {string} algorithmServiceIndex The index of the service in the algorithm * @param {MetaData} algorithmMeta Metadata about the algorithm being run if `algorithm` is being used. This is ignored when `algorithmDid` is specified. - * @return {Promise} Returns the transaction details + * @return {SubscribablePromise} Returns the transaction details * * Note: algorithmDid and algorithmMeta are optional, but if they are not passed, * you can end up in the situation that you are ordering and paying for your compute job, @@ -521,6 +541,7 @@ export class Compute extends Instantiable { algorithm: ComputeAlgorithm, mpAddress?: string, computeAddress?: string, + userCustomParameters?: UserCustomParameters, searchPreviousOrders = true ): SubscribablePromise { return new SubscribablePromise(async (observer) => { @@ -543,6 +564,7 @@ export class Compute extends Instantiable { -1, mpAddress, computeAddress, + userCustomParameters, searchPreviousOrders ) return order @@ -570,6 +592,7 @@ export class Compute extends Instantiable { serviceIndex = -1, mpAddress?: string, consumerAddress?: string, + userCustomParameters?: UserCustomParameters, searchPreviousOrders = true ): Promise { // this is only a convienince function, which calls ocean.assets.order @@ -581,6 +604,7 @@ export class Compute extends Instantiable { serviceIndex, mpAddress, consumerAddress, + userCustomParameters, searchPreviousOrders ) } catch (error) { diff --git a/src/ocean/interfaces/Compute.ts b/src/ocean/interfaces/Compute.ts index 378a63e4..47ecdb1d 100644 --- a/src/ocean/interfaces/Compute.ts +++ b/src/ocean/interfaces/Compute.ts @@ -43,4 +43,5 @@ export interface ComputeAlgorithm { meta?: MetadataAlgorithm transferTxId?: string dataToken?: string + algoCustomParameters?: { [key: string]: any } } diff --git a/src/provider/Provider.ts b/src/provider/Provider.ts index 3ca6209b..1b05bb02 100644 --- a/src/provider/Provider.ts +++ b/src/provider/Provider.ts @@ -18,6 +18,10 @@ export interface ServiceEndpoint { urlPath: string } +export interface UserCustomParameters { + [key: string]: any +} + /** * Provides an interface for provider service. * Provider service is the technical component executed @@ -188,7 +192,8 @@ export class Provider extends Instantiable { asset: DDO | string, serviceIndex: number, serviceType: string, - consumerAddress: string + consumerAddress: string, + userCustomParameters?: UserCustomParameters ): Promise { const { did, ddo } = await assetResolve(asset, this.ocean) let initializeUrl = this.getInitializeEndpoint() @@ -200,6 +205,8 @@ export class Provider extends Instantiable { initializeUrl += `&serviceType=${serviceType}` initializeUrl += `&dataToken=${ddo.dataToken}` initializeUrl += `&consumerAddress=${consumerAddress}` + if (userCustomParameters) + initializeUrl += '&userdata=' + encodeURI(JSON.stringify(userCustomParameters)) try { const response = await this.ocean.utils.fetch.get(initializeUrl) return await response.text() @@ -218,7 +225,8 @@ export class Provider extends Instantiable { destination: string, account: Account, files: File[], - index = -1 + index = -1, + userCustomParameters?: UserCustomParameters ): Promise { await this.getNonce(account.getId()) const signature = await this.createSignature(account, did + this.nonce) @@ -236,7 +244,8 @@ export class Provider extends Instantiable { consumeUrl += `&transferTxId=${txId}` consumeUrl += `&consumerAddress=${account.getId()}` consumeUrl += `&signature=${signature}` - + if (userCustomParameters) + consumeUrl += '&userdata=' + encodeURI(JSON.stringify(userCustomParameters)) try { !destination ? await this.ocean.utils.fetch.downloadFileBrowser(consumeUrl) @@ -262,7 +271,8 @@ export class Provider extends Instantiable { serviceIndex?: string, serviceType?: string, tokenAddress?: string, - additionalInputs?: ComputeInput[] + additionalInputs?: ComputeInput[], + userCustomParameters?: UserCustomParameters ): Promise { const address = consumerAccount.getId() await this.getNonce(consumerAccount.getId()) @@ -291,6 +301,9 @@ export class Provider extends Instantiable { if (tokenAddress) payload.dataToken = tokenAddress if (additionalInputs) payload.additionalInputs = additionalInputs + if (userCustomParameters) payload.userData = userCustomParameters + if (algorithm.algoCustomParameters) + payload.algouserdata = algorithm.algoCustomParameters const path = this.getComputeStartEndpoint() ? this.getComputeStartEndpoint().urlPath : null diff --git a/test/integration/ComputeFlow.test.ts b/test/integration/ComputeFlow.test.ts index 0a789f2c..95c8609b 100644 --- a/test/integration/ComputeFlow.test.ts +++ b/test/integration/ComputeFlow.test.ts @@ -42,6 +42,7 @@ describe('Compute flow', () => { let algorithmAssetwithCompute: DDO let algorithmAssetRemoteProvider: DDO let algorithmAssetRemoteProviderWithCompute: DDO + let algorithmAssetWithCustomData: DDO let contracts: TestContractHandler let datatoken: DataTokens let tokenAddress: string @@ -54,6 +55,7 @@ describe('Compute flow', () => { let tokenAddressAlgorithmRemoteProviderWithCompute: string let tokenAddressAdditional1: string let tokenAddressAdditional2: string + let tokenAddressWithCustomData: string let price: string let ocean: Ocean let data: { t: number; url: string } @@ -209,6 +211,18 @@ describe('Compute flow', () => { 'Add2' ) assert(tokenAddressAdditional2 != null, 'Creation of tokenAddressAdditional2 failed') + + tokenAddressWithCustomData = await datatoken.create( + blob, + alice.getId(), + '10000000000', + 'WCD', + 'WCD' + ) + assert( + tokenAddressWithCustomData != null, + 'Creation of tokenAddressWithCustomData failed' + ) }) it('Generates metadata', async () => { @@ -704,6 +718,95 @@ describe('Compute flow', () => { ) }) + it('should publish an algorithm whith CustomData', async () => { + const assetWithCustomData: Metadata = { + main: { + type: 'algorithm', + name: 'Test Algo with CustomData', + dateCreated: dateCreated, + datePublished: dateCreated, + author: 'DevOps', + license: 'CC-BY', + files: [ + { + url: 'https://raw.githubusercontent.com/oceanprotocol/test-algorithm/master/javascript/algo.js', + contentType: 'text/js', + encoding: 'UTF-8' + } + ], + algorithm: { + language: 'js', + format: 'docker-image', + version: '0.1', + container: { + entrypoint: 'node $ALGO', + image: 'node', + tag: '10' + } + } + } + } + const customdata = { + userCustomParameters: [ + { + name: 'firstname', + type: 'text', + label: 'Your first name', + required: true, + description: 'Your name' + }, + { + name: 'lastname', + type: 'text', + label: 'Your last name', + required: false, + description: 'Your last name' + } + ], + algoCustomParameters: [ + { + name: 'iterations', + type: 'number', + label: 'Iterations', + required: true, + description: 'No of passes' + }, + { + name: 'chunk', + type: 'number', + label: 'Chunks', + required: false, + description: 'No of chunks' + } + ] + } + const service1 = await ocean.assets.createAccessServiceAttributes( + alice, + price, + dateCreated, + 0, + null, + customdata + ) + algorithmAssetWithCustomData = await ocean.assets.create( + assetWithCustomData, + alice, + [service1], + tokenAddressWithCustomData + ) + assert( + algorithmAssetWithCustomData.dataToken === tokenAddressWithCustomData, + 'algorithmAssetWithCustomData.dataToken !== tokenAddressWithCustomData' + ) + const storeTx = await ocean.onChainMetadata.publish( + algorithmAssetWithCustomData.id, + algorithmAssetWithCustomData, + alice.getId() + ) + assert(storeTx) + await ocean.metadataCache.waitForAqua(algorithmAssetWithCustomData.id) + }) + it('Alice mints 100 DTs and tranfers them to the compute marketplace', async () => { await datatoken.mint(tokenAddress, alice.getId(), tokenAmount) await datatoken.mint(tokenAddressNoRawAlgo, alice.getId(), tokenAmount) @@ -719,90 +822,98 @@ describe('Compute flow', () => { ) await datatoken.mint(tokenAddressAdditional1, alice.getId(), tokenAmount) await datatoken.mint(tokenAddressAdditional2, alice.getId(), tokenAmount) + await datatoken.mint(tokenAddressWithCustomData, alice.getId(), tokenAmount) }) it('Bob gets datatokens from Alice to be able to try the compute service', async () => { + let balance const dTamount = '200' - await datatoken - .transfer(tokenAddress, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddress, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressNoRawAlgo, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressNoRawAlgo, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressWithTrustedAlgo, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressWithTrustedAlgo, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) + await datatoken.transfer(tokenAddress, bob.getId(), dTamount, alice.getId()) + balance = await datatoken.balance(tokenAddress, bob.getId()) + assert(balance.toString() === dTamount.toString()) - await datatoken - .transfer(tokenAddressWithBogusProvider, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance( - tokenAddressWithBogusProvider, - bob.getId() - ) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressAlgorithm, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressAlgorithm, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressAlgorithmwithCompute, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance( - tokenAddressAlgorithmwithCompute, - bob.getId() - ) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressAlgorithmRemoteProvider, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance( - tokenAddressAlgorithmRemoteProvider, - bob.getId() - ) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer( - tokenAddressAlgorithmRemoteProviderWithCompute, - bob.getId(), - dTamount, - alice.getId() - ) - .then(async () => { - const balance = await datatoken.balance( - tokenAddressAlgorithmRemoteProviderWithCompute, - bob.getId() - ) - assert(balance.toString() === dTamount.toString()) - }) + await datatoken.transfer(tokenAddressNoRawAlgo, bob.getId(), dTamount, alice.getId()) + balance = await datatoken.balance(tokenAddressNoRawAlgo, bob.getId()) + assert(balance.toString() === dTamount.toString()) - await datatoken - .transfer(tokenAddressAdditional1, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressAdditional1, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) + await datatoken.transfer( + tokenAddressWithTrustedAlgo, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressWithTrustedAlgo, bob.getId()) + assert(balance.toString() === dTamount.toString()) - await datatoken - .transfer(tokenAddressAdditional2, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressAdditional2, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) + await datatoken.transfer( + tokenAddressWithBogusProvider, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressWithBogusProvider, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer(tokenAddressAlgorithm, bob.getId(), dTamount, alice.getId()) + balance = await datatoken.balance(tokenAddressAlgorithm, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressAlgorithmwithCompute, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressAlgorithmwithCompute, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressAlgorithmRemoteProvider, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressAlgorithmRemoteProvider, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressAlgorithmRemoteProviderWithCompute, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance( + tokenAddressAlgorithmRemoteProviderWithCompute, + bob.getId() + ) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressAdditional1, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressAdditional1, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressAdditional2, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressAdditional2, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressWithCustomData, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressWithCustomData, bob.getId()) + assert(balance.toString() === dTamount.toString()) }) it('Bob starts compute job with a raw Algo', async () => { @@ -1370,7 +1481,176 @@ describe('Compute flow', () => { assert(response.status >= 1, 'Invalid response.status') assert(response.jobId, 'Invalid jobId') }) + it('should not be able start a compute job with a published algo that requires userdata without providing that', async () => { + const computeService = ddo.findServiceByType('compute') + assert(algorithmAssetWithCustomData != null, 'algorithmAsset should not be null') + const serviceAlgo = algorithmAssetWithCustomData.findServiceByType('access') + // get the compute address first + computeAddress = await ocean.compute.getComputeAddress(ddo.id, computeService.index) + assert(ddo != null, 'ddo should not be null') + const algoDefinition: ComputeAlgorithm = { + did: algorithmAssetWithCustomData.id, + serviceIndex: serviceAlgo.index + } + // check if asset is orderable. otherwise, you might pay for it, but it has some algo restrictions + const allowed = await ocean.compute.isOrderable( + ddo, + computeService.index, + algoDefinition, + algorithmAssetWithCustomData + ) + assert(allowed === true) + const order = await ocean.compute.orderAsset( + bob.getId(), + ddo, + computeService.index, + algoDefinition, + null, // no marketplace fee + computeAddress // CtD is the consumer of the dataset + ) + assert(order != null, 'Order should not be null') + // order the algorithm, without providing userdata + try { + const orderalgo = await ocean.compute.orderAlgorithm( + algorithmAssetWithCustomData, + serviceAlgo.type, + bob.getId(), + serviceAlgo.index, + null, // no marketplace fee + computeAddress // CtD is the consumer of the dataset + ) + assert(orderalgo === null, 'Order should be null') + } catch (error) { + assert(error != null, 'Order should throw error') + } + }) + it('should not be able to start a compute job with a published algo that requires algodata without providing that', async () => { + const output = {} + const computeService = ddo.findServiceByType('compute') + assert(algorithmAssetWithCustomData != null, 'algorithmAsset should not be null') + const serviceAlgo = algorithmAssetWithCustomData.findServiceByType('access') + // get the compute address first + computeAddress = await ocean.compute.getComputeAddress(ddo.id, computeService.index) + assert(ddo != null, 'ddo should not be null') + const algoDefinition: ComputeAlgorithm = { + did: algorithmAssetWithCustomData.id, + serviceIndex: serviceAlgo.index + } + // check if asset is orderable. otherwise, you might pay for it, but it has some algo restrictions + const allowed = await ocean.compute.isOrderable( + ddo, + computeService.index, + algoDefinition, + algorithmAssetWithCustomData + ) + assert(allowed === true) + const bobUserData = { + firstname: 'Bob', + lastname: 'Doe' + } + const order = await ocean.compute.orderAsset( + bob.getId(), + ddo, + computeService.index, + algoDefinition, + null, // no marketplace fee + computeAddress // CtD is the consumer of the dataset + ) + assert(order != null, 'Order should not be null') + + const orderalgo = await ocean.compute.orderAlgorithm( + algorithmAssetWithCustomData, + serviceAlgo.type, + bob.getId(), + serviceAlgo.index, + null, // no marketplace fee + computeAddress, // CtD is the consumer of the dataset + bobUserData + ) + assert(orderalgo != null, 'Order should be null') + algoDefinition.transferTxId = orderalgo + algoDefinition.dataToken = algorithmAsset.dataToken + const response = await ocean.compute.start( + ddo, + order, + tokenAddress, + bob, + algoDefinition, + output, + `${computeService.index}`, + computeService.type, + undefined + ) + assert(response === null, 'Compute should not start') + }) + it('should be able to start a compute job with a published algo that requires algodata by providing that', async () => { + const output = {} + const computeService = ddo.findServiceByType('compute') + assert(algorithmAssetWithCustomData != null, 'algorithmAsset should not be null') + const serviceAlgo = algorithmAssetWithCustomData.findServiceByType('access') + // get the compute address first + computeAddress = await ocean.compute.getComputeAddress(ddo.id, computeService.index) + assert(ddo != null, 'ddo should not be null') + const algoDefinition: ComputeAlgorithm = { + did: algorithmAssetWithCustomData.id, + serviceIndex: serviceAlgo.index, + algoCustomParameters: { + iterations: 20, + chunk: 1 + } + } + // check if asset is orderable. otherwise, you might pay for it, but it has some algo restrictions + const allowed = await ocean.compute.isOrderable( + ddo, + computeService.index, + algoDefinition, + algorithmAssetWithCustomData + ) + assert(allowed === true) + const bobUserData = { + firstname: 'Bob', + lastname: 'Doe' + } + + const order = await ocean.compute.orderAsset( + bob.getId(), + ddo, + computeService.index, + algoDefinition, + null, // no marketplace fee + computeAddress // CtD is the consumer of the dataset + ) + assert(order != null, 'Order should not be null') + + const orderalgo = await ocean.compute.orderAlgorithm( + algorithmAssetWithCustomData, + serviceAlgo.type, + bob.getId(), + serviceAlgo.index, + null, // no marketplace fee + computeAddress, // CtD is the consumer of the dataset + bobUserData + ) + assert(orderalgo != null, 'Order should be null') + algoDefinition.transferTxId = orderalgo + algoDefinition.dataToken = algorithmAssetWithCustomData.dataToken + const response = await ocean.compute.start( + ddo, + order, + tokenAddress, + bob, + algoDefinition, + output, + `${computeService.index}`, + computeService.type, + undefined + ) + assert(response, 'Compute error') + jobId = response.jobId + assert(response.status >= 1, 'Invalid response status') + assert(response.jobId, 'Invalid jobId') + }) it('Alice updates Compute Privacy, allowing some published algos', async () => { const computeService = ddo.findServiceByType('compute') assert(computeService, 'ComputeIndex should be >0') diff --git a/test/integration/Marketplaceflow.test.ts b/test/integration/Marketplaceflow.test.ts index a24ff12c..c4b6ce90 100644 --- a/test/integration/Marketplaceflow.test.ts +++ b/test/integration/Marketplaceflow.test.ts @@ -35,11 +35,13 @@ describe('Marketplace flow', () => { let ddoWithCredentialsAllowList let ddoWithCredentialsDenyList let ddoWithCredentials + let ddoWithUserData let asset let assetWithPool let assetWithBadUrl let assetWithEncrypt let assetInvalidNoName + let assetWithUserData let marketplace: Account let contracts: TestContractHandler let datatoken: DataTokens @@ -48,6 +50,7 @@ describe('Marketplace flow', () => { let tokenAddressForBadUrlAsset: string let tokenAddressEncrypted: string let tokenAddressInvalidNoName: string + let tokenAddressWithUserData let service1: ServiceAccess let price: string let ocean: Ocean @@ -133,6 +136,15 @@ describe('Marketplace flow', () => { 'DTA' ) assert(tokenAddressInvalidNoName != null) + + tokenAddressWithUserData = await datatoken.create( + blob, + alice.getId(), + '10000000000', + 'AliceDT', + 'DTA' + ) + assert(tokenAddressWithUserData != null) }) it('Generates metadata', async () => { @@ -235,6 +247,27 @@ describe('Marketplace flow', () => { ] } } + + assetWithUserData = { + main: { + type: 'dataset', + name: 'test-dataset-with-pools', + dateCreated: new Date(Date.now()).toISOString().split('.')[0] + 'Z', // remove milliseconds + datePublished: new Date(Date.now()).toISOString().split('.')[0] + 'Z', // remove milliseconds + author: 'oceanprotocol-team', + license: 'MIT', + files: [ + { + url: 'https://s3.amazonaws.com/testfiles.oceanprotocol.com/info.0.json', + checksum: 'efb2c764274b745f5fc37f97c6b0e761', + contentLength: '4535431', + contentType: 'text/csv', + encoding: 'UTF-8', + compression: 'zip' + } + ] + } + } }) it('Should validate local metadata', async () => { const valid = await ocean.metadataCache.validateMetadata(asset) @@ -342,6 +375,46 @@ describe('Marketplace flow', () => { false ) assert(storeTxWithCredentials) + + const userdata = { + userCustomParameters: [ + { + name: 'firstname', + type: 'text', + label: 'Your first name', + required: true, + description: 'Your name' + }, + { + name: 'lastname', + type: 'text', + label: 'Your last name', + required: false, + description: 'Your last name' + } + ] + } + const serviceWithUserData = await ocean.assets.createAccessServiceAttributes( + alice, + price, + publishedDate, + timeout, + null, + userdata + ) + ddoWithUserData = await ocean.assets.create( + asset, + alice, + [serviceWithUserData], + tokenAddressWithUserData + ) + const storeTxWithUserData = await ocean.onChainMetadata.publish( + ddoWithUserData.id, + ddoWithUserData, + alice.getId(), + false + ) + assert(storeTxWithUserData) // wait for all this assets to be published await ocean.metadataCache.waitForAqua(ddo.id) await ocean.metadataCache.waitForAqua(ddoWithBadUrl.id) @@ -350,6 +423,7 @@ describe('Marketplace flow', () => { await ocean.metadataCache.waitForAqua(ddoWithCredentialsAllowList.id) await ocean.metadataCache.waitForAqua(ddoWithCredentialsDenyList.id) await ocean.metadataCache.waitForAqua(ddoWithCredentials.id) + await ocean.metadataCache.waitForAqua(ddoWithUserData.id) }) it('Alice should fail to publish invalid dataset', async () => { @@ -404,6 +478,7 @@ describe('Marketplace flow', () => { await datatoken.mint(tokenAddressForBadUrlAsset, alice.getId(), tokenAmount) await datatoken.mint(tokenAddressEncrypted, alice.getId(), tokenAmount) await datatoken.mint(tokenAddressWithPool, alice.getId(), tokenAmount) + await datatoken.mint(tokenAddressWithUserData, alice.getId(), tokenAmount) // since we are in barge, we can do this await datatoken.mint(ocean.pool.oceanAddress, owner.getId(), tokenAmount) await datatoken.transfer(ocean.pool.oceanAddress, alice.getId(), '200', owner.getId()) @@ -447,18 +522,28 @@ describe('Marketplace flow', () => { it('Bob gets datatokens', async () => { const dTamount = '20' - await datatoken - .transfer(tokenAddress, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddress, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) - await datatoken - .transfer(tokenAddressForBadUrlAsset, bob.getId(), dTamount, alice.getId()) - .then(async () => { - const balance = await datatoken.balance(tokenAddressForBadUrlAsset, bob.getId()) - assert(balance.toString() === dTamount.toString()) - }) + let balance + await datatoken.transfer(tokenAddress, bob.getId(), dTamount, alice.getId()) + balance = await datatoken.balance(tokenAddress, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressForBadUrlAsset, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressForBadUrlAsset, bob.getId()) + assert(balance.toString() === dTamount.toString()) + + await datatoken.transfer( + tokenAddressWithUserData, + bob.getId(), + dTamount, + alice.getId() + ) + balance = await datatoken.balance(tokenAddressWithUserData, bob.getId()) + assert(balance.toString() === dTamount.toString()) }) it('Bob consumes asset 1', async () => { @@ -772,4 +857,41 @@ describe('Marketplace flow', () => { assert(consumable.status === 3) assert(consumable.result === false) }) + + it('Bob tries to order asset with Custom Data, but he does not provide all the params', async () => { + try { + const order = await ocean.assets.order( + ddoWithUserData.id, + accessService.type, + bob.getId() + ) + assert(order === null, 'Order should be null') + } catch (error) { + assert(error != null, 'Order should throw error') + } + }) + + it('Bob tries to order asset with Custom Data, providing all required user inputs', async () => { + const bobUserData = { + firstname: 'Bob', + lastname: 'Doe' + } + + try { + const service = ddoWithUserData.findServiceByType('access') + const serviceIndex = service.index + const order = await ocean.assets.order( + ddoWithUserData.id, + accessService.type, + bob.getId(), + serviceIndex, + null, + null, + bobUserData + ) + assert(order != null, 'Order should not be null') + } catch (error) { + assert(error === null, 'Order should not throw error') + } + }) })