Write chaincode

Every consortium member should follow the instructions on this page.

It’s now time to write our chaincode. We will be building our chaincode using test-driven development. Begin by replacing products.js with the following code. This file is where all the business logic resides. It keeps track of the state of each product in the supply chain and ensures that a product can only transition from one state to another if the appropriate conditions are met. It also verifies that inputs sent to the contract are correct before taking any action.

'use strict';

const shim = require('fabric-shim');
const log = require('./loglevel').getLogger('products');
log.setLevel('trace');
const StateMachine = require('./state-machine');


////////////////////////////////////////////////////////////////////////////
// FSM (Finite State Machine)
// Used to ensure state transitions are valid
////////////////////////////////////////////////////////////////////////////

const FSM = new StateMachine.factory({
  init: 'manufactured',
  transitions: [
    { name: 'inspect', from: 'manufactured', to: 'inspected' }, // supplier
    { name: 'ship', from: 'inspected', to: 'shipped' },         // supplier
    { name: 'receive', from: 'shipped', to: 'stocked' },        // retailer
    { name: 'label', from: 'stocked', to: 'labeled' },          // retailer
    { name: 'sell', from: 'labeled', to: 'sold' },              // retailer
    { name: 'goto', from: '*', to: function(s) { return s } }
  ]
});

////////////////////////////////////////////////////////////////////////////
// ProductsChaincode
// Used to track all products in the supply chain
////////////////////////////////////////////////////////////////////////////

