1
0
mirror of https://github.com/oceanprotocol/ocean.js.git synced 2024-07-01 06:11:45 +02:00

first cut, highly WIP

This commit is contained in:
alexcos20 2020-05-26 01:11:08 -07:00
parent 5072178816
commit 67af4d1d59
58 changed files with 20044 additions and 0 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# https://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{json,yml,yaml,md}]
indent_size = 2

48
.eslintrc Normal file
View File

@ -0,0 +1,48 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
},
"project": [
"./tsconfig.json",
"./test/unit/tsconfig.json",
"./test/integration/tsconfig.json"
]
},
"extends": [
"oceanprotocol",
"prettier/standard",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"@typescript-eslint/member-delimiter-style": [
"error",
{ "multiline": { "delimiter": "none" } }
],
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-object-literal-type-assertion": "off",
"@typescript-eslint/no-parameter-properties": "off",
"no-empty": ["error", { "allowEmptyCatch": true }],
"prefer-destructuring": ["warn"],
"no-dupe-class-members": ["warn"],
"no-useless-constructor": ["warn"]
},
"env": {
"es6": true,
"browser": true,
"mocha": true
}
}

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
node_modules/
dist/
.nyc_output/
coverage/
doc/
test/**/*.js
src/**/*.js
src/metadata\.json
.idea
.vscode

21
.npmignore Normal file
View File

@ -0,0 +1,21 @@
node_modules/
coverage/
.github
.nyc_output
.travis.yml
test/
src/
tsconfig.json
tslint.json
oceanprotocol-squid-*.tgz
squid-js.json
barge/
integration/
plugins/
scripts/
webpack*
ganache*
.prettierrc
.editorconfig
.eslintrc
SQUID_INTERFACE.md

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v12

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 90,
"trailingComma": "none"
}

10
library.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "Ocean Library",
"repository": {
"type": "git",
"url": "https://github.com/oceanprotocol/ocean-lib-js.git"
},
"dependencies": [
]
}

15592
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

124
package.json Normal file
View File

@ -0,0 +1,124 @@
{
"name": "@oceanprotocol/lib",
"version": "0.0.1",
"description": "JavaScript client library for Ocean Protocol",
"main": "./dist/node/lib.js",
"typings": "./dist/node/lib.d.ts",
"unpkg": "./dist/browser/lib.cjs2.min.js",
"scripts": {
"build": "npm run clean && npm run build:tsc && npm run build:dist",
"build:tsc": "tsc --sourceMap",
"build:dist": "cross-env NODE_ENV=production webpack",
"clean": "rm -rf ./dist/ ./doc/ ./.nyc_output",
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx .",
"format": "prettier --parser typescript --ignore-path .gitignore --write '**/*.{js,jsx,ts,tsx}'",
"run": "ts-node",
"release": "release-it --non-interactive",
"changelog": "auto-changelog -p",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/oceanprotocol/ocean-lib-js.git"
},
"keywords": [],
"author": "Ocean Protocol <devops@oceanprotocol.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/oceanprotocol/ocean-lib-js/issues"
},
"homepage": "https://github.com/oceanprotocol/ocean-lib-js#readme",
"peerDependencies": {
"web3": "^1.2.3"
},
"dependencies": {
"@ethereum-navigator/navigator": "^0.5.0",
"@oceanprotocol/keeper-contracts": "^0.13.2",
"@oceanprotocol/secret-store-client": "^0.0.15",
"bignumber.js": "^9.0.0",
"deprecated-decorator": "^0.1.6",
"node-fetch": "^2.6.0",
"save-file": "^2.3.1",
"uuid": "^8.0.0",
"web3": "^1.2.6",
"whatwg-url": "^8.0.0"
},
"devDependencies": {
"@release-it/bumper": "^1.1.0",
"@truffle/hdwallet-provider": "^1.0.33",
"@types/chai": "^4.2.11",
"@types/chai-spies": "^1.0.1",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.0",
"@types/node-fetch": "^2.5.5",
"@types/sinon": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0",
"auto-changelog": "^2.0.0",
"chai": "^4.2.0",
"chai-spies": "^1.0.0",
"cross-env": "^7.0.2",
"eslint": "^6.8.0",
"eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"lcov-result-merger": "^3.1.0",
"mocha": "^7.1.0",
"mock-local-storage": "^1.1.11",
"nyc": "^15.0.0",
"ora": "^4.0.2",
"prettier": "^1.19.1",
"sinon": "^9.0.1",
"source-map-support": "^0.5.16",
"ts-node": "^8.6.2",
"typedoc": "^0.17.1",
"typescript": "^3.8.3",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-merge": "^4.2.2"
},
"nyc": {
"include": [
"src/**/*.ts"
],
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"lcov",
"html"
],
"sourceMap": true,
"instrument": true
},
"release-it": {
"hooks": {
"after:bump": "npm run changelog && npm run doc:json"
},
"plugins": {
"@release-it/bumper": {
"out": [
"package.json",
"package-lock.json"
]
}
},
"git": {
"tagName": "v${version}"
},
"github": {
"release": true,
"assets": [
"dist/squid-js.json"
]
},
"npm": {
"publish": false
}
}
}

View File

@ -0,0 +1,31 @@
const { ConcatSource } = require('webpack-sources')
module.exports = class AddVendorsPlugin {
constructor(base) {
this.base = base
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
`AddVendorsPlugin ${this.base}`,
(compilation, callback) => {
const main = compilation.assets[`main.${this.base}`]
const mainMap = compilation.assets[`main.${this.base}.map`]
const vendor = compilation.assets[`vendors.${this.base}`]
if (main && vendor) {
const compiledAsset = new ConcatSource(main.children[0])
compiledAsset.add(vendor)
compiledAsset.add(main.children[1])
compilation.assets = {}
compilation.assets[this.base] = compiledAsset
} else if (main && mainMap) {
compilation.assets = {}
compilation.assets[this.base] = main
compilation.assets[`${this.base}.map`] = mainMap
}
callback()
}
)
}
}

48
src/.eslintrc Normal file
View File

@ -0,0 +1,48 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
},
"project": [
"./tsconfig.json",
"./test/unit/tsconfig.json",
"./test/integration/tsconfig.json"
]
},
"extends": [
"oceanprotocol",
"prettier/standard",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"@typescript-eslint/member-delimiter-style": [
"error",
{ "multiline": { "delimiter": "none" } }
],
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-object-literal-type-assertion": "off",
"@typescript-eslint/no-parameter-properties": "off",
"no-empty": ["error", { "allowEmptyCatch": true }],
"prefer-destructuring": ["warn"],
"no-dupe-class-members": ["warn"],
"no-useless-constructor": ["warn"]
},
"env": {
"es6": true,
"browser": true,
"mocha": true
}
}

1
src/@types/node_modules.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '@ethereum-navigator/navigator'

View File

@ -0,0 +1,94 @@
import Web3 from 'web3'
import Config from './models/Config'
import { Logger, LoggerInstance, LogLevel } from './utils'
import { Ocean } from './ocean/Ocean'
import { OceanFactoryABI } from './datatokens/FactoryABI'
import { OceanDataTokenABI } from './datatokens/DatatokensABI'
export interface InstantiableConfig {
ocean: Ocean
config?: Config
web3?: Web3
logger?: Logger
}
export function generateIntantiableConfigFromConfig(
config: Config
): Partial<InstantiableConfig> {
const logLevel =
typeof config.verbose !== 'number'
? config.verbose
? LogLevel.Log
: LogLevel.None
: (config.verbose as LogLevel)
return {
config,
web3: Web3Provider.getWeb3(config),
logger: new Logger(logLevel)
}
}
export abstract class Instantiable {
protected get ocean() {
if (!this._ocean) {
this.logger.error('Ocean instance is not defined.')
}
return this._ocean
}
protected get web3() {
if (!this._web3) {
this.logger.error('Web3 instance is not defined.')
}
return this._web3
}
protected get config() {
if (!this._config) {
this.logger.error('Config instance is not defined.')
}
return this._config
}
protected get logger() {
if (!this._logger) {
LoggerInstance.error('Logger instance is not defined.')
LoggerInstance.error('Using default instance.')
return LoggerInstance
}
return this._logger
}
protected get instanceConfig(): InstantiableConfig {
const { ocean, web3, config, logger } = this
return { ocean, web3, config, logger }
}
public static async getInstance(...args: any[]): Promise<any>
public static async getInstance(config: InstantiableConfig): Promise<any> {
LoggerInstance.warn('getInstance() methods has needs to be added to child class.')
}
protected static setInstanceConfig<T extends Instantiable>(
instance: T,
{ ocean, config, web3, logger }: InstantiableConfig
) {
instance._ocean = ocean
instance._config = config
instance._web3 = web3
instance._logger = logger
}
private _ocean: Ocean
private _web3: Web3
private _config: Config
private _logger: Logger
protected setInstanceConfig(config: InstantiableConfig) {
Instantiable.setInstanceConfig(this, config)
}
}

421
src/aquarius/Aquarius.ts Normal file
View File

