diff --git a/.travis.yml b/.travis.yml index 162d1f47..aaf87cac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ before_script: - git clone https://github.com/oceanprotocol/barge - cd barge - git checkout feature/ocean-contracts - - export PROVIDER_VERSION=latest + - export PROVIDER_VERSION=v0.3.0 - export ADDRESS_FILE="${HOME}/.ocean/ocean-contracts/artifacts/address.json" - export AQUARIUS_URI="http://172.15.0.5:5000" - export DEPLOY_CONTRACTS=true diff --git a/package-lock.json b/package-lock.json index dff86387..8ce9e48c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -939,9 +939,9 @@ } }, "@oceanprotocol/contracts": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.4.1.tgz", - "integrity": "sha512-gc6bCt3pq9cpk1mYDKfsZhLlaM+8yQDFmOjtmT1KGXRmnTBcvmwCQXMrL5VohFaFi7Iqio3FZtuhYyRaEjikCw==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-0.4.2.tgz", + "integrity": "sha512-IKRUe60pcBcEmQhkpusSc+w2CIEHxmyNzth+VRU5Je6lrP6/XnDaFRHjeYbSy306I/WxLjma88S1xOn86BMCOA==" }, "@octokit/auth-token": { "version": "2.4.2", diff --git a/package.json b/package.json index d08f6452..5c5ead3c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@ethereum-navigator/navigator": "^0.5.0", - "@oceanprotocol/contracts": "^0.4.1", + "@oceanprotocol/contracts": "^0.4.2", "decimal.js": "^10.2.0", "fs": "0.0.1-security", "lzma": "^2.3.2", diff --git a/src/datatokens/Datatokens.ts b/src/datatokens/Datatokens.ts index 224caf0d..7843726e 100644 --- a/src/datatokens/Datatokens.ts +++ b/src/datatokens/Datatokens.ts @@ -5,7 +5,9 @@ import defaultFactoryABI from '@oceanprotocol/contracts/artifacts/DTFactory.json import defaultDatatokensABI from '@oceanprotocol/contracts/artifacts/DataTokenTemplate.json' import wordListDefault from '../data/words.json' - +import { TransactionReceipt } from 'web3-core' +import { time } from 'console' +import BigNumber from 'bignumber.js' /** * Provides a interface to DataTokens */ @@ -350,4 +352,79 @@ export class DataTokens { public fromWei(amount: string): string { return this.web3.utils.fromWei(amount) } + + /** Start Order + * @param {String} dataTokenAddress + * @param {String} amount + * @param {String} did + * @param {Number} serviceId + * @param {String} mpFeeAddress + * @param {String} address consumer Address + * @return {Promise} string + */ + public async startOrder( + dataTokenAddress: string, + amount: string, + did: string, + serviceId: number, + mpFeeAddress: string, + address: string + ): Promise { + const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, { + from: address + }) + if (!mpFeeAddress) mpFeeAddress = '0x0000000000000000000000000000000000000000' + try { + const trxReceipt = await datatoken.methods + .startOrder(this.web3.utils.toWei(amount), did, String(serviceId), mpFeeAddress) + .send({ from: address, gas: 600000 }) + return trxReceipt + } catch (e) { + console.error(e) + return null + } + } + + /** Search and return txid for a previous valid order with the same params + * @param {String} dataTokenAddress + * @param {String} amount + * @param {String} did + * @param {Number} serviceId + * @param {Number} timeout service timeout + * @param {String} address consumer Address + * @return {Promise} string + */ + public async getPreviousValidOrders( + dataTokenAddress: string, + amount: string, + did: string, + serviceId: number, + timeout: number, + address: string + ): Promise { + const datatoken = new this.web3.eth.Contract(this.datatokensABI, dataTokenAddress, { + from: address + }) + const events = await datatoken.getPastEvents('OrderStarted', { + fromBlock: 0, + toBlock: 'latest' + }) + for (let i = 0; i < events.length; i++) { + if ( + events[i].returnValues.did === did && + String(events[i].returnValues.amount) === String(amount) && + String(events[i].returnValues.serviceId) === String(serviceId) + ) { + const transaction = await this.web3.eth.getTransaction(events[i].transactionHash) + if (transaction.from === address) { + if (timeout === 0) return events[i].transactionHash + const blockDetails = await this.web3.eth.getBlock(events[i].blockHash) + const expiry = new BigNumber(blockDetails.timestamp).plus(timeout) + const unixTime = new BigNumber(Math.floor(Date.now() / 1000)) + if (unixTime.isLessThan(expiry)) return events[i].transactionHash + } + } + } + return null + } } diff --git a/src/ocean/Assets.ts b/src/ocean/Assets.ts index 162a998a..27332382 100644 --- a/src/ocean/Assets.ts +++ b/src/ocean/Assets.ts @@ -11,9 +11,11 @@ import { import { EditableMetadata } from '../ddo/interfaces/EditableMetadata' import Account from './Account' import DID from './DID' -import { SubscribablePromise } from '../utils' +import { SubscribablePromise, didZeroX } from '../utils' import { Instantiable, InstantiableConfig } from '../Instantiable.abstract' import { WebServiceConnector } from './utils/WebServiceConnector' +import { DataTokens } from '../lib' +import BigNumber from 'bignumber.js' export enum CreateProgressStep { CreatingDataToken, @@ -395,11 +397,49 @@ export class Assets extends Instantiable { } } - public async order( + /** + * Initialize a service + * Can be used to compute totalCost for ordering a service + * @param {String} did + * @param {String} serviceType + * @param {String} consumerAddress + * @param {Number} serviceIndex + * @param {String} mpFeePercent will be converted to Wei + * @param {String} mpAddress mp fee collector address + * @return {Promise} Order details + */ + public async initialize( did: string, serviceType: string, consumerAddress: string, serviceIndex = -1 + ): Promise { + const res = await this.ocean.provider.initialize( + did, + serviceIndex, + serviceType, + consumerAddress + ) + if (res === null) return null + const providerData = JSON.parse(res) + return providerData + } + + /** + * Orders & pays for a service + * @param {String} did + * @param {String} serviceType + * @param {String} consumerAddress + * @param {Number} serviceIndex + * @param {String} mpAddress mp fee collector address + * @return {Promise} transactionHash of the payment + */ + public async order( + did: string, + serviceType: string, + consumerAddress: string, + serviceIndex = -1, + mpAddress?: string ): Promise { if (serviceIndex === -1) { const service = await this.getServiceByType(did, serviceType) @@ -408,12 +448,53 @@ export class Assets extends Instantiable { const service = await this.getServiceByIndex(did, serviceIndex) serviceType = service.type } - return await this.ocean.provider.initialize( - did, - serviceIndex, - serviceType, - consumerAddress - ) + const { datatokens } = this.ocean + try { + const providerData = await this.initialize( + did, + serviceType, + consumerAddress, + serviceIndex + ) + if (!providerData) return null + const service = await this.getServiceByIndex(did, serviceIndex) + const previousOrder = await datatokens.getPreviousValidOrders( + providerData.dataToken, + providerData.numTokens, + didZeroX(did), + serviceIndex, + service.attributes.main.timeout, + consumerAddress + ) + if (previousOrder) return previousOrder + const balance = new BigNumber( + await datatokens.balance(providerData.dataToken, consumerAddress) + ) + const totalCost = new BigNumber( + this.web3.utils.fromWei(String(providerData.numTokens)) + ) + if (balance.isLessThanOrEqualTo(totalCost)) { + console.error( + 'Not enough funds. Needed ' + + totalCost.toString() + + ' but balance is ' + + balance.toString() + ) + return null + } + const txid = await datatokens.startOrder( + providerData.dataToken, + this.web3.utils.fromWei(String(providerData.numTokens)), + didZeroX(did), + serviceIndex, + mpAddress, + consumerAddress + ) + if (txid) return txid.transactionHash + } catch (e) { + console.error(e) + } + return null } // marketplace flow diff --git a/src/ocean/Compute.ts b/src/ocean/Compute.ts index 658e6aec..597bc454 100644 --- a/src/ocean/Compute.ts +++ b/src/ocean/Compute.ts @@ -87,7 +87,7 @@ export class Compute extends Instantiable { algorithmDataToken?: string ): Promise { output = this.checkOutput(consumerAccount, output) - if (did) { + if (did && txId) { const computeJobsList = await this.ocean.provider.compute( 'post', did, @@ -349,7 +349,8 @@ export class Compute extends Instantiable { datasetDid: string, serviceIndex: number, algorithmDid?: string, - algorithmMeta?: MetadataAlgorithm + algorithmMeta?: MetadataAlgorithm, + mpAddress?: string ): SubscribablePromise { return new SubscribablePromise(async (observer) => { const ddo: DDO = await this.ocean.assets.resolve(datasetDid) @@ -380,7 +381,9 @@ export class Compute extends Instantiable { const order = await this.ocean.assets.order( datasetDid, service.type, - consumerAccount + consumerAccount, + -1, + mpAddress ) return order }) diff --git a/src/utils/ConversionTypeHelpers.ts b/src/utils/ConversionTypeHelpers.ts index 331578d7..52015021 100644 --- a/src/utils/ConversionTypeHelpers.ts +++ b/src/utils/ConversionTypeHelpers.ts @@ -22,6 +22,7 @@ export function didTransformer(input = '', prefixOutput: boolean): string { // 0x + did:op: transformer export const didZeroX = (input: string): string => zeroX(didTransformer(input, false)) +export const didNoZeroX = (input: string): string => noZeroX(didTransformer(input, false)) // Shared functions function inputMatch( diff --git a/test/integration/ComputeFlow.test.ts b/test/integration/ComputeFlow.test.ts index b20759da..eeb6632f 100644 --- a/test/integration/ComputeFlow.test.ts +++ b/test/integration/ComputeFlow.test.ts @@ -282,16 +282,9 @@ describe('Compute flow', () => { algorithmMeta ) assert(order != null) - const computeOrder = JSON.parse(order) - const tx = await datatoken.transferWei( - computeOrder.dataToken, - computeOrder.to, - String(computeOrder.numTokens), - computeOrder.from - ) const response = await ocean.compute.start( ddo.id, - tx.transactionHash, + order, tokenAddress, bob, undefined, @@ -305,6 +298,7 @@ describe('Compute flow', () => { }) it('Bob should get status of a compute job', async () => { + assert(jobId != null) const response = await ocean.compute.status(bob, ddo.id, jobId) assert(response[0].jobId === jobId) }) @@ -314,6 +308,7 @@ describe('Compute flow', () => { assert(response.length > 0) }) it('Bob should stop compute job', async () => { + assert(jobId != null) await ocean.compute.stop(bob, ddo.id, jobId) const response = await ocean.compute.status(bob, ddo.id, jobId) assert(response[0].stopreq === 1) @@ -350,13 +345,7 @@ describe('Compute flow', () => { serviceAlgo.type, bob.getId() ) - const algoOrder = JSON.parse(orderalgo) - const algoTx = await datatoken.transferWei( - algoOrder.dataToken, - algoOrder.to, - String(algoOrder.numTokens), - algoOrder.from - ) + assert(orderalgo != null) const order = await ocean.compute.order( bob.getId(), ddo.id, @@ -365,16 +354,9 @@ describe('Compute flow', () => { undefined ) assert(order != null) - const computeOrder = JSON.parse(order) - const tx = await datatoken.transferWei( - computeOrder.dataToken, - computeOrder.to, - String(computeOrder.numTokens), - computeOrder.from - ) const response = await ocean.compute.start( ddo.id, - tx.transactionHash, + order, tokenAddress, bob, algorithmAsset.id, @@ -382,7 +364,7 @@ describe('Compute flow', () => { output, computeService.index, computeService.type, - algoTx.transactionHash, + orderalgo, algorithmAsset.dataToken ) jobId = response.jobId diff --git a/test/integration/Marketplaceflow.test.ts b/test/integration/Marketplaceflow.test.ts index 972f6505..7942772e 100644 --- a/test/integration/Marketplaceflow.test.ts +++ b/test/integration/Marketplaceflow.test.ts @@ -170,26 +170,32 @@ describe('Marketplace flow', () => { }) it('Bob consumes asset 1', async () => { - await ocean.assets - .order(ddo.id, accessService.type, bob.getId()) - .then(async (res: any) => { - res = JSON.parse(res) - return await datatoken.transferWei( - res.dataToken, - res.to, - String(res.numTokens), - res.from - ) - }) - .then(async (tx) => { - await ocean.assets.download( - ddo.id, - tx.transactionHash, - tokenAddress, - bob, - './node_modules/my-datasets' - ) - }) + await ocean.assets.order(ddo.id, accessService.type, bob.getId()).then(async (tx) => { + assert(tx != null) + await ocean.assets.download( + ddo.id, + tx, + tokenAddress, + bob, + './node_modules/my-datasets' + ) + }) + }) + + it('Bob consumes same asset again, without paying', async () => { + const balanceBefore = await datatoken.balance(tokenAddress, bob.getId()) + await ocean.assets.order(ddo.id, accessService.type, bob.getId()).then(async (tx) => { + assert(tx != null) + await ocean.assets.download( + ddo.id, + tx, + tokenAddress, + bob, + './node_modules/my-datasets' + ) + }) + const balanceAfter = await datatoken.balance(tokenAddress, bob.getId()) + assert(balanceBefore === balanceAfter) }) it('owner can list there assets', async () => { const assets = await ocean.assets.ownerAssets(alice.getId())