├── rest-server ├── .gitignore ├── .dockerignore ├── README.md ├── src │ ├── interfaces │ │ ├── swagger.ts │ │ └── metadata_interfaces.ts │ ├── server.config.ts │ ├── fabricproxy.config.ts │ ├── routes │ │ ├── index.ts │ │ ├── channels │ │ │ ├── index.ts │ │ │ └── chaincode │ │ │ │ ├── index.ts │ │ │ │ └── contract │ │ │ │ ├── index.ts │ │ │ │ └── transaction │ │ │ │ └── index.ts │ │ └── utils.ts │ ├── server.ts │ ├── cli.ts │ └── fabricproxy.ts ├── tsconfig.json ├── unittest │ ├── utils.ts │ ├── fabricproxy.spec.ts │ ├── routes │ │ ├── index.spec.ts │ │ ├── channels │ │ │ ├── index.spec.ts │ │ │ └── chaincode │ │ │ │ ├── index.spec.ts │ │ │ │ └── contract │ │ │ │ ├── index.spec.ts │ │ │ │ └── transaction │ │ │ │ └── index.spec.ts │ │ └── utils.spec.ts │ └── greeting-spec.json ├── tslint.json ├── dockerfile ├── package.json └── fullfvtest │ └── fabcar.spec.ts ├── docs ├── _config.yml ├── starter-rest-server │ └── index.md └── index.md ├── README.md ├── .gitignore └── LICENSE /rest-server/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /rest-server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /rest-server/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Add in a dockerfile for creating a docker images 4 | - Wallet where is this from 5 | - Configuration file 6 | 7 | - Get and write out meta-data file to disk 8 | - -------------------------------------------------------------------------------- /rest-server/src/interfaces/swagger.ts: -------------------------------------------------------------------------------- 1 | import { ComponentMetadata } from './metadata_interfaces'; 2 | 3 | export interface Swagger { 4 | openapi?: string; 5 | components?: ComponentMetadata; 6 | paths?: {[path: string]: string}; 7 | } 8 | -------------------------------------------------------------------------------- /rest-server/src/server.config.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | import FabricConfig from './fabricproxy.config'; 8 | 9 | export default interface Config { 10 | localFile: string; 11 | outputFile: string; 12 | fabric: FabricConfig; 13 | port: number; 14 | } 15 | -------------------------------------------------------------------------------- /rest-server/src/fabricproxy.config.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | export default interface FabricConfig { 8 | aslocalhost: boolean; 9 | walletpath: string; 10 | identityLabel: string; 11 | gateway: string; 12 | contract: string; 13 | network: string; 14 | } 15 | -------------------------------------------------------------------------------- /rest-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2017", 5 | "moduleResolution": "node", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "sourceMap":true 11 | }, 12 | "include": [ 13 | "./src/**/*" 14 | ], 15 | "exclude": [ 16 | "./unittest/**/*", 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /rest-server/unittest/utils.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | import FabricProxy from '../src/fabricproxy'; 3 | import { Swagger } from '../src/interfaces/swagger'; 4 | 5 | export function sampleFabricProxy(): FabricProxy { 6 | return new FabricProxy({aslocalhost: null, walletpath: '.', identityLabel: null, yamlCP: null}); 7 | } 8 | 9 | export function fakeExtendsPaths(someValue): Swagger { 10 | const returnObj = { paths: {}, components: { schemas: {}} }; 11 | returnObj.paths[someValue] = 'a path'; 12 | returnObj.components.schemas[someValue] = 'a schema'; 13 | 14 | return returnObj; 15 | } 16 | -------------------------------------------------------------------------------- /rest-server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from 'express'; 2 | import FabricProxy from '../fabricproxy'; 3 | import { Swagger } from '../interfaces/swagger'; 4 | import { ChannelRouter } from './channels'; 5 | 6 | export class Router { 7 | public static async getRoutes(fabricProxy: FabricProxy): Promise<{ router: ExpressRouter, swagger: Swagger }> { 8 | const router = ExpressRouter(); 9 | 10 | const channelRoutes = await ChannelRouter.getRoutes(fabricProxy); 11 | 12 | router.use(channelRoutes.router); 13 | 14 | return { router, swagger: channelRoutes.swagger }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/starter-rest-server/index.md: -------------------------------------------------------------------------------- 1 | # Starter Server 2 | 3 | ## Running the prototype 4 | 5 | This is based on a standard Express.js application, with inbuilt Swagger UI support. It pulls down the metadata from the contract, and constructs the OpenAPI document 6 | and a rest server based upon it. 7 | 8 | Change to the `starter-rest-server` direcory, `npm install` and then `npm run build && npm start`. the start script is running. 9 | 10 | ``` 11 | node dist/cli.js --identity User1@org1.example.com --wallet ./_idwallet --connection /home/matthew/fabric-application-examples/infrastructure/basic-network/network.yaml --network mychannel --contract tradenet" 12 | ``` -------------------------------------------------------------------------------- /rest-server/unittest/fabricproxy.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import * as chai from 'chai'; 9 | // import sinon from 'sinon'; 10 | import 'mocha'; 11 | import * as sinonChai from 'sinon-chai'; 12 | 13 | const should = chai.should(); 14 | chai.use(sinonChai); 15 | const expect = chai.expect; 16 | 17 | describe('MyContract', () => { 18 | 19 | // let sandbox = sinon.createSandbox(); 20 | 21 | // beforeEach(()=>{ 22 | // }); 23 | 24 | // afterEach(() => { 25 | // // sandbox.restore(); 26 | // }); 27 | 28 | describe('DataUtils', () => { 29 | console.log('hello'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fabric REST Server 2 | **Welcome to the REST Server for working with Hyperledger Fabric.** 3 | 4 | > *tl;dr;* 5 | >Given a Hyperledger Fabric infrastructure, you give the rest server a Connection Profile of your organization's 'gateway' Peer; the rest server then stands up a OpenAPIv3 standard REST server allowing access all the way to the transaction functions of the smart contracts. 6 | > Stand this up next to your 'gateway' peer with a conneciton profile and identity. 7 | 8 | # Status 9 | 10 | Please note that this is an Apache-2.0 project, it is offered without any formal support from IBM or anybody else. 11 | 12 | ## Getting Started 13 | 14 | For full details see [How to run Starter Server](./docs/starter-rest-server/index.md) 15 | -------------------------------------------------------------------------------- /rest-server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "indent": [true, "spaces", 4], 9 | "linebreak-style": [true, "LF"], 10 | "quotemark": [true, "single"], 11 | "semicolon": [true, "always"], 12 | "no-console": false, 13 | "curly": true, 14 | "triple-equals": true, 15 | "no-string-throw": true, 16 | "no-var-keyword": true, 17 | "no-trailing-whitespace": true, 18 | "object-literal-key-quotes": [true, "as-needed"], 19 | "interface-name": [true, "never-prefix"], 20 | "max-line-length": [true, 160] 21 | }, 22 | "rulesDirectory": [] 23 | } 24 | -------------------------------------------------------------------------------- /rest-server/src/interfaces/metadata_interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | $ref?: string; 3 | type?: string; 4 | properties?: [ParameterMetadata]; 5 | } 6 | 7 | export interface ParameterMetadata { 8 | name: string; 9 | schema: Schema; 10 | } 11 | 12 | export interface TransactionMetadata { 13 | parameters: ParameterMetadata[]; 14 | name: string; 15 | tag: string[]; 16 | } 17 | 18 | export interface ContractMetadata { 19 | name: string; 20 | transactions: TransactionMetadata[]; 21 | } 22 | 23 | export interface ComponentMetadata { 24 | schemas: {[s: string]: object}; 25 | } 26 | 27 | export interface ChaincodeMetadata { 28 | name: string; 29 | components: ComponentMetadata; 30 | contracts: {[s: string]: ContractMetadata}; 31 | } 32 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Fabric REST Server 2 | 3 | How to [start using the Starter Server](./starter-rest-server/index.md) 4 | 5 | 6 | # Use cases 7 | Three use-cases identified 8 | 9 | - Starter Server 10 | - Hackathons, quick rest server up and working 11 | - Something simple, perhaps with basic authentication, eg based on token given on server startup that needs supplying on all REST requests 12 | - Will connect to running network, or using static metadata create REST API 13 | - Swagger UI 14 | - Access to metadata for debugging 15 | - Production REST Support 16 | - Run a dockerized image based on static metadata 17 | - Should be ammenable to being scalled within a environment such as K8S 18 | - 'Craft-your-own' 19 | - Previous experience has shown a 'one-size-fits-all' does not work or at least struggles with the edge cases 20 | - Provide the essential building blocks in NPM module that permit a 'craft-your-own-server' for such cases -------------------------------------------------------------------------------- /rest-server/src/routes/channels/index.ts: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from 'express'; 2 | import FabricProxy from '../../fabricproxy'; 3 | import { Swagger } from '../../interfaces/swagger'; 4 | import { extendPaths } from '../utils'; 5 | import { ChaincodeRouter } from './chaincode'; 6 | 7 | export class ChannelRouter { 8 | public static async getRoutes(fabricProxy: FabricProxy): Promise<{ router: ExpressRouter, swagger: Swagger }> { 9 | const router = ExpressRouter(); 10 | 11 | const swagger = { 12 | components: { 13 | schemas: {}, 14 | }, 15 | paths: {}, 16 | } as Swagger; 17 | 18 | for (const channel of fabricProxy.getChannels()) { 19 | const ccRoutes = await ChaincodeRouter.getRoutes(fabricProxy, channel); 20 | 21 | const extendedSwagger = extendPaths(channel, ccRoutes.swagger); 22 | swagger.paths = Object.assign(swagger.paths, extendedSwagger.paths); 23 | swagger.components.schemas = Object.assign(swagger.components.schemas, extendedSwagger.components.schemas); 24 | 25 | router.use('/' + channel, ccRoutes.router); 26 | } 27 | 28 | return { router, swagger }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rest-server/dockerfile: -------------------------------------------------------------------------------- 1 | # Base build from node:carbon 2 | FROM node:carbon as base 3 | 4 | WORKDIR /app 5 | 6 | # ---- Dependencies ---- 7 | FROM base AS dependencies 8 | COPY package*.json ./ 9 | RUN npm install 10 | 11 | # ---- Copy Files/Build ---- 12 | FROM dependencies AS build 13 | WORKDIR /app 14 | COPY src ./src/ 15 | COPY tsconfig.json ./ 16 | COPY tslint.json ./ 17 | 18 | # Build the core TypeScript files 19 | RUN npm run build 20 | 21 | # --- Build release with Alpine ---- 22 | FROM node:8.9-alpine AS builder 23 | # Create app directory 24 | WORKDIR /app 25 | 26 | COPY --from=build /app/dist ./dist/ 27 | COPY package.json ./ 28 | 29 | # Add the compilation tools required for alpine 30 | RUN apk add --no-cache --virtual .gyp python make g++ 31 | 32 | # Install app dependencies in production mode 33 | RUN npm install --only=production 34 | 35 | # Assemble final release image 36 | FROM node:8.9-alpine AS release 37 | 38 | WORKDIR /app 39 | COPY --from=builder /app/dist ./dist/ 40 | COPY --from=builder /app/node_modules ./node_modules/ 41 | 42 | # Standard port to be exposed 43 | EXPOSE 3000 44 | ENV NODE_ENV production 45 | 46 | # Entrypoint for the REST, setting port as 3000, when docker is run port map then if wished 47 | ENTRYPOINT [ "node","dist/cli.js"] 48 | CMD ["--port","3000"] -------------------------------------------------------------------------------- /rest-server/src/routes/channels/chaincode/index.ts: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from 'express'; 2 | import FabricProxy from '../../../fabricproxy'; 3 | import { Swagger } from '../../../interfaces/swagger'; 4 | import { extendPaths } from '../../utils'; 5 | import { ContractRouter } from './contract'; 6 | 7 | export class ChaincodeRouter { 8 | public static async getRoutes(fabricProxy: FabricProxy, channel: string): Promise<{ router: ExpressRouter, swagger: Swagger }> { 9 | const router = ExpressRouter(); 10 | 11 | const swagger = { 12 | components: { 13 | schemas: {}, 14 | }, 15 | paths: {}, 16 | } as Swagger; 17 | 18 | for (const contract of fabricProxy.getChannelContracts(channel)) { 19 | const contractRoutes = await ContractRouter.getRoutes(fabricProxy, channel, contract); 20 | 21 | const extendedSwagger = extendPaths(contract, contractRoutes.swagger); 22 | swagger.paths = Object.assign(swagger.paths, extendedSwagger.paths); 23 | swagger.components.schemas = Object.assign(swagger.components.schemas, extendedSwagger.components.schemas); 24 | 25 | router.use('/' + contract, contractRoutes.router); 26 | } 27 | 28 | return { router, swagger }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rest-server/src/routes/utils.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { Swagger } from '../interfaces/swagger'; 3 | 4 | export function extendPaths(pathStart: string, existingSwagger: Swagger): Swagger { 5 | const newSwagger = {} as Swagger; 6 | 7 | if (existingSwagger.hasOwnProperty('paths')) { 8 | newSwagger.paths = {}; 9 | for (const path in existingSwagger.paths) { 10 | if (existingSwagger.paths.hasOwnProperty(path)) { 11 | newSwagger.paths[`/${pathStart}${path}`] = existingSwagger.paths[path]; 12 | } 13 | } 14 | } 15 | 16 | if (existingSwagger.hasOwnProperty('components')) { 17 | newSwagger.components = { 18 | schemas: {}, 19 | }; 20 | 21 | for (const component in existingSwagger.components.schemas) { 22 | if (existingSwagger.components.schemas.hasOwnProperty(component)) { 23 | newSwagger.components.schemas[pathStart + '.' + component] = existingSwagger.components.schemas[component]; 24 | 25 | // Update ref paths to new component paths, only pass new swagger as just want paths updating 26 | extendRefs(pathStart, component, newSwagger); 27 | } 28 | } 29 | } 30 | 31 | return newSwagger; 32 | } 33 | 34 | function extendRefs(pathStart: string, component: string, json: object) { 35 | Object.entries(json).forEach(([key, val]) => { 36 | if (typeof val === 'object' && !Array.isArray(val)) { 37 | extendRefs(pathStart, component, val); 38 | } else if (key === '$ref') { 39 | json[key] = json[key].replace(`#/components/schemas/${component}`, `#/components/schemas/${pathStart}.${component}`); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /rest-server/src/routes/channels/chaincode/contract/index.ts: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from 'express'; 2 | import FabricProxy from '../../../../fabricproxy'; 3 | import { ChaincodeMetadata, ComponentMetadata, Schema } from '../../../../interfaces/metadata_interfaces'; 4 | import { Swagger } from '../../../../interfaces/swagger'; 5 | import { extendPaths } from '../../../utils'; 6 | import { TransactionRouter } from './transaction'; 7 | 8 | export class ContractRouter { 9 | public static async getRoutes(fabricProxy: FabricProxy, channel: string, chaincode: string): Promise<{ router: ExpressRouter, swagger: Swagger }> { // tslint:disable:max-line-length 10 | const router = ExpressRouter(); 11 | 12 | const chaincodeMetadata = await fabricProxy.getMetaData(channel, chaincode) as ChaincodeMetadata; 13 | 14 | const swagger = { 15 | components: this.processComponents(chaincodeMetadata.components), 16 | paths: {}, 17 | }; 18 | 19 | for (const contractName in chaincodeMetadata.contracts) { 20 | if (chaincodeMetadata.contracts.hasOwnProperty(contractName)) { 21 | const contract = chaincodeMetadata.contracts[contractName]; 22 | 23 | const txRoutes = TransactionRouter.getRoutes(fabricProxy, channel, chaincode, contract); 24 | 25 | swagger.paths = Object.assign(swagger.paths, extendPaths(contractName, txRoutes.swagger).paths); 26 | 27 | router.use('/' + contractName, txRoutes.router); 28 | } 29 | } 30 | 31 | return { router, swagger }; 32 | } 33 | 34 | private static processComponents(metadataComponents: ComponentMetadata): ComponentMetadata { 35 | const components = { schemas: { } }; 36 | 37 | for (const s in metadataComponents.schemas) { 38 | if (metadataComponents.schemas.hasOwnProperty(s)) { 39 | const schema = metadataComponents.schemas[s] as Schema; 40 | const props = {}; 41 | for (const [propName, propSchema] of Object.entries(schema.properties)) { 42 | props[propName] = propSchema; 43 | } 44 | 45 | components.schemas[s] = metadataComponents.schemas[s]; 46 | components.schemas[s].properties = props; 47 | delete components.schemas[s].$id; 48 | } 49 | } 50 | 51 | return components; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Copyright IBM Corp All Rights Reserved 5 | * 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | import * as chai from 'chai'; 10 | import 'mocha'; 11 | import rewire = require('rewire'); 12 | import * as sinon from 'sinon'; 13 | import * as sinonChai from 'sinon-chai'; 14 | import FabricProxy from '../../src/fabricproxy'; 15 | import { Router as RouterImport } from '../../src/routes'; 16 | import { ChannelRouter } from '../../src/routes/channels'; 17 | import { sampleFabricProxy } from '../utils'; 18 | 19 | const RouterRewired = rewire('../../src/routes'); 20 | const Router: typeof RouterImport & typeof RouterRewired = RouterRewired.Router as any; 21 | 22 | chai.use(sinonChai); 23 | const expect = chai.expect; 24 | 25 | describe('Router', () => { 26 | describe('getRoutes', () => { 27 | let fabricProxy: FabricProxy; 28 | 29 | let sandbox: sinon.SinonSandbox; 30 | 31 | beforeEach(() => { 32 | sandbox = sinon.createSandbox(); 33 | }); 34 | 35 | afterEach(() => { 36 | sandbox.restore(); 37 | }); 38 | 39 | it ('should build up its router and swagger using channel', async () => { 40 | fabricProxy = sampleFabricProxy(); 41 | 42 | const channelRouterGetRoutesStub = sandbox.stub(ChannelRouter, 'getRoutes').resolves({ 43 | router: 'some channel router' as any, 44 | swagger: 'some channel swagger' as any, 45 | }); 46 | 47 | const useStub = sinon.stub(); 48 | 49 | const express = RouterRewired.__get__('express_1'); 50 | const oldRouter = express.Router; 51 | 52 | express.Router = () => { 53 | return { 54 | special: 'router', 55 | use: useStub, 56 | }; 57 | }; 58 | 59 | const { router, swagger } = await Router.getRoutes(fabricProxy); 60 | 61 | sinon.assert.calledOnce(channelRouterGetRoutesStub); 62 | sinon.assert.calledWithExactly(channelRouterGetRoutesStub, fabricProxy); 63 | 64 | sinon.assert.calledOnce(useStub); 65 | sinon.assert.calledWithExactly(useStub, 'some channel router'); 66 | 67 | expect(router).to.deep.eq(express.Router()); 68 | expect(swagger).to.deep.eq('some channel swagger'); 69 | 70 | express.Router = oldRouter; 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /rest-server/src/server.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import * as express from 'express'; 9 | 10 | import debug from 'debug'; 11 | import * as swaggerUi from 'swagger-ui-express'; 12 | import FabricProxy from './fabricproxy'; 13 | import { Swagger } from './interfaces/swagger'; 14 | import Config from './server.config'; 15 | const LOG = debug('contractrest:restserver'); 16 | import { Router } from './routes'; 17 | 18 | interface ParameterMetadata { 19 | name: string; 20 | schema: object; 21 | } 22 | 23 | interface TransactionMetadata { 24 | parameters: ParameterMetadata[]; 25 | name: string; 26 | tag: string[]; 27 | } 28 | 29 | interface ContractMetadata { 30 | name: string; 31 | transactions: TransactionMetadata[]; 32 | } 33 | 34 | interface ChaincodeMetadata { 35 | contracts: {[s: string]: ContractMetadata}; 36 | } 37 | 38 | export default class StarterServer { 39 | 40 | /** Obtain the metadata either from the contract or from the filesystem 41 | * 42 | * @return {Object} metadata object 43 | */ 44 | private swagger: any; 45 | 46 | private app: express.Application; 47 | 48 | private fabricProxy: FabricProxy; 49 | private config: Config; 50 | 51 | /** 52 | * Start the server based on the suplied configuration 53 | */ 54 | public constructor(config: Config) { 55 | this.config = config; 56 | this.swagger = { 57 | openapi: '3.0.0', 58 | } as Swagger; 59 | } 60 | 61 | public async start() { 62 | // establish connection to Fabric 63 | LOG(`Starting the FabricProxy`); 64 | this.fabricProxy = new FabricProxy(this.config.fabric); 65 | await this.fabricProxy.setup(); 66 | await this.fabricProxy.connectToContract(); 67 | 68 | // create express and setup basic routing 69 | this.app = express(); 70 | this.app.use((req: express.Request, res: express.Response, next: () => void) => { 71 | res.header('Access-Control-Allow-Origin', '*'); 72 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 73 | next(); 74 | }); 75 | 76 | const routes = await Router.getRoutes(this.fabricProxy); 77 | this.swagger = Object.assign(this.swagger, routes.swagger) as Swagger; 78 | 79 | // route to respond with the swagger json file 80 | this.app.get('/swagger.json', (req: express.Request, res: express.Response) => { 81 | res.json(this.swagger); 82 | }); 83 | 84 | // // set to use the Swagger UI on the api-docs route 85 | this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(this.swagger)); 86 | this.app.use(routes.router); 87 | 88 | this.app.listen(this.config.port, () => { 89 | LOG(`Server listening on port ${this.config.port}!`); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rest-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperledgendary/fabric-rest-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/cli.js", 6 | "typings": "dist/index.d.ts", 7 | "engines": { 8 | "node": ">=12", 9 | "npm": ">=6" 10 | }, 11 | "scripts": { 12 | "lint": "tslint -c tslint.json 'src/**/*.ts'", 13 | "pretest": "npm run lint", 14 | "test": "nyc mocha -r ts-node/register 'unittest/**/*.ts'", 15 | "test:fv": "nyc mocha -r ts-node/register 'fullfvtest/**/*.ts' --timeout 10000", 16 | "start:lf": "DEBUG=contractrest:* node dist/cli.js --identity User1@org1.example.com --wallet ../demo-zone/wallet --connection ../demo-zone/basic-network/network.yaml ", 17 | "start:swaggerui": "docker run -p 8080:8080 -e API_URL=http://localhost:3000/swagger.json swaggerapi/swagger-ui", 18 | "build": "tsc --skipLibCheck", 19 | "build:watch": "tsc -w", 20 | "build:docker": "(docker rmi $(docker images -q -f dangling=true) || true) && docker build -t hyperlegendary/fabric-rest-server .", 21 | "start:dockerlf": "docker run -p 3000:3000 --network net_basic --env DEBUG=contractrest:* --env FRS_IDENTITY=User1@org1.example.com --env FRS_CONTRACT=hellonet --env FRS_NETWORK=mychannel --env FRS_WALLET=/usr/src/wallet --env FRS_CONNECTION=/usr/src/connection/network.docker.yaml --mount type=bind,source=/home/matthew/ghe/fabric-rest-server/idwallet/,target=/usr/src/wallet --mount type=bind,source=/home/matthew/fabric-application-examples/infrastructure/basic-network/,target=/usr/src/connection calanais/fabric-rest-server" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "dependencies": { 26 | "body-parser": "^1.19.0", 27 | "ejs": "^3.1.3", 28 | "express": "^4.17.1", 29 | "fabric-client": "^2.0.0-beta.2", 30 | "fabric-contract-api": "^2.1.1", 31 | "fabric-network": "^2.1.0", 32 | "js-yaml": "^3.13.1", 33 | "jsome": "^2.5.0", 34 | "swagger-ui-express": "^4.1.4", 35 | "yargs": "^15.3.1" 36 | }, 37 | "devDependencies": { 38 | "@types/chai": "^4.1.7", 39 | "@types/chai-http": "^3.0.5", 40 | "@types/express": "^4.16.0", 41 | "@types/mocha": "^5.2.5", 42 | "@types/node": "^10.3.6", 43 | "@types/sinon": "^7.0.3", 44 | "@types/sinon-chai": "^3.2.2", 45 | "@types/swagger-ui-express": "^3.0.0", 46 | "chai": "^4.2.0", 47 | "chai-http": "^4.2.1", 48 | "eslint": "^5.6.0", 49 | "mocha": "^7.1.2", 50 | "nyc": "^15.0.1", 51 | "rewire": "^5.0.0", 52 | "sinon": "^9.0.2", 53 | "sinon-chai": "^3.3.0", 54 | "ts-node": "^7.0.0", 55 | "tslint": "^5.10.0", 56 | "typescript": "^3.9.2" 57 | }, 58 | "nyc": { 59 | "extension": [ 60 | ".ts", 61 | ".tsx" 62 | ], 63 | "exclude": [ 64 | "coverage/**", 65 | "dist/**", 66 | "unittest/**" 67 | ], 68 | "reporter": [ 69 | "text-summary", 70 | "html" 71 | ], 72 | "all": true, 73 | "check-coverage": false, 74 | "statements": 100, 75 | "branches": 100, 76 | "functions": 100, 77 | "lines": 100 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rest-server/unittest/greeting-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "contracts": { 3 | "Greeting": { 4 | "name": "Greeting", 5 | "contractInstance": { 6 | "name": "Greeting" 7 | }, 8 | "transactions": [ 9 | { 10 | "tag": [ 11 | "submitTx" 12 | ], 13 | "parameters": [], 14 | "name": "instantiate" 15 | }, 16 | { 17 | "tag": [ 18 | "submitTx" 19 | ], 20 | "parameters": [ 21 | { 22 | "name": "text", 23 | "description": "", 24 | "schema": { 25 | "type": "string" 26 | } 27 | } 28 | ], 29 | "name": "setGreetingText" 30 | }, 31 | { 32 | "tag": [ 33 | "submitTx" 34 | ], 35 | "parameters": [ 36 | { 37 | "name": "greeting", 38 | "description": "", 39 | "schema": { 40 | "$ref": "#/components/schemas/Greeting" 41 | } 42 | } 43 | ], 44 | "name": "setGreeting" 45 | }, 46 | { 47 | "returns": [ 48 | { 49 | "name": "success", 50 | "schema": { 51 | "$ref": "#/components/schemas/Greeting" 52 | } 53 | } 54 | ], 55 | "name": "getGreeting", 56 | "tag": [ 57 | "submitTx" 58 | ], 59 | "parameters": [] 60 | } 61 | ], 62 | "info": { 63 | "title": "", 64 | "version": "" 65 | } 66 | }, 67 | "org.hyperledger.fabric": { 68 | "name": "org.hyperledger.fabric", 69 | "contractInstance": { 70 | "name": "org.hyperledger.fabric" 71 | }, 72 | "transactions": [ 73 | { 74 | "name": "GetMetadata" 75 | } 76 | ], 77 | "info": { 78 | "title": "", 79 | "version": "" 80 | } 81 | } 82 | }, 83 | "info": { 84 | "version": "0.0.1", 85 | "title": "helloworld-ts" 86 | }, 87 | "components": { 88 | "schemas": { 89 | "Greeting": { 90 | "$id": "Greeting", 91 | "type": "object", 92 | "additionalProperties": false, 93 | "properties": [ 94 | { 95 | "name": "text", 96 | "schema": { 97 | "type": "string" 98 | } 99 | } 100 | ] 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /rest-server/src/cli.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | import debug from 'debug'; 8 | import * as yargs from 'yargs'; 9 | import Server from './server'; 10 | import Config from './server.config'; 11 | 12 | const LOG = debug('contractrest:config'); 13 | 14 | /** 15 | * Normalize a port into a number, string, or false. 16 | */ 17 | 18 | function normalizePort(val) { 19 | const port = parseInt(val, 10); 20 | 21 | if (isNaN(port)) { 22 | // named pipe 23 | return val; 24 | } 25 | 26 | if (port >= 0) { 27 | // port number 28 | return port; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | const results = yargs 35 | .options({ 36 | aslocalhost: { 37 | default: false, 38 | demandOption: false, 39 | describe: 'Development Option ', 40 | type: 'boolean', 41 | }, 42 | gateway: { 43 | alias: 'p', 44 | demandOption: true, 45 | describe: 'path to the yaml or json gateway profile', 46 | requiresArg: true, 47 | type: 'string', 48 | }, 49 | identity: { 50 | alias: 'i', 51 | demandOption: true, 52 | describe: 'Identity of user to connect as for discovery and runtime', 53 | }, 54 | localfile: { 55 | alias: 'f', 56 | default: '', 57 | demandOption: false, 58 | describe: 'read the metadata from a local file rather than contract', 59 | }, 60 | outputfile: { 61 | alias: 'o', 62 | default: '', 63 | demandOption: false, 64 | describe: 'Location of the file that the metadata should be written to, will overwrite any existing file', 65 | requiresArg: true, 66 | }, 67 | wallet: { 68 | alias: 'w', 69 | demandOption: true, 70 | describe: 'provide a path to wallet directory', 71 | }, 72 | network: { 73 | alias: 'n', 74 | demandOption: true, 75 | describe: 'Network (channel) name to connect to', 76 | }, 77 | contract: { 78 | alias: 'c', 79 | demandOption: true, 80 | describe: 'name of the contract to connect to', 81 | }, 82 | }) 83 | .help() 84 | .example('starter-rest-server --identity --gateway ') 85 | .wrap(null) 86 | .epilogue('REST server for Smart Contracts') 87 | .alias('v', 'version') 88 | .version('pre-alpha') 89 | .describe('v', 'show version information') 90 | .env('FRS') 91 | .argv; 92 | 93 | LOG(results); 94 | // setup the config here.. 95 | let config: Config; 96 | config = { 97 | fabric: { 98 | aslocalhost: results.aslocalhost, 99 | network: results.network, 100 | contract: results.contract, 101 | identityLabel: results.identity, 102 | walletpath: results.wallet, 103 | gateway: results.gateway, 104 | 105 | }, 106 | localFile: results.localfile, 107 | outputFile: results.outputfile, 108 | port: normalizePort(process.env.PORT || '3000'), 109 | }; 110 | LOG(config); 111 | const server = new Server(config); 112 | 113 | try { 114 | server.start().catch((e) => { 115 | LOG(e); 116 | throw e; 117 | }); 118 | } catch (error) { 119 | LOG(error); 120 | process.exit(-1); 121 | } 122 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/channels/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import * as chai from 'chai'; 9 | import 'mocha'; 10 | import rewire = require('rewire'); 11 | import * as sinon from 'sinon'; 12 | import * as sinonChai from 'sinon-chai'; 13 | import FabricProxy from '../../../src/fabricproxy'; 14 | import { ChannelRouter as ChannelRouterImport } from '../../../src/routes/channels'; 15 | import { ChaincodeRouter } from '../../../src/routes/channels/chaincode'; 16 | import { fakeExtendsPaths, sampleFabricProxy } from '../../utils'; 17 | 18 | chai.use(sinonChai); 19 | const expect = chai.expect; 20 | 21 | const ChannelRouterRewired = rewire('../../../src/routes/channels'); 22 | const ChannelRouter: typeof ChannelRouterImport & typeof ChannelRouterRewired = ChannelRouterRewired.ChannelRouter as any; 23 | 24 | describe('Chaincode', () => { 25 | let fabricProxy: FabricProxy; 26 | 27 | let sandbox: sinon.SinonSandbox; 28 | 29 | beforeEach(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | afterEach(() => { 34 | sandbox.restore(); 35 | }); 36 | 37 | it ('should build up its router and swagger using chaincodes in the channel', async () => { 38 | fabricProxy = sampleFabricProxy(); 39 | 40 | const getChannelContractsStub = sandbox.stub(fabricProxy, 'getChannels').returns(['some channel', 'another channel']); 41 | const channelRouterGetRoutesStub = sandbox.stub(ChaincodeRouter, 'getRoutes').onFirstCall().resolves({ 42 | router: 'some channel router' as any, 43 | swagger: 'some channel swagger' as any, 44 | }).onSecondCall().resolves({ 45 | router: 'another channel router' as any, 46 | swagger: 'another channel swagger' as any, 47 | }); 48 | 49 | const utils = ChannelRouterRewired.__get__('utils_1'); // _1 as typescript adds suffix 50 | const oldExtendPaths = utils.extendPaths; 51 | 52 | utils.extendPaths = fakeExtendsPaths; 53 | 54 | const useStub = sinon.stub(); 55 | 56 | const express = ChannelRouterRewired.__get__('express_1'); 57 | const oldRouter = express.Router; 58 | 59 | express.Router = () => { 60 | return { 61 | special: 'router', 62 | use: useStub, 63 | }; 64 | }; 65 | 66 | const {router, swagger} = await ChannelRouter.getRoutes(fabricProxy, 'some channel'); 67 | 68 | sinon.assert.calledOnce(getChannelContractsStub); 69 | sinon.assert.calledWithExactly(getChannelContractsStub); 70 | 71 | sinon.assert.calledTwice(channelRouterGetRoutesStub); 72 | sinon.assert.calledWithExactly(channelRouterGetRoutesStub, fabricProxy, 'some channel'); 73 | sinon.assert.calledWithExactly(channelRouterGetRoutesStub, fabricProxy, 'another channel'); 74 | 75 | sinon.assert.calledTwice(useStub); 76 | sinon.assert.calledWithExactly(useStub, '/some channel', 'some channel router'); 77 | sinon.assert.calledWithExactly(useStub, '/another channel', 'another channel router'); 78 | 79 | expect(router).to.deep.eq(express.Router()); 80 | expect(swagger).to.deep.eq({ 81 | components: { 82 | schemas: { 83 | 'another channel': 'a schema', 84 | 'some channel': 'a schema', 85 | }, 86 | }, 87 | paths: { 88 | 'another channel': 'a path', 89 | 'some channel': 'a path', 90 | }, 91 | }); 92 | 93 | utils.extendPaths = oldExtendPaths; 94 | express.Router = oldRouter; 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/channels/chaincode/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import * as chai from 'chai'; 9 | import 'mocha'; 10 | import rewire = require('rewire'); 11 | import * as sinon from 'sinon'; 12 | import * as sinonChai from 'sinon-chai'; 13 | import FabricProxy from '../../../../src/fabricproxy'; 14 | import { ChaincodeRouter as ChaincodeRouterImport } from '../../../../src/routes/channels/chaincode'; 15 | import { ContractRouter } from '../../../../src/routes/channels/chaincode/contract'; 16 | import { fakeExtendsPaths, sampleFabricProxy } from '../../../utils'; 17 | 18 | chai.use(sinonChai); 19 | const expect = chai.expect; 20 | 21 | const ChaincodeRouterRewired = rewire('../../../../src/routes/channels/chaincode'); 22 | const ChaincodeRouter: typeof ChaincodeRouterImport & typeof ChaincodeRouterRewired = ChaincodeRouterRewired.ChaincodeRouter as any; 23 | 24 | describe('Chaincode', () => { 25 | let fabricProxy: FabricProxy; 26 | 27 | let sandbox: sinon.SinonSandbox; 28 | 29 | beforeEach(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | afterEach(() => { 34 | sandbox.restore(); 35 | }); 36 | 37 | describe('getRoutes', () => { 38 | it ('should build up its router and swagger using chaincodes in the channel', async () => { 39 | fabricProxy = sampleFabricProxy(); 40 | 41 | const getChannelContractsStub = sandbox.stub(fabricProxy, 'getChannelContracts').returns(['some chaincode', 'another chaincode']); 42 | const contractRouterGetRoutesStub = sandbox.stub(ContractRouter, 'getRoutes').onFirstCall().resolves({ 43 | router: 'some chaincode router' as any, 44 | swagger: 'some chaincode swagger' as any, 45 | }).onSecondCall().resolves({ 46 | router: 'another chaincode router' as any, 47 | swagger: 'another chaincode swagger' as any, 48 | }); 49 | 50 | const utils = ChaincodeRouterRewired.__get__('utils_1'); // _1 as typescript adds suffix 51 | const oldExtendPaths = utils.extendPaths; 52 | 53 | utils.extendPaths = fakeExtendsPaths; 54 | 55 | const useStub = sinon.stub(); 56 | 57 | const express = ChaincodeRouterRewired.__get__('express_1'); 58 | const oldRouter = express.Router; 59 | 60 | express.Router = () => { 61 | return { 62 | special: 'router', 63 | use: useStub, 64 | }; 65 | }; 66 | 67 | const {router, swagger} = await ChaincodeRouter.getRoutes(fabricProxy, 'some channel'); 68 | 69 | sinon.assert.calledOnce(getChannelContractsStub); 70 | sinon.assert.calledWithExactly(getChannelContractsStub, 'some channel'); 71 | 72 | sinon.assert.calledTwice(contractRouterGetRoutesStub); 73 | sinon.assert.calledWithExactly(contractRouterGetRoutesStub, fabricProxy, 'some channel', 'some chaincode'); 74 | sinon.assert.calledWithExactly(contractRouterGetRoutesStub, fabricProxy, 'some channel', 'another chaincode'); 75 | 76 | sinon.assert.calledTwice(useStub); 77 | sinon.assert.calledWithExactly(useStub, '/some chaincode', 'some chaincode router'); 78 | sinon.assert.calledWithExactly(useStub, '/another chaincode', 'another chaincode router'); 79 | 80 | expect(router).to.deep.eq(express.Router()); 81 | expect(swagger).to.deep.eq({ 82 | components: { 83 | schemas: { 84 | 'another chaincode': 'a schema', 85 | 'some chaincode': 'a schema', 86 | }, 87 | }, 88 | paths: { 89 | 'another chaincode': 'a path', 90 | 'some chaincode': 'a path', 91 | }, 92 | }); 93 | 94 | utils.extendPaths = oldExtendPaths; 95 | express.Router = oldRouter; 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /rest-server/src/routes/channels/chaincode/contract/transaction/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import { Request, Response, Router as ExpressRouter } from 'express'; 3 | import FabricProxy from '../../../../../fabricproxy'; 4 | import { ContractMetadata, TransactionMetadata } from '../../../../../interfaces/metadata_interfaces'; 5 | import { Swagger } from '../../../../../interfaces/swagger'; 6 | 7 | export class TransactionRouter { 8 | public static getRoutes(fabricProxy: FabricProxy, channel: string, chaincode: string, contract: ContractMetadata): { router: ExpressRouter, swagger: Swagger } { // tslint:disable:max-line-length 9 | const router = ExpressRouter(); 10 | 11 | const swagger = { 12 | paths: {}, 13 | }; 14 | 15 | for (const transaction of contract.transactions) { 16 | this.addSwaggerPath(swagger, transaction, [`${channel}/${chaincode}`]); 17 | 18 | this.addRouterPath(router, transaction, fabricProxy, channel, chaincode, contract.name); 19 | } 20 | 21 | return { router, swagger }; 22 | } 23 | 24 | private static addSwaggerPath(swagger: {paths: object }, transaction: TransactionMetadata, tags: string[]) { 25 | const requestBody = this.processTxParameters(transaction.parameters); 26 | 27 | const action = { 28 | operationId: transaction.name, 29 | requestBody, 30 | responses: { 31 | 200: { 32 | description: 'successful operation', 33 | }, 34 | }, 35 | tags, 36 | }; 37 | swagger.paths['/' + transaction.name] = {}; 38 | swagger.paths['/' + transaction.name].post = action; 39 | } 40 | 41 | private static addRouterPath(router: ExpressRouter, transaction: TransactionMetadata, fabricProxy: FabricProxy, channel: string, chaincode: string, contract: string) { 42 | const jsonParser = bodyParser.json(); 43 | 44 | router.post('/' + transaction.name, jsonParser, async (req: Request, res: Response) => { 45 | const args = []; 46 | 47 | if (transaction.parameters) { 48 | const missingParams = []; 49 | 50 | transaction.parameters.forEach((param) => { 51 | if (req.body.hasOwnProperty(param.name)) { 52 | const rawData = req.body[param.name]; 53 | 54 | if (param.schema.type && param.schema.type === 'string') { 55 | args.push(rawData); 56 | } else { 57 | args.push(JSON.stringify(rawData)); 58 | } 59 | } else { 60 | missingParams.push(param.name); 61 | } 62 | }); 63 | 64 | if (missingParams.length > 0) { 65 | res.status(400); 66 | res.json({ 67 | msg: ['Bad request. Missing parameters: ' + missingParams.join(', ')], 68 | }); 69 | return; 70 | } 71 | } 72 | 73 | try { 74 | let data; 75 | if (transaction.tag && transaction.tag.includes('submitTx')) { 76 | data = await fabricProxy.submitTransaction(channel, chaincode, contract, transaction.name, ...args); 77 | } else { 78 | data = await fabricProxy.evaluateTransaction(channel, chaincode, contract, transaction.name, ...args); 79 | } 80 | 81 | if (data && data.length > 0) { 82 | try { 83 | const response = JSON.parse(data.toString()); 84 | res.json(response); 85 | } catch (err) { 86 | if (err instanceof SyntaxError) { 87 | res.send(data.toString()); 88 | } else { 89 | throw err; 90 | } 91 | } 92 | } else { 93 | res.sendStatus(204); 94 | } 95 | } catch (error) { 96 | const e = { 97 | msg: error.message.split('\n'), 98 | }; 99 | console.log(e); 100 | res.status(500); 101 | res.json(e); 102 | } 103 | 104 | }); 105 | } 106 | 107 | private static processTxParameters(parameters): object { 108 | if (!parameters) { 109 | parameters = []; 110 | } 111 | 112 | const properties = {}; 113 | parameters.forEach((p) => { 114 | properties[p.name] = p.schema; 115 | }); 116 | 117 | const requestBody = { 118 | content : { 119 | 'application/json': { 120 | schema: { 121 | properties, 122 | type: 'object', 123 | }, 124 | }, 125 | }, 126 | }; 127 | return requestBody; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /rest-server/src/fabricproxy.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import { Wallets, Gateway, Wallet, Network, Contract } from 'fabric-network'; 9 | import * as path from 'path'; 10 | import * as fs from 'fs'; 11 | import * as yaml from 'js-yaml'; 12 | import FabricConfig from './fabricproxy.config'; 13 | 14 | import debug from 'debug'; 15 | const LOG = debug('contractrest:fabricproxy'); 16 | 17 | 18 | export default class FabricProxy { 19 | 20 | public wallet: Wallet; 21 | public config: FabricConfig; 22 | 23 | private connectionProfile: any; 24 | 25 | private gateway: Gateway | undefined; 26 | private network: Network | undefined; 27 | private contract: Contract | undefined; 28 | 29 | constructor(config: FabricConfig) { 30 | this.config = config; 31 | } 32 | 33 | public async setup() { 34 | this.wallet = await Wallets.newFileSystemWallet(this.config.walletpath); 35 | // load the profile from either json or yaml 36 | this.readGatewayProfile(); 37 | } 38 | 39 | public getChannels(): string[] { 40 | return [this.config.network]; 41 | } 42 | 43 | public getChannelContracts(name: string): string[] { 44 | return [this.config.contract]; 45 | } 46 | 47 | public async connectToContract() { 48 | 49 | // A gateway defines the peers used to access Fabric networks 50 | this.gateway = new Gateway(); 51 | 52 | // Main try/catch block 53 | try { 54 | 55 | // define the identity to use 56 | const identityLabel = this.config.identityLabel; 57 | 58 | LOG(`Read the connection profile`); 59 | // Set connection options; use 'admin' identity from application wallet 60 | const connectionOptions = { 61 | discovery: { enabled: true, asLocalhost: this.config.aslocalhost }, 62 | identity: identityLabel, 63 | wallet: this.wallet, 64 | }; 65 | 66 | LOG('Connecting to Gateway'); 67 | // Connect to gateway using application specified parameters 68 | await this.gateway.connect(this.connectionProfile, connectionOptions); 69 | this.network = await this.gateway.getNetwork(this.config.network); 70 | this.contract = await this.network.getContract(this.config.contract); 71 | 72 | } catch (error) { 73 | LOG(`Error processing connection. ${error}`); 74 | LOG(error.stack); 75 | throw error; 76 | } 77 | } 78 | 79 | public async getMetaData(channelName: string, contractName: string): Promise { 80 | try { 81 | const response = await this.evaluateTransaction(channelName, contractName, 'org.hyperledger.fabric', 'GetMetadata'); 82 | 83 | return JSON.parse(response.toString()); 84 | } catch (error) { 85 | LOG(`Error getting metadata. ${error}`); 86 | LOG(error.stack); 87 | throw error; 88 | } 89 | } 90 | 91 | public async evaluateTransaction(channelName: string, contractName: string, namespace: string, functionName: string, ...args: string[]): Promise { 92 | try { 93 | return await this.contract.evaluateTransaction(`${namespace}:${functionName}`, ...args); 94 | } catch (error) { 95 | LOG(`Error evaluating transaction. ${error}`); 96 | LOG(error.stack); 97 | throw error; 98 | } 99 | } 100 | 101 | public async submitTransaction(channelName: string, contractName: string, namespace: string, functionName: string, ...args: string[]): Promise { 102 | try { 103 | return await this.contract.submitTransaction(`${namespace}:${functionName}`, ...args); 104 | } catch (error) { 105 | LOG(`Error submitting transaction. ${error}`); 106 | LOG(error.stack); 107 | throw error; 108 | } 109 | } 110 | 111 | private readGatewayProfile() { 112 | LOG('Parsing connection profile'); 113 | try { 114 | let filename = path.resolve(this.config.gateway); 115 | if (!fs.existsSync(filename)){ 116 | throw new Error(`Gateway profile does not exist ${this.config.gateway}`); 117 | } 118 | 119 | let jsonRegex = /jso?n/i; 120 | let yamlRegex = /ya?ml/i; 121 | let extension = path.extname(filename); 122 | if (jsonRegex.test(extension)){ 123 | this.connectionProfile = JSON.parse(fs.readFileSync(filename,'utf8')); 124 | } else if (yamlRegex.test(extension)){ 125 | this.connectionProfile = yaml.safeLoad(fs.readFileSync(filename, 'utf8')); 126 | } else { 127 | throw new Error (`Unclear what format the gateway profile is in ${filename}`) 128 | } 129 | 130 | } catch (error) { 131 | LOG(`Error parsing connection profile. ${error}`); 132 | LOG(error.stack); 133 | throw error; 134 | } 135 | } 136 | 137 | 138 | 139 | 140 | 141 | } 142 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/utils.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Copyright IBM Corp All Rights Reserved 5 | * 6 | * SPDX-License-Identifier: Apache-2.0 7 | */ 8 | 9 | import * as chai from 'chai'; 10 | import 'mocha'; 11 | import rewire = require('rewire'); 12 | import * as sinon from 'sinon'; 13 | import * as sinonChai from 'sinon-chai'; 14 | import * as UtilsImport from '../../src/routes/utils'; 15 | 16 | const UtilsRewired = rewire('../../src/routes/utils'); 17 | const utils: typeof UtilsImport & typeof UtilsRewired = UtilsRewired as any; 18 | 19 | chai.use(sinonChai); 20 | const expect = chai.expect; 21 | 22 | describe('Router - utils', () => { 23 | describe('extendPaths', () => { 24 | 25 | let oldExtendRefs: (pathStart: string, component: string, json: object) => void; 26 | let extendRefsStub: sinon.SinonStub; 27 | 28 | before(() => { 29 | oldExtendRefs = utils.__get__('extendRefs'); 30 | }); 31 | 32 | beforeEach(() => { 33 | extendRefsStub = sinon.stub(); 34 | utils.__set__('extendRefs', extendRefsStub); 35 | }); 36 | 37 | after(() => { 38 | utils.__set__('extendRefs', oldExtendRefs); 39 | }); 40 | 41 | it ('should update the paths key to include start passed', () => { 42 | const swagger = utils.extendPaths('some start', { 43 | paths: { 44 | '/another existing path': 'another special path', 45 | '/existing path': 'some special path', 46 | }, 47 | }); 48 | 49 | expect(swagger).to.deep.equal({ 50 | paths: { 51 | '/some start/another existing path': 'another special path', 52 | '/some start/existing path': 'some special path', 53 | }, 54 | }); 55 | }); 56 | 57 | it ('should replace component keys with start passed and call function to update refs', () => { 58 | const swagger = utils.extendPaths('some start', { 59 | components: { 60 | schemas: { 61 | 'another component': 'another component\'s info', 62 | 'some component': 'some component\'s info', 63 | }, 64 | }, 65 | extraField: 'some extra field', 66 | }); 67 | 68 | const objectMatcher = (value) => { 69 | return value.hasOwnProperty('components') && !value.hasOwnProperty('extraField'); 70 | }; 71 | 72 | expect(swagger).to.deep.equal({ 73 | components: { 74 | schemas: { 75 | 'some start.another component': 'another component\'s info', 76 | 'some start.some component': 'some component\'s info', 77 | }, 78 | }, 79 | }); 80 | 81 | sinon.assert.calledTwice(extendRefsStub); 82 | sinon.assert.calledWithExactly(extendRefsStub, 'some start', 'another component', sinon.match(objectMatcher)); 83 | sinon.assert.calledWithExactly(extendRefsStub, 'some start', 'some component', sinon.match(objectMatcher)); 84 | }); 85 | 86 | it ('should handle both paths and components', () => { 87 | const swagger = utils.extendPaths('some start', { 88 | components: { 89 | schemas: { 90 | 'another component': 'another component\'s info', 91 | 'some component': 'some component\'s info', 92 | }, 93 | }, 94 | extraField: 'some extra field', 95 | paths: { 96 | '/another existing path': 'another special path', 97 | '/existing path': 'some special path', 98 | }, 99 | }); 100 | 101 | const objectMatcher = (value) => { 102 | return value.hasOwnProperty('paths') && value.hasOwnProperty('components') && !value.hasOwnProperty('extraField'); 103 | }; 104 | 105 | expect(swagger).to.deep.equal({ 106 | components: { 107 | schemas: { 108 | 'some start.another component': 'another component\'s info', 109 | 'some start.some component': 'some component\'s info', 110 | }, 111 | }, 112 | paths: { 113 | '/some start/another existing path': 'another special path', 114 | '/some start/existing path': 'some special path', 115 | }, 116 | }); 117 | 118 | sinon.assert.calledTwice(extendRefsStub); 119 | sinon.assert.calledWithExactly(extendRefsStub, 'some start', 'another component', sinon.match(objectMatcher)); 120 | sinon.assert.calledWithExactly(extendRefsStub, 'some start', 'some component', sinon.match(objectMatcher)); 121 | }); 122 | }); 123 | 124 | describe('extendRefs', () => { 125 | it ('should replace a $ref that points to a component in json with the passed start included', () => { 126 | const sampleJson = { 127 | $ref: '#/components/schemas/some component', 128 | }; 129 | 130 | utils.__get__('extendRefs')('some start', 'some component', sampleJson); 131 | 132 | expect(sampleJson).to.deep.eq({ 133 | $ref: '#/components/schemas/some start.some component', 134 | }); 135 | }); 136 | 137 | it ('should replace nested $refs', () => { 138 | const sampleJson = { 139 | $ref: '#/components/schemas/some component', 140 | anotherField: { 141 | $ref: '#/components/schemas/some component', 142 | downAgain: { 143 | $ref: '#/components/schemas/some component', 144 | }, 145 | }, 146 | }; 147 | 148 | utils.__get__('extendRefs')('some start', 'some component', sampleJson); 149 | 150 | expect(sampleJson).to.deep.eq({ 151 | $ref: '#/components/schemas/some start.some component', 152 | anotherField: { 153 | $ref: '#/components/schemas/some start.some component', 154 | downAgain: { 155 | $ref: '#/components/schemas/some start.some component', 156 | }, 157 | }, 158 | }); 159 | }); 160 | 161 | it ('should leave refs to other components alone', () => { 162 | const sampleJson = { 163 | $ref: '#/components/schemas/some component', 164 | anotherField: { 165 | $ref: '#/components/schemas/another component', 166 | downAgain: { 167 | $ref: '#/components/schemas/some component', 168 | }, 169 | }, 170 | }; 171 | 172 | utils.__get__('extendRefs')('some start', 'some component', sampleJson); 173 | 174 | expect(sampleJson).to.deep.eq({ 175 | $ref: '#/components/schemas/some start.some component', 176 | anotherField: { 177 | $ref: '#/components/schemas/another component', 178 | downAgain: { 179 | $ref: '#/components/schemas/some start.some component', 180 | }, 181 | }, 182 | }); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /rest-server/fullfvtest/fabcar.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | import * as chai from 'chai'; 9 | import 'mocha'; 10 | import * as sinon from 'sinon'; 11 | import * as sinonChai from 'sinon-chai'; 12 | 13 | import chaiHttp = require('chai-http'); 14 | 15 | const should = chai.should(); 16 | chai.use(sinonChai); 17 | chai.use(chaiHttp); 18 | const expect = chai.expect; 19 | 20 | const baseURL = 'http://localhost:3000'; 21 | // tslint:disable:no-unused-expression 22 | describe('FabCar FV Tests', () => { 23 | 24 | describe('Basic operations', () => { 25 | it('should get back all the default cars', async () => { 26 | // need to make a rest call now 27 | // curl -X POST "http://localhost:3000/fabcarchannel/fabcarnetwork/FabCar/queryAllCars" 28 | // -H "accept: */*" -H "Content-Type: application/json" -d "{}" 29 | const response = await chai.request(baseURL) 30 | .post('/fabcarchannel/fabcarnetwork/FabCar/queryAllCars') 31 | .send({}); 32 | 33 | expect(response).to.have.status(200); 34 | expect(response).to.be.json; 35 | const body = response.body; 36 | body.should.deep.equal([ 37 | { 38 | Key: 'CAR0', 39 | car: { 40 | color: 'blue', 41 | docType: 'car', 42 | make: 'Toyota', 43 | model: 'Prius', 44 | owner: 'Tomoko', 45 | }, 46 | }, 47 | { 48 | Key: 'CAR1', 49 | car: { 50 | color: 'red', 51 | docType: 'car', 52 | make: 'Ford', 53 | model: 'Mustang', 54 | owner: 'Brad', 55 | }, 56 | }, 57 | { 58 | Key: 'CAR2', 59 | car: { 60 | color: 'green', 61 | docType: 'car', 62 | make: 'Hyundai', 63 | model: 'Tucson', 64 | owner: 'Jin Soo', 65 | }, 66 | }, 67 | { 68 | Key: 'CAR3', 69 | car: { 70 | color: 'yellow', 71 | docType: 'car', 72 | make: 'Volkswagen', 73 | model: 'Passat', 74 | owner: 'Max', 75 | }, 76 | }, 77 | { 78 | Key: 'CAR4', 79 | car: { 80 | color: 'black', 81 | docType: 'car', 82 | make: 'Tesla', 83 | model: 'S', 84 | owner: 'Adriana', 85 | }, 86 | }, 87 | { 88 | Key: 'CAR5', 89 | car: { 90 | color: 'purple', 91 | docType: 'car', 92 | make: 'Peugeot', 93 | model: '205', 94 | owner: 'Michel', 95 | }, 96 | }, 97 | { 98 | Key: 'CAR6', 99 | car: { 100 | color: 'white', 101 | docType: 'car', 102 | make: 'Chery', 103 | model: 'S22L', 104 | owner: 'Aarav', 105 | }, 106 | }, 107 | { 108 | Key: 'CAR7', 109 | car: { 110 | color: 'violet', 111 | docType: 'car', 112 | make: 'Fiat', 113 | model: 'Punto', 114 | owner: 'Pari', 115 | }, 116 | }, 117 | { 118 | Key: 'CAR8', 119 | car: { 120 | color: 'indigo', 121 | docType: 'car', 122 | make: 'Tata', 123 | model: 'Nano', 124 | owner: 'Valeria', 125 | }, 126 | }, 127 | { 128 | Key: 'CAR9', 129 | car: { 130 | color: 'brown', 131 | docType: 'car', 132 | make: 'Holden', 133 | model: 'Barina', 134 | owner: 'Shotaro', 135 | }, 136 | }, 137 | ]); 138 | 139 | }); 140 | 141 | it('should get a single car', async () => { 142 | const response = await chai.request(baseURL) 143 | .post('/fabcarchannel/fabcarnetwork/FabCar/queryCar') 144 | .send({ carNumber: 'CAR2' }); 145 | expect(response).to.have.status(200); 146 | expect(response).to.be.json; 147 | const body = response.body; 148 | body.should.deep.equal({ 149 | color: 'green', 150 | docType: 'car', 151 | make: 'Hyundai', 152 | model: 'Tucson', 153 | owner: 'Jin Soo', 154 | }); 155 | }); 156 | 157 | it('should update a single car', async () => { 158 | const response1 = await chai.request(baseURL) 159 | .post('/fabcarchannel/fabcarnetwork/FabCar/changeCarOwner') 160 | .send({ carNumber: 'CAR2', newOwner: 'Barny Rubble' }); 161 | 162 | expect(response1).to.have.status(204); 163 | expect(response1.body).to.deep.equal({}); 164 | const response2 = await chai.request(baseURL) 165 | .post('/fabcarchannel/fabcarnetwork/FabCar/queryCar') 166 | .send({ carNumber: 'CAR2' }); 167 | 168 | expect(response2).to.have.status(200); 169 | expect(response2).to.be.json; 170 | const body = response2.body; 171 | body.should.deep.equal({ 172 | color: 'green', 173 | docType: 'car', 174 | make: 'Hyundai', 175 | model: 'Tucson', 176 | owner: 'Barny Rubble', 177 | }); 178 | }); 179 | 180 | it('should create a single car', async () => { 181 | const response1 = await chai.request(baseURL) 182 | .post('/fabcarchannel/fabcarnetwork/FabCar/createCar') 183 | .send({ 184 | carNumber: 'CARA', color: 'grey', 185 | make: 'Stone', 186 | model: 'impractical', owner: 'Fred Flintsone', 187 | }); 188 | 189 | expect(response1).to.have.status(204); 190 | expect(response1.body).to.deep.equal({}); 191 | 192 | 193 | const response2 = await chai.request(baseURL) 194 | .post('/fabcarchannel/fabcarnetwork/FabCar/queryCar') 195 | .send({ carNumber: 'CARA' }); 196 | expect(response2).to.have.status(200); 197 | expect(response2).to.be.json; 198 | const body = response2.body; 199 | body.should.deep.equal({ 200 | color: 'grey', 201 | docType: 'car', 202 | make: 'Stone', 203 | model: 'impractical', owner: 'Fred Flintsone', 204 | }); 205 | 206 | }); 207 | 208 | }); 209 | 210 | }); 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/channels/chaincode/contract/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | // tslint:disable:max-line-length no-string-literal 9 | 10 | import * as chai from 'chai'; 11 | import 'mocha'; 12 | import rewire = require('rewire'); 13 | import * as sinon from 'sinon'; 14 | import * as sinonChai from 'sinon-chai'; 15 | import FabricProxy from '../../../../../src/fabricproxy'; 16 | import { ContractRouter as ContractRouterImport } from '../../../../../src/routes/channels/chaincode/contract'; 17 | import { TransactionRouter } from '../../../../../src/routes/channels/chaincode/contract/transaction'; 18 | import { fakeExtendsPaths, sampleFabricProxy } from '../../../../utils'; 19 | 20 | chai.use(sinonChai); 21 | const expect = chai.expect; 22 | 23 | const ContractRouterRewired = rewire('../../../../../src/routes/channels/chaincode/contract'); 24 | const ContractRouter: typeof ContractRouterImport & typeof ContractRouterRewired = ContractRouterRewired.ContractRouter as any; 25 | 26 | describe('Contract', () => { 27 | let fabricProxy: FabricProxy; 28 | 29 | const sampleChaincodeMetadata = { 30 | components: { 31 | schemas: { 32 | Duck: { 33 | $id: 'Duck', 34 | additionalProperties: false, 35 | properties: [ 36 | { 37 | name: 'id', 38 | schema: { 39 | type: 'string', 40 | }, 41 | }, 42 | { 43 | name: 'name', 44 | schema: { 45 | type: 'string', 46 | }, 47 | }, 48 | ], 49 | type: 'object', 50 | }, 51 | Flock: { 52 | $id: 'Flock', 53 | additionalProperties: false, 54 | properties: [ 55 | { 56 | name: 'id', 57 | schema: { 58 | type: 'string', 59 | }, 60 | }, 61 | { 62 | name: 'consistsOf', 63 | schema: { 64 | items: { 65 | $ref: '#/components/schemas/Duck', 66 | }, 67 | type: 'array', 68 | }, 69 | }, 70 | ], 71 | type: 'object', 72 | }, 73 | }, 74 | }, 75 | contracts: { 76 | TestContract: { 77 | contractInstance: { 78 | default: true, 79 | name: 'TestContract', 80 | }, 81 | info: { 82 | title: 'TestContract', 83 | version: '1.0.0', 84 | }, 85 | name: 'TestContract', 86 | transactions: [ 87 | { 88 | name: 'createDuck', 89 | parameters: [ 90 | { 91 | description: '', 92 | name: 'id', 93 | schema: { 94 | type: 'string', 95 | }, 96 | }, 97 | { 98 | description: '', 99 | name: 'name', 100 | schema: { 101 | type: 'string', 102 | }, 103 | }, 104 | ], 105 | tag: [ 106 | 'submitTx', 107 | ], 108 | }, 109 | { 110 | name: 'getDuck', 111 | parameters: [ 112 | { 113 | description: '', 114 | name: 'id', 115 | schema: { 116 | type: 'string', 117 | }, 118 | }, 119 | ], 120 | returns: [ 121 | { 122 | name: 'success', 123 | schema: { 124 | $ref: '#/components/schemas/Duck', 125 | }, 126 | }, 127 | ], 128 | tag: [], 129 | }, 130 | { 131 | name: 'getDucks', 132 | parameters: [ 133 | { 134 | description: '', 135 | name: 'ids', 136 | schema: { 137 | items: { 138 | $ref: '#/components/schemas/Duck', 139 | }, 140 | type: 'array', 141 | }, 142 | }, 143 | ], 144 | returns: [ 145 | { 146 | name: 'success', 147 | schema: { 148 | items: { 149 | $ref: '#/components/schemas/Duck', 150 | }, 151 | type: 'array', 152 | }, 153 | }, 154 | ], 155 | tag: [], 156 | }, 157 | { 158 | name: 'createFlock', 159 | parameters: [ 160 | { 161 | description: '', 162 | name: 'id', 163 | schema: { 164 | type: 'string', 165 | }, 166 | }, 167 | ], 168 | tag: [ 169 | 'submitTx', 170 | ], 171 | }, 172 | { 173 | name: 'getFlock', 174 | parameters: [ 175 | { 176 | description: '', 177 | name: 'id', 178 | schema: { 179 | type: 'string', 180 | }, 181 | }, 182 | ], 183 | returns: [ 184 | { 185 | name: 'success', 186 | schema: { 187 | $ref: '#/components/schemas/Flock', 188 | }, 189 | }, 190 | ], 191 | tag: [], 192 | }, 193 | { 194 | name: 'addToFlock', 195 | parameters: [ 196 | { 197 | description: '', 198 | name: 'flockId', 199 | schema: { 200 | type: 'string', 201 | }, 202 | }, 203 | { 204 | description: '', 205 | name: 'duckId', 206 | schema: { 207 | type: 'string', 208 | }, 209 | }, 210 | ], 211 | returns: [ 212 | { 213 | name: 'success', 214 | schema: { 215 | type: 'string', 216 | }, 217 | }, 218 | ], 219 | tag: [ 220 | 'submitTx', 221 | ], 222 | }, 223 | ], 224 | }, 225 | 'org.hyperledger.fabric': { 226 | contractInstance: { 227 | name: 'org.hyperledger.fabric', 228 | }, 229 | info: { 230 | title: 'org.hyperledger.fabric', 231 | version: '1.0.0', 232 | }, 233 | name: 'org.hyperledger.fabric', 234 | transactions: [ 235 | { 236 | name: 'GetMetadata', 237 | }, 238 | ], 239 | }, 240 | }, 241 | info: { 242 | title: 'ts_chaincode', 243 | version: '0.0.3', 244 | }, 245 | }; 246 | 247 | const sampleSwaggerComponent = { 248 | schemas: { 249 | Duck: { 250 | additionalProperties: false, 251 | properties: { 252 | id: { 253 | type: 'string', 254 | }, 255 | name: { 256 | type: 'string', 257 | }, 258 | }, 259 | type: 'object', 260 | }, 261 | Flock: { 262 | additionalProperties: false, 263 | properties: { 264 | consistsOf: { 265 | items: { 266 | $ref: '#/components/schemas/Duck', 267 | }, 268 | type: 'array', 269 | }, 270 | id: { 271 | type: 'string', 272 | }, 273 | }, 274 | type: 'object', 275 | }, 276 | }, 277 | }; 278 | 279 | let sandbox: sinon.SinonSandbox; 280 | 281 | beforeEach(() => { 282 | sandbox = sinon.createSandbox(); 283 | }); 284 | 285 | afterEach(() => { 286 | sandbox.restore(); 287 | }); 288 | 289 | describe('getRoutes', () => { 290 | it('should build up its router and swagger using contracts in the chaincode', async () => { 291 | fabricProxy = sampleFabricProxy(); 292 | 293 | sandbox.stub(fabricProxy, 'getMetaData').resolves(sampleChaincodeMetadata); 294 | 295 | const processComponentsStub = sandbox.stub(ContractRouter, 'processComponents' as any).returns(sampleSwaggerComponent); 296 | const transactionRouterGetRoutesStub = sandbox.stub(TransactionRouter, 'getRoutes').onFirstCall().returns({ 297 | router: 'some TestContract router' as any, 298 | swagger: 'some TestContract swagger' as any, 299 | }).onSecondCall().returns({ 300 | router: 'some org.hyperledger.fabric router' as any, 301 | swagger: 'some org.hyperledger.fabric swagger' as any, 302 | }); 303 | 304 | const utils = ContractRouterRewired.__get__('utils_1'); // _1 as typescript adds suffix 305 | const oldExtendPaths = utils.extendPaths; 306 | 307 | utils.extendPaths = fakeExtendsPaths; 308 | 309 | const useStub = sinon.stub(); 310 | 311 | const express = ContractRouterRewired.__get__('express_1'); 312 | const oldRouter = express.Router; 313 | 314 | express.Router = () => { 315 | return { 316 | special: 'router', 317 | use: useStub, 318 | }; 319 | }; 320 | 321 | const {router, swagger} = await ContractRouter.getRoutes(fabricProxy, 'some channel', 'some chaincode'); 322 | 323 | sinon.assert.calledOnce(processComponentsStub); 324 | sinon.assert.calledWithExactly(processComponentsStub, sampleChaincodeMetadata.components); 325 | 326 | sinon.assert.calledTwice(transactionRouterGetRoutesStub); 327 | sinon.assert.calledWithExactly(transactionRouterGetRoutesStub, fabricProxy, 'some channel', 'some chaincode', sampleChaincodeMetadata.contracts.TestContract); 328 | sinon.assert.calledWithExactly(transactionRouterGetRoutesStub, fabricProxy, 'some channel', 'some chaincode', sampleChaincodeMetadata.contracts['org.hyperledger.fabric']); 329 | 330 | sinon.assert.calledTwice(useStub); 331 | sinon.assert.calledWithExactly(useStub, '/TestContract', 'some TestContract router'); 332 | sinon.assert.calledWithExactly(useStub, '/org.hyperledger.fabric', 'some org.hyperledger.fabric router'); 333 | 334 | expect(router).to.deep.eq(express.Router()); 335 | expect(swagger).to.deep.eq({ 336 | components: sampleSwaggerComponent, 337 | paths: { 338 | TestContract: 'a path', 339 | 'org.hyperledger.fabric': 'a path', 340 | }, 341 | }); 342 | 343 | utils.extendPaths = oldExtendPaths; 344 | express.Router = oldRouter; 345 | }); 346 | }); 347 | 348 | describe('processComponents', () => { 349 | 350 | it ('should format passed components into the swagger format, making the array and object', () => { 351 | expect(ContractRouter['processComponents'](sampleChaincodeMetadata.components)).to.deep.eq(sampleSwaggerComponent); 352 | }); 353 | }); 354 | }); 355 | -------------------------------------------------------------------------------- /rest-server/unittest/routes/channels/chaincode/contract/transaction/index.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | * Copyright IBM Corp All Rights Reserved 4 | * 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | // tslint:disable:max-line-length no-string-literal 9 | 10 | import * as chai from 'chai'; 11 | import { Router as ExpressRouter } from 'express'; 12 | import 'mocha'; 13 | import * as sinon from 'sinon'; 14 | import * as sinonChai from 'sinon-chai'; 15 | import FabricProxy from '../../../../../../src/fabricproxy'; 16 | import { ContractMetadata } from '../../../../../../src/interfaces/metadata_interfaces'; 17 | import { TransactionRouter } from '../../../../../../src/routes/channels/chaincode/contract/transaction'; 18 | import { sampleFabricProxy } from '../../../../../utils'; 19 | chai.use(sinonChai); 20 | const expect = chai.expect; 21 | 22 | describe('Transaction', () => { 23 | let fabricProxy: FabricProxy; 24 | const sandbox: sinon.SinonSandbox = sinon.createSandbox(); 25 | 26 | const mockTransaction = { 27 | name: 'some transaction', 28 | parameters: [{ 29 | name: 'some param', 30 | schema: { 31 | type: 'string', 32 | }, 33 | }, 34 | { 35 | name: 'another param', 36 | schema: { 37 | type: 'string', 38 | }, 39 | }], 40 | tag: [], 41 | }; 42 | 43 | const basicSwagger = { 44 | paths: {}, 45 | }; 46 | 47 | beforeEach(() => { 48 | fabricProxy = sampleFabricProxy(); 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | describe('getRoutes', () => { 56 | it ('should return a set of routes and swagger paths for a given contracts transactions', () => { 57 | const myContract = { 58 | name: 'some contract', 59 | transactions: [mockTransaction], 60 | } as ContractMetadata; 61 | 62 | const addSwaggerStub = sandbox.stub(TransactionRouter, 'addSwaggerPath' as any); 63 | const addRouterStub = sandbox.stub(TransactionRouter, 'addRouterPath' as any); 64 | 65 | const {router, swagger} = TransactionRouter.getRoutes(fabricProxy, 'some channel', 'some chaincode', myContract); 66 | 67 | sinon.assert.callCount(addSwaggerStub, myContract.transactions.length); 68 | sinon.assert.calledWithExactly(addSwaggerStub.getCall(0), basicSwagger, mockTransaction, ['some channel/some chaincode']); 69 | 70 | sinon.assert.callCount(addRouterStub, myContract.transactions.length); 71 | sinon.assert.calledWithExactly(addRouterStub.getCall(0), sinon.match.func, mockTransaction, fabricProxy, 'some channel', 'some chaincode', myContract.name); 72 | 73 | expect(swagger).to.deep.eq(basicSwagger); 74 | expect(router.prototype).to.deep.eq(ExpressRouter().prototype); 75 | }); 76 | }); 77 | 78 | describe('addSwaggerPath', () => { 79 | it ('should update the swagger object passed with extra paths', () => { 80 | const processTxParametersStub = sandbox.stub(TransactionRouter, 'processTxParameters' as any).returns({a: 'body'}); 81 | 82 | const expectedAction = { 83 | operationId: mockTransaction.name, 84 | requestBody: {a: 'body'}, 85 | responses: { 86 | 200: { 87 | description: 'successful operation', 88 | }, 89 | }, 90 | tags: ['some', 'tags'], 91 | }; 92 | 93 | TransactionRouter['addSwaggerPath'](basicSwagger, mockTransaction, ['some', 'tags']); 94 | 95 | sinon.assert.calledOnce(processTxParametersStub); 96 | sinon.assert.calledWithExactly(processTxParametersStub, mockTransaction.parameters); 97 | expect(basicSwagger.paths['/some transaction'].post).to.deep.eq(expectedAction); 98 | }); 99 | }); 100 | 101 | describe('addRouterPath', () => { 102 | 103 | let router: ExpressRouter; 104 | let routerPostStub: sinon.SinonStub; 105 | 106 | const mockReq = { 107 | body: { 108 | 'another param': 'another param value', 109 | 'some param': 'some param value', 110 | }, 111 | }; 112 | 113 | let mockRes: { 114 | json: sinon.SinonSpy; 115 | send: sinon.SinonSpy; 116 | sendStatus: sinon.SinonSpy; 117 | status: sinon.SinonSpy; 118 | }; 119 | 120 | beforeEach(() => { 121 | router = ExpressRouter(); 122 | routerPostStub = sandbox.stub(router, 'post'); 123 | 124 | mockRes = { 125 | json: sinon.spy(), 126 | send: sinon.spy(), 127 | sendStatus: sinon.spy(), 128 | status: sinon.spy(), 129 | }; 130 | }); 131 | 132 | it ('should call the router post function', () => { 133 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some contrachannelct', 'some chaincode', 'some contract'); 134 | 135 | sinon.assert.calledOnce(routerPostStub); 136 | sinon.assert.calledWithExactly(routerPostStub, '/' + mockTransaction.name, sinon.match.func, sinon.match.func); 137 | }); 138 | 139 | describe('post', () => { 140 | let submitTransactionStub: sinon.SinonStub; 141 | let evaluateTransactionStub: sinon.SinonStub; 142 | 143 | beforeEach(() => { 144 | submitTransactionStub = sandbox.stub(fabricProxy, 'submitTransaction'); 145 | evaluateTransactionStub = sandbox.stub(fabricProxy, 'evaluateTransaction'); 146 | }); 147 | 148 | it ('should return error 400 when missing a parameter the transaction requires', async () => { 149 | const mockReq2 = { 150 | body: {}, 151 | }; 152 | 153 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 154 | 155 | await routerPostStub.getCall(0).args[2](mockReq2, mockRes); 156 | 157 | sinon.assert.notCalled(evaluateTransactionStub); 158 | sinon.assert.notCalled(submitTransactionStub); 159 | 160 | sinon.assert.notCalled(mockRes.send); 161 | sinon.assert.notCalled(mockRes.sendStatus); 162 | sinon.assert.calledOnce(mockRes.status); 163 | sinon.assert.calledWithExactly(mockRes.status, 400); 164 | sinon.assert.calledOnce(mockRes.json); 165 | sinon.assert.calledWithExactly(mockRes.json, { 166 | msg: ['Bad request. Missing parameters: some param, another param'], 167 | }); 168 | }); 169 | 170 | it ('should return status 500 and error thrown when sending the transaction', async () => { 171 | evaluateTransactionStub.rejects(Error('some\nmultiline\nerror')); 172 | 173 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 174 | 175 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 176 | 177 | sinon.assert.notCalled(mockRes.send); 178 | sinon.assert.notCalled(mockRes.sendStatus); 179 | sinon.assert.calledOnce(mockRes.status); 180 | sinon.assert.calledWithExactly(mockRes.status, 500); 181 | sinon.assert.calledOnce(mockRes.json); 182 | sinon.assert.calledWithExactly(mockRes.json, { 183 | msg: ['some', 'multiline', 'error'], 184 | }); 185 | }); 186 | 187 | it ('should return status 500 and the error when error is thrown on data retrieval that is not a syntax error', async () => { 188 | evaluateTransactionStub.resolves(Buffer.from('{"some": "json"}')); 189 | mockRes.json = sandbox.stub().onFirstCall().throws(Error('some non syntax error')); 190 | 191 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 192 | 193 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 194 | 195 | sinon.assert.notCalled(mockRes.send); 196 | sinon.assert.notCalled(mockRes.sendStatus); 197 | sinon.assert.calledOnce(mockRes.status); 198 | sinon.assert.calledTwice(mockRes.json); 199 | sinon.assert.calledWithExactly(mockRes.json, { 200 | msg: ['some non syntax error'], 201 | }); 202 | }); 203 | 204 | it ('should send no data and a 204 status if no data returned from transaction', async () => { 205 | evaluateTransactionStub.resolves(); 206 | 207 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 208 | 209 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 210 | 211 | sinon.assert.notCalled(mockRes.json); 212 | sinon.assert.notCalled(mockRes.send); 213 | sinon.assert.notCalled(mockRes.status); 214 | sinon.assert.calledOnce(mockRes.sendStatus); 215 | sinon.assert.calledWithExactly(mockRes.sendStatus, 204); 216 | }); 217 | 218 | it ('should send the returned data as json when it is parseable', async () => { 219 | evaluateTransactionStub.resolves(Buffer.from('{"some": "json"}')); 220 | 221 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 222 | 223 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 224 | 225 | sinon.assert.calledOnce(mockRes.json); 226 | sinon.assert.notCalled(mockRes.send); 227 | sinon.assert.notCalled(mockRes.sendStatus); 228 | sinon.assert.notCalled(mockRes.status); 229 | sinon.assert.calledWithExactly(mockRes.json, {some: 'json'}); 230 | }); 231 | 232 | it ('should send back the returned data as not JSON when it fails parsing', async () => { 233 | evaluateTransactionStub.resolves(Buffer.from('Hello World')); 234 | 235 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 236 | 237 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 238 | 239 | sinon.assert.notCalled(mockRes.json); 240 | sinon.assert.notCalled(mockRes.sendStatus); 241 | sinon.assert.notCalled(mockRes.status); 242 | sinon.assert.calledOnce(mockRes.send); 243 | sinon.assert.calledWithExactly(mockRes.send, 'Hello World'); 244 | }); 245 | 246 | it ('should call submitTransaction when metadata tag has submitTx', async () => { 247 | mockTransaction.tag = ['submitTx']; 248 | 249 | submitTransactionStub.resolves(Buffer.from('Hello World')); 250 | 251 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 252 | 253 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 254 | 255 | sinon.assert.notCalled(evaluateTransactionStub); 256 | sinon.assert.calledOnce(submitTransactionStub); 257 | sinon.assert.calledWithExactly(submitTransactionStub, 'some channel', 'some chaincode', 'some contract', mockTransaction.name, mockReq.body['some param'], mockReq.body['another param']); 258 | 259 | sinon.assert.notCalled(mockRes.json); 260 | sinon.assert.notCalled(mockRes.sendStatus); 261 | sinon.assert.notCalled(mockRes.status); 262 | sinon.assert.calledOnce(mockRes.send); 263 | sinon.assert.calledWithExactly(mockRes.send, 'Hello World'); 264 | 265 | mockTransaction.tag = []; 266 | }); 267 | 268 | it ('should call evaluateTransaction when metadata tag is empty', async () => { 269 | evaluateTransactionStub.resolves(Buffer.from('Hello World')); 270 | 271 | TransactionRouter['addRouterPath'](router, mockTransaction, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 272 | 273 | await routerPostStub.getCall(0).args[2](mockReq, mockRes); 274 | 275 | sinon.assert.notCalled(submitTransactionStub); 276 | sinon.assert.calledOnce(evaluateTransactionStub); 277 | sinon.assert.calledWithExactly(evaluateTransactionStub, 'some channel', 'some chaincode', 'some contract', mockTransaction.name, mockReq.body['some param'], mockReq.body['another param']); 278 | 279 | sinon.assert.notCalled(mockRes.json); 280 | sinon.assert.notCalled(mockRes.sendStatus); 281 | sinon.assert.notCalled(mockRes.status); 282 | sinon.assert.calledOnce(mockRes.send); 283 | sinon.assert.calledWithExactly(mockRes.send, 'Hello World'); 284 | }); 285 | 286 | it ('should stringify non string elements for use as args', async () => { 287 | const mockTransaction2 = { 288 | name: 'some transaction', 289 | parameters: [{ 290 | name: 'some json param', 291 | schema: { 292 | type: 'object', 293 | }, 294 | }, 295 | { 296 | name: 'some integer param', 297 | schema: { 298 | type: 'integer', 299 | }, 300 | }], 301 | tag: [], 302 | }; 303 | 304 | const mockReq3 = { 305 | body: { 306 | 'some integer param': 100, 307 | 'some json param': {scary: 'json object'}, 308 | }, 309 | }; 310 | 311 | evaluateTransactionStub.resolves(Buffer.from('Hello World')); 312 | 313 | TransactionRouter['addRouterPath'](router, mockTransaction2, fabricProxy, 'some channel', 'some chaincode', 'some contract'); 314 | 315 | await routerPostStub.getCall(0).args[2](mockReq3, mockRes); 316 | 317 | sinon.assert.notCalled(submitTransactionStub); 318 | sinon.assert.calledOnce(evaluateTransactionStub); 319 | sinon.assert.calledWithExactly(evaluateTransactionStub, 'some channel', 'some chaincode', 'some contract', mockTransaction.name, '{"scary":"json object"}', '100'); 320 | 321 | sinon.assert.notCalled(mockRes.json); 322 | sinon.assert.notCalled(mockRes.sendStatus); 323 | sinon.assert.notCalled(mockRes.status); 324 | sinon.assert.calledOnce(mockRes.send); 325 | sinon.assert.calledWithExactly(mockRes.send, 'Hello World'); 326 | }); 327 | }); 328 | }); 329 | 330 | describe('processTxParameters', () => { 331 | it ('should generate a request body for given set of parameters', () => { 332 | const mockParams = [{ 333 | name: 'some param', 334 | schema: { 335 | type: 'string', 336 | }, 337 | }, 338 | { 339 | name: 'another param', 340 | schema: { 341 | type: 'string', 342 | }, 343 | }]; 344 | 345 | const requestBody = TransactionRouter['processTxParameters'](mockParams); 346 | 347 | const expectedRequestBody = { 348 | content: { 349 | 'application/json': { 350 | schema: { 351 | properties: {}, 352 | type: 'object', 353 | }, 354 | }, 355 | }, 356 | }; 357 | 358 | expectedRequestBody.content['application/json'].schema.properties['some param'] = mockParams[0].schema; 359 | expectedRequestBody.content['application/json'].schema.properties['another param'] = mockParams[1].schema; 360 | 361 | expect(requestBody).to.deep.eq(expectedRequestBody); 362 | }); 363 | }); 364 | }); 365 | --------------------------------------------------------------------------------