@ -0,0 +1,421 @@
import { URL } from 'whatwg-url'
import { DDO } from '../ddo/DDO'
import DID from '../ocean/DID'
import { EditableMetaData } from '../ddo/MetaData'
import { Logger } from '../utils'
import { WebServiceConnector } from '../ocean/utils/WebServiceConnector'
const apiPath = '/api/v1/aquarius/assets/ddo'
export interface QueryResult {
results: DDO[]
page: number
totalPages: number
totalResults: number
}
export interface SearchQuery {
text?: string
offset?: number
page?: number
query: { [property: string]: string | number | string[] | number[] }
sort?: { [jsonPath: string]: number }
}
/**
* Provides an interface with Aquarius.
* Aquarius provides an off-chain database store for metadata about data assets.
*/
export class Aquarius {
public fetch: WebServiceConnector
private logger: Logger
private aquariusUri: string
private get url() {
return this.aquariusUri
}
/**
* Instantiate Aquarius (independently of Ocean) for off-chain interaction.
* @param {String} aquariusUri
* @param {Logger} logger
*/
constructor(aquariusUri: string, logger: Logger) {
this.fetch = new WebServiceConnector(logger)
this.logger = logger
this.aquariusUri = aquariusUri
}
public async getVersionInfo() {
return (await this.fetch.get(this.url)).json()
}
public async getAccessUrl(accessToken: any, payload: any): Promise<string> {
const accessUrl: string = await this.fetch
.post(`${accessToken.service_endpoint}/${accessToken.resource_id}`, payload)
.then((response: any): string => {
if (response.ok) {
return response.text()
}
this.logger.error('Failed: ', response.status, response.statusText)
return null
})
.then((consumptionUrl: string): string => {
this.logger.error('Success accessing consume endpoint: ', consumptionUrl)
return consumptionUrl
})
.catch(error => {
this.logger.error(
'Error fetching the data asset consumption url: ',
error
)
return null
})
return accessUrl
}
/**
* Search over the DDOs using a query.
* @param {SearchQuery} query Query to filter the DDOs.
* @return {Promise<QueryResult>}
*/
public async queryMetadata(query: SearchQuery): Promise<QueryResult> {
const result: QueryResult = await this.fetch
.post(`${this.url}${apiPath}/query`, JSON.stringify(query))
.then((response: any) => {
if (response.ok) {
return response.json() as DDO[]
}
this.logger.error(
'queryMetadata failed:',
response.status,
response.statusText
)
return this.transformResult()
})
.then(results => {
return this.transformResult(results)
})
.catch(error => {
this.logger.error('Error fetching querying metadata: ', error)
return this.transformResult()
})
return result
}
/**
* Search over the DDOs using a query.
* @param {SearchQuery} query Query to filter the DDOs.
* @return {Promise<QueryResult>}
*/
public async queryMetadataByText(query: SearchQuery): Promise<QueryResult> {
const fullUrl = new URL(`${this.url}${apiPath}/query`)
fullUrl.searchParams.append('text', query.text)
fullUrl.searchParams.append(
'sort',
decodeURIComponent(JSON.stringify(query.sort))
)
fullUrl.searchParams.append('offset', query.offset.toString())
fullUrl.searchParams.append('page', query.page.toString())
const result: QueryResult = await this.fetch
.get(fullUrl)
.then((response: any) => {
if (response.ok) {
return response.json() as DDO[]
}
this.logger.log(
'queryMetadataByText failed:',
response.status,
response.statusText
)
return this.transformResult()
})
.then(results => {
return this.transformResult(results)
})
.catch(error => {
this.logger.error('Error fetching querying metadata by text: ', error)
return this.transformResult()
})
return result
}
/**
* Stores a DDO in Aquarius.
* @param {DDO} ddo DDO to be stored.
* @return {Promise<DDO>} Final DDO.
*/
public async storeDDO(ddo: DDO): Promise<DDO> {
const fullUrl = `${this.url}${apiPath}`
const result: DDO = await this.fetch
.post(fullUrl, DDO.serialize(ddo))
.then((response: any) => {
if (response.ok) {
return response.json()
}
this.logger.error(
'storeDDO failed:',
response.status,
response.statusText,
ddo
)
return null as DDO
})
.then((response: DDO) => {
return new DDO(response) as DDO
})
.catch(error => {
this.logger.error('Error fetching querying metadata: ', error)
return null as DDO
})
return result
}
/**
* Retrieves a DDO by DID.
* @param {DID | string} did DID of the asset.
* @return {Promise<DDO>} DDO of the asset.
*/
public async retrieveDDO(
did: DID | string,
metadataServiceEndpoint?: string
): Promise<DDO> {
did = did && DID.parse(did)
const fullUrl = metadataServiceEndpoint || `${this.url}${apiPath}/${did.getDid()}`
const result = await this.fetch
.get(fullUrl)
.then((response: any) => {
if (response.ok) {
return response.json()
}
this.logger.log(
'retrieveDDO failed:',
response.status,
response.statusText,
did
)
return null as DDO
})
.then((response: DDO) => {
return new DDO(response) as DDO
})
.catch(error => {
this.logger.error('Error fetching querying metadata: ', error)
return null as DDO
})
return result
}
public async retrieveDDOByUrl(metadataServiceEndpoint?: string) {
return this.retrieveDDO(undefined, metadataServiceEndpoint)
}
/**
* Transfer ownership of a DDO
* @param {DID | string} did DID of the asset to update.
* @param {String} newOwner New owner of the DDO
* @param {String} updated Updated field of the DDO
* @param {String} signature Signature using updated field to verify that the consumer has rights
* @return {Promise<String>} Result.
*/
public async transferOwnership(
did: DID | string,
newOwner: string,
updated: string,
signature: string
): Promise<string> {
did = did && DID.parse(did)
const fullUrl = `${this.url}${apiPath}/owner/update/${did.getDid()}`
const result = await this.fetch
.put(
fullUrl,
JSON.stringify({
signature: signature,
updated: updated,
newOwner: newOwner
})
)
.then((response: any) => {
if (response.ok) {
return response.text
}
this.logger.log(
'transferownership failed:',
response.status,
response.statusText
)
return null
})
.catch(error => {
this.logger.error('Error transfering ownership metadata: ', error)
return null
})
return result
}
/**
* Update Compute Privacy
* @param {DID | string} did DID of the asset to update.
* @param {number } serviceIndex Service index
* @param {boolean} allowRawAlgorithm Allow Raw Algorithms
* @param {boolean} allowNetworkAccess Allow Raw Algorithms
* @param {String[]} trustedAlgorithms Allow Raw Algorithms
* @param {String} updated Updated field of the DDO
* @param {String} signature Signature using updated field to verify that the consumer has rights
* @return {Promise<String>} Result.
*/
public async updateComputePrivacy(
did: DID | string,
serviceIndex: number,
allowRawAlgorithm: boolean,
allowNetworkAccess: boolean,
trustedAlgorithms: string[],
updated: string,
signature: string
): Promise<string> {
did = did && DID.parse(did)
const fullUrl = `${this.url}${apiPath}/computePrivacy/update/${did.getDid()}`
const result = await this.fetch
.put(
fullUrl,
JSON.stringify({
signature: signature,
updated: updated,
serviceIndex: serviceIndex,
allowRawAlgorithm: allowRawAlgorithm,
allowNetworkAccess: allowNetworkAccess,
trustedAlgorithms: trustedAlgorithms
})
)
.then((response: any) => {
if (response.ok) {
return response.text
}
this.logger.log(
'update compute privacy failed:',
response.status,
response.statusText
)
return null
})
.catch(error => {
this.logger.error('Error updating compute privacy: ', error)
return null
})
return result
}
/**
* Edit Metadata for a DDO.
* @param {did} string DID.
* @param {newMetadata} EditableMetaData Metadata fields & new values.
* @param {String} updated Updated field of the DDO
* @param {String} signature Signature using updated field to verify that the consumer has rights
* @return {Promise<String>} Result.
*/
public async editMetadata(
did: DID | string,
newMetadata: EditableMetaData,
updated: string,
signature: string
): Promise<string> {
did = did && DID.parse(did)
const fullUrl = `${this.url}${apiPath}/metadata/${did.getDid()}`
const data = Object()
if (newMetadata.description != null) data.description = newMetadata.description
if (newMetadata.title != null) data.title = newMetadata.title
if (newMetadata.servicePrices != null)
data.servicePrices = newMetadata.servicePrices
if (newMetadata.links != null) data.links = newMetadata.links
data.updated = updated
data.signature = signature
const result = await this.fetch
.put(fullUrl, JSON.stringify(data))
.then((response: any) => {
if (response.ok) {
return response.text
}
this.logger.log(
'editMetaData failed:',
response.status,
response.statusText
)
return null
})
.catch(error => {
this.logger.error('Error transfering ownership metadata: ', error)
return null
})
return result
}
/**
* Retire a DDO (Delete)
* @param {DID | string} did DID of the asset to update.
* @param {String} updated Updated field of the DDO
* @param {String} signature Signature using updated field to verify that the consumer has rights
* @return {Promise<String>} Result.
*/
public async retire(
did: DID | string,
updated: string,
signature: string
): Promise<string> {
did = did && DID.parse(did)
const fullUrl = `${this.url}${apiPath}/${did.getDid()}`
const result = await this.fetch
.delete(
fullUrl,
JSON.stringify({
signature: signature,
updated: updated
})
)
.then((response: any) => {
if (response.ok) {
return response.text
}
this.logger.log('retire failed:', response.status, response.statusText)
return null
})
.catch(error => {
this.logger.error('Error transfering ownership metadata: ', error)
return null
})
return result
}
public getServiceEndpoint(did: DID) {
return `${this.url}/api/v1/aquarius/assets/ddo/did:op:${did.getId()}`
}
private transformResult(
{ results, page, total_pages: totalPages, total_results: totalResults }: any = {
result: [],
page: 0,
total_pages: 0, // eslint-disable-line @typescript-eslint/camelcase
total_results: 0 // eslint-disable-line @typescript-eslint/camelcase
}
): QueryResult {
return {
results: (results || []).map(ddo => new DDO(ddo as DDO)),
page,
totalPages,
totalResults
}
}
}

242
src/brizo/Brizo.ts Normal file
View File

@ -0,0 +1,242 @@
import { File, MetaDataAlgorithm } from '../ddo/MetaData'
import Account from '../ocean/Account'
import { noZeroX } from '../utils'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
import { DDO } from '../ddo/DDO'
import { ServiceType } from '../ddo/Service'
import { ComputeJob, Output } from '../ocean/OceanCompute'
const apiPath = '/api/v1/brizo/services'
/**
* Provides a interface with Brizo.
* Brizo is the technical component executed by the Publishers allowing to them to provide extended data services.
*/
export class Brizo extends Instantiable {
private get url() {
return this.config.brizoUri
}
constructor(config: InstantiableConfig) {
super()
this.setInstanceConfig(config)
}
public async getVersionInfo() {
return (await this.ocean.utils.fetch.get(this.url)).json()
}
public getPurchaseEndpoint() {
return `${this.url}${apiPath}/access/initialize`
}
public getConsumeEndpoint() {
return `${this.url}${apiPath}/consume`
}
public getEncryptEndpoint() {
return `${this.url}${apiPath}/publish`
}
public getComputeEndpoint() {
return `${this.url}${apiPath}/compute`
}
public async getEndpointFromAgreement(
type: ServiceType,
agreementId: string
): Promise<string> {
const { assets, keeper } = this.ocean
const { did } = await keeper.agreementStoreManager.getAgreement(agreementId)
const ddo: DDO = await assets.resolve(did)
const { serviceEndpoint } = ddo.findServiceByType(type)
return serviceEndpoint
}
public async initializeServiceAgreement(
did: string,
serviceAgreementId: string,
serviceIndex: number,
signature: string,
consumerAddress: string
): Promise<any> {
const args = {
did,
serviceAgreementId,
serviceIndex,
signature,
consumerAddress
}
try {
return await this.ocean.utils.fetch.post(
this.getPurchaseEndpoint(),
decodeURI(JSON.stringify(args))
)
} catch (e) {
this.logger.error(e)
throw new Error('HTTP request failed')
}
}
public async consumeService(
agreementId: string,
serviceEndpoint: string,
account: Account,
files: File[],
destination: string,
index: number = -1
): Promise<string> {
const signature = await this.createSignature(account, agreementId)
const filesPromises = files
.filter((_, i) => index === -1 || i === index)
.map(async ({ index: i }) => {
let consumeUrl = serviceEndpoint
consumeUrl += `?index=${i}`
consumeUrl += `&serviceAgreementId=${noZeroX(agreementId)}`
consumeUrl += `&consumerAddress=${account.getId()}`
consumeUrl += `&signature=${signature}`
try {
await this.ocean.utils.fetch.downloadFile(consumeUrl, destination, i)
} catch (e) {
this.logger.error('Error consuming assets')
this.logger.error(e)
throw e
}
})
await Promise.all(filesPromises)
return destination
}
public async compute(
method: string,
serviceAgreementId: string,
consumerAccount: Account,
algorithmDid?: string,
algorithmMeta?: MetaDataAlgorithm,
jobId?: string,
output?: Output
): Promise<ComputeJob | ComputeJob[]> {
const address = consumerAccount.getId()
let signatureMessage = address
signatureMessage += jobId || ''
signatureMessage += (serviceAgreementId && `${noZeroX(serviceAgreementId)}`) || ''
const signature = await this.createHashSignature(
consumerAccount,
signatureMessage
)
const serviceEndpoint = serviceAgreementId
? await this.getEndpointFromAgreement('compute', serviceAgreementId)
: this.getComputeEndpoint()
if (!serviceEndpoint) {
throw new Error(
'Computing on asset failed, service definition is missing the `serviceEndpoint`.'
)
}
// construct Brizo URL
let url = serviceEndpoint
url += `?signature=${signature}`
url += `&consumerAddress=${address}`
url += `&serviceAgreementId=${noZeroX(serviceAgreementId)}`
url += (algorithmDid && `&algorithmDid=${algorithmDid}`) || ''
url +=
(algorithmMeta &&
`&algorithmMeta=${encodeURIComponent(JSON.stringify(algorithmMeta))}`) ||
''
url += (output && `&output=${JSON.stringify(output)}`) || ''
url += (jobId && `&jobId=${jobId}`) || ''
// switch fetch method
let fetch
switch (method) {
case 'post':
fetch = this.ocean.utils.fetch.post(url, '')
break
case 'put':
fetch = this.ocean.utils.fetch.put(url, '')
break
case 'delete':
fetch = this.ocean.utils.fetch.delete(url)
break
default:
fetch = this.ocean.utils.fetch.get(url)
break
}
const result = await fetch
.then((response: any) => {
if (response.ok) {
return response.json()
}
this.logger.error(
'Compute job failed:',
response.status,
response.statusText
)
return null
})
.catch((error: Error) => {
this.logger.error('Error with compute job')
this.logger.error(error.message)
throw error
})
return result
}
public async createSignature(account: Account, agreementId: string): Promise<string> {
const signature =
(await account.getToken()) ||
(await this.ocean.utils.signature.signText(
noZeroX(agreementId),
account.getId()
))
return signature
}
public async createHashSignature(account: Account, message: string): Promise<string> {
const signature =
(await account.getToken()) ||
(await this.ocean.utils.signature.signWithHash(message, account.getId()))
return signature
}
public async encrypt(
did: string,
signature: string,
document: any,
publisher: string
): Promise<string> {
const args = {
documentId: did,
signature,
document: JSON.stringify(document),
publisherAddress: publisher
}
try {
const response = await this.ocean.utils.fetch.post(
this.getEncryptEndpoint(),
decodeURI(JSON.stringify(args))
)
if (!response.ok) {
throw new Error('HTTP request failed')
}
return await response.text()
} catch (e) {
this.logger.error(e)
throw new Error('HTTP request failed')
}
}
}

View File

@ -0,0 +1,120 @@
import Account from '../ocean/Account'
/**
* Provides a interface to DataTokens
*/
export class DataTokens {
public factoryAddress: string
public factoryABI: object
public datatokensABI: object
public web3: any
/**
* Instantiate DataTokens (independently of Ocean).
* @param {String} factoryAddress
* @param {Object} factoryABI
* @param {Object} datatokensABI
* @param {Object} web3
*/
constructor(
factoryAddress: string,
factoryABI: object,
datatokensABI: object,
web3: any
) {
this.factoryAddress = factoryAddress
this.factoryABI = factoryABI
this.datatokensABI = datatokensABI
this.web3 = web3
}
/**
* Create new datatoken
* @param {String} metaDataStoreURI
* @param {Account} account
* @return {Promise<string>} datatoken address
*/
public async create(metaDataStoreURI: string, account: Account): Promise<string> {
// TO DO
}
/**
* Approve
* @param {String} dataTokenAddress
* @param {String} toAddress
* @param {Number} amount
* @param {Account} account
* @return {Promise<string>} transactionId
*/
public async approve(
dataTokenAddress: string,
toAddress: string,
amount: number,
account: Account
): Promise<string> {
// TO DO
}
/**
* Mint
* @param {String} dataTokenAddress
* @param {Account} account
* @param {Number} amount
* @param {String} toAddress - only if toAddress is different from the minter
* @return {Promise<string>} transactionId
*/
public async mint(
dataTokenAddress: string,
account: Account,
amount: number,
toAddress?: string
): Promise<string> {
// TO DO
}
/**
* Transfer from Account to Address
* @param {String} dataTokenAddress
* @param {String} toAddress
* @param {Number} amount
* @param {Account} account
* @return {Promise<string>} transactionId
*/
public async transfer(
dataTokenAddress: string,
toAddress: string,
amount: number,
account: Account
): Promise<string> {
// TO DO
}
/**
* Transfer from Address to Account (needs an Approve operation before)
* @param {String} dataTokenAddress
* @param {String} fromAddress
* @param {Number} amount
* @param {Account} account
* @return {Promise<string>} transactionId
*/
public async transferFrom(
dataTokenAddress: string,
fromAddress: string,
amount: number,
account: Account
): Promise<string> {
// TO DO
}
/**
* Get Account Balance for datatoken
* @param {String} dataTokenAddress
* @param {Account} account
* @return {Promise<string>} transactionId
*/
public async balance(dataTokenAddress: string, account: Account): Promise<number> {
// TO DO
}
}