const ProductsChaincode = class {
  constructor(cid = shim.ClientIdentity) {
    this.clientIdentity = cid;
  }

  ////////////////////////////////////////////////////////////////////////////
  // requireAffiliationAndPermissions
  // Checks that invoke() caller belongs to the specified blockchain member
  // and has the specified permission. Throws an exception if not.
  ////////////////////////////////////////////////////////////////////////////

  requireAffiliationAndPermissions(stub, affiliation, permission) {
    const cid = new this.clientIdentity(stub);
    let permissions = cid.getAttributeValue('permissions') || 'default';
    permissions = permissions.split('_');
    const hasBoth =
      cid.assertAttributeValue('hf.Affiliation', affiliation) &&
      permissions.includes(permission);
    if (!hasBoth) {
      const msg = `Unauthorized access: affiliation ${affiliation}` +
        ` and permission ${permission} required`;
      throw new Error(msg);
    }
  }

  ////////////////////////////////////////////////////////////////////////////
  // assertCanPerformOperation
  // Determines which membership affiliations are required for which
  // operations. Called by other methods. Calls
  // requireAffiliationAndPermissions as a subroutine.
  ////////////////////////////////////////////////////////////////////////////

  assertCanPerformTransition(stub, transition) {
    let requiredAffiliation = 'undefined';
    switch (transition) {
      case 'manufacture':
      case 'inspect':
      case 'ship': requiredAffiliation = 'Supplier'; break;
      case 'receive':
      case 'label':
      case 'sell': requiredAffiliation = 'Retailer';
    }
    this.requireAffiliationAndPermissions(stub, requiredAffiliation, transition);
  }

  ////////////////////////////////////////////////////////////////////////////
  // Initialize the chaincode
  ////////////////////////////////////////////////////////////////////////////

  async Init(stub) {
    const ret = stub.getFunctionAndParameters();
    if (ret.params.length > 0) {
      return shim.error('Init() does not expect any arguments');
    }
    // initialize list of all product IDs so that they can be iterated over
    await stub.putState('productIDs', Buffer.from('[]'));
    return shim.success();
  }

  ////////////////////////////////////////////////////////////////////////////
  // Invoke chaincode and dispatch the appropriate method
  ////////////////////////////////////////////////////////////////////////////

  async Invoke(stub) {
    const ret = stub.getFunctionAndParameters();
    log.debug(ret);

    if (!ret.fcn) {
      return shim.error('Missing method parameter in invoke');
    }

    let method = this[ret.fcn];
    let returnval;

    if (!method) {
      return shim.error(`Unrecognized method ${ret.fcn} in invoke`);
    }
    try {
      let payload = await method(this, stub, ret.params);
      log.debug(`Payload from call to ${ret.fcn} was ${JSON.stringify(payload)}.`);
      returnval = shim.success(Buffer.from(payload));
    } catch (err) {
      log.error(`Error in Invoke ${ret.fcn}: ${err.message}`);
      returnval = shim.error(Buffer.from(err.message));
    }
    log.debug(`exiting Invoke`)
    return returnval;
  }

  ////////////////////////////////////////////////////////////////////////////
  // createProduct
  // Add a newly-manufactured product to the blockchain
  ////////////////////////////////////////////////////////////////////////////

  async createProduct(self, stub, args) {
    log.debug(`in createProduct(self, stub, ${JSON.stringify(args)})...`);
    if (args.length !== 1) {
      throw new Error('createProduct expects one argument');
    }

    self.assertCanPerformTransition(stub, 'manufacture');
    const now = new Date();
    const payload = {
      "state": "manufactured",
      "history": {
        "manufactured": now.toISOString()
      }
    };
    const strPayload = JSON.stringify(payload);
    const productId = args[0];
    const key = `product_${productId}`;
    let productStateBytes = await stub.getState(key);

    if (!productStateBytes || productStateBytes.length === 0) {
      log.debug(`Calling stub.putState(${key}, Buffer.from(JSON.stringify(${strPayload})))...`);
      await stub.putState(key, Buffer.from(strPayload));
    } else {
      throw new Error('Product with same ID already exists.');
    }

    // add productID to list of product IDs
    log.debug(`Retrieving product ID list...`);
    let arr = await stub.getState('productIDs');
    let productIDs = JSON.parse(arr.toString());
    productIDs = [...productIDs, key].sort();
    log.debug(`Storing updated product ID list...`);
    await stub.putState('productIDs', Buffer.from(JSON.stringify(productIDs)));

    log.debug(`exiting createProduct(self, stub, ${JSON.stringify(args)})...`);
    return strPayload;
  }

  ////////////////////////////////////////////////////////////////////////////
  // updateProductState
  // Update an existing product as it moves through the supply chain
  ////////////////////////////////////////////////////////////////////////////

  async updateProductState(self, stub, args) {
    if (args.length !== 2) {
      throw new Error('updateProductState expects two arguments');
    }
    const productId = args[0];
    const transition = args[1];
    self.assertCanPerformTransition(stub, transition);
    const key = `product_${productId}`;
    const productDataBytes = await stub.getState(key);
    const productData = JSON.parse(productDataBytes.toString());
    const product = new FSM();
    product.goto(productData.state);
    product[transition]();
    productData.state = product.state;
    const now = new Date();
    productData.history = productData.history || {};
    productData.history[product.state] = now.toISOString();
    const stringProductData = JSON.stringify(productData);
    await stub.putState(key, Buffer.from(stringProductData));
    return stringProductData;
  }


  ////////////////////////////////////////////////////////////////////////////
  // query blockchain state
  ////////////////////////////////////////////////////////////////////////////

  async query() {
    const params = Array.from(arguments);
    let ctx, stub, args, keyIndex = 0, expectedArgLength = 1;
    if (params.length === 2) { // we're being called in unit tests
      [stub, args] = params;
      keyIndex = 1;
      expectedArgLength = 2;
    } else {                   // we're being called in a live environment
      [ctx, stub, args] = params;
    }
    if (args.length !== expectedArgLength) {
      throw new Error(`Incorrect number of arguments. Arguments contains: ${JSON.stringify(args)}`);
    }

    let key = args[keyIndex];

    // Get the state from the ledger
    let resultBytes = await stub.getState(key);
    if (!resultBytes) {
      const message = `No value for key ${key}`;
      throw new Error(message);
    }

    log.debug('Query Response:', resultBytes.toString());
    return resultBytes;
  }
};

module.exports = ProductsChaincode;

if (require.main === module) {
  shim.start(new ProductsChaincode());
}

Save that file, then replace products_test.js with the following code. This file contains unit tests for the chaincode logic. It ensures that the logic is behaving properly by passing different inputs into the chaincode in a sandboxed environment, and verifying that the expected outputs are given. It is usually best to test your application logic thoroughly before deploying it into production. These tests will also become a useful way of verifying that new application logic hasn’t broken any existing features. Developers can run them any time new features are implemented to make sure things continue to behave as expected.

'use strict';

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const chaiDateTime = require('chai-datetime');
chai.use(chaiAsPromised);
chai.use(chaiDateTime);
const expect = chai.expect;
const moment = require('moment');

