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