View File

@ -0,0 +1 @@
export const OceanDataTokenABI = { }

View File

@ -0,0 +1 @@
export const OceanFactoryABI = {}

View File

@ -0,0 +1 @@
export const OceanFactoryABi = {}

View File

@ -0,0 +1,4 @@
export interface Authentication {
type: string
publicKey: string
}

160
src/ddo/DDO.ts Normal file
View File

@ -0,0 +1,160 @@
import Web3Provider from '../keeper/Web3Provider'
import { Ocean } from '../ocean/Ocean'
import { Authentication } from './Authentication'
import { Proof } from './Proof'
import { PublicKey } from './PublicKey'
import { Service, ServiceType } from './Service'
/**
* DID Descriptor Object.
* Contains all the data related to an asset.
*/
export class DDO {
/**
* Serializes the DDO object.
* @param {DDO} DDO to be serialized.
* @return {string} DDO serialized.
*/
public static serialize(ddo: DDO): string {
return JSON.stringify(ddo, null, 2)
}
/**
* Deserializes the DDO object.
* @param {DDO} DDO to be deserialized.
* @return {string} DDO deserialized.
*/
public static deserialize(ddoString: string): DDO {
const ddo = JSON.parse(ddoString)
return new DDO(ddo)
}
public '@context': string = 'https://w3id.org/did/v1'
/**
* DID, descentralized ID.
* @type {string}
*/
public id: string = null
public created: string
public updated: string
public dtAddress: string
public publicKey: PublicKey[] = []
public authentication: Authentication[] = []
public service: Service[] = []
public proof: Proof
public constructor(ddo: Partial<DDO> = {}) {
Object.assign(this, ddo, {
created:
(ddo && ddo.created) || new Date().toISOString().replace(/\.[0-9]{3}/, '')
})
}
public shortId(): string {
return this.id.replace('did:op:', '')
}
/**
* Finds a service of a DDO by index.
* @param {number} Service index.
* @return {Service} Service.
*/
public findServiceById<T extends ServiceType>(index: number): Service<T> {
if (isNaN(index)) {
throw new Error('index is not set')
}
const service = this.service.find(s => s.index === index)
return service as Service<T>
}
/**
* Finds a service of a DDO by type.
* @param {string} serviceType Service type.
* @return {Service} Service.
*/
public findServiceByType<T extends ServiceType>(serviceType: T): Service<T> {
if (!serviceType) {
throw new Error('serviceType not set')
}
return this.service.find(s => s.type === serviceType) as Service<T>
}
/**
* Generate the checksum using the current content.
* @return {string[]} DDO checksum.
*/
public getChecksum(): string {
const { attributes } = this.findServiceByType('metadata')
const { files, name, author, license } = attributes.main
const values = [
...(files || []).map(({ checksum }) => checksum).filter(_ => !!_),
name,
author,
license,
this.id
]
return Web3Provider.getWeb3()
.utils.sha3(values.join(''))
.replace(/^0x([a-f0-9]{64})(:!.+)?$/i, '0x$1')
}
/**
* Generates proof using personal sing.
* @param {Ocean} ocean Ocean instance.
* @param {string} publicKey Public key to be used on personal sign.
* @param {string} password Password if it's required.
* @return {Promise<Proof>} Proof object.
*/
public async generateProof(
ocean: Ocean,
publicKey: string,
password?: string
): Promise<Proof> {
const checksum = this.getChecksum()
const signature = await ocean.utils.signature.signText(
checksum,
publicKey,
password
)
return {
created: new Date().toISOString().replace(/\.[0-9]{3}/, ''),
creator: publicKey,
type: 'DDOIntegritySignature',
signatureValue: signature
}
}
/**
* Generates and adds a proof using personal sing on the DDO.
* @param {Ocean} ocean Ocean instance.
* @param {string} publicKey Public key to be used on personal sign.
* @param {string} password Password if it's required.
* @return {Promise<Proof>} Proof object.
*/
public async addProof(
ocean: Ocean,
publicKey: string,
password?: string
): Promise<void> {
if (this.proof) {
throw new Error('Proof already exists')
}
this.proof = await this.generateProof(ocean, publicKey, password)
}
}

303
src/ddo/MetaData.ts Normal file
View File

@ -0,0 +1,303 @@
export interface File {
/**
* File name.
* @type {string}
*/
name?: string
/**
* File URL.
* @type {string}
*/
url: string
/**
* File index.
* @type {number}
*/
index?: number
/**
* File format, if applicable.
* @type {string}
* @example "text/csv"
*/
contentType: string
/**
* File checksum.
* @type {[type]}
*/
checksum?: string
/**
* Checksum hash algorithm.
* @type {[type]}
*/
checksumType?: string
/**
* File content length.
* @type {[type]}
*/
contentLength?: string
/**
* Resource ID (depending on the source).
* @type {[type]}
*/
resourceId?: string
/**
* File encoding.
* @type {string}
* @example "UTF-8"
*/
encoding?: string
/**
* File compression (e.g. no, gzip, bzip2, etc).
* @type {string}
* @example "zip"
*/
compression?: string
}
export interface MetaDataAlgorithm {
url?: string
rawcode?: string
language?: string
format?: string
version?: string
container: {
entrypoint: string
image: string
tag: string
}
}
/**
* Main attributes of assets metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8
*/
export interface MetaDataMain {
/**
* Descriptive name of the Asset.
* @type {string}
* @example "UK Weather information 2011"
*/
name: string
/**
* Type of the Asset. Helps to filter by the type of asset ("dataset" or "algorithm").
* @type {string}
* @example "dataset"
*/
type: 'dataset' | 'algorithm'
/**
* The date on which the asset was created by the originator in
* ISO 8601 format, Coordinated Universal Time.
* @type {string}
* @example "2019-01-31T08:38:32Z"
*/
dateCreated: string
/**
* The date on which the asset DDO was registered into the metadata store.
* This value is created automatically by Aquarius upon registering,
* so this value can't be set.
* @type {string}
* @example "2019-01-31T08:38:32Z"
*/
datePublished?: string
/**
* Name of the entity generating this data (e.g. Tfl, Disney Corp, etc.).
* @type {string}
* @example "Met Office"
*/
author: string
/**
* Short name referencing the license of the asset (e.g. Public Domain, CC-0, CC-BY, No License Specified, etc. ).
* If it's not specified, the following value will be added: "No License Specified".
* @type {string}
* @example "CC-BY"
*/
license: string
/**
* Price of the asset in vodka (attoOCEAN). It must be an integer encoded as a string.
* @type {string}
* @example "1000000000000000000"
*/
price: string
/**
* Array of File objects including the encrypted file urls and some additional information.
* @type {File[]}
*/
files: File[]
/**
* Metadata used only for assets with type `algorithm`.
* @type {MetaDataAlgorithm}
*/
algorithm?: MetaDataAlgorithm
}
/**
* Curation attributes of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8
*/
export interface Curation {
/**
* Decimal value between 0 and 1. 0 is the default value.
* @type {number}
* @example 0.93
*/
rating: number
/**
* Number of votes. 0 is the default value.
* @type {number}
* @example 123
*/
numVotes: number
/**
* Schema applied to calculate the rating.
* @type {string}
* @example "Binary Voting"
*/
schema?: string
/**
* Flag unsuitable content.
* @type {boolean}
* @example true
*/
isListed?: boolean
}
/**
* Additional Information of Assets Metadata.
* @see https://github.com/oceanprotocol/OEPs/tree/master/8#additional-information
*/
export interface AdditionalInformation {
/**
* Details of what the resource is. For a dataset, this attribute
* explains what the data represents and what it can be used for.
* @type {string}
* @example "Weather information of UK including temperature and humidity"
*/
description?: string
/**
* The party holding the legal copyright. Empty by default.
* @type {string}
* @example "Met Office"
*/
copyrightHolder?: string
/**
* Example of the concept of this asset. This example is part
* of the metadata, not an external link.
* @type {string}
* @example "423432fsd,51.509865,-0.118092,2011-01-01T10:55:11+00:00,7.2,68"
*/
workExample?: string
/**
* Mapping of links for data samples, or links to find out more information.
* Links may be to either a URL or another Asset. We expect marketplaces to
* converge on agreements of typical formats for linked data: The Ocean Protocol
* itself does not mandate any specific formats as these requirements are likely
* to be domain-specific.
* @type {any[]}
* @example
* [
* {
* anotherSample: "http://data.ceda.ac.uk/badc/ukcp09/data/gridded-land-obs/gridded-land-obs-daily/",
* },
* {
* fieldsDescription: "http://data.ceda.ac.uk/badc/ukcp09/",
* },
* ]
*/
links?: { [name: string]: string }[]
/**
* The language of the content. Please use one of the language
* codes from the {@link https://tools.ietf.org/html/bcp47 IETF BCP 47 standard}.
* @type {String}
* @example "en"
*/
inLanguage?: string
/**
* Categories used to describe this content. Empty by default.
* @type {string[]}
* @example ["Economy", "Data Science"]
*/
categories?: string[]
/**
* Keywords or tags used to describe this content. Empty by default.
* @type {string[]}
* @example ["weather", "uk", "2011", "temperature", "humidity"]
*/
tags?: string[]
/**
* An indication of update latency - i.e. How often are updates expected (seldom,
* annually, quarterly, etc.), or is the resource static that is never expected
* to get updated.
* @type {string}
* @example "yearly"
*/
updateFrequency?: string
/**
* A link to machine-readable structured markup (such as ttl/json-ld/rdf)
* describing the dataset.
* @type {StructuredMarkup[]}
*/
structuredMarkup?: {
uri: string
mediaType: string
}[]
}
export interface MetaData {
main: MetaDataMain
encryptedFiles?: string
additionalInformation?: AdditionalInformation
curation?: Curation
}
/** Warning. serviceIndex is the index of a services in Services array, and not service.index attribute.
Let's assume that you have the following services array:
[
{"index":1,"type":"access","main":{"price":3}},
{"index":0,"type":"compute","main":{"price":1}}
]
then calling update with { serviceIndex:1,price:2} will update the 'compute' service, and not the access one
**/
export interface ServicePrices {
serviceIndex: number
price: string
}
export interface EditableMetaDataLinks {
name: string
url: string
type: string
}
export interface EditableMetaData {
description?: string
title?: string
links?: EditableMetaDataLinks[]
servicePrices?: ServicePrices[]
}

6
src/ddo/Proof.ts Normal file
View File

@ -0,0 +1,6 @@
export interface Proof {
type: string
created: string
creator: string
signatureValue: string
}

32
src/ddo/PublicKey.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Public key data.
*/
export interface PublicKey {
/**
* ID of the key.
* @type {string}
* @example "did:op:123456789abcdefghi#keys-1"
*/
id: string
/**
* Type of key.
* @type {string}
*/
type:
| 'Ed25519VerificationKey2018'
| 'RsaVerificationKey2018'
| 'EdDsaSAPublicKeySecp256k1'
| 'EthereumECDSAKey'
/**
* Key owner.
* @type {string}
* @example "did:op:123456789abcdefghi"
*/
owner: string
publicKeyPem?: string
publicKeyBase58?: string
publicKeyHex?: string
}

95
src/ddo/Service.ts Normal file
View File