const ProductsChaincode = require('./products');
const MockStub = require('@theledger/fabric-mock-stub');
const { ChaincodeMockStub } = MockStub;
const log = require('./loglevel').getLogger('products');
const log_level = process.env['LOG_LEVEL'] || 'warn';
log.setLevel(log_level.toUpperCase());


////////////////////////////////////////////////////////////////////////////
// suppressLogging
// helper function to turn off logging during tests
// that are supposed to produce errors
////////////////////////////////////////////////////////////////////////////

const suppressLogging = async (func) => {
  const previousLevel = log.getLevel();
  log.setLevel(log.levels.SILENT);
  await func();
  log.setLevel(previousLevel);
};


////////////////////////////////////////////////////////////////////////////
// spCIDMock
// Mock used to imitate the behavior of the ClientIdentity object.
////////////////////////////////////////////////////////////////////////////

class spCIDMock {
  constructor(stub) {
    this._attributes = {
      'hf.Affiliation': 'Supplier',
      'permissions': 'manufacture'
    };
  }
  getAttributeValue(key) { return this._attributes[key]; }
  assertAttributeValue(key, value) { return this._attributes[key] === value; }
}


describe('Products', () => {
  let chaincode, stub;

  beforeEach(() => {
    chaincode = new ProductsChaincode();
    stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
  });

  describe('init', () => {
    it("should succeed", async () => {
      const response = await stub.mockInit('tx1', []);
      expect(response.status).to.eql(200);
    });

    it("should initialize the ProductIDs list", async () => {
      let response = await stub.mockInit('tx1', []);
      response = await chaincode.query(stub, ['query', 'productIDs']);
      expect(response.toString()).to.eql('[]');
    });

    it("should expect no arguments", async () => {
      const response = await stub.mockInit('tx1', ['fn', 'invalid']);
      expect(response.status).to.eql(500);
      expect(response.message).to.eql('Init() does not expect any arguments');
    });
  });

  describe('invoke', () => {
    it('should reject unrecognized commands', async () => {
      const response = await stub.mockInvoke('tx1', ['blah']);
      expect(response.status).to.eql(500);
      expect(response.message).to.eql('Unrecognized method blah in invoke');
    });

    it('should reject invocations with no arguments', async () => {
      const response = await stub.mockInvoke('tx1', []);
      expect(response.status).to.eql(500);
      expect(response.message).to.eql('Missing method parameter in invoke');
    });
  });

  describe('query', () => {
    beforeEach(async () => {
      chaincode = new ProductsChaincode(spCIDMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
    });

    it('should throw an error if called with too many arguments', async () => {
      await expect(chaincode.query(stub, ['query', 'product_1', 'product_2']))
        .to.eventually.be.rejected.and.match(/Incorrect number of arguments/m);
    });

    it('should throw an error if a key is not found', async () => {
      await expect(chaincode.query(stub, ['query', 'product_1']))
        .to.eventually.be.rejected.and.match(/No value for key/m);
    });

    it('should return an empty array for productIDs', async () => {
      const response = await chaincode.query(stub, ['query', 'productIDs']);
      expect(response.toString()).to.eql('[]');
    });
  });

  describe('createProduct', () => {
    beforeEach(async () => {
      chaincode = new ProductsChaincode(spCIDMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
    });

    it("should only accept one argument", async () => {
      suppressLogging(async () => {
        await expect(stub.mockInvoke('tx2', ['createProduct', '1', 'extra']))
          .to.eventually.have.property('message')
          .and.match(/createProduct expects one argument/m);
      });
    });

    it("should not overwrite an existing product with the same ID", async () => {
      await stub.mockInvoke('tx2', ['createProduct', '1']);
      suppressLogging(async () => {
        await expect(stub.mockInvoke('tx3', ['createProduct', '1']))
          .to.eventually.have.property('message')
          .and.match(/Product with same ID already exists/m);
      });
    });

    it("should set new products' state to 'manufactured'", async () => {
      let response = await stub.mockInvoke('tx2', ['createProduct', '1']);
      response = await chaincode.query(stub, ['query', 'product_1']);
      expect(JSON.parse(response.toString())).to.have.property('state', 'manufactured');
    });

    it('should add an element to the list of productIDs', async () => {
      await stub.mockInit('tx2', []);
      let response = await stub.mockInvoke('tx3', ['createProduct', '1']);
      response = await chaincode.query(stub, ['query', 'productIDs']);
      expect(response.toString()).to.equal('["product_1"]');
    });

    it('should not allow clients without the appropriate permissions', async () => {
      const productID = '1';
      chaincode = new ProductsChaincode();
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
      suppressLogging(async () => {
        await expect(stub.mockInvoke('tx2', ['createProduct', productID]))
          .to.eventually.have.property('message')
          .and.match(/affiliation Supplier and permission manufacture required/m);
      });
    });
  });

  describe('updateProductState', () => {
    const productID = '1';
    let chaincode, stub;

    beforeEach(async () => {
      chaincode = new ProductsChaincode();
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
    });

    it('should only accept two arguments', async () => {
      chaincode = new ProductsChaincode(spCIDMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
      const args = [
        'updateProductState',
        productID,
        'arrivedAtSupplier',
        'invalid'
      ];
      suppressLogging(async () => {
        await expect(stub.mockInvoke('tx3', args))
          .to.eventually.have.property('message')
          .and.match(/updateProductState expects two arguments/m);
      });
    });

    it('should not allow invalid state transitions', async () => {
      class cidMock {
        constructor(stub) {
          this._attributes = {
            'hf.Affiliation': 'Supplier',
            'permissions': 'manufacture_ship'

          };
        }
        getAttributeValue(key) { return this._attributes[key]; }
        assertAttributeValue(key, value) { return this._attributes[key] === value; }
      }
      chaincode = new ProductsChaincode(cidMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
      const args = ['updateProductState', productID, 'ship'];
      await stub.mockInvoke('tx2', ['createProduct', productID]);
      suppressLogging(async () => {
        let result = await stub.mockInvoke('tx3', args);
        expect(result.message.toString()).to.eql('transition is invalid in current state');
      });
    });

    it('should store a history of timestamped state transitions', async () => {
      class cidMock {
        constructor(stub) {
          this._attributes = {
            'hf.Affiliation': 'Supplier',
            'permissions': 'manufacture_inspect'
          };
        }
        getAttributeValue(key) { return this._attributes[key]; }
        assertAttributeValue(key, value) { return this._attributes[key] === value; }
      }
      chaincode = new ProductsChaincode(cidMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
      await stub.mockInvoke('tx2', ['createProduct', productID]);
      await stub.mockInvoke('tx3', ['updateProductState', productID, 'inspect']);
      const response = await chaincode.query(stub, ['query', 'product_1']);
      const expectedTime = new Date();
      const parsedResponse = JSON.parse(response.toString());
      expect(parsedResponse).to.have.property('history');
      const time = moment(parsedResponse.history.inspected).toDate();
      expect(time).to.be.closeToTime(expectedTime, 1);
    });

    it('should not allow clients without the appropriate permissions', async () => {
      chaincode = new ProductsChaincode(spCIDMock);
      stub = new ChaincodeMockStub("ProductsMockStub", chaincode);
      await stub.mockInit('tx1', []);
      await stub.mockInvoke('tx2', ['createProduct', productID]);
      const args = ['updateProductState', productID, 'inspect'];
      suppressLogging(async () => {
        let result = await stub.mockInvoke('tx3', args);
        expect(result.message.toString()).to.match(/affiliation Supplier and permission inspect required/);
      });
    });
  });
});

Save that file. At this point, you should be able to run your unit tests and have them pass successfully. Run the following commands from the terminal:

nvm use lts/carbon
cd ~/environment/chaincode
npm test

You should see the following output:

> chaincode@1.0.0 test $HOME/environment/chaincode
> mocha *_test.js



Products
  init
    ✓ should succeed
    ✓ should initialize the ProductIDs list
    ✓ should expect no arguments
  invoke
    ✓ should reject unrecognized commands
    ✓ should reject invocations with no arguments
  query
    ✓ should throw an error if called with too many arguments
    ✓ should throw an error if a key is not found
    ✓ should return an empty array for productIDs
  createProduct
    ✓ should only accept one argument
    ✓ should not overwrite an existing product with the same ID
    ✓ should set new products' state to 'manufactured'
    ✓ should add an element to the list of productIDs
    ✓ should not allow clients without the appropriate permissions
  updateProductState
    ✓ should only accept two arguments
    ✓ should not allow invalid state transitions
    ✓ should store a history of timestamped state transitions
    ✓ should not allow clients without the appropriate permissions


17 passing (36ms)