601 lines
20 KiB
TypeScript
601 lines
20 KiB
TypeScript
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()
|
|
)
|
|
}
|
|
}
|