/*
* 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
*
* 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.
*/
'use strict';
const { Certificate, CertificateUtil, Connection, Util } = require('composer-common');
const { EmbeddedContainer, EmbeddedContext, EmbeddedDataService } = require('composer-runtime-embedded');
const EmbeddedSecurityContext = require('./embeddedsecuritycontext');
const { Engine, InstalledBusinessNetwork } = require('composer-runtime');
const uuid = require('uuid');
// A map of installed chaincodes keyed by name and version
const installedChaincodes = new Map();
// A mapping of business networks to chaincode IDs.
const businessNetworks = {};
// A mapping of chaincode IDs to their instance objects.
const chaincodes = {};
/**
* Base class representing a connection to a business network.
* @protected
* @abstract
*/
class EmbeddedConnection extends Connection {
/**
* Clear any registered business networks and chaincodes.
*/
static reset() {
for (let id in businessNetworks) {
delete businessNetworks[id];
}
for (let id in chaincodes) {
delete chaincodes[id];
}
installedChaincodes.clear();
}
/**
* Add chaincode package to the list of installed chaincodes, keyed off of the name and version only.
* @param {BusinessNetworkDefinition} businessNetworkDefinition the business network definition which defines the chaincode
*/
static addInstalledChaincode(businessNetworkDefinition) {
const key = `${businessNetworkDefinition.getName()}@${businessNetworkDefinition.getVersion()}`;
installedChaincodes.set(key, businessNetworkDefinition);
}
/**
* Retrieve a business network definition chaincode keyed off name and version.∂
* @param {string} name The name of the chaincode to retrieve
* @param {string} version The version of the chaincode to retrieve
* @returns {BusinessNetworkDefinition} A business network definition chaincode or null if not found.
*/
static getInstalledChaincode(name, version) {
return installedChaincodes.get(`${name}@${version}`);
}
/**
* Add a business network.
* @param {string} businessNetworkIdentifier The business network identifier.
* @param {string} connectionProfile The connection profile name.
* @param {string} chaincodeUUID The chaincode UUID.
*/
static addBusinessNetwork(businessNetworkIdentifier, connectionProfile, chaincodeUUID) {
businessNetworks[`${businessNetworkIdentifier}@${connectionProfile}`] = chaincodeUUID;
}
/**
* Get the specified business network.
* @param {string} businessNetworkIdentifier The business network identifier.
* @param {string} connectionProfile The connection profile name.
* @return {string} The chaincode UUID.
*/
static getBusinessNetwork(businessNetworkIdentifier, connectionProfile) {
return businessNetworks[`${businessNetworkIdentifier}@${connectionProfile}`];
}
/**
* Add a chaincode.
* @param {string} chaincodeUUID The chaincode UUID.
* @param {Container} container The container.
* @param {Engine} engine The engine.
* @param {InstalledBusinessNetwork} ibn The Installed Business Network
*/
static addChaincode(chaincodeUUID, container, engine, ibn) {
chaincodes[chaincodeUUID] = {
uuid: chaincodeUUID,
container: container,
engine: engine,
installedBusinessNetwork : ibn
};
}
/**
* Get the specified chaincode.
* @param {string} chaincodeUUID The chaincode UUID.
* @return {object} The chaincode.
*/
static getChaincode(chaincodeUUID) {
return chaincodes[chaincodeUUID];
}
/**
* Create a new container.
* @return {Container} The new container.
*/
static createContainer() {
return new EmbeddedContainer();
}
/**
* Create a new engine.
* @param {Container} container The container.
* @return {Engine} The new engine.
*/
static createEngine(container) {
return new Engine(container);
}
/**
* Constructor.
* @param {ConnectionManager} connectionManager The owning connection manager.
* @param {string} connectionProfile The name of the connection profile associated with this connection
* @param {string} businessNetworkIdentifier The identifier of the business network for this connection
*/
constructor(connectionManager, connectionProfile, businessNetworkIdentifier) {
super(connectionManager, connectionProfile, businessNetworkIdentifier);
this.dataService = new EmbeddedDataService(null, true);
}
/**
* Terminate the connection to the business network.
*/
async disconnect() {
}
/**
* Login as a participant on the business network.
* @param {string} enrollmentID The enrollment ID of the participant.
* @param {string} enrollmentSecret The enrollment secret of the participant.
* @return {Promise} A promise that is resolved with a {@link SecurityContext}
* object representing the logged in participant, or rejected with a login error.
*/
async login(enrollmentID, enrollmentSecret) {
const identity = await this.testIdentity(enrollmentID, enrollmentSecret);
// no businessNetworkIdentitier implies fabric identity, not an identity
// that can be enrolled and should already be imported.
if (!this.businessNetworkIdentifier) {
return new EmbeddedSecurityContext(this, identity);
}
let chaincodeUUID = EmbeddedConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile);
if (!chaincodeUUID) {
throw new Error(`No chaincode ID found for business network '${this.businessNetworkIdentifier}'`);
}
// simulate enrolment and import which would occur on a login if
// no imported credentials. Note that no enrolment counting is done in
// this simulation.
if (!identity.imported) {
identity.imported = true;
const identities = await this.getIdentities();
await identities.update(enrollmentID, identity);
}
const result = new EmbeddedSecurityContext(this, identity);
result.setChaincodeID(chaincodeUUID);
return result;
}
/**
* For the embedded connector, this is just a no-op, there is nothing to install. *** I Don't think this is true now ***
* @param {SecurityContext} securityContext The participant's security context.
* @param {string} businessNetworkDefinition The business network definition that will be started
* @param {Object} installOptions connector specific installation options
* @returns {Promise} returns a resolved promise to indicate success
*/
async install(securityContext, businessNetworkDefinition, installOptions) {
EmbeddedConnection.addInstalledChaincode(businessNetworkDefinition);
return Promise.resolve();
}
/**
* Start a business network.
* @param {HFCSecurityContext} securityContext The participant's security context.
* @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime
* @param {string} businessNetworkVersion The version of the Business network that will be started in this installed runtime
* @param {string} startTransaction The serialized start transaction.
* @param {Object} startOptions connector specific start options
*/
async start(securityContext, businessNetworkIdentifier, businessNetworkVersion, startTransaction, startOptions) {
const installedChaincode = EmbeddedConnection.getInstalledChaincode(businessNetworkIdentifier, businessNetworkVersion);
if (!installedChaincode) {
throw new Error(`${businessNetworkIdentifier} version ${businessNetworkVersion} has not been installed`);
}
const installedBusinessNetwork = await InstalledBusinessNetwork.newInstance(installedChaincode);
const container = EmbeddedConnection.createContainer();
const identity = securityContext.getIdentity();
const chaincodeUUID = container.getUUID();
const engine = EmbeddedConnection.createEngine(container);
EmbeddedConnection.addBusinessNetwork(businessNetworkIdentifier, this.connectionProfile, chaincodeUUID);
EmbeddedConnection.addChaincode(chaincodeUUID, container, engine, installedBusinessNetwork);
let context = new EmbeddedContext(engine, identity, this, installedBusinessNetwork);
await engine.init(context, 'start', [startTransaction]);
}
/**
* Upgrade a business network. This connector implementation effectively allows you to
* switch between installed chaincodes, and doesn't complain even if you switch to the
* same chaincode.
* @param {HFCSecurityContext} securityContext The participant's security context.
* @param {string} businessNetworkIdentifier The name of the business network to upgrade.
* @param {String} businessNetworkVersion The version of the business network to upgrade.
* @param {Object} upgradeOptions connector specific start options
* @async
*/
async upgrade(securityContext, businessNetworkIdentifier, businessNetworkVersion, upgradeOptions) {
let installedChaincode = EmbeddedConnection.getInstalledChaincode(businessNetworkIdentifier, businessNetworkVersion);
if (!installedChaincode) {
throw new Error(`${businessNetworkIdentifier} version ${businessNetworkVersion} has not been installed`);
}
let chaincodeUUID = EmbeddedConnection.getBusinessNetwork(businessNetworkIdentifier, this.connectionProfile);
if (!chaincodeUUID) {
throw new Error(`Unable to upgrade, ${businessNetworkIdentifier} has not been started`);
}
const {container, engine} = EmbeddedConnection.getChaincode(chaincodeUUID);
let installedBusinessNetwork = await InstalledBusinessNetwork.newInstance(installedChaincode);
// This adds or replaces
EmbeddedConnection.addChaincode(chaincodeUUID, container, engine, installedBusinessNetwork);
const identity = securityContext.getIdentity();
let context = new EmbeddedContext(engine, identity, this, installedBusinessNetwork);
await engine.init(context, 'upgrade');
}
/**
* Test ("ping") the connection to the business network.
* @param {SecurityContext} securityContext The participant's security context.
* @return {Promise} A promise that is resolved once the connection to the
* business network has been tested, or rejected with an error.
*/
async ping(securityContext) {
const buffer = await this.queryChainCode(securityContext, 'ping', []);
return JSON.parse(buffer.toString());
}
/**
* Invoke a "query" chaincode function with the specified name and arguments.
* @param {SecurityContext} securityContext The participant's security context.
* @param {string} functionName The name of the chaincode function to invoke.
* @param {string[]} args The arguments to pass to the chaincode function.
* @return {Buffer} A buffer containing the data returned by the chaincode function,
* or null if no data was returned.
*/
async queryChainCode(securityContext, functionName, args) {
if (!this.businessNetworkIdentifier) {
throw new Error('No business network has been specified for this connection');
}
let identity = securityContext.getIdentity();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
let context = new EmbeddedContext(chaincode.engine, identity, this, chaincode.installedBusinessNetwork);
const data = await chaincode.engine.query(context, functionName, args);
return !Util.isNull(data) ? Buffer.from(JSON.stringify(data)) : null;
}
/**
* Invoke a "invoke" chaincode function with the specified name and arguments.
* @param {SecurityContext} securityContext The participant's security context.
* @param {string} functionName The name of the chaincode function to invoke.
* @param {string[]} args The arguments to pass to the chaincode function.
* @param {Object} [additionalConnectorOptions] Additional connector specific options for this transaction.
* @return {Buffer} A buffer containing the data returned by the chaincode function,
* or null if no data was returned.
*/
async invokeChainCode(securityContext, functionName, args, additionalConnectorOptions = {}) {
if (!this.businessNetworkIdentifier) {
throw new Error('No business network has been specified for this connection');
}
let identity = securityContext.getIdentity();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
let context = new EmbeddedContext(chaincode.engine, identity, this, chaincode.installedBusinessNetwork, additionalConnectorOptions);
const data = await chaincode.engine.invoke(context, functionName, args);
return !Util.isNull(data) ? Buffer.from(JSON.stringify(data)) : null;
}
/**
* Get the data collection that stores identities.
* @return {Promise} A promise that is resolved with the data collection
* that stores identities.
*/
async getIdentities() {
return await this.dataService.ensureCollection('identities');
}
/**
* Get the identity for the specified name.
* @param {string} identityName The name for the identity.
* @return {Promise} A promise that is resolved with the identity, or
* rejected with an error.
*/
async getIdentity(identityName) {
const identities = await this.getIdentities();
try {
return await identities.get(identityName);
} catch (error) {
if (identityName === 'admin') {
return await this._createAdminIdentity();
}
throw error;
}
}
/**
* Create the default admin identity.
* @return {Promise} A promise that is resolved with the admin identity when complete,
* or rejected with an error.
*/
async _createAdminIdentity() {
const { publicKey, privateKey, certificate } = CertificateUtil.generate({ commonName: 'admin' });
const certificateObj = new Certificate(certificate);
const identifier = certificateObj.getIdentifier();
const name = certificateObj.getName();
const issuer = certificateObj.getIssuer();
const identity = {
identifier,
name,
issuer,
secret: 'adminpw',
certificate,
publicKey,
privateKey,
imported: false,
options: {
issuer: true
}
};
const identities = await this.getIdentities();
await identities.add('admin', identity);
return identity;
}
/**
* Test the specified identity name and secret to ensure that it is valid.
* admin userid doesn't require a secret.
* @param {string} identityName The name for the identity.
* @param {string} identitySecret The secret for the identity.
* @return {Promise} A promise that is resolved if the user ID and secret
* is valid, or rejected with an error.
*/
async testIdentity(identityName, identitySecret) {
const identity = await this.getIdentity(identityName);
if (identity.imported) {
return identity;
} else if (identityName === 'admin') {
return identity;
} else if (identity.secret !== identitySecret) {
throw new Error(`The secret ${identitySecret} specified for the identity ${identityName} does not match the stored secret ${identity.secret}`);
} else {
return identity;
}
}
/**
* Return whether a registry check is required before executing createIdentity to prevent duplicates.
* @return {boolean} true.
*/
registryCheckRequired() {
return true;
}
/**
* Create a new identity for the specified name.
* @param {SecurityContext} securityContext The participant's security context.
* @param {string} identityName The name for the new identity.
* @param {object} [options] Options for the new identity.
* @param {boolean} [options.issuer] Whether or not the new identity should have
* permissions to create additional new identities. False by default.
* @param {string} [options.affiliation] Specify the affiliation for the new
* identity. Defaults to 'institution_a'.
* @return {Promise} A promise that is resolved with a generated user
* secret once the new identity has been created, or rejected with an error.
*/
async createIdentity(securityContext, identityName, options) {
const currentIdentity = securityContext.getIdentity();
if (!currentIdentity.options.issuer) {
throw new Error(`The identity ${currentIdentity.name} does not have permission to create a new identity ${identityName}`);
}
const identities = await this.getIdentities();
const exists = await identities.exists(identityName);
if (exists) {
const identity = await identities.get(identityName);
return {
userID: identityName,
userSecret: identity.secret
};
}
const { publicKey, privateKey, certificate } = CertificateUtil.generate({ commonName: identityName });
const certificateObj = new Certificate(certificate);
const identifier = certificateObj.getIdentifier();
const name = certificateObj.getName();
const issuer = certificateObj.getIssuer();
const secret = uuid.v4().substring(0, 8);
const identity = {
identifier,
name,
issuer,
secret,
certificate,
publicKey,
privateKey,
imported: false,
options: options || {}
};
await identities.add(identityName, identity);
return {
userID: identityName,
userSecret: identity.secret
};
}
/**
* Create a new transaction id
* Note: as this is not a real fabric it returns null to let the composer-common use uuid to create one.
* @param {SecurityContext} securityContext The participant's security context.
* @return {Promise} A promise that is resolved with a null
*/
async createTransactionId(securityContext) {
return null;
}
/**
* Undeploy a business network definition.
* @param {SecurityContext} securityContext The participant's security context.
* @param {String} networkName Name of the business network to remove
* @async
*/
async undeploy(securityContext, networkName) {
await this.dataService.removeAllData();
delete businessNetworks[networkName];
delete chaincodes[networkName];
}
/**
* Get the native API for this connection. The native API returned is specific
* to the underlying blockchain platform, and may throw an error if there is no
* native API available.
*/
getNativeAPI() {
throw new Error('native API not available when using the embedded connector');
}
}
module.exports = EmbeddedConnection;