github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/kat/src/handlers/asset-trade.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 { v4 as uuidV4 } from 'uuid'; 16 import Ajv from 'ajv'; 17 import { config } from '../lib/config'; 18 import { AssetTradeMessage, IApp2AppMessageHeader, IApp2AppMessageListener, IAssetTradePrivateAssetInstanceAuthorizationRequest, IAssetTradePrivateAssetInstancePush, IAssetTradePrivateAssetInstanceRequest, IAssetTradePrivateAssetInstanceResponse, IDBAssetDefinition, IDBAssetInstance, IDBMember, IDocExchangeListener, IDocExchangeTransferData } from "../lib/interfaces"; 19 import * as utils from '../lib/utils'; 20 import * as database from '../clients/database'; 21 import * as app2app from '../clients/app2app'; 22 import * as docExchange from '../clients/doc-exchange'; 23 import { pendingAssetInstancePrivateContentDeliveries } from './asset-instances'; 24 const log = utils.getLogger('handlers/asset-trade.ts'); 25 26 const ajv = new Ajv(); 27 28 export const assetTradeHandler = (headers: IApp2AppMessageHeader, content: AssetTradeMessage) => { 29 if (content.type === 'private-asset-instance-request') { 30 processPrivateAssetInstanceRequest(headers, content); 31 } else if (content.type === 'private-asset-instance-push') { 32 processPrivateAssetInstancePush(headers, content); 33 } 34 }; 35 36 const processPrivateAssetInstanceRequest = async (headers: IApp2AppMessageHeader, request: IAssetTradePrivateAssetInstanceRequest) => { 37 let tradeResponse: IAssetTradePrivateAssetInstanceResponse = { 38 type: "private-asset-instance-response", 39 tradeID: request.tradeID, 40 assetInstanceID: request.assetInstanceID 41 }; 42 const requester = await database.retrieveMemberByAddress(request.requester.address); 43 try { 44 if (requester === null) { 45 throw new Error(`Unknown requester ${request.requester.address}`); 46 } 47 if (requester.assetTrailInstanceID !== request.requester.assetTrailInstanceID) { 48 throw new Error(`Requester asset trail instance mismatch. Expected ${requester.assetTrailInstanceID}, ` + 49 `actual ${request.requester.assetTrailInstanceID}`); 50 } 51 if (requester.app2appDestination !== headers.from) { 52 throw new Error(`Requester App2App destination mismatch. Expected ${requester.app2appDestination}, ` + 53 `actual ${headers.from}`); 54 } 55 const assetInstance = await database.retrieveAssetInstanceByID(request.assetDefinitionID, request.assetInstanceID); 56 if (assetInstance === null) { 57 throw new Error(`Unknown asset instance ${request.assetInstanceID}`); 58 } 59 const author = await database.retrieveMemberByAddress(assetInstance.author); 60 if (author === null) { 61 throw new Error(`Unknown asset instance author`); 62 } 63 if (author.assetTrailInstanceID !== config.assetTrailInstanceID) { 64 throw new Error(`Asset instance ${assetInstance.assetInstanceID} not authored`); 65 } 66 const assetDefinition = await database.retrieveAssetDefinitionByID(assetInstance.assetDefinitionID); 67 if (assetDefinition === null) { 68 throw new Error(`Unknown asset definition ${assetInstance.assetDefinitionID}`); 69 } 70 if (!assetDefinition.isContentPrivate) { 71 throw new Error(`Asset instance ${assetInstance.assetInstanceID} not private`); 72 } 73 const authorized = await handlePrivateAssetInstanceAuthorization(assetInstance, requester, request.metadata); 74 if (authorized !== true) { 75 throw new Error('Access denied'); 76 } 77 if (assetDefinition.contentSchema) { 78 tradeResponse.content = assetInstance.content; 79 } else { 80 await docExchange.transfer(config.docExchange.destination, requester.docExchangeDestination, 81 utils.getUnstructuredFilePathInDocExchange(request.assetInstanceID)); 82 tradeResponse.filename = assetInstance.filename; 83 log.info(`Private asset instance trade request (instance=${assetInstance.assetDefinitionID}, requester=${request.requester.address}, tradeId=${request.tradeID}) successfully completed`); 84 } 85 } catch (err) { 86 tradeResponse.rejection = err.message; 87 } finally { 88 app2app.dispatchMessage(headers.from, tradeResponse); 89 } 90 }; 91 92 const handlePrivateAssetInstanceAuthorization = (assetInstance: IDBAssetInstance, requester: IDBMember, metadata: object | undefined): Promise<boolean> => { 93 return new Promise((resolve, reject) => { 94 const authorizationID = uuidV4(); 95 const authorizationRequest: IAssetTradePrivateAssetInstanceAuthorizationRequest = { 96 type: 'private-asset-instance-authorization-request', 97 authorizationID, 98 assetInstance, 99 requester, 100 metadata 101 }; 102 const timeout = setTimeout(() => { 103 app2app.removeListener(app2appListener); 104 reject(new Error('Asset instance authorization response timed out')); 105 }, utils.constants.TRADE_AUTHORIZATION_TIMEOUT_SECONDS * 1000); 106 const app2appListener: IApp2AppMessageListener = (headers: IApp2AppMessageHeader, content: AssetTradeMessage) => { 107 if (headers.from === config.app2app.destinations.client && content.type === 'private-asset-instance-authorization-response' && 108 content.authorizationID === authorizationID) { 109 clearTimeout(timeout); 110 app2app.removeListener(app2appListener); 111 resolve(content.authorized); 112 } 113 }; 114 app2app.addListener(app2appListener); 115 app2app.dispatchMessage(config.app2app.destinations.client, authorizationRequest); 116 }); 117 }; 118 119 export const coordinateAssetTrade = async (assetInstance: IDBAssetInstance, assetDefinition: IDBAssetDefinition, 120 requesterAddress: string, metadata: object | undefined, authorDestination: string) => { 121 const tradeID = uuidV4(); 122 const tradeRequest: IAssetTradePrivateAssetInstanceRequest = { 123 type: 'private-asset-instance-request', 124 tradeID, 125 assetInstanceID: assetInstance.assetInstanceID, 126 assetDefinitionID: assetInstance.assetDefinitionID, 127 requester: { 128 assetTrailInstanceID: config.assetTrailInstanceID, 129 address: requesterAddress 130 }, 131 metadata 132 }; 133 const docExchangePromise = assetDefinition.contentSchema === undefined ? getDocumentExchangePromise(assetInstance.assetInstanceID) : Promise.resolve(); 134 const app2appPromise: Promise<void> = new Promise((resolve, reject) => { 135 const timeout = setTimeout(() => { 136 app2app.removeListener(app2appListener); 137 reject(new Error('Asset instance author response timed out')); 138 }, utils.constants.ASSET_INSTANCE_TRADE_TIMEOUT_SECONDS * 1000); 139 const app2appListener: IApp2AppMessageListener = (_headers: IApp2AppMessageHeader, content: AssetTradeMessage) => { 140 if (content.type === 'private-asset-instance-response' && content.tradeID === tradeID) { 141 clearTimeout(timeout); 142 app2app.removeListener(app2appListener); 143 if (content.rejection) { 144 reject(new Error(`Asset instance request rejected. ${content.rejection}`)); 145 } else { 146 const contentHash = `0x${utils.getSha256(JSON.stringify(content.content))}`; 147 if (contentHash !== assetInstance.contentHash) { 148 reject(new Error('Asset instance content hash mismatch')); 149 } else if (assetDefinition.contentSchema && !ajv.validate(assetDefinition.contentSchema, content.content)) { 150 reject(new Error('Asset instance content does not conform to schema')); 151 } else { 152 database.setAssetInstancePrivateContent(assetInstance.assetDefinitionID, content.assetInstanceID, content.content, content.filename); 153 resolve(); 154 } 155 } 156 } 157 }; 158 app2app.addListener(app2appListener); 159 app2app.dispatchMessage(authorDestination, tradeRequest); 160 }); 161 await Promise.all([app2appPromise, docExchangePromise]); 162 }; 163 164 const getDocumentExchangePromise = (assetInstanceID: string): Promise<void> => { 165 return new Promise((resolve, reject) => { 166 const timeout = setTimeout(() => { 167 docExchange.removeListener(docExchangeListener); 168 reject(new Error('Off-chain asset transfer timeout')); 169 }, utils.constants.DOCUMENT_EXCHANGE_TRANSFER_TIMEOUT_SECONDS * 1000); 170 const docExchangeListener: IDocExchangeListener = (event: IDocExchangeTransferData) => { 171 if (event.document === utils.getUnstructuredFilePathInDocExchange(assetInstanceID)) { 172 clearTimeout(timeout); 173 docExchange.removeListener(docExchangeListener); 174 resolve(); 175 } 176 }; 177 docExchange.addListener(docExchangeListener); 178 }); 179 }; 180 181 const processPrivateAssetInstancePush = async (headers: IApp2AppMessageHeader, push: IAssetTradePrivateAssetInstancePush) => { 182 log.trace(`Handling private asset instance push event (instance=${push.assetInstanceID}, filename=${push.filename})`); 183 const assetInstance = await database.retrieveAssetInstanceByID(push.assetDefinitionID, push.assetInstanceID); 184 if (assetInstance !== null) { 185 log.trace(`Found existing asset instance, ${JSON.stringify(assetInstance, null, 2)}`); 186 const author = await database.retrieveMemberByAddress(assetInstance.author); 187 if (author === null) { 188 throw new Error(`Unknown author for asset ${assetInstance.assetInstanceID}`); 189 } 190 if (author.app2appDestination !== headers.from) { 191 throw new Error(`Asset instance author destination mismatch ${author.app2appDestination} - ${headers.from}`); 192 } 193 if (push.content) { 194 const contentHash = `0x${utils.getSha256(JSON.stringify(push.content))}`; 195 if (assetInstance.contentHash !== contentHash) { 196 throw new Error('Private asset content hash mismatch'); 197 } 198 } 199 await database.setAssetInstancePrivateContent(push.assetDefinitionID, push.assetInstanceID, push.content, push.filename); 200 log.info(`Private asset instance from push event (instance=${push.assetInstanceID}, filename=${push.filename}) saved in local database`); 201 } else { 202 log.info(`Private asset instance ${push.assetDefinitionID}/${push.assetInstanceID} from push event not found in local database, adding to pending instances`); 203 pendingAssetInstancePrivateContentDeliveries[push.assetInstanceID] = { ...push, fromDestination: headers.from }; 204 } 205 }