github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/kat/src/handlers/asset-instances.ts (about)

     1  // Copyright © 2021 Kaleido, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import Ajv from 'ajv';
    16  import { v4 as uuidV4 } from 'uuid';
    17  import * as apiGateway from '../clients/api-gateway';
    18  import * as app2app from '../clients/app2app';
    19  import * as database from '../clients/database';
    20  import * as docExchange from '../clients/doc-exchange';
    21  import * as ipfs from '../clients/ipfs';
    22  import { config } from '../lib/config';
    23  import { IAPIGatewayAsyncResponse, IAPIGatewaySyncResponse, IAssetInstance, IAssetInstancePropertySet, IAssetTradePrivateAssetInstancePush, IBatchRecord, IDBAssetInstance, IDBBlockchainData, IEventAssetInstanceBatchCreated, IEventAssetInstanceCreated, IEventAssetInstancePropertySet, IPendingAssetInstancePrivateContentDelivery, BatchRecordType } from '../lib/interfaces';
    24  import RequestError from '../lib/request-handlers';
    25  import * as utils from '../lib/utils';
    26  import { assetInstancesPinning } from './asset-instances-pinning';
    27  import * as assetTrade from './asset-trade';
    28  
    29  
    30  const log = utils.getLogger('handlers/asset-instances.ts');
    31  
    32  const ajv = new Ajv();
    33  
    34  export let pendingAssetInstancePrivateContentDeliveries: { [assetInstanceID: string]: IPendingAssetInstancePrivateContentDelivery } = {};
    35  
    36  export const handleGetAssetInstancesRequest = (assetDefinitionID: string, query: object, sort: object, skip: number, limit: number) => {
    37    return database.retrieveAssetInstances(assetDefinitionID, query, sort, skip, limit);
    38  };
    39  
    40  export const handleCountAssetInstancesRequest = async (assetDefinitionID: string, query: object) => {
    41    return { count: await database.countAssetInstances(assetDefinitionID, query) };
    42  };
    43  
    44  export const handleGetAssetInstanceRequest = async (assetDefinitionID: string, assetInstanceID: string, content: boolean) => {
    45    const assetInstance = await database.retrieveAssetInstanceByID(assetDefinitionID, assetInstanceID);
    46    if (assetInstance === null) {
    47      throw new RequestError('Asset instance not found', 404);
    48    }
    49    const assetDefinition = await database.retrieveAssetDefinitionByID(assetDefinitionID);
    50    if (assetDefinition === null) {
    51      throw new RequestError('Asset definition not found', 500);
    52    }
    53    if (content) {
    54      if (assetDefinition.contentSchema) {
    55        return assetInstance.content;
    56      } else {
    57        try {
    58          return await docExchange.downloadStream(utils.getUnstructuredFilePathInDocExchange(assetInstance.assetInstanceID));
    59        } catch (err) {
    60          if (err.response?.status === 404) {
    61            throw new RequestError('Asset instance content not present in off-chain storage', 404);
    62          } else {
    63            throw new RequestError(`Failed to obtain asset content from off-chain storage. ${err}`, 500);
    64          }
    65        }
    66      }
    67    }
    68    return assetInstance;
    69  };
    70  
    71  export const handleCreateStructuredAssetInstanceRequest = async (author: string, assetDefinitionID: string, description: Object | undefined, content: Object, isContentPrivate: boolean | undefined, participants: string[] | undefined, sync: boolean) => {
    72    let descriptionHash: string | undefined;
    73    let contentHash: string;
    74    const assetDefinition = await database.retrieveAssetDefinitionByID(assetDefinitionID);
    75    if (assetDefinition === null) {
    76      throw new RequestError('Unknown asset definition', 400);
    77    }
    78    if (assetDefinition.conflict === true) {
    79      throw new RequestError('Cannot instantiate assets of conflicted definition', 400);
    80    }
    81    // For ethereum, we need to make assert definition transaction is mined
    82    if (config.protocol === 'ethereum' && assetDefinition.transactionHash === undefined) {
    83      throw new RequestError('Asset definition transaction must be mined', 400);
    84    }
    85    if (!assetDefinition.contentSchema) {
    86      throw new RequestError('Unstructured asset instances must be created using multipart/form-data', 400);
    87    }
    88    if (assetDefinition.descriptionSchema) {
    89      if (!description) {
    90        throw new RequestError('Missing asset description', 400);
    91      }
    92      if (!ajv.validate(assetDefinition.descriptionSchema, description)) {
    93        throw new RequestError('Description does not conform to asset definition schema', 400);
    94      }
    95      descriptionHash = `0x${utils.getSha256(JSON.stringify(description))}`;
    96    }
    97    if (!ajv.validate(assetDefinition.contentSchema, content)) {
    98      throw new RequestError('Content does not conform to asset definition schema', 400);
    99    }
   100    if(isContentPrivate === undefined) {
   101      isContentPrivate = assetDefinition.isContentPrivate;
   102    }
   103    contentHash = `0x${utils.getSha256(JSON.stringify(content))}`;
   104    if (assetDefinition.isContentUnique && (await database.retrieveAssetInstanceByDefinitionIDAndContentHash(assetDefinition.assetDefinitionID, contentHash)) !== null) {
   105      throw new RequestError(`Asset instance content conflict`);
   106    }
   107    if (config.protocol === 'corda') {
   108      // validate participants are registered members
   109      if (participants !== undefined) {
   110        for (const participant of participants) {
   111          if (await database.retrieveMemberByAddress(participant) === null) {
   112            throw new RequestError('One or more participants are not registered', 400);
   113          }
   114        }
   115      } else {
   116        throw new RequestError('Missing asset participants', 400);
   117      }
   118    }
   119    const assetInstanceID = uuidV4();
   120    const timestamp = utils.getTimestamp();
   121    const assetInstance: IAssetInstance = {
   122      assetInstanceID,
   123      author,
   124      assetDefinitionID,
   125      descriptionHash,
   126      description,
   127      contentHash,
   128      content,
   129      isContentPrivate
   130    };
   131  
   132    let dbAssetInstance: IDBAssetInstance = assetInstance;
   133    dbAssetInstance.submitted = timestamp;
   134    if (config.protocol === 'corda') {
   135      dbAssetInstance.participants = participants;
   136    }
   137    // If there are public IPFS shared parts of this instance, we can batch it together with all other
   138    // assets we are publishing for performance. Reducing both the data we write to the blockchain, and
   139    // most importantly the number of IPFS transactions.
   140    // Curently we do batch only for ethereum
   141    if ((assetDefinition.descriptionSchema || !isContentPrivate) && config.protocol === 'ethereum') {
   142      dbAssetInstance.batchID = await assetInstancesPinning.pin(assetInstance);
   143      await database.upsertAssetInstance(dbAssetInstance);
   144      log.info(`Structured asset instance batch ${dbAssetInstance.batchID} saved in local database and pinned to blockchain`);
   145    } else {
   146      await database.upsertAssetInstance(dbAssetInstance);
   147      // One-for-one blockchain transactions to instances
   148      let apiGatewayResponse: IAPIGatewayAsyncResponse | IAPIGatewaySyncResponse;
   149      if (descriptionHash) {
   150        apiGatewayResponse = await apiGateway.createDescribedAssetInstance(assetInstanceID, assetDefinitionID, author, descriptionHash, contentHash, participants, sync);
   151      } else {
   152        apiGatewayResponse = await apiGateway.createAssetInstance(assetInstanceID, assetDefinitionID, author, contentHash, participants, sync);
   153      }
   154      log.info(`Structured asset instance ${assetInstanceID} saved in local database and pinning transaction submitted to the blockchain`);
   155      // dbAssetInstance.receipt = apiGatewayResponse.type === 'async' ? apiGatewayResponse.id : undefined;
   156      if(apiGatewayResponse.type === 'async') {
   157        await database.setAssetInstanceReceipt(assetDefinitionID, assetInstanceID, apiGatewayResponse.id);
   158        log.trace(`Structured asset instance ${assetInstanceID} published in the blockchain (gateway receipt=${apiGatewayResponse.id})`);
   159      }
   160    }
   161    return assetInstanceID;
   162  };
   163  
   164  export const handleCreateUnstructuredAssetInstanceRequest = async (author: string, assetDefinitionID: string, description: Object | undefined, content: NodeJS.ReadableStream, filename: string, isContentPrivate: boolean | undefined, participants: string[] | undefined, sync: boolean) => {
   165    let descriptionHash: string | undefined;
   166    let contentHash: string;
   167    const assetDefinition = await database.retrieveAssetDefinitionByID(assetDefinitionID);
   168    if (assetDefinition === null) {
   169      throw new RequestError('Unknown asset definition', 400);
   170    }
   171    if (assetDefinition.contentSchema) {
   172      throw new RequestError('Structured asset instances must be created using JSON', 400);
   173    }
   174    if (assetDefinition.descriptionSchema) {
   175      if (!ajv.validate(assetDefinition.descriptionSchema, description)) {
   176        throw new RequestError('Description does not conform to asset definition schema', 400);
   177      }
   178      descriptionHash = utils.ipfsHashToSha256(await ipfs.uploadString(JSON.stringify(description)));
   179    }
   180    if(isContentPrivate === undefined) {
   181      isContentPrivate = assetDefinition.isContentPrivate;
   182    }
   183    const assetInstanceID = uuidV4();
   184    if (assetDefinition.isContentPrivate) {
   185      contentHash = `0x${await docExchange.uploadStream(content, utils.getUnstructuredFilePathInDocExchange(assetInstanceID))}`;
   186    } else {
   187      contentHash = utils.ipfsHashToSha256(await ipfs.uploadString(JSON.stringify(content)));
   188    }
   189    if (assetDefinition.isContentUnique && (await database.retrieveAssetInstanceByDefinitionIDAndContentHash(assetDefinitionID, contentHash)) !== null) {
   190      throw new RequestError('Asset instance content conflict', 409);
   191    }
   192    if (config.protocol === 'corda') {
   193      // validate participants are registered
   194      if (participants) {
   195        for (const participant of participants) {
   196          if (await database.retrieveMemberByAddress(participant) === null) {
   197            throw new RequestError(`One or more participants are not registered`, 400);
   198          }
   199        }
   200      } else {
   201        throw new RequestError(`Missing asset participants`, 400);
   202      }
   203    }
   204    let apiGatewayResponse: IAPIGatewayAsyncResponse | IAPIGatewaySyncResponse;
   205    const timestamp = utils.getTimestamp();
   206    await database.upsertAssetInstance({
   207      assetInstanceID,
   208      author,
   209      assetDefinitionID,
   210      descriptionHash,
   211      description,
   212      contentHash,
   213      filename,
   214      isContentPrivate,
   215      participants,
   216      submitted: timestamp
   217    });
   218    if (descriptionHash) {
   219      apiGatewayResponse = await apiGateway.createDescribedAssetInstance(assetInstanceID, assetDefinitionID, author, descriptionHash, contentHash, participants, sync);
   220    } else {
   221      apiGatewayResponse = await apiGateway.createAssetInstance(assetInstanceID, assetDefinitionID, author, contentHash, participants, sync);
   222    }
   223    log.info(`Unstructured asset instance ${assetInstanceID} saved in local database and pinning transaction submitted to the blockchain`);
   224    if(apiGatewayResponse.type === 'async') {
   225      await database.setAssetInstanceReceipt(assetDefinitionID, assetInstanceID, apiGatewayResponse.id);
   226      log.trace(`Unstructured asset instance ${assetInstanceID} published in the blockchain (gateway receipt=${apiGatewayResponse.id})`);
   227    }
   228    return assetInstanceID;
   229  }
   230  
   231  export const handleSetAssetInstancePropertyRequest = async (assetDefinitionID: string, assetInstanceID: string, author: string, key: string, value: string, sync: boolean) => {
   232    const assetInstance = await database.retrieveAssetInstanceByID(assetDefinitionID, assetInstanceID);
   233    if (assetInstance === null) {
   234      throw new RequestError('Unknown asset instance', 400);
   235    }
   236    if (assetInstance.transactionHash === undefined) {
   237      throw new RequestError('Asset instance transaction must be mined', 400);
   238    }
   239    if (assetInstance.properties) {
   240      const authorMetadata = assetInstance.properties[author];
   241      if (authorMetadata) {
   242        const valueData = authorMetadata[key];
   243        if (valueData?.value === value && valueData.history !== undefined) {
   244          const keys = Object.keys(valueData.history);
   245          const lastConfirmedValue = valueData.history[keys[keys.length - 1]];
   246          if (lastConfirmedValue.value === value) {
   247            throw new RequestError('Property already set', 409);
   248          }
   249        }
   250      }
   251    }
   252    const submitted = utils.getTimestamp();  
   253    if (config.protocol === 'ethereum') {
   254      const property: IAssetInstancePropertySet = {
   255        assetDefinitionID,
   256        assetInstanceID,
   257        author,
   258        key,
   259        value,
   260      };
   261      const batchID = await assetInstancesPinning.pinProperty(property);
   262      await database.setSubmittedAssetInstanceProperty(assetDefinitionID, assetInstanceID, author, key, value, submitted, batchID);
   263      log.info(`Asset instance property ${key} (instance=${assetInstanceID}) set via batch`);
   264    } else {
   265      await database.setSubmittedAssetInstanceProperty(assetDefinitionID, assetInstanceID, author, key, value, submitted);
   266      log.info(`Asset instance property ${key} (instance=${assetInstanceID}) set in local database`);
   267      const apiGatewayResponse = await apiGateway.setAssetInstanceProperty(assetDefinitionID, assetInstanceID, author, key, value, assetInstance.participants, sync);
   268      if(apiGatewayResponse.type === 'async') {
   269        await database.setAssetInstancePropertyReceipt(assetDefinitionID, assetInstanceID, author, key, apiGatewayResponse.id);
   270      }  
   271      log.info(`Asset instance property ${key} (instance=${assetInstanceID}) pinning transaction submitted to blockchain`);
   272    }
   273  
   274  };
   275  
   276  export const handleAssetInstanceBatchCreatedEvent = async (event: IEventAssetInstanceBatchCreated, { blockNumber, transactionHash }: IDBBlockchainData) => {
   277  
   278    let batch = await database.retrieveBatchByHash(event.batchHash);
   279    if (!batch) {
   280      batch = await ipfs.downloadJSON(utils.sha256ToIPFSHash(event.batchHash));
   281    }
   282    if (!batch) {
   283      throw new Error('Unknown batch hash: ' + event.batchHash);
   284    }
   285  
   286    // Process each record within the batch, as if it is an individual event
   287    const records: IBatchRecord[] = batch.records || [];
   288    for (let record of records) {
   289      if (!record.recordType || record.recordType === BatchRecordType.assetInstance) {
   290        const recordEvent: IEventAssetInstanceCreated = {
   291          assetDefinitionID: '',
   292          assetInstanceID: '',
   293          author: record.author,
   294          contentHash: record.contentHash!,
   295          descriptionHash: record.descriptionHash!,
   296          timestamp: event.timestamp,
   297          isContentPrivate: record.isContentPrivate
   298        };
   299        try {
   300          await handleAssetInstanceCreatedEvent(recordEvent, { blockNumber, transactionHash }, record);
   301        } catch (err) {
   302          // We failed to process this record, but continue to attempt the other records in the batch
   303          log.error(`Record ${record.assetDefinitionID}/${record.assetInstanceID} in batch ${batch.batchID} with hash ${event.batchHash} failed`, err.stack);
   304        }  
   305      } else if (record.recordType === BatchRecordType.assetProperty) {
   306        try {
   307          const propertyEvent: IEventAssetInstancePropertySet = {
   308            assetDefinitionID: record.assetDefinitionID,
   309            assetInstanceID: record.assetInstanceID,
   310            author: record.author,
   311            key: record.key,
   312            value: record.value,
   313            timestamp: event.timestamp,
   314          };
   315          await handleSetAssetInstancePropertyEvent(propertyEvent, { blockNumber, transactionHash }, true);
   316        } catch (err) {
   317          // We failed to process this record, but continue to attempt the other records in the batch
   318          log.error(`Property ${record.assetDefinitionID}/${record.assetInstanceID}/${record.key} in batch ${batch.batchID} with hash ${event.batchHash} failed`, err.stack);
   319        }
   320      } else {
   321        log.error(`Batch record type '${record.recordType}' not known`, record);
   322      }
   323    }
   324  
   325    // Write the batch itself to our local database
   326    await database.upsertBatch({
   327      ...batch,
   328      timestamp: Number(event.timestamp),
   329      blockNumber,
   330      transactionHash
   331    });
   332    log.info(`Asset instance batch ${event.batchHash} from blockchain event (blockNumber=${blockNumber} hash=${transactionHash}) saved in local database`);
   333  
   334  }
   335  
   336  export const handleAssetInstanceCreatedEvent = async (event: IEventAssetInstanceCreated, { blockNumber, transactionHash }: IDBBlockchainData, batchInstance?: IBatchRecord) => {
   337    let eventAssetInstanceID: string;
   338    let eventAssetDefinitionID: string;
   339    if (batchInstance === undefined) {
   340      switch (config.protocol) {
   341        case 'corda':
   342          eventAssetInstanceID = event.assetInstanceID;
   343          eventAssetDefinitionID = event.assetDefinitionID;
   344          break;
   345        case 'ethereum':
   346          eventAssetInstanceID = utils.hexToUuid(event.assetInstanceID);
   347          eventAssetDefinitionID = utils.hexToUuid(event.assetDefinitionID);
   348          break;
   349      }
   350    } else {
   351      eventAssetInstanceID = batchInstance.assetInstanceID;
   352      eventAssetDefinitionID = batchInstance.assetDefinitionID;
   353      log.info(`batch instance ${eventAssetDefinitionID}:${eventAssetInstanceID}`);
   354    }
   355    const dbAssetInstance = await database.retrieveAssetInstanceByID(eventAssetDefinitionID, eventAssetInstanceID);
   356    if (dbAssetInstance !== null && dbAssetInstance.transactionHash !== undefined) {
   357      throw new Error(`Duplicate asset instance ID`);
   358    }
   359    const assetDefinition = await database.retrieveAssetDefinitionByID(eventAssetDefinitionID);
   360    if (assetDefinition === null) {
   361      throw new Error('Unkown asset definition');
   362    }
   363    // For ethereum, we need to make asset definition transaction is mined
   364    if (config.protocol === 'ethereum' && assetDefinition.transactionHash === undefined) {
   365      throw new Error('Asset definition transaction must be mined');
   366    }
   367    if (assetDefinition.isContentUnique) {
   368      const assetInstanceByContentID = await database.retrieveAssetInstanceByDefinitionIDAndContentHash(eventAssetDefinitionID, event.contentHash);
   369      if (assetInstanceByContentID !== null && eventAssetInstanceID !== assetInstanceByContentID.assetInstanceID) {
   370        if (assetInstanceByContentID.transactionHash !== undefined) {
   371          throw new Error(`Asset instance content conflict ${event.contentHash}`);
   372        } else {
   373          await database.markAssetInstanceAsConflict(eventAssetDefinitionID, assetInstanceByContentID.assetInstanceID, Number(event.timestamp));
   374        }
   375      }
   376    }
   377    let description: Object | undefined = batchInstance?.description;
   378    if (assetDefinition.descriptionSchema && !description) {
   379      if (event.descriptionHash) {
   380        if (event.descriptionHash === dbAssetInstance?.descriptionHash) {
   381          description = dbAssetInstance.description;
   382        } else {
   383          description = await ipfs.downloadJSON(utils.sha256ToIPFSHash(event.descriptionHash));
   384          if (!ajv.validate(assetDefinition.descriptionSchema, description)) {
   385            throw new Error('Description does not conform to schema');
   386          }
   387        }
   388      } else {
   389        throw new Error('Missing asset instance description');
   390      }
   391    }
   392    let content: Object | undefined = batchInstance?.content;
   393    if (assetDefinition.contentSchema && !content) {
   394      if (event.contentHash === dbAssetInstance?.contentHash) {
   395        content = dbAssetInstance.content;
   396      } else if (!assetDefinition.isContentPrivate) {
   397        content = await ipfs.downloadJSON(utils.sha256ToIPFSHash(event.contentHash));
   398        if (!ajv.validate(assetDefinition.contentSchema, content)) {
   399          throw new Error('Content does not conform to schema');
   400        }
   401      }
   402    }
   403    log.trace(`Updating asset instance ${eventAssetInstanceID} with blockchain pinned info blockNumber=${blockNumber} hash=${transactionHash}`);
   404    let assetInstanceDB: IDBAssetInstance = {
   405      assetInstanceID: eventAssetInstanceID,
   406      author: event.author,
   407      assetDefinitionID: assetDefinition.assetDefinitionID,
   408      descriptionHash: event.descriptionHash,
   409      description,
   410      contentHash: event.contentHash,
   411      timestamp: Number(event.timestamp),
   412      content,
   413      blockNumber,
   414      transactionHash,
   415      isContentPrivate: event.isContentPrivate ?? assetDefinition.isContentPrivate
   416    };
   417    if (config.protocol === 'corda') {
   418      assetInstanceDB.participants = event.participants;
   419    }
   420    await database.upsertAssetInstance(assetInstanceDB);
   421    if (assetInstanceDB.isContentPrivate) {
   422      const privateData = pendingAssetInstancePrivateContentDeliveries[eventAssetInstanceID];
   423      if (privateData !== undefined) {
   424        const author = await database.retrieveMemberByAddress(event.author);
   425        if (author === null) {
   426          throw new Error('Pending private data author unknown');
   427        }
   428        if (author.app2appDestination !== privateData.fromDestination) {
   429          throw new Error('Pending private data destination mismatch');
   430        }
   431        if (privateData.content !== undefined) {
   432          const privateDataHash = `0x${utils.getSha256(JSON.stringify(privateData.content))}`;
   433          if (privateDataHash !== event.contentHash) {
   434            throw new Error('Pending private data content hash mismatch');
   435          }
   436        }
   437        await database.setAssetInstancePrivateContent(eventAssetDefinitionID, eventAssetInstanceID, privateData.content, privateData.filename);
   438        delete pendingAssetInstancePrivateContentDeliveries[eventAssetInstanceID];
   439      }
   440    }
   441    log.info(`Asset instance ${eventAssetDefinitionID}/${eventAssetInstanceID} from blockchain event (blockNumber=${blockNumber} hash=${transactionHash}) saved in local database`);
   442  };
   443  
   444  export const handleSetAssetInstancePropertyEvent = async (event: IEventAssetInstancePropertySet, blockchainData: IDBBlockchainData, isBatch?: boolean) => {
   445    let eventAssetInstanceID: string;
   446    let eventAssetDefinitionID: string;
   447    if (config.protocol === 'corda' || isBatch) {
   448      eventAssetInstanceID = event.assetInstanceID;
   449      eventAssetDefinitionID = event.assetDefinitionID;
   450    } else {
   451      eventAssetInstanceID = utils.hexToUuid(event.assetInstanceID);
   452      eventAssetDefinitionID = utils.hexToUuid(event.assetDefinitionID);
   453    }
   454    const dbAssetInstance = await database.retrieveAssetInstanceByID(eventAssetDefinitionID, eventAssetInstanceID);
   455    if (dbAssetInstance === null) {
   456      throw new Error('Uknown asset instance');
   457    }
   458    if (dbAssetInstance.transactionHash === undefined) {
   459      throw new Error('Unconfirmed asset instance');
   460    }
   461    if (!event.key) {
   462      throw new Error('Invalid property key');
   463    }
   464    await database.setConfirmedAssetInstanceProperty(eventAssetDefinitionID, eventAssetInstanceID, event.author, event.key, event.value, Number(event.timestamp), blockchainData);
   465    log.info(`Asset instance property ${event.key} (instance=${eventAssetDefinitionID}) from blockchain event (blockNumber=${blockchainData.blockNumber} hash=${blockchainData.transactionHash}) saved in local database`);
   466  };
   467  
   468  export const handleAssetInstanceTradeRequest = async (assetDefinitionID: string, requesterAddress: string, assetInstanceID: string, metadata: object | undefined) => {
   469    const assetInstance = await database.retrieveAssetInstanceByID(assetDefinitionID, assetInstanceID);
   470    if (assetInstance === null) {
   471      throw new RequestError('Uknown asset instance', 404);
   472    }
   473    const author = await database.retrieveMemberByAddress(assetInstance.author);
   474    if (author === null) {
   475      throw new RequestError('Asset author must be registered', 400);
   476    }
   477    if (author.assetTrailInstanceID === config.assetTrailInstanceID) {
   478      throw new RequestError('Asset instance authored', 400);
   479    }
   480    const assetDefinition = await database.retrieveAssetDefinitionByID(assetDefinitionID);
   481    if (assetDefinition === null) {
   482      throw new RequestError('Unknown asset definition', 500);
   483    }
   484    if (assetDefinition.contentSchema !== undefined) {
   485      if (assetInstance.content !== undefined) {
   486        throw new RequestError('Asset content already available', 400);
   487      }
   488    } else {
   489      try {
   490        const documentDetails = await docExchange.getDocumentDetails(utils.getUnstructuredFilePathInDocExchange(assetInstanceID));
   491        if (documentDetails.hash === assetInstance.contentHash) {
   492          throw new RequestError('Asset content already available', 400);
   493        }
   494      } catch (err) {
   495        if (err.response?.status !== 404) {
   496          throw new RequestError(err, 500);
   497        }
   498      }
   499    }
   500    const requester = await database.retrieveMemberByAddress(requesterAddress);
   501    if (requester === null) {
   502      throw new RequestError('Requester must be registered', 400);
   503    }
   504    await assetTrade.coordinateAssetTrade(assetInstance, assetDefinition, requester.address, metadata, author.app2appDestination);
   505    log.info(`Asset instance trade request from requester ${requesterAddress} (instance=${assetInstanceID}) successfully completed`);
   506  };
   507  
   508  export const handlePushPrivateAssetInstanceRequest = async (assetDefinitionID: string, assetInstanceID: string, memberAddress: string) => {
   509    const member = await database.retrieveMemberByAddress(memberAddress);
   510    if (member === null) {
   511      throw new RequestError('Unknown member', 400);
   512    }
   513    const assetInstance = await database.retrieveAssetInstanceByID(assetDefinitionID, assetInstanceID);
   514    if (assetInstance === null) {
   515      throw new RequestError('Unknown asset instance', 400);
   516    }
   517    const author = await database.retrieveMemberByAddress(assetInstance.author);
   518    if (author === null) {
   519      throw new RequestError('Unknown asset author', 500);
   520    }
   521    if (author.assetTrailInstanceID !== config.assetTrailInstanceID) {
   522      throw new RequestError('Must be asset instance author', 403);
   523    }
   524    const assetDefinition = await database.retrieveAssetDefinitionByID(assetInstance.assetDefinitionID);
   525    if (assetDefinition === null) {
   526      throw new RequestError('Unknown asset definition', 500);
   527    }
   528    let privateAssetTradePrivateInstancePush: IAssetTradePrivateAssetInstancePush = {
   529      type: 'private-asset-instance-push',
   530      assetInstanceID,
   531      assetDefinitionID
   532    };
   533    if (assetDefinition.contentSchema !== undefined) {
   534      privateAssetTradePrivateInstancePush.content = assetInstance.content;
   535    } else {
   536      await docExchange.transfer(author.docExchangeDestination, member.docExchangeDestination,
   537        utils.getUnstructuredFilePathInDocExchange(assetInstanceID));
   538      privateAssetTradePrivateInstancePush.filename = assetInstance.filename;
   539      log.info(`Private asset instance push request for member ${memberAddress} (instance=${assetInstanceID}) successfully completed`);
   540    }
   541    app2app.dispatchMessage(member.app2appDestination, privateAssetTradePrivateInstancePush);
   542  };