@ -0,0 +1,95 @@
import { MetaData } from './MetaData'
export type ServiceType = 'authorization' | 'metadata' | 'access' | 'compute'
export interface ServiceCommon {
type: ServiceType
index: number
serviceEndpoint?: string
attributes: ServiceCommonAttributes
}
export interface ServiceCommonAttributes {
main: { [key: string]: any }
additionalInformation?: { [key: string]: any }
}
export interface ServiceAccessAttributes extends ServiceCommonAttributes {
main: {
creator: string
name: string
datePublished: string
dtCost: number
timeout: number
}
}
export interface ServiceComputePrivacy {
allowRawAlgorithm: boolean
allowNetworkAccess: boolean
trustedAlgorithms: string[]
}
export interface ServiceComputeAttributes extends ServiceCommonAttributes {
main: {
creator: string
datePublished: string
price: string
timeout: number
provider?: ServiceComputeProvider
name: string
privacy?: ServiceComputePrivacy
}
}
export interface ServiceComputeProvider {
type: string
description: string
environment: {
cluster: {
type: string
url: string
}
supportedContainers: {
image: string
tag: string
checksum: string
}[]
supportedServers: {
serverId: string
serverType: string
price: string
cpu: string
gpu: string
memory: string
disk: string
maxExecutionTime: number
}[]
}
}
export interface ServiceMetadata extends ServiceCommon {
type: 'metadata'
attributes: MetaData
}
export interface ServiceAccess extends ServiceCommon {
type: 'access'
templateId?: string
attributes: ServiceAccessAttributes
}
export interface ServiceCompute extends ServiceCommon {
type: 'compute'
templateId?: string
attributes: ServiceComputeAttributes
}
export type Service<T extends ServiceType | 'default' = 'default'> = T extends 'metadata'
? ServiceMetadata
: T extends 'access'
? ServiceAccess
: T extends 'compute'
? ServiceCompute
: T extends 'default'
? ServiceCommon
: ServiceCommon

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './lib'

25
src/lib.ts Normal file
View File

@ -0,0 +1,25 @@
import Config from './models/Config'
import Account from './ocean/Account'
import DID from './ocean/DID'
import { Ocean } from './ocean/Ocean'
import { LoggerInstance as Logger } from './utils/Logger'
import { Aquarius } from './aquarius/Aquarius'
import { DataTokens } from './datatokens/Datatokens'
import * as utils from './utils'
// Exports
export * from './ddo/DDO'
export * from './ddo/MetaData'
export { CreateProgressStep } from './ocean/OceanAssets'
export { ComputeJob, ComputeJobStatus } from './ocean/OceanCompute'
export { OrderProgressStep } from './ocean/utils/ServiceUtils'
export {
OceanPlatformTechStatus,
OceanPlatformTech,
OceanPlatformKeeperTech,
OceanPlatformVersions
} from './ocean/OceanVersions'
export { Ocean, Account, Config, DID, Logger, Aquarius, DataTokens, utils }

View File

@ -0,0 +1,9 @@
enum AccessStatus {
Requested,
Committed,
Delivered,
Verified,
Revoked
}
export default AccessStatus

5
src/models/Balance.ts Normal file
View File

@ -0,0 +1,5 @@
export default class Balance {
public eth: number
public ocn: number
}

65
src/models/Config.ts Normal file
View File

@ -0,0 +1,65 @@
import { LogLevel } from '../utils/Logger'
export { LogLevel } from '../utils/Logger'
export class Config {
/**
* Aquarius URL.
* @type {string}
*/
public aquariusUri: string
/**
* Brizo URL.
* @type {string}
*/
public brizoUri: string
/**
* Web3 Provider.
* @type {any}
*/
public web3Provider: any
/**
* Factory address
* @type {string}
*/
public factoryAddress: string
/**
* Factory ABI
* @type {string}
*/
public factoryABI: object
/**
* datatokens ABI
* @type {string}
*/
public datatokensABI: object
/**
* Log level.
* @type {boolean | LogLevel}
*/
public verbose?: boolean | LogLevel
/**
* Message shown when the user creates its own token.
* @type {string}
*/
public authMessage?: string
/**
* Token expiration time in ms.
* @type {number}
*/
public authTokenExpiration?: number
// Parity config
public parityUri?: string
public threshold?: number
}
export default Config

5
src/models/InputType.ts Normal file
View File

@ -0,0 +1,5 @@
export default class InputType {
public name: string
public type: string
}

View File

@ -0,0 +1,13 @@
import InputType from './InputType'
export default class MethodReflection {
public contractName: string
public methodName: string
public address: string
public signature: string
public inputs: InputType[]
}

5
src/models/ValuePair.ts Normal file
View File

@ -0,0 +1,5 @@
export default class ValuePair {
public type: string
public value: any
}

8
src/models/ValueType.ts Normal file
View File

@ -0,0 +1,8 @@
enum ValueType {
DID, // DID string e.g. 'did:op:xxx'
DIDRef, // hash of DID same as in parameter (bytes32 _did) in text 0x0123abc.. or 0123abc..
URL, // URL string e.g. 'http(s)://xx'
DDO // DDO string in JSON e.g. '{ "id": "did:op:xxx"...
}
export default ValueType

126
src/ocean/Account.ts Normal file
View File

@ -0,0 +1,126 @@
import BigNumber from 'bignumber.js'
import Balance from '../models/Balance'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
/**
* Account information.
*/
export default class Account extends Instantiable {
private password?: string
private token?: string
constructor(private id: string = '0x0', config?: InstantiableConfig) {
super()
if (config) {
this.setInstanceConfig(config)
}
}
public getId() {
return this.id
}
public setId(id) {
this.id = id
}
/**
* Set account password.
* @param {string} password Password for account.
*/
public setPassword(password: string): void {
this.password = password
}
/**
* Returns account password.
* @return {string} Account password.
*/
public getPassword(): string {
return this.password
}
/**
* Set account token.
* @param {string} token Token for account.
*/
public setToken(token: string): void {
this.token = token
}
/**
* Returns account token.
* @return {Promise<string>} Account token.
*/
public async getToken(): Promise<string> {
return this.token || this.ocean.auth.restore(this)
}
/**
* Returns if account token is stored.
* @return {Promise<boolean>} Is stored.
*/
public isTokenStored(): Promise<boolean> {
return this.ocean.auth.isStored(this)
}
/**
* Authenticate the account.
*/
public authenticate() {
return this.ocean.auth.store(this)
}
/**
* Balance of Ocean Token.
* @return {Promise<number>}
*/
public async getOceanBalance(): Promise<number> {
const { token } = this.ocean.keeper
return (await token.balanceOf(this.id)) / 10 ** (await token.decimals())
}
/**
* Balance of Ether.
* @return {Promise<number>}
*/
public async getEtherBalance(): Promise<number> {
return this.web3.eth
.getBalance(this.id, 'latest')
.then((balance: string): number => {
return new BigNumber(balance).toNumber()
})
}
/**
* Balances of Ether and Ocean Token.
* @return {Promise<Balance>}
*/
public async getBalance(): Promise<Balance> {
return {
eth: await this.getEtherBalance(),
ocn: await this.getOceanBalance()
}
}
/**
* Request Ocean Tokens.
* @param {number} amount Tokens to be requested.
* @return {Promise<number>}
*/
public async requestTokens(amount: number | string): Promise<string> {
amount = String(amount)
if (!this.ocean.keeper.dispenser) {
throw new Error('Dispenser not available on this network.')
}
try {
await this.ocean.keeper.dispenser.requestTokens(amount, this.id)
} catch (e) {
this.logger.error(e)
throw new Error('Error requesting tokens')
}
return amount
}
}

65
src/ocean/DID.ts Normal file
View File

@ -0,0 +1,65 @@
import { generateId } from '../utils/GeneratorHelpers'
const prefix = 'did:op:'
/**
* Decentralized ID.
*/
export default class DID {
/**
* Parses a DID from a string.
* @param {string} didString DID in string.
* @return {DID}
*/
public static parse(didString: string | DID): DID {
if (didString instanceof DID) {
didString = didString.getDid()
}
let did: DID
const didMatch = didString.match(/^did:op:([a-f0-9]{64})$/i)
if (didMatch) {
did = new DID(didMatch[1])
}
if (!did) {
throw new Error(`Parsing DID failed, ${didString}`)
}
return did
}
/**
* Returns a new DID.
* @return {DID}
*/
public static generate(): DID {
return new DID(generateId())
}
/**
* ID.
* @type {string}
*/
private id: string
private constructor(id: string) {
this.id = id
}
/**
* Returns the DID.
* @return {string}
*/
public getDid(): string {
return `${prefix}${this.id}`
}
/**
* Returns the ID.
* @return {string}
*/
public getId(): string {
return this.id
}
}

127
src/ocean/Ocean.ts Normal file
View File

@ -0,0 +1,127 @@
import { OceanAccounts } from './OceanAccounts'
import { OceanAssets } from './OceanAssets'
import { OceanAuth } from './OceanAuth'
import { OceanCompute } from './OceanCompute'
import { OceanTokens } from './OceanTokens'
import { OceanVersions } from './OceanVersions'
import { OceanUtils } from './utils/OceanUtils'
import { Aquarius } from '../aquarius/Aquarius'
import { Brizo } from '../brizo/Brizo'
import { Config } from '../models/Config'
import {
Instantiable,
generateIntantiableConfigFromConfig
} from '../Instantiable.abstract'
import { DataTokens } from '../lib'
/**
* Main interface for Ocean Protocol.
*/
export class Ocean extends Instantiable {
/**
* Returns the instance of Ocean.
* @param {Config} config Ocean instance configuration.
* @return {Promise<Ocean>}
*/
public static async getInstance(config: Config): Promise<Ocean> {
const instance = new Ocean()
const instanceConfig = {
...generateIntantiableConfigFromConfig(config),
ocean: instance
}
instance.setInstanceConfig(instanceConfig)
instance.utils = await OceanUtils.getInstance(instanceConfig)
instance.brizo = new Brizo(instanceConfig)
instance.aquarius = new Aquarius(
instanceConfig.config.aquariusUri,
instanceConfig.logger
)
instance.accounts = await OceanAccounts.getInstance(instanceConfig)
instance.auth = await OceanAuth.getInstance(instanceConfig)
instance.assets = await OceanAssets.getInstance(instanceConfig)
instance.compute = await OceanCompute.getInstance(instanceConfig)
instance.datatokens = new DataTokens(
instanceConfig.config.factoryAddress,
instanceConfig.config.factoryABI,
instanceConfig.config.datatokensABI,
instanceConfig.config.web3Provider
)
instance.tokens = await OceanTokens.getInstance(instanceConfig)
instance.versions = await OceanVersions.getInstance(instanceConfig)
return instance
}
/**
* Brizo instance.
* @type {Brizo}
*/
public brizo: Brizo
/**
* Aquarius instance.
* @type {Aquarius}
*/
public aquarius: Aquarius
/**
* Ocean account submodule
* @type {OceanAccounts}
*/
public accounts: OceanAccounts
/**
* Ocean auth submodule
* @type {OceanAuth}
*/
public auth: OceanAuth
/**
* Ocean assets submodule
* @type {OceanAssets}
*/
public assets: OceanAssets
/**
* Ocean compute submodule
* @type {OceanCompute}
*/
public compute: OceanCompute
/**
* Ocean secretStore submodule
* @type {OceanSecretStore}
*/
public datatokens: DataTokens
/**
* Ocean tokens submodule
* @type {OceanTokens}
*/
public tokens: OceanTokens
/**
* Ocean versions submodule
* @type {OceanVersions}
*/
public versions: OceanVersions
/**
* Ocean utils submodule
* @type {OceanUtils}
*/
public utils: OceanUtils
private constructor() {
super()
}
}

View File

@ -0,0 +1,57 @@
import Balance from '../models/Balance'
import Account from './Account'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
/**
* Account submodule of Ocean Protocol.
*/
export class OceanAccounts extends Instantiable {
/**
* Returns the instance of OceanAccounts.
* @return {Promise<OceanAccounts>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanAccounts> {
const instance = new OceanAccounts()
instance.setInstanceConfig(config)
return instance
}
/**
* Returns the list of accounts.
* @return {Promise<Account[]>}
*/
public async list(): Promise<Account[]> {
// retrieve eth accounts
const ethAccounts: string[] = await this.web3.eth.getAccounts()
const accountPromises = ethAccounts.map(
address => new Account(address, this.instanceConfig)
)
return Promise.all(accountPromises)
}
/**
* Return account balance.
* @param {Account} account Account instance.
* @return {Promise<Balance>} Ether and Ocean Token balance.
*/
public balance(account: Account): Promise<Balance> {
return account.getBalance()
}
/**
* Request tokens for an account.
* @param {Account} account Account instance.
* @param {number} amount Token amount.
* @return {Promise<boolean>} Success.
*/
public async requestTokens(account: Account, amount: number): Promise<boolean> {
try {
await account.requestTokens(amount)
return true
} catch (e) {
return false
}
}
}

600
src/ocean/OceanAssets.ts Normal file
View File

