Skip to content

Commit

Permalink
[FABN-948] Add top-level Package class to SDK
Browse files Browse the repository at this point in the history
Introduce a new top-level class called Package, for working with chaincode
packages. A chaincode package is the same format as created by the "peer
chaincode package" command.

This commit allows a user of the SDK to:
- Load a chaincode package from an existing buffer/file and get information
  about it
- Create a new chaincode package from a directory
- Save a chaincode package to a buffer/file

See FABN-948 for the details.

Change-Id: I499c5bf7dd03a6aedad9c28f06a6c850e1a8ce41
Signed-off-by: Simon Stone <[email protected]>
  • Loading branch information
Simon Stone committed Oct 3, 2018
1 parent cedc8cb commit a50f68d
Show file tree
Hide file tree
Showing 45 changed files with 1,108 additions and 24 deletions.
2 changes: 1 addition & 1 deletion build/tasks/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ gulp.task('test-mocha', ['mocha-fabric-client'],

gulp.task('mocha-fabric-client',
() => {
return gulp.src(['./fabric-client/test/**/*.js'], { read: false })
return gulp.src(['./fabric-client/test/**/*.js', '!./fabric-client/test/data/**/*.js'], { read: false })
.pipe(mocha({ reporter: 'list', exit: true }));
}
);
Expand Down
2 changes: 2 additions & 0 deletions fabric-client/lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const api = require('./api.js');
const BaseClient = require('./BaseClient.js');
const User = require('./User.js');
const Channel = require('./Channel.js');
const Package = require('./Package.js');
const Packager = require('./Packager.js');
const Peer = require('./Peer.js');
const ChannelEventHub = require('./ChannelEventHub');
Expand Down Expand Up @@ -1844,3 +1845,4 @@ module.exports.ChannelEventHub = ChannelEventHub;
module.exports.Orderer = Orderer;
module.exports.Channel = Channel;
module.exports.User = User;
module.exports.Package = Package;
145 changes: 145 additions & 0 deletions fabric-client/lib/Package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Copyright 2018 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const clientUtils = require('./client-utils.js');
const grpc = require('grpc');
const Packager = require('./Packager.js');
const tar = require('tar-stream');
const utils = require('./utils.js');
const zlib = require('zlib');

const _ccProto = grpc.load(__dirname + '/protos/peer/chaincode.proto').protos;
const logger = utils.getLogger('package');

/**
* A class representing a smart contract package.
*/
class Package {

/**
* Find the list of file names in the specified chaincode deployment specification.
* @private
* @param {ChaincodeDeploymentSpec} chaincodeDeploymentSpec The chaincode deployment specification.
* @returns {string[]} The list of file names.
*/
static async _findFileNames(chaincodeDeploymentSpec) {
const codePackage = chaincodeDeploymentSpec.getCodePackage().toBuffer();
const gunzip = zlib.createGunzip();
const extract = tar.extract();
return new Promise((resolve) => {
const fileNames = [];
extract.on('entry', (header, stream, next) => {
logger.debug('Package._findFileNames - found entry %s', header.name);
fileNames.push(header.name);
stream.on('end', () => {
next();
});
stream.resume();
});
extract.on('finish', () => {
resolve(fileNames.sort());
});
gunzip.pipe(extract);
gunzip.end(codePackage);
});
}

/**
* Load a smart contract package from the specified buffer.
* @param {Buffer} buffer A buffer containing the serialized smart contract package.
* @returns {Package} The smart contract package.
*/
static async fromBuffer(buffer) {
const chaincodeDeploymentSpec = _ccProto.ChaincodeDeploymentSpec.decode(buffer);
const fileNames = await Package._findFileNames(chaincodeDeploymentSpec);
return new Package(chaincodeDeploymentSpec, fileNames);
}

/**
* Create a new smart contract package from the specified directory.
* @param {Object} options The options for the packager.
* @param {string} options.name The name of the smart contract.
* @param {string} options.version The version of the smart contract.
* @param {string} options.path The directory containing the smart contract.
* @param {string} options.type The type of the smart contract, one of 'golang', 'car', 'node' or 'java'.
* @param {string} [options.metadataPath] The directory containing the metadata descriptors.
* @returns {Package} The smart contract package.
*/
static async fromDirectory({ name, version, path, type, metadataPath }) {
logger.debug('Package.fromDirectory - entry - %s, %s, %s, %s', name, version, path, type);
const codePackage = await Packager.package(path, type, false, metadataPath);
logger.debug('Package.fromDirectory - code package is %s bytes', codePackage.length);
const chaincodeSpec = {
type: clientUtils.translateCCType(type),
chaincode_id: {
name,
path,
version
}
};
logger.debug('Package.fromDirectory - built chaincode specification %s', JSON.stringify(chaincodeSpec));
const chaincodeDeploymentSpec = new _ccProto.ChaincodeDeploymentSpec();
chaincodeDeploymentSpec.setChaincodeSpec(chaincodeSpec);
chaincodeDeploymentSpec.setCodePackage(codePackage);
const fileNames = await Package._findFileNames(chaincodeDeploymentSpec);
return new Package(chaincodeDeploymentSpec, fileNames);
}

/**
* Constructor.
* @private
* @param {ChaincodeDeploymentSpec} chaincodeDeploymentSpec The chaincode deployment specification.
*/
constructor(chaincodeDeploymentSpec, fileNames) {
this.chaincodeDeploymentSpec = chaincodeDeploymentSpec;
this.fileNames = fileNames;
}

/**
* Get the name of the smart contract package.
* @returns {string} The name of the smart contract package.
*/
getName() {
return this.chaincodeDeploymentSpec.getChaincodeSpec().getChaincodeId().getName();
}

/**
* Get the version of the smart contract package.
* @returns {string} The version of the smart contract package.
*/
getVersion() {
return this.chaincodeDeploymentSpec.getChaincodeSpec().getChaincodeId().getVersion();
}

/**
* Get the type of the smart contract package.
* @returns {string} The type of the smart contract package, one of 'golang', 'car', 'node' or 'java'.
*/
getType() {
return clientUtils.ccTypeToString(this.chaincodeDeploymentSpec.getChaincodeSpec().getType());
}

/**
* Get the list of file names in this smart contract package.
* @returns {string[]} The list of file names in this smart contract package.
*/
getFileNames() {
return this.fileNames;
}

/**
* Save the smart contract package to a buffer.
* @returns {Buffer} A buffer containing the serialized smart contract package.
*/
async toBuffer() {
return this.chaincodeDeploymentSpec.toBuffer();
}

}

module.exports = Package;
25 changes: 14 additions & 11 deletions fabric-client/lib/client-utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
/**
* Copyright 2018 IBM All Rights Reserved.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';
Expand Down Expand Up @@ -195,6 +187,17 @@ module.exports.translateCCType = (type) => {
return value;
};

module.exports.ccTypeToString = (ccType) => {
const map = {};
map[_ccProto.ChaincodeSpec.Type.GOLANG] = 'golang';
map[_ccProto.ChaincodeSpec.Type.CAR] = 'car';
map[_ccProto.ChaincodeSpec.Type.JAVA] = 'java';
map[_ccProto.ChaincodeSpec.Type.NODE] = 'node';
const value = map[ccType];

return value;
};

/*
* This function will create a timestamp from the current time
*/
Expand Down
165 changes: 165 additions & 0 deletions fabric-client/test/Package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Copyright 2018 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const fs = require('fs');
const Package = require('..').Package;
const path = require('path');

const chai = require('chai');
chai.should();
chai.use(require('chai-as-promised'));

const languages = ['golang', 'javascript', 'typescript', 'java'];

const languageToType = {
golang: 'golang',
javascript: 'node',
typescript: 'node',
java: 'java'
};

const fileNames = {
golang: [
'src/golang-contract/chaincode.go',
'src/golang-contract/chaincode_test.go',
'src/golang-contract/main.go'
],
javascript: [
'src/.editorconfig',
'src/.eslintignore',
'src/.eslintrc.js',
'src/index.js',
'src/lib/chaincode.js',
'src/lib/start.js',
'src/package.json',
'src/test/chaincode.js',
'src/test/start.js'
],
typescript: [
'src/.editorconfig',
'src/package.json',
'src/src/chaincode.spec.ts',
'src/src/chaincode.ts',
'src/src/index.ts',
'src/src/start.spec.ts',
'src/src/start.ts',
'src/tsconfig.json',
'src/tslint.json'
],
java: [
'src/build.gradle',
'src/settings.gradle',
'src/src/main/java/org/example/Chaincode.java',
'src/src/main/java/org/example/Start.java',
'src/src/test/java/org/example/ChaincodeTest.java'
]
};

const metadataFileNames = [
'META-INF/statedb/couchdb/indexes/indexOwner.json'
];

describe('Package', () => {

let GOPATH;

beforeEach(() => {
GOPATH = process.env.GOPATH;
process.env.GOPATH = path.resolve(__dirname, 'data', 'go');
});

afterEach(() => {
process.env.GOPATH = GOPATH;
});

describe('#fromBuffer', () => {

for (const language of languages) {

const type = languageToType[language];

it(`should load a smart contract package from a buffer [${language}]`, async () => {
const pkgFile = path.resolve(__dirname, 'data', `${language}-contract.cds`);
const pkgBuffer = fs.readFileSync(pkgFile);
const pkg = await Package.fromBuffer(pkgBuffer);
pkg.getName().should.equal('my-contract');
pkg.getVersion().should.equal('1.2.3');
pkg.getType().should.equal(type);
pkg.getFileNames().should.deep.equal(fileNames[language]);
});

it(`should load a smart contract package from a buffer with metadata [${language}]`, async () => {
const pkgFile = path.resolve(__dirname, 'data', `${language}-contract-metadata.cds`);
const pkgBuffer = fs.readFileSync(pkgFile);
const pkg = await Package.fromBuffer(pkgBuffer);
pkg.getName().should.equal('my-contract');
pkg.getVersion().should.equal('1.2.3');
pkg.getType().should.equal(type);
pkg.getFileNames().should.deep.equal(metadataFileNames.concat(fileNames[language]));
});

}

});

describe('#fromDirectory', () => {

for (const language of languages) {

const type = languageToType[language];

it(`should create a smart contract package from a directory [${language}]`, async () => {
let pkgDirectory;
if (language === 'golang') {
pkgDirectory = `${language}-contract`;
} else {
pkgDirectory = path.resolve(__dirname, 'data', `${language}-contract`);
}
const pkg = await Package.fromDirectory({ name: 'my-contract', version: '1.2.3', path: pkgDirectory, type });
pkg.getName().should.equal('my-contract');
pkg.getVersion().should.equal('1.2.3');
pkg.getType().should.equal(type);
pkg.getFileNames().should.deep.equal(fileNames[language]);
});

it(`should create a smart contract package from a directory with metadata [${language}]`, async () => {
let pkgDirectory;
if (language === 'golang') {
pkgDirectory = `${language}-contract`;
} else {
pkgDirectory = path.resolve(__dirname, 'data', `${language}-contract`);
}
const metaDirectory = path.resolve(__dirname, 'data', 'META-INF');
const pkg = await Package.fromDirectory({ name: 'my-contract', version: '1.2.3', path: pkgDirectory, type, metadataPath: metaDirectory });
pkg.getName().should.equal('my-contract');
pkg.getVersion().should.equal('1.2.3');
pkg.getType().should.equal(type);
pkg.getFileNames().should.deep.equal(metadataFileNames.concat(fileNames[language]));
});

}

});

describe('#toBuffer', async () => {

for (const language of languages) {

it(`should save a smart contract package to a buffer [${language}]`, async () => {
const pkgFile = path.resolve(__dirname, 'data', `${language}-contract.cds`);
const pkgBuffer1 = fs.readFileSync(pkgFile);
const pkg = await Package.fromBuffer(pkgBuffer1);
const pkgBuffer2 = await pkg.toBuffer();
Buffer.compare(pkgBuffer1, pkgBuffer2).should.equal(0);
});

}

});

});
Loading

0 comments on commit a50f68d

Please sign in to comment.