github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/kat/src/handlers/payment-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 database from '../clients/database'; 18 import * as ipfs from '../clients/ipfs'; 19 import * as utils from '../lib/utils'; 20 import * as apiGateway from '../clients/api-gateway'; 21 import RequestError from '../lib/request-handlers'; 22 import { IAPIGatewayAsyncResponse, IAPIGatewaySyncResponse, IDBBlockchainData, IDBPaymentInstance, IEventPaymentInstanceCreated } from '../lib/interfaces'; 23 import { config } from '../lib/config'; 24 25 const ajv = new Ajv(); 26 27 export const handleGetPaymentInstancesRequest = (query: object, sort: object, skip: number, limit: number) => { 28 return database.retrievePaymentInstances(query, sort, skip, limit); 29 }; 30 31 export const handleCountPaymentInstancesRequest = async (query: object) => { 32 return { count: await database.countPaymentInstances(query) }; 33 }; 34 35 export const handleGetPaymentInstanceRequest = async (paymentInstanceID: string) => { 36 const assetInstance = await database.retrievePaymentInstanceByID(paymentInstanceID); 37 if (assetInstance === null) { 38 throw new RequestError('Payment instance not found', 404); 39 } 40 return assetInstance; 41 }; 42 43 export const handleCreatePaymentInstanceRequest = async (author: string, paymentDefinitionID: string, 44 member: string, description: object | undefined, amount: number, participants: string[] | undefined, sync: boolean) => { 45 const paymentDefinition = await database.retrievePaymentDefinitionByID(paymentDefinitionID); 46 if (paymentDefinition === null) { 47 throw new RequestError('Unknown payment definition', 400); 48 } 49 if (paymentDefinition.transactionHash === undefined) { 50 throw new RequestError('Payment definition transaction must be mined', 400); 51 } 52 if(config.protocol === 'ethereum' && participants !== undefined) { 53 throw new RequestError('Participants not supported in Ethereum', 400); 54 } 55 if(config.protocol === 'corda') { 56 // validate participants are registered members 57 if(participants !== undefined) { 58 for(const participant of participants) { 59 if (await database.retrieveMemberByAddress(participant) === null) { 60 throw new RequestError('One or more participants are not registered', 400); 61 } 62 } 63 } else { 64 throw new RequestError('Missing payment participants', 400); 65 } 66 } 67 let descriptionHash: string | undefined; 68 if (paymentDefinition.descriptionSchema) { 69 if (!description) { 70 throw new RequestError('Missing payment description', 400); 71 } 72 if (!ajv.validate(paymentDefinition.descriptionSchema, description)) { 73 throw new RequestError('Description does not conform to payment definition schema', 400); 74 } 75 descriptionHash = utils.ipfsHashToSha256(await ipfs.uploadString(JSON.stringify(description))); 76 } 77 const paymentInstanceID = uuidV4(); 78 const timestamp = utils.getTimestamp(); 79 let apiGatewayResponse: IAPIGatewayAsyncResponse | IAPIGatewaySyncResponse; 80 if (descriptionHash) { 81 apiGatewayResponse = await apiGateway.createDescribedPaymentInstance(paymentInstanceID, 82 paymentDefinitionID, author, member, amount, descriptionHash, participants,sync); 83 } else { 84 apiGatewayResponse = await apiGateway.createPaymentInstance(paymentInstanceID, 85 paymentDefinitionID, author, member, amount, participants, sync); 86 } 87 const receipt = apiGatewayResponse.type === 'async' ? apiGatewayResponse.id : undefined; 88 await database.upsertPaymentInstance({ 89 paymentInstanceID, 90 author, 91 paymentDefinitionID: paymentDefinition.paymentDefinitionID, 92 descriptionHash, 93 description, 94 member, 95 participants, 96 amount, 97 receipt, 98 submitted: timestamp 99 }); 100 return paymentInstanceID; 101 }; 102 103 export const handlePaymentInstanceCreatedEvent = async (event: IEventPaymentInstanceCreated, { blockNumber, transactionHash }: IDBBlockchainData) => { 104 let eventPaymentInstanceID: string; 105 let eventPaymentDefinitionID: string; 106 switch(config.protocol) { 107 case 'corda': 108 eventPaymentDefinitionID = event.paymentDefinitionID; 109 eventPaymentInstanceID = event.paymentInstanceID; 110 break; 111 case 'ethereum': 112 eventPaymentDefinitionID = utils.hexToUuid(event.paymentDefinitionID) 113 eventPaymentInstanceID = utils.hexToUuid(event.paymentInstanceID); 114 break; 115 } 116 const dbPaymentInstance = await database.retrievePaymentInstanceByID(eventPaymentInstanceID); 117 if (dbPaymentInstance !== null && dbPaymentInstance.transactionHash !== undefined) { 118 throw new Error(`Duplicate payment instance ID`); 119 } 120 const paymentDefinition = await database.retrievePaymentDefinitionByID(eventPaymentDefinitionID); 121 if (paymentDefinition === null) { 122 throw new Error('Uknown payment definition'); 123 } 124 if (config.protocol === 'ethereum' && paymentDefinition.transactionHash === undefined) { 125 throw new Error('Payment definition transaction must be mined'); 126 } 127 let description: Object | undefined = undefined; 128 if (paymentDefinition.descriptionSchema) { 129 if (event.descriptionHash) { 130 if (event.descriptionHash === dbPaymentInstance?.descriptionHash) { 131 description = dbPaymentInstance.description; 132 } else { 133 description = await ipfs.downloadJSON(utils.sha256ToIPFSHash(event.descriptionHash)); 134 if (!ajv.validate(paymentDefinition.descriptionSchema, description)) { 135 throw new Error('Description does not conform to schema'); 136 } 137 } 138 } else { 139 throw new Error('Missing payment instance description'); 140 } 141 } 142 let paymentInstanceDB: IDBPaymentInstance = { 143 paymentInstanceID: eventPaymentInstanceID, 144 author: event.author, 145 paymentDefinitionID: paymentDefinition.paymentDefinitionID, 146 descriptionHash: event.descriptionHash, 147 description, 148 member: event.member, 149 amount: Number(event.amount), 150 timestamp: Number(event.timestamp), 151 blockNumber, 152 transactionHash 153 }; 154 if(config.protocol === 'corda') { 155 paymentInstanceDB.participants = event.participants; 156 } 157 database.upsertPaymentInstance(paymentInstanceDB); 158 };