@ -0,0 +1,600 @@
import { TransactionReceipt } from 'web3-core'
import { SearchQuery } from '../aquarius/Aquarius'
import { DDO } from '../ddo/DDO'
import { MetaData, EditableMetaData } from '../ddo/MetaData'
import { Service, ServiceAccess, ServiceComputePrivacy } from '../ddo/Service'
import Account from './Account'
import DID from './DID'
import { fillConditionsWithDDO, SubscribablePromise, didZeroX } from '../utils'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
import { OrderProgressStep } from './utils/ServiceUtils'
export enum CreateProgressStep {
EncryptingFiles,
FilesEncrypted,
GeneratingProof,
ProofGenerated,
RegisteringDid,
DidRegistred,
StoringDdo,
DdoStored
}
/**
* Assets submodule of Ocean Protocol.
*/
export class OceanAssets extends Instantiable {
/**
* Returns the instance of OceanAssets.
* @return {Promise<OceanAssets>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanAssets> {
const instance = new OceanAssets()
instance.setInstanceConfig(config)
return instance
}
/**
* Returns a DDO by DID.
* @param {string} did Decentralized ID.
* @return {Promise<DDO>}
*/
public async resolve(did: string): Promise<DDO> {
const {
serviceEndpoint
} = await this.ocean.keeper.didRegistry.getAttributesByDid(did)
return this.ocean.aquarius.retrieveDDOByUrl(serviceEndpoint)
}
/**
* Creates a new DDO.
* @param {MetaData} metadata DDO metadata.
* @param {Account} publisher Publisher account.
* @param {list} services list of Service description documents
* @return {Promise<DDO>}
*/
public create(
metadata: MetaData,
publisher: Account,
services: Service[] = []
): SubscribablePromise<CreateProgressStep, DDO> {
this.logger.log('Creating asset')
return new SubscribablePromise(async observer => {
const { secretStoreUri } = this.config
const { didRegistry, templates } = this.ocean.keeper
const did: DID = DID.generate()
this.logger.log('Encrypting files')
observer.next(CreateProgressStep.EncryptingFiles)
const encryptedFiles = await this.ocean.secretStore.encrypt(
did.getId(),
metadata.main.files,
publisher
)
this.logger.log('Files encrypted')
observer.next(CreateProgressStep.FilesEncrypted)
// make sure that access service is defined if services is empty
if (services.length === 0) {
const accessService = await this.createAccessServiceAttributes(
publisher,
metadata.main.price,
metadata.main.datePublished
)
services.push(accessService)
}
const serviceAgreementTemplate = await templates.escrowAccessSecretStoreTemplate.getServiceAgreementTemplate()
const serviceEndpoint = this.ocean.aquarius.getServiceEndpoint(did)
let indexCount = 0
// create ddo itself
const ddo: DDO = new DDO({
id: did.getDid(),
authentication: [
{
type: 'RsaSignatureAuthentication2018',
publicKey: did.getDid()
}
],
publicKey: [
{
id: did.getDid(),
type: 'EthereumECDSAKey',
owner: publisher.getId()
}
],
service: [
{
type: 'authorization',
service: 'SecretStore',
serviceEndpoint: secretStoreUri,
attributes: { main: {} }
},
{
type: 'metadata',
serviceEndpoint,
attributes: {
// Default values
curation: {
rating: 0,
numVotes: 0
},
// Overwrites defaults
...metadata,
encryptedFiles,
// Cleaning not needed information
main: {
...metadata.main,
files: metadata.main.files.map((file, index) => ({
...file,
index,
url: undefined
}))
} as any
}
},
...services
]
// Remove duplications
.reverse()
.filter(
({ type }, i, list) =>
list.findIndex(({ type: t }) => t === type) === i
)
.reverse()
// Adding index
.map(_ => ({
..._,
index: indexCount++
})) as Service[]
})
// Overwrite initial service agreement conditions
serviceAgreementTemplate.conditions = fillConditionsWithDDO(
await templates.escrowAccessSecretStoreTemplate.getServiceAgreementTemplateConditions(),
ddo
)
for (const service of services) {
if (service.type === 'compute') {
service.attributes.serviceAgreementTemplate.conditions = fillConditionsWithDDO(
await templates.escrowComputeExecutionTemplate.getServiceAgreementTemplateConditions(),
ddo
)
}
}
this.logger.log('Generating proof')
observer.next(CreateProgressStep.GeneratingProof)
await ddo.addProof(this.ocean, publisher.getId(), publisher.getPassword())
this.logger.log('Proof generated')
observer.next(CreateProgressStep.ProofGenerated)
this.logger.log('Registering DID')
observer.next(CreateProgressStep.RegisteringDid)
await didRegistry.registerAttribute(
did.getId(),
ddo.getChecksum(),
[this.config.brizoAddress],
serviceEndpoint,
publisher.getId()
)
this.logger.log('DID registred')
observer.next(CreateProgressStep.DidRegistred)
this.logger.log('Storing DDO')
observer.next(CreateProgressStep.StoringDdo)
const storedDdo = await this.ocean.aquarius.storeDDO(ddo)
this.logger.log('DDO stored')
observer.next(CreateProgressStep.DdoStored)
return storedDdo
})
}
public async consume(
agreementId: string,
did: string,
consumerAccount: Account,
resultPath: string,
index?: number,
useSecretStore?: boolean
): Promise<string>
/* eslint-disable no-dupe-class-members */
public async consume(
agreementId: string,
did: string,
consumerAccount: Account,
resultPath?: undefined | null,
index?: number,
useSecretStore?: boolean
): Promise<true>
public async consume(
agreementId: string,
did: string,
consumerAccount: Account,
resultPath?: string,
index: number = -1,
useSecretStore?: boolean
): Promise<string | true> {
const ddo = await this.resolve(did)
const { attributes } = ddo.findServiceByType('metadata')
const accessService = ddo.findServiceByType('access')
const { files } = attributes.main
const { serviceEndpoint } = accessService
if (!serviceEndpoint) {
throw new Error(
'Consume asset failed, service definition is missing the `serviceEndpoint`.'
)
}
this.logger.log('Consuming files')
resultPath = resultPath
? `${resultPath}/datafile.${ddo.shortId()}.${accessService.index}/`
: undefined
if (!useSecretStore) {
await this.ocean.brizo.consumeService(
agreementId,
serviceEndpoint,
consumerAccount,
files,
resultPath,
index
)
} else {
const files = await this.ocean.secretStore.decrypt(
did,
ddo.findServiceByType('metadata').attributes.encryptedFiles,
consumerAccount,
ddo.findServiceByType('authorization').serviceEndpoint
)
const downloads = files
.filter(({ index: i }) => index === -1 || index === i)
.map(({ url, index: i }) =>
this.ocean.utils.fetch.downloadFile(url, resultPath, i)
)
await Promise.all(downloads)
}
this.logger.log('Files consumed')
if (resultPath) {
return resultPath
}
return true
}
/* eslint-enable no-dupe-class-members */
/**
* Start the purchase/order of an asset's service. Starts by signing the service agreement
* then sends the request to the publisher via the service endpoint (Brizo http service).
* @param {string} did Decentralized ID.
* @param {Account} consumerAccount Consumer account.
* @param {string} provider ethereum address of service provider (optional)
* @return {Promise<string>} Returns Agreement ID
*/
public order(
did: string,
consumerAccount: Account,
provider?: string
): SubscribablePromise<OrderProgressStep, string> {
return new SubscribablePromise(async observer => {
const { keeper, utils } = this.ocean
const ddo: DDO = await this.resolve(did)
const condition = keeper.conditions.accessSecretStoreCondition
const agreementId = await utils.services.order(
'access',
condition,
observer,
consumerAccount,
ddo,
provider
)
return agreementId
})
}
/**
* Returns the owner of an asset.
* @param {string} did Decentralized ID.
* @return {Promise<string>} Returns Account ID
*/
public async owner(did: string): Promise<string> {
const owner = await this.ocean.keeper.didRegistry.getDIDOwner(did)
return owner
}
/**
* Returns the creator of a asset.
* @param {string} did Decentralized ID.
* @return {Promise<string>} Returns eth address
*/
public async creator(did: string): Promise<string> {
const ddo = await this.resolve(did)
const checksum = ddo.getChecksum()
const { creator, signatureValue } = ddo.proof
const signer = await this.ocean.utils.signature.verifyText(
checksum,
signatureValue
)
if (signer.toLowerCase() !== creator.toLowerCase()) {
this.logger.warn(
`Owner of ${ddo.id} doesn't match. Expected ${creator} instead of ${signer}.`
)
}
return creator
}
/**
* Returns the assets of a owner.
* @param {string} owner Owner address.
* @return {Promise<string[]>} List of DIDs.
*/
public async ownerAssets(owner: string): Promise<string[]> {
return this.ocean.keeper.didRegistry.getAttributesByOwner(owner)
}
/**
* Transfer ownership of an asset.
* @param {string} did Asset DID.
* @param {string} newOwner Ethereum address of the new owner of the DID.
* @param {Account} account Ethereum account of original/old owner to sign and prove the ownership.
* @return {Promise<TransactionReceipt>} Returns Web3 transaction receipt.
*/
public async transferOwnership(
did: string,
newOwner: string,
account: Account
): Promise<TransactionReceipt> {
const oldOwner = await this.ocean.assets.owner(did)
const oldDdo = await this.ocean.aquarius.retrieveDDO(did)
// update owner on-chain
const txReceipt = this.ocean.keeper.didRegistry.transferDIDOwnership(
did,
newOwner,
oldOwner
)
// get a signature
const signature = await this.ocean.utils.signature.signForAquarius(
oldDdo.updated,
account
)
if (signature != null)
await this.ocean.aquarius.transferOwnership(
did,
newOwner,
oldDdo.updated,
signature
)
return txReceipt
}
/**
* Edit Metadata for a DDO.
* @param {did} string DID.
* @param {newMetadata} EditableMetaData Metadata fields & new values.
* @param {Account} account Ethereum account of owner to sign and prove the ownership.
* @return {Promise<string>}
*/
public async editMetadata(
did: string,
newMetadata: EditableMetaData,
account: Account
): Promise<string> {
const oldDdo = await this.ocean.aquarius.retrieveDDO(did)
// get a signature
const signature = await this.ocean.utils.signature.signForAquarius(
oldDdo.updated,
account
)
let result = null
if (signature != null)
result = await this.ocean.aquarius.editMetadata(
did,
newMetadata,
oldDdo.updated,
signature
)
return result
}
/**
* Update Compute Privacy
* @param {did} string DID.
* @param {number} serviceIndex Index of the compute service in the DDO
* @param {ServiceComputePrivacy} computePrivacy ComputePrivacy fields & new values.
* @param {Account} account Ethereum account of owner to sign and prove the ownership.
* @return {Promise<string>}
*/
public async updateComputePrivacy(
did: string,
serviceIndex: number,
computePrivacy: ServiceComputePrivacy,
account: Account
): Promise<string> {
const oldDdo = await this.ocean.aquarius.retrieveDDO(did)
// get a signature
const signature = await this.ocean.utils.signature.signForAquarius(
oldDdo.updated,
account
)
let result = null
if (signature != null)
result = await this.ocean.aquarius.updateComputePrivacy(
did,
serviceIndex,
computePrivacy.allowRawAlgorithm,
computePrivacy.allowNetworkAccess,
computePrivacy.trustedAlgorithms,
oldDdo.updated,
signature
)
return result
}
/**
* Retire a DDO (Delete)
* @param {did} string DID.
* @param {Account} account Ethereum account of owner to sign and prove the ownership.
* @return {Promise<string>}
*/
public async retire(did: string, account: Account): Promise<string> {
const oldDdo = await this.ocean.aquarius.retrieveDDO(did)
// get a signature
const signature = await this.ocean.utils.signature.signForAquarius(
oldDdo.updated,
account
)
let result = null
if (signature != null)
result = await this.ocean.aquarius.retire(did, oldDdo.updated, signature)
return result
}
/**
* Returns the assets of a consumer.
* @param {string} consumer Consumer address.
* @return {Promise<string[]>} List of DIDs.
*/
public async consumerAssets(consumer: string): Promise<string[]> {
return (
await this.ocean.keeper.conditions.accessSecretStoreCondition.getGrantedDidByConsumer(
consumer
)
).map(({ did }) => did)
}
/**
* Search over the assets using a query.
* @param {SearchQuery} query Query to filter the assets.
* @return {Promise<DDO[]>}
*/
public async query(query: SearchQuery) {
return this.ocean.aquarius.queryMetadata(query)
}
/**
* Search over the assets using a keyword.
* @param {SearchQuery} text Text to filter the assets.
* @return {Promise<DDO[]>}
*/
public async search(text: string) {
return this.ocean.aquarius.queryMetadataByText({
text,
page: 1,
offset: 100,
query: {
value: 1
},
sort: {
value: 1
}
} as SearchQuery)
}
public async createAccessServiceAttributes(
consumerAccount: Account,
price: string,
datePublished: string,
timeout: number = 0
): Promise<ServiceAccess> {
const { templates } = this.ocean.keeper
const serviceAgreementTemplate = await templates.escrowAccessSecretStoreTemplate.getServiceAgreementTemplate()
return {
type: 'access',
index: 2,
serviceEndpoint: this.ocean.brizo.getConsumeEndpoint(),
templateId: templates.escrowAccessSecretStoreTemplate.getId(),
attributes: {
main: {
creator: consumerAccount.getId(),
datePublished,
price,
timeout: timeout,
name: 'dataAssetAccessServiceAgreement'
},
serviceAgreementTemplate
}
}
}
/**
* Get FreeWhiteList for a DID
* @param {string} did Asset DID.
* @return {Promise<string[]>} List of addresses.
*/
public async getFreeWhiteList(did: string): Promise<string[]> {
const events = await this.ocean.keeper.didRegistry.getPastEvents(
'DIDPermissionGranted',
{
_did: didZeroX(did)
}
)
const list = events.map(({ returnValues }) => returnValues._grantee)
const filteredList = []
for (let index = 0; index < list.length; index++) {
const address = list[index]
const hasPermission = await this.ocean.keeper.didRegistry.getPermission(
did,
address
)
if (hasPermission) filteredList.push(address)
}
return filteredList
}
/**
* Add consumer to FreeWhiteList for a DID
* @param {string} did Asset DID.
* @param {string} consumer Ethereum address to add to the list.
* @param {Account} account Ethereum account of DID owner
* @return None
*/
public async addConsumerToFreeWhiteList(
did: string,
consumer: string,
account: Account
): Promise<TransactionReceipt> {
await this.ocean.keeper.didRegistry.grantPermission(
did,
consumer,
account.getId()
)
}
/**
* Remove consumer from DID's FreeWhiteList
* @param {string} did Asset DID.
* @param {string} consumer Ethereum address to add to the list.
* @param {Account} account Ethereum account of DID owner
* @return None
*/
public async removeConsumerFromFreeWhiteList(
did: string,
consumer: string,
account: Account
): Promise<TransactionReceipt> {
await this.ocean.keeper.didRegistry.revokePermission(
did,
consumer,
account.getId()
)
}
}

