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 };