144
src/ocean/OceanAuth.ts Normal file
View File

@ -0,0 +1,144 @@
import Account from './Account'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
const defaultAuthMessage = 'Ocean Protocol Authentication'
const defaultExpirationTime = 30 * 24 * 60 * 60 * 1000 // 30 days
const localStorageKey = 'SquidTokens'
/**
* Tokens submodule of Ocean Protocol.
*/
export class OceanAuth extends Instantiable {
/**
* Returns the instance of OceanAuth.
* @return {Promise<OceanAuth>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanAuth> {
const instance = new OceanAuth()
instance.setInstanceConfig(config)
return instance
}
/**
* Returns a token for a account.
* @param {Account} account Signer account.
* @return {Promise<string>} Token
*/
public async get(account: Account): Promise<string> {
const time = Math.floor(Date.now() / 1000)
const message = `${this.getMessage()}\n${time}`
try {
const signature = await this.ocean.utils.signature.signText(
message,
account.getId(),
account.getPassword()
)
return `${signature}-${time}`
} catch {
throw new Error('User denied the signature.')
}
}
/**
* Returns the address of signed token.
* @param {string} token Token.
* @return {Promise<string>} Signer address.
*/
public async check(token: string): Promise<string> {
const expiration = this.getExpiration()
const [signature, timestamp] = token.split('-')
const message = `${this.getMessage()}\n${timestamp}`
if (+timestamp * 1000 + expiration < Date.now()) {
return `0x${'0'.repeat(40)}`
}
return this.web3.utils.toChecksumAddress(
await this.ocean.utils.signature.verifyText(message, signature)
)
}
/**
* Generates and stores the token for a account.
* @param {Account} account Signer account.
*/
public async store(account: Account) {
const token = await this.get(account)
this.writeToken(account.getId(), token)
}
/**
* Returns a stored token.
* @param {Account} account Signer account.
*/
public async restore(account: Account): Promise<string> {
let token
try {
token = this.readToken(account.getId())
} catch {
return
}
if (!token) {
return
}
const signer = await this.check(token)
if (signer.toLowerCase() !== account.getId().toLowerCase()) {
return
}
return token
}
/**
* Returns if the token is stored and is valid.
* @param {Account} account Signer account.
* @return {Promise<boolean>} Is stored and valid.
*/
public async isStored(account: Account): Promise<boolean> {
return !!(await this.restore(account))
}
private writeToken(address: string, token: string) {
const localStorage = this.getLocalStorage()
const storedTokens = localStorage.getItem(localStorageKey)
const tokens = storedTokens ? JSON.parse(storedTokens) : {}
localStorage.setItem(
localStorageKey,
JSON.stringify({
...tokens,
[address]: token
})
)
}
private readToken(address: string) {
const localStorage = this.getLocalStorage()
const storedTokens = localStorage.getItem(localStorageKey)
const tokens = storedTokens ? JSON.parse(storedTokens) : {}
return tokens[address]
}
private getLocalStorage() {
try {
localStorage.getItem('')
} catch {
throw new Error(
'LocalStorage is not supported. This feature is only available on browsers.'
)
}
return localStorage
}
private getMessage() {
return this.config.authMessage || defaultAuthMessage
}
private getExpiration() {
return this.config.authTokenExpiration || defaultExpirationTime
}
}

325
src/ocean/OceanCompute.ts Normal file
View File

@ -0,0 +1,325 @@
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
import { MetaData, MetaDataAlgorithm } from '../ddo/MetaData'
import Account from './Account'
import { DDO } from '../ddo/DDO'
import { SubscribablePromise } from '../utils'
import { OrderProgressStep } from './utils/ServiceUtils'
import { DID } from '../squid'
import { Service, ServiceCompute, ServiceComputePrivacy } from '../ddo/Service'
export const ComputeJobStatus = Object.freeze({
Started: 10,
ConfiguringVolumes: 20,
ProvisioningSuccess: 30,
DataProvisioningFailed: 31,
AlgorithmProvisioningFailed: 32,
RunningAlgorithm: 40,
FilteringResults: 50,
PublishingResult: 60,
Completed: 70,
Stopped: 80,
Deleted: 90
})
export interface Output {
publishAlgorithmLog?: boolean
publishOutput?: boolean
brizoAddress?: string
brizoUri?: string
metadata?: MetaData
metadataUri?: string
nodeUri?: string
owner?: string
secretStoreUri?: string
whitelist?: string[]
}
export interface ComputeJob {
owner: string
agreementId: string
jobId: string
dateCreated: string
dateFinished: string
status: number
statusText: string
algorithmLogUrl: string
resultsUrls: string[]
resultsDid?: DID
}
/**
* Compute submodule of Ocean Protocol.
*/
export class OceanCompute extends Instantiable {
/**
* Returns the instance of OceanCompute.
* @return {Promise<OceanCompute>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanCompute> {
const instance = new OceanCompute()
instance.setInstanceConfig(config)
return instance
}
/**
* Starts an order of a compute service that is defined in an asset's services.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} datasetDid The DID of the dataset asset (of type `dataset`) to run the algorithm on.
* @param {string} algorithmDid The DID of the algorithm asset (of type `algorithm`) to run on the asset.
* @param {MetaData} algorithmMeta Metadata about the algorithm being run if `algorithm` is being used. This is ignored when `algorithmDid` is specified.
* @return {Promise<string>} Returns the Service Agreement ID, representation of `bytes32` ID.
*
* 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 agreement,
* but brizo will not allow the compute, due to privacy settings of the ddo
*/
public order(
consumerAccount: Account,
datasetDid: string,
algorithmDid?: string,
algorithmMeta?: MetaDataAlgorithm,
provider?: string
): SubscribablePromise<OrderProgressStep, string> {
return new SubscribablePromise(async observer => {
const { assets, keeper, utils } = this.ocean
const ddo: DDO = await assets.resolve(datasetDid)
const service: Service = ddo.findServiceByType('compute')
if (!service) return null
if (algorithmMeta) {
// check if raw algo is allowed
if (service.attributes.main.privacy)
if (!service.attributes.main.privacy.allowRawAlgorithm) return null
}
if (algorithmDid) {
// check if did is in trusted list
if (service.attributes.main.privacy)
if (service.attributes.main.privacy.trustedAlgorithms)
if (service.attributes.main.privacy.trustedAlgorithms.length > 0)
if (
!service.attributes.main.privacy.trustedAlgorithms.includes(
algorithmDid
)
)
return null
}
const condition = keeper.conditions.computeExecutionCondition
const agreementId = await utils.services.order(
'compute',
condition,
observer,
consumerAccount,
ddo,
provider
)
return agreementId
})
}
/**
* Check the output object and add default properties if needed
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {Output} output Output section used for publishing the result.
* @return {Promise<Output>} Returns output object
*/
public checkOutput(consumerAccount: Account, output?: Output): Output {
const isDefault =
!output || (!output.publishAlgorithmLog && !output.publishOutput)
if (isDefault) {
return {
publishAlgorithmLog: false,
publishOutput: false
}
}
return {
publishAlgorithmLog: output.publishAlgorithmLog,
publishOutput: output.publishOutput,
brizoAddress: output.brizoAddress || this.config.brizoAddress,
brizoUri: output.brizoUri || this.config.brizoUri,
metadataUri: output.metadataUri || this.config.aquariusUri,
nodeUri: output.nodeUri || this.config.nodeUri,
owner: output.owner || consumerAccount.getId(),
secretStoreUri: output.secretStoreUri || this.config.secretStoreUri
}
}
/**
* Start the execution of a compute job.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} algorithmDid The DID of the algorithm asset (of type `algorithm`) to run on the asset.
* @param {MetaData} algorithmMeta Metadata about the algorithm being run if `algorithm` is being used. This is ignored when `algorithmDid` is specified.
* @param {Output} output Define algorithm output publishing. Publishing the result of a compute job is turned off by default.
* @return {Promise<ComputeJob>} Returns compute job ID under status.jobId
*/
public async start(
consumerAccount: Account,
agreementId: string,
algorithmDid?: string,
algorithmMeta?: MetaDataAlgorithm,
output?: Output
): Promise<ComputeJob> {
output = this.checkOutput(consumerAccount, output)
if (agreementId) {
const computeJobsList = await this.ocean.brizo.compute(
'post',
agreementId,
consumerAccount,
algorithmDid,
algorithmMeta,
undefined,
output
)
return computeJobsList[0] as ComputeJob
} else return null
}
/**
* Ends a running compute job.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} jobId The ID of the compute job to be stopped
* @return {Promise<ComputeJob>} Returns the new status of a job
*/
public async stop(
consumerAccount: Account,
agreementId: string,
jobId: string
): Promise<ComputeJob> {
const computeJobsList = await this.ocean.brizo.compute(
'put',
agreementId,
consumerAccount,
undefined,
undefined,
jobId
)
return computeJobsList[0] as ComputeJob
}
/**
* Deletes a compute job and all resources associated with the job. If job is running it will be stopped first.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} jobId The ID of the compute job to be stopped
* @return {Promise<ComputeJob>} Returns the new status of a job
*/
public async delete(
consumerAccount: Account,
agreementId: string,
jobId: string
): Promise<ComputeJob> {
const computeJobsList = await this.ocean.brizo.compute(
'delete',
agreementId,
consumerAccount,
undefined,
undefined,
jobId
)
return computeJobsList[0] as ComputeJob
}
/**
* Ends a running compute job and starts it again.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} jobId The ID of the compute job to be stopped
* @return {Promise<ComputeJob>} Returns the new status of a job
*/
public async restart(
consumerAccount: Account,
agreementId: string,
jobId: string
): Promise<ComputeJob> {
await this.stop(consumerAccount, agreementId, jobId)
const result = await this.start(consumerAccount, agreementId, jobId)
return result
}
/**
* Returns information about the status of all compute jobs, or a single compute job.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} jobId The ID of the compute job to be stopped
* @return {Promise<ComputeJob[]>} Returns the status
*/
public async status(
consumerAccount: Account,
agreementId?: string,
jobId?: string
): Promise<ComputeJob[]> {
const computeJobsList = await this.ocean.brizo.compute(
'get',
agreementId,
consumerAccount,
undefined,
undefined,
jobId
)
return computeJobsList as ComputeJob[]
}
/**
* Returns the final result of a specific compute job published as an asset.
* @param {Account} consumerAccount The account of the consumer ordering the service.
* @param {string} agreementId The service agreement ID.
* @param {string} jobId The ID of the compute job to be stopped.
* @return {Promise<ComputeJob>} Returns the DDO of the result asset.
*/
public async result(
consumerAccount: Account,
agreementId: string,
jobId: string
): Promise<ComputeJob> {
const computeJobsList = await this.ocean.brizo.compute(
'get',
agreementId,
consumerAccount,
undefined,
undefined,
jobId
)
return computeJobsList[0] as ComputeJob
}
public async createComputeServiceAttributes(
consumerAccount: Account,
price: string,
datePublished: string,
computePrivacy?: ServiceComputePrivacy,
timeout?: number
): Promise<ServiceCompute> {
const { templates } = this.ocean.keeper
const serviceAgreementTemplate = await templates.escrowComputeExecutionTemplate.getServiceAgreementTemplate()
const name = 'dataAssetComputingServiceAgreement'
if (!timeout) timeout = 3600
const service = {
type: 'compute',
index: 3,
serviceEndpoint: this.ocean.brizo.getComputeEndpoint(),
templateId: templates.escrowComputeExecutionTemplate.getId(),
attributes: {
main: {
creator: consumerAccount.getId(),
datePublished,
price,
privacy: {},
timeout: timeout,
name
},
serviceAgreementTemplate
}
}
if (computePrivacy) service.attributes.main.privacy = computePrivacy
return service as ServiceCompute
}
}

View File

@ -0,0 +1,94 @@
import SecretStore from '@oceanprotocol/secret-store-client'
import SecretStoreConfig from '@oceanprotocol/secret-store-client/dist/models/SecretStoreConfig'
import Account from './Account'
import { noDidPrefixed } from '../utils'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
/**
* SecretStore submodule of Ocean Protocol.
*/
export class OceanSecretStore extends Instantiable {
/**
* Returns the instance of OceanSecretStore.
* @return {Promise<OceanSecretStore>}
*/
public static async getInstance(
config: InstantiableConfig
): Promise<OceanSecretStore> {
const instance = new OceanSecretStore()
instance.setInstanceConfig(config)
return instance
}
/**
* Encrypt the given text and store the encryption keys using the `did`.
* The encrypted text can be decrypted using the same keys identified by the `did`.
* @param {string} did Decentralized ID.
* @param {string} content Content to be encrypted.
* @param {string} publisher Publisher account.
* @return {Promise<string>} Encrypted text.
*/
public async encrypt(
did: string,
document: any,
publisher: Account
): Promise<string> {
const signature =
(await publisher.getToken()) ||
(await this.ocean.utils.signature.signText(
noDidPrefixed(did),
publisher.getId(),
publisher.getPassword()
))
return this.ocean.brizo.encrypt(
noDidPrefixed(did),
signature,
document,
publisher.getId()
)
}
/**
* Decrypt an encrypted text using the stored encryption keys associated with the `did`.
* Decryption requires that the account owner has access permissions for this `did`
* @param {string} did Decentralized ID.
* @param {string} content Content to be encrypted.
* @param {string} consumer cONSUMER account.
* @return {Promise<string>} Encrypted text.
*/
public async decrypt(
did: string,
content: string,
consumer?: Account,
secretStoreUrl?: string
): Promise<any> {
return this.getSecretStoreByAccount(consumer, secretStoreUrl).decryptDocument(
noDidPrefixed(did),
content
)
}
private getSecretStoreByAccount(account: Account, secretStoreUrl?: string) {
const config: any = { ...this.config }
if (account) {
config.address = account.getId()
}
if (account && account.getPassword()) {
config.password = account.getPassword()
}
if (secretStoreUrl) {
config.secretStoreUri = secretStoreUrl
}
return this.getSecretStore(config)
}
private getSecretStore(config: SecretStoreConfig): SecretStore {
const { secretStoreUri, parityUri, password, address, threshold } = config
config = { secretStoreUri, parityUri, password, address, threshold }
return new SecretStore(config)
}
}

45
src/ocean/OceanTokens.ts Normal file
View File

@ -0,0 +1,45 @@
import Account from './Account'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
/**
* Tokens submodule of Ocean Protocol.
*/
export class OceanTokens extends Instantiable {
/**
* Returns the instance of OceanTokens.
* @return {Promise<OceanTokens>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanTokens> {
const instance = new OceanTokens()
instance.setInstanceConfig(config)
return instance
}
/**
* Transfer a number of tokens to the mentioned account.
* @param {string} to Address that receives the tokens.
* @param {number} amount Tokens to transfer.
* @param {Account} from Sender account address.
* @return {Promise<boolean>} Success,
*/
public async transfer(to: string, amount: number, from: Account): Promise<boolean> {
this.ocean.keeper.token.transfer(to, amount, from.getId())
return true
}
/**
* Request tokens for an account.
* @param {Account} account Account instance.
* @param {number} amount Token amount.
* @return {Promise<boolean>} Success.
*/
public async request(account: Account, amount: number): Promise<boolean> {
try {
await account.requestTokens(amount)
return true
} catch (e) {
return false
}
}
}

150
src/ocean/OceanVersions.ts Normal file
View File

@ -0,0 +1,150 @@
import * as keeperPackageJson from '@oceanprotocol/keeper-contracts/package.json'
import * as metadata from '../metadata.json'
import { Instantiable, InstantiableConfig } from '../Instantiable.abstract'
export enum OceanPlatformTechStatus {
Loading = 'Loading',
Unknown = 'Unknown',
Stopped = 'Stopped',
Working = 'Working'
}
export interface OceanPlatformTech {
name: string
version?: string
commit?: string
status: OceanPlatformTechStatus
}
export interface OceanPlatformKeeperTech extends OceanPlatformTech {
network?: string
keeperVersion?: string
contracts?: { [contractName: string]: string }
}
export interface OceanPlatformVersions {
squid: OceanPlatformKeeperTech
aquarius: OceanPlatformTech
brizo: OceanPlatformKeeperTech
status: {
ok: boolean
contracts: boolean
network: boolean
}
}
/**
* Versions submodule of Ocean Protocol.
*/
export class OceanVersions extends Instantiable {
/**
* Returns the instance of OceanVersions.
* @return {Promise<OceanVersions>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanVersions> {
const instance = new OceanVersions()
instance.setInstanceConfig(config)
return instance
}
public async get(): Promise<OceanPlatformVersions> {
const versions = {} as OceanPlatformVersions
// Squid
versions.squid = {
name: 'Squid-js',
version: metadata.version,
commit: metadata.commit,
status: OceanPlatformTechStatus.Working,
network: (await this.ocean.keeper.getNetworkName()).toLowerCase(),
keeperVersion: keeperPackageJson.version,
contracts: Object.values(await this.ocean.keeper.getAllInstances())
.filter(_ => !!_)
.reduce(
(acc, { contractName, address }) => ({
...acc,
[contractName]: address
}),
{}
)
}
// Brizo
try {
const {
contracts,
'keeper-version': keeperVersion,
network,
software: name,
version
} = await this.ocean.brizo.getVersionInfo()
versions.brizo = {
name,
status: OceanPlatformTechStatus.Working,
version,
contracts,
network,
keeperVersion: keeperVersion.replace(/^v/, '')
}
} catch {
versions.brizo = {
name: 'Brizo',
status: OceanPlatformTechStatus.Stopped
}
}
// Aquarius
try {
const { software: name, version } = await this.ocean.aquarius.getVersionInfo()
versions.aquarius = {
name,
status: OceanPlatformTechStatus.Working,
version
}
} catch {
versions.aquarius = {
name: 'Aquarius',
status: OceanPlatformTechStatus.Stopped
}
}
// Status
const techs: OceanPlatformKeeperTech[] = Object.values(versions as any)
const networks = techs
.map(({ network }) => network)
.filter(_ => !!_)
.reduce((acc, network) => ({ ...acc, [network]: true }), {})
let contractStatus = true
const contractList = techs.map(({ contracts }) => contracts).filter(_ => !!_)
Array.from(contractList.map(Object.keys))
.reduce((acc, _) => [...acc, ..._], [])
.filter((_, i, list) => list.indexOf(_) === i)
.forEach(name => {
let address
contractList
.map(_ => _[name])
.forEach(_ => {
if (!address) {
address = _
return
}
if (address !== _) {
this.logger.warn(`Error on contract ${name}`)
contractStatus = false
}
})
})
versions.status = {
ok: !techs.find(({ status }) => status !== OceanPlatformTechStatus.Working),
network: Object.keys(networks).length === 1,
contracts: contractStatus
}
return versions
}
}

View File

@ -0,0 +1,55 @@
import { Instantiable, InstantiableConfig } from '../../Instantiable.abstract'
import { ServiceUtils } from './ServiceUtils'
import { ServiceAgreement } from './ServiceAgreement'
import { SignatureUtils } from './SignatureUtils'
import { WebServiceConnector } from './WebServiceConnector'
/**
* Utils internal submodule of Ocean Protocol.
*/
export class OceanUtils extends Instantiable {
/**
* Returns the instance of OceanUtils.
* @return {Promise<OceanUtils>}
*/
public static async getInstance(config: InstantiableConfig): Promise<OceanUtils> {
const instance = new OceanUtils()
instance.setInstanceConfig(config)
instance.agreements = new ServiceAgreement(
config.ocean,
config.logger,
config.web3
)
instance.services = new ServiceUtils(config.ocean, config.logger)
instance.signature = new SignatureUtils(config.web3, config.logger)
instance.fetch = new WebServiceConnector(config.logger)
return instance
}
/**
* Agreement utils.
* @type {ServiceAgreement}
*/
public agreements: ServiceAgreement
/**
* Service utils.
* @type {ServiceUtils}
*/
public services: ServiceUtils
/**
* Signature utils.
* @type {SignatureUtils}
*/
public signature: SignatureUtils
/**
* Fetch utils.
* @type {WebServiceConnector}
*/
public fetch: WebServiceConnector
}

View File

@ -0,0 +1,104 @@
import { ServiceAgreementTemplateCondition } from '../../ddo/ServiceAgreementTemplate'
import { DDO } from '../../ddo/DDO'
import { ServiceAccess } from '../../ddo/Service'
import Account from '../Account'
import { zeroX, Logger } from '../../utils'
import { Ocean } from '../../squid'
import Web3 from 'web3'
export class ServiceAgreement {
private ocean: Ocean
private logger: Logger
private web3: Web3
constructor(ocean: Ocean, logger: Logger, web3: Web3) {
this.ocean = ocean
this.logger = logger
this.web3 = web3
}
public async signServiceAgreement(
ddo: DDO,
index: number,
serviceAgreementId: string,
agreementConditionsIds: string[],
consumer: Account
): Promise<string> {
const service = ddo.findServiceById<'access'>(index)
const timelockValues: number[] = this.getTimeValuesFromService(
service,
'timelock'
)
const timeoutValues: number[] = this.getTimeValuesFromService(service, 'timeout')
if (!service.templateId) {
throw new Error('TemplateId not found in DDO.')
}
const serviceAgreementHashSignature = await this.createHashSignature(
service.templateId,
serviceAgreementId,
agreementConditionsIds,
timelockValues,
timeoutValues,
consumer
)
this.logger.debug('SA hash signature:', serviceAgreementHashSignature)
return zeroX(serviceAgreementHashSignature)
}
public async createHashSignature(
templateId: string,
serviceAgreementId: string,
valueHashes: string[],
timelockValues: number[],
timeoutValues: number[],
consumer: Account
): Promise<string> {
const serviceAgreementHash = this.hashServiceAgreement(
templateId,
serviceAgreementId,
valueHashes,
timelockValues,
timeoutValues
)
const serviceAgreementHashSignature = await this.ocean.utils.signature.signText(
serviceAgreementHash,
consumer.getId(),
consumer.getPassword()
)
return serviceAgreementHashSignature
}
public hashServiceAgreement(
serviceAgreementTemplateId: string,
serviceAgreementId: string,
valueHashes: string[],
timelocks: number[],
timeouts: number[]
): string {
const args: any = [
{ type: 'bytes32', value: zeroX(serviceAgreementTemplateId) },
{ type: 'bytes32[]', value: valueHashes.map(zeroX) },
{ type: 'uint256[]', value: timelocks },
{ type: 'uint256[]', value: timeouts },
{ type: 'bytes32', value: zeroX(serviceAgreementId) }
]
return this.web3.utils.soliditySha3(...args)
}
private getTimeValuesFromService(
service: ServiceAccess,
type: 'timeout' | 'timelock'
): number[] {
const timeoutValues: number[] = service.attributes.serviceAgreementTemplate.conditions.map(
(condition: ServiceAgreementTemplateCondition) => condition[type]
)
return timeoutValues
}
}

View File

@ -0,0 +1,115 @@
import { DDO } from '../../ddo/DDO'
import Account from '../Account'
import { zeroX, Logger, generateId } from '../../utils'
import { Ocean } from '../../squid'
import { Condition } from '../../keeper/contracts/conditions'
import { ServiceType, Service } from '../../ddo/Service'
export enum OrderProgressStep {
CreatingAgreement,
AgreementInitialized,
LockingPayment,
LockedPayment
}
export class ServiceUtils {
private ocean: Ocean
private logger: Logger
constructor(ocean: Ocean, logger: Logger) {
this.ocean = ocean
this.logger = logger
}
public async order(
type: ServiceType,
condition: Condition,
observer: any,
consumerAccount: Account,
ddo: DDO,
provider?: string
): Promise<string> {
const { keeper, agreements } = this.ocean
const agreementId = zeroX(generateId())
const service: Service = ddo.findServiceByType(type)
const metadata = ddo.findServiceByType('metadata')
const templateName = service.attributes.serviceAgreementTemplate.contractName
const template = keeper.getTemplateByName(templateName)
// use price from compute service,
// otherwise always take the price from metadata
const price =
type === 'compute'
? service.attributes.main.price
: metadata.attributes.main.price
// eslint-disable-next-line no-async-promise-executor
const paymentFlow = new Promise(async (resolve, reject) => {
await template.getAgreementCreatedEvent(agreementId).once()
this.logger.log('Agreement initialized')
observer.next(OrderProgressStep.AgreementInitialized)
this.logger.log('Locking payment')
const serviceGranted = condition
.getConditionFulfilledEvent(agreementId)
.once()
observer.next(OrderProgressStep.LockingPayment)
const paid = await agreements.conditions.lockReward(
agreementId,
price,
consumerAccount
)
observer.next(OrderProgressStep.LockedPayment)
if (paid) {
this.logger.log('Payment was OK')
} else {
this.logger.error('Payment was KO')
this.logger.error('Agreement ID: ', agreementId)
this.logger.error('DID: ', ddo.id)
reject(new Error('Error on payment'))
}
await serviceGranted
this.logger.log(`Service ${type} granted`)
resolve()
})
observer.next(OrderProgressStep.CreatingAgreement)
this.logger.log('Creating agreement')
// Get provider from didRegistry if not given in arguments
let _provider = provider
if (!provider) {
const providers = await keeper.didRegistry.getDIDProviders(ddo.shortId())
if (providers) {
_provider = providers[0]
}
}
await agreements.create(
ddo.id,
agreementId,
service.index,
undefined,
consumerAccount,
_provider,
consumerAccount
)
this.logger.log('Agreement created')
try {
await paymentFlow
} catch (e) {
throw new Error(`Error paying the ${type} service.`)
}
return agreementId
}
}

View File

@ -0,0 +1,103 @@
import Web3 from 'web3'
import { Logger } from '../../utils'
import { Account } from '../../squid'
export class SignatureUtils {
private web3: Web3
private logger: Logger
constructor(web3: Web3, logger: Logger) {
this.web3 = web3
this.logger = logger
}
public async signText(
text: string,
publicKey: string,
password?: string
): Promise<string> {
const isMetaMask =
this.web3 &&
this.web3.currentProvider &&
(this.web3.currentProvider as any).isMetaMask
try {
return await this.web3.eth.personal.sign(text, publicKey, password)
} catch (e) {
if (isMetaMask) {
throw e
}
this.logger.warn('Error on personal sign.')
this.logger.warn(e)
try {
return await this.web3.eth.sign(text, publicKey)
} catch (e2) {
this.logger.error('Error on sign.')
this.logger.error(e2)
throw new Error('Error executing personal sign')
}
}
}
public async signWithHash(
text: string,
publicKey: string,
password?: string
): Promise<string> {
const hash = this.web3.utils.utf8ToHex(text)
const isMetaMask =
this.web3 &&
this.web3.currentProvider &&
(this.web3.currentProvider as any).isMetaMask
try {
return await this.web3.eth.personal.sign(hash, publicKey, password)
} catch (e) {
if (isMetaMask) {
throw e
}
this.logger.warn('Error on personal sign.')
this.logger.warn(e)
try {
return await this.web3.eth.sign(hash, publicKey)
} catch (e2) {
this.logger.error('Error on sign.')
this.logger.error(e2)
throw new Error('Error executing personal sign')
}
}
}
public async verifyText(text: string, signature: string): Promise<string> {
return this.web3.eth.personal.ecRecover(text, signature)
}
public async getHash(message: string): Promise<string> {
let hex = ''
for (let i = 0; i < message.length; i++) {
hex += '' + message.charCodeAt(i).toString(16)
}
const hexMessage = '0x' + hex
return hexMessage as string
}
public async signForAquarius(message: string, account: Account): Promise<string> {
const hash = await this.getHash(message)
const isMetaMask =
this.web3 &&
this.web3.currentProvider &&
(this.web3.currentProvider as any).isMetaMask
try {
return this.web3.eth.personal.sign(
hash,
account.getId(),
account.getPassword()
)
} catch (e) {
if (isMetaMask) {
throw e
}
this.logger.warn('Error on personal sign.')
this.logger.warn(e)
return null
}
}
}

View File

@ -0,0 +1,112 @@
import { BodyInit, RequestInit, Response } from 'node-fetch'
import fs from 'fs'
import { Logger } from '../../utils'
const fetch = require('node-fetch')
import save = require('save-file')
/**
* Provides a common interface to web services.
*/
export class WebServiceConnector {
public logger: Logger
constructor(logger: Logger) {
this.logger = logger
}
public post(url: string, payload: BodyInit): Promise<Response> {
return this.fetch(url, {
method: 'POST',
body: payload,
headers: {
'Content-type': 'application/json'
}
})
}
public get(url: string): Promise<Response> {
return this.fetch(url, {
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
}
public put(url: string, payload: BodyInit): Promise<Response> {
return this.fetch(url, {
method: 'PUT',
body: payload,
headers: {
'Content-type': 'application/json'
}
})
}
public delete(url: string, payload?: BodyInit): Promise<Response> {
if (payload != null) {
return this.fetch(url, {
method: 'DELETE',
body: payload,
headers: {
'Content-type': 'application/json'
}
})
} else {
return this.fetch(url, {
method: 'DELETE',
headers: {
'Content-type': 'application/json'
}
})
}
}
public async downloadFile(
url: string,
destination?: string,
index?: number
): Promise<string> {
const response = await this.get(url)
if (!response.ok) {
throw new Error('Response error.')
}
let filename: string
try {
filename = response.headers
.get('content-disposition')
.match(/attachment;filename=(.+)/)[1]
} catch {
try {
filename = url.split('/').pop()
} catch {
filename = `file${index}`
}
}
if (destination) {
// eslint-disable-next-line no-async-promise-executor
await new Promise(async (resolve, reject) => {
fs.mkdirSync(destination, { recursive: true })
const fileStream = fs.createWriteStream(`${destination}${filename}`)
response.body.pipe(fileStream)
response.body.on('error', reject)
fileStream.on('finish', resolve)
})
return destination
} else {
save(await response.arrayBuffer(), filename)
}
}
private async fetch(url: string, opts: RequestInit): Promise<Response> {
const result = await fetch(url, opts)
if (!result.ok) {
this.logger.error(`Error requesting [${opts.method}] ${url}`)
this.logger.error(`Response message: \n${await result.text()}`)
throw result
}
return result
}
}

View File

@ -0,0 +1,47 @@
import { LoggerInstance } from './Logger'
// Ox transformer
export const zeroX = (input: string) => zeroXTransformer(input, true)
export const noZeroX = (input: string) => zeroXTransformer(input, false)
export function zeroXTransformer(input: string = '', zeroOutput: boolean) {
const { valid, output } = inputMatch(
input,
/^(?:0x)*([a-f0-9]+)$/i,
'zeroXTransformer'
)
return (zeroOutput && valid ? '0x' : '') + output
}
// did:op: transformer
export const didPrefixed = (input: string) => didTransformer(input, true)
export const noDidPrefixed = (input: string) => didTransformer(input, false)
export function didTransformer(input: string = '', prefixOutput: boolean) {
const { valid, output } = inputMatch(
input,
/^(?:0x|did:op:)*([a-f0-9]{64})$/i,
'didTransformer'
)
return (prefixOutput && valid ? 'did:op:' : '') + output
}
// 0x + did:op: transformer
export const didZeroX = (input: string) => zeroX(didTransformer(input, false))
// Shared functions
function inputMatch(
input: string,
regexp: RegExp,
conversorName: string
): { valid: boolean; output: string } {
if (typeof input !== 'string') {
LoggerInstance.debug('Not input string:')
LoggerInstance.debug(input)
throw new Error(`[${conversorName}] Expected string, input type: ${typeof input}`)
}
const match = input.match(regexp)
if (!match) {
LoggerInstance.warn(`[${conversorName}] Input transformation failed.`)
return { valid: false, output: input }
}
return { valid: true, output: match[1] }
}

47
src/utils/DDOHelpers.ts Normal file
View File

@ -0,0 +1,47 @@
import { DDO } from '../ddo/DDO'
import {
ServiceAgreementTemplateCondition,
ServiceAgreementTemplateParameter
} from '../ddo/ServiceAgreementTemplate'
function fillParameterWithDDO(
parameter: ServiceAgreementTemplateParameter,
ddo: DDO
): ServiceAgreementTemplateParameter {
const getValue = name => {
switch (name) {
case 'amount':
case 'price':
return String(ddo.findServiceByType('metadata').attributes.main.price)
case 'assetId':
case 'documentId':
case 'documentKeyId':
return ddo.shortId()
case 'rewardAddress':
return ddo.publicKey[0].owner
}
return ''
}
const value = getValue(parameter.name.replace(/^_/, ''))
return { ...parameter, value }
}
/**
* Fill some static parameters that depends on the metadata.
* @param {ServiceAgreementTemplateCondition[]} conditions Conditions to fill.
* @param {DDO} ddo DDO related to this conditions.
* @return {ServiceAgreementTemplateCondition[]} Filled conditions.
*/
export function fillConditionsWithDDO(
conditions: ServiceAgreementTemplateCondition[],
ddo: DDO
): ServiceAgreementTemplateCondition[] {
return conditions.map(condition => ({
...condition,
parameters: condition.parameters.map(parameter => ({
...fillParameterWithDDO(parameter, ddo)
}))
}))
}

View File

@ -0,0 +1,9 @@
import { v4 } from 'uuid'
export function generateId(length = 64) {
let id = ''
while (id.length < length) {
id += v4().replace(/-/g, '')
}
return id.substr(0, length)
}

44
src/utils/Logger.ts Normal file
View File

@ -0,0 +1,44 @@
export enum LogLevel {
None = -1,
Error = 0,
Warn = 1,
Log = 2,
Verbose = 3
}
export class Logger {
constructor(private logLevel: LogLevel = LogLevel.Verbose) {}
public setLevel(logLevel: LogLevel) {
this.logLevel = logLevel
}
public bypass(...args: any[]) {
this.dispatch('log', -Infinity as any, ...args)
}
public debug(...args: any[]) {
this.dispatch('debug', LogLevel.Verbose, ...args)
}
public log(...args: any[]) {
this.dispatch('log', LogLevel.Log, ...args)
}
public warn(...args: any[]) {
this.dispatch('warn', LogLevel.Warn, ...args)
}
public error(...args: any[]) {
this.dispatch('error', LogLevel.Error, ...args)
}
private dispatch(verb: string, level: LogLevel, ...args: any[]) {
if (this.logLevel >= level) {
console[verb](...args) // eslint-disable-line
}
}
}
export const LoggerInstance = new Logger()
export default LoggerInstance

View File

@ -0,0 +1,15 @@
const zipObject = (keys = [], values = []) => {
return keys.reduce(
(acc, key, index) => ({
...acc,
[key]: values[index]
}),
{}
)
}
export const objectPromiseAll = async (obj: { [key: string]: Promise<any> }) => {
const keys = Object.keys(obj)
const result = await Promise.all(Object.values(obj))
return zipObject(keys, result)
}

View File

@ -0,0 +1,51 @@
export class SubscribableObserver<T, P> {
public completed: boolean = false
private subscriptions = new Set<{
onNext?: (next: T) => void
onComplete?: (complete: P) => void
onError?: (error: any) => void
}>()
public subscribe(
onNext?: (next: T) => void,
onComplete?: (complete: P) => void,
onError?: (error: any) => void
) {
if (this.completed) {
throw new Error('Observer completed.')
}
const subscription = { onNext, onComplete, onError }
this.subscriptions.add(subscription)
return {
unsubscribe: () => this.subscriptions.delete(subscription)
}
}
public next(next?: T): void {
this.emit('onNext', next)
}
public complete(resolve?: P): void {
this.emit('onComplete', resolve)
this.unsubscribe()
}
public error(error?: any): void {
this.emit('onError', error)
this.unsubscribe()
}
private emit(type: 'onNext' | 'onComplete' | 'onError', value: any) {
Array.from(this.subscriptions)
.map(subscription => subscription[type])
.filter((callback: any) => callback && typeof callback === 'function')
.forEach((callback: any) => callback(value))
}
private unsubscribe() {
this.completed = true
this.subscriptions.clear()
}
}

View File

@ -0,0 +1,56 @@
import { SubscribableObserver } from './SubscribableObserver'
export class SubscribablePromise<T extends any, P extends any> {
private observer = new SubscribableObserver<T, P>()
private promise = Object.assign(
new Promise<P>((resolve, reject) => {
setTimeout(() => {
this.observer.subscribe(undefined, resolve, reject)
}, 0)
}),
this
)
constructor(executor: (observer: SubscribableObserver<T, P>) => void | Promise<P>) {
// Defear
setTimeout(() => this.init(executor), 1)
}
public subscribe(onNext: (next: T) => void) {
return this.observer.subscribe(onNext)
}
public next(onNext: (next: T) => void) {
this.observer.subscribe(onNext)
return this
}
public then(onfulfilled?: (value: P) => any, onrejected?: (error: any) => any) {
return Object.assign(this.promise.then(onfulfilled, onrejected), this)
}
public catch(onrejected?: (error: any) => any) {
return Object.assign(this.promise.catch(onrejected), this)
}
public finally(onfinally?: () => any) {
return Object.assign(this.promise.finally(onfinally), this)
}
private init(executor: (observer: SubscribableObserver<T, P>) => void | Promise<P>) {
const execution = executor(this.observer)
Promise.resolve(execution as any)
.then(result => {
if (typeof (execution as any).then === 'function') {
this.observer.complete(result)
}
})
.catch(result => {
if (typeof (execution as any).then === 'function') {
this.observer.error(result)
}
})
}
}

7
src/utils/index.ts Normal file
View File

@ -0,0 +1,7 @@
export * from './PromiseResolver'
export * from './Logger'
export * from './ConversionTypeHelpers'
export * from './GeneratorHelpers'
export * from './DDOHelpers'
export * from './SubscribablePromise'
export * from './SubscribableObserver'

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"lib": ["es2017", "es6", "es7", "dom"],
"declaration": true,
"module": "commonjs",
"target": "es5",
"noImplicitAny": false,
"removeComments": true,
"experimentalDecorators": true,
"preserveConstEnums": true,
"outDir": "./dist/node/",
"rootDir": "./src/",
"sourceMap": true,
"typeRoots": ["node_modules/@types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}