import Long from "long";
import _ from "lodash";
import { rep } from "./protos/peer"; // use generated static js code
import {
GetHashVal, ImportKey, Sign, VerifySign, GetKeyPEM,
} from "./crypto";
const txEnumTypes = ["CHAINCODE_DEPLOY", "CHAINCODE_INVOKE", "CHAINCODE_SET_STATE"];
const chaincodeLanguageEnumTypes = ["CODE_SCALA", "CODE_JAVASCRIPT"];
// Private properties
const txMsgCollection = new WeakMap();
const txMsgType = rep.protos.Transaction;
const signatureMsgType = rep.protos.Signature;
// Private methods
const getTimestamp = (millis) => {
const timestampMillis = millis || Date.now();
const timestampJsonObj = {
seconds: new Long(timestampMillis / 1000),
nanos: ((timestampMillis % 1000) * 1000000),
};
return timestampJsonObj;
};
class Transaction {
/**
* 构建RepChain交易对象
* @param {Object} consArgs - 交易对象实例构造参数
* @param {Buffer|Uint8Array} [consArgs.txBytes] - 二进制交易数据,当使用该参数时,将忽略其他参数
* @param {string} consArgs.type - 交易类型,需与RepChain的交易类型定义一致,可为CHAINCODE_DEPLOY,
* CHAINCODE_INVOKE, CHAINCODE_SET_STATE
* @param {string} consArgs.chaincodeName - 目标合约的名称
* @param {number} consArgs.chaincodeVersion - 目标合约的版本号
* @param {Object} consArgs.chaincodeDeployParams - 部署合约时(即type为CHAINCODE_DEPLOY)所需参数
* @param {number} consArgs.chaincodeDeployParams.timeout
* @param {string} consArgs.chaincodeDeployParams.codePackage - 待部署合约的代码内容
* @param {string} consArgs.chaincodeDeployParams.legalProse - 待部署合约的法律文本
* @param {string} consArgs.chaincodeDeployParams.codeLanguageType - 待部署合约代码语言类型,
* 目前只支持CODE_SCALA和CODE_JAVASCRIPT
* @param {Object} consArgs.chaincodeInvokeParams - 调用合约时(即type为CHAINCODE_INVOKE)所需参数
* @param {string} consArgs.chaincodeInvokeParams.chaincodeFunction - 待被调用的合约方法名
* @param {Array.<string>} consArgs.chaincodeInvokeParams.chaincodeFunctionArgs - 给待调用的合约方法的参数
* @param {Object} consArgs.chaincodeSetStateParams - 设置合约状态时(即type为CHAINCODE_SET_STATE)所需参数
* @param {boolean} consArgs.chaincodeSetStateParams.state - 目标合约的新状态,当值为false时表示使该合约无效
* @returns {Transaction} Transaction对象实例
*/
constructor({
txBytes, type, chaincodeName, chaincodeVersion,
chaincodeDeployParams: {
timeout, codePackage, legalProse, codeLanguageType,
} = { timeout: 1000 },
chaincodeInvokeParams: { chaincodeFunction, chaincodeFunctionArgs } = {},
chaincodeSetStateParams: { state } = {},
}) {
if (txBytes) { // 此时直接使用该参数构造交易对象
if (Buffer.isBuffer(txBytes) || txBytes.constructor.name === "Uint8Array") {
try {
const msg = txMsgType.decode(txBytes);
txMsgCollection.set(this, msg);
} catch (e) {
throw e;
}
} else {
throw new TypeError("The txBytes field should be a Buffer or Uint8array");
}
} else {
if (_.indexOf(txEnumTypes, type) === -1) {
throw new Error(`The type field should be one of ${txEnumTypes}`);
}
if (!_.isString(chaincodeName)) {
throw new TypeError("The chaincodeName field should be a string");
}
if (!_.isInteger(chaincodeVersion)) {
throw new TypeError("The chaincodeversion field should be an integer");
}
const txJsonObj = {
id: "",
cid: {
chaincodeName,
version: chaincodeVersion,
},
};
switch (type) {
case "CHAINCODE_DEPLOY": {
if (!_.isInteger(timeout)) {
throw new TypeError("The timeout field should be an integer");
}
if (!_.isString(codePackage)) {
throw new TypeError("The codePackage field should be a string");
}
if (!_.isString(legalProse)) {
throw new TypeError("The legalProse field should be a string");
}
if (_.indexOf(chaincodeLanguageEnumTypes, codeLanguageType) === -1) {
throw new Error(`The codeLanguageType field should be one of ${chaincodeLanguageEnumTypes}`);
}
txJsonObj.type = 1;
txJsonObj.spec = {
timeout,
codePackage,
legalProse,
};
switch (codeLanguageType) {
case "CODE_JAVASCRIPT":
txJsonObj.spec.ctype = 1;
break;
case "CODE_SCALA":
txJsonObj.spec.ctype = 2;
break;
default:
break;
}
break;
}
case "CHAINCODE_INVOKE": {
if (!_.isString(chaincodeFunction)) {
throw new TypeError("The chaincodeFunction field should be a string");
}
if (!_.isArray(chaincodeFunctionArgs)) {
throw new TypeError("The chaincodeFunctionArgs field should be an Array<string>");
}
for (let i = 0; i < chaincodeFunctionArgs.length; i++) {
if (!_.isString(chaincodeFunctionArgs[i])) {
throw new TypeError("The chaincodeFunctionArgs field should be an Array<string>");
}
}
txJsonObj.type = 2;
txJsonObj.ipt = {
function: chaincodeFunction,
args: chaincodeFunctionArgs,
};
break;
}
case "CHAINCODE_SET_STATE": {
if (!_.isBoolean(state)) {
throw new TypeError("The state field should be a Boolean");
}
txJsonObj.type = 3;
txJsonObj.state = state;
break;
}
default:
throw new Error("Wrong Transaction type");
}
const err = txMsgType.verify(txJsonObj);
if (err) throw err;
// 计算txid
const msg = txMsgType.create(txJsonObj);
// 在Browser环境下protobufjs中的encode().finish()返回原始的Uint8Array,
// 为了屏蔽其与Buffer经browserify或webpack转译后的Uint8Array的差异,这里需转为Buffer
const txBuffer = Buffer.from(txMsgType.encode(msg).finish());
const timeStampBuffer = Buffer.from(new Date().toISOString());
const dataBuffer = Buffer.concat([txBuffer, timeStampBuffer],
txBuffer.length + timeStampBuffer.length);
msg.id = GetHashVal({ data: dataBuffer, alg: "sha256" }).toString("hex");
txMsgCollection.set(this, msg);
}
}
getTxMsg() {
return txMsgCollection.get(this);
}
/**
* 对新创建的交易实例进行签名
* @param {Object} signArgs - 签名所需参数
* @param {string} signArgs.prvKey - 签名者的pem格式私钥
* @param {string} signArgs.pubKey - 签名者的pem格式公钥
* @param {string} signArgs.alg - 使用的签名算法名称
* @param {string} [signArgs.pass] - 私钥解密密码,如果prvKey为已加密的pem格式私钥,则需要提供此解密密码
* @param {string} signArgs.creditCode - 签名者的信用代码
* @param {string} signArgs.certName - 代表签名者的证书名
* @returns {Buffer} - 已签名交易数据
*/
sign({
prvKey, pubKey, alg, pass, creditCode, certName,
}) {
if (!_.isString(prvKey)) throw new Error("The prvKey field should be a string");
if (!_.isString(pubKey)) throw new Error("The pubKey field should be a string");
if (!_.isString(alg)) throw new Error("The alg field should be a string");
if (pass && !_.isString(pass)) throw new Error("The pass field should be a string");
if (!_.isString(creditCode)) throw new Error("The creditCode field should be a string");
if (!_.isString(certName)) throw new Error("The certName field should be a string");
const msg = txMsgCollection.get(this);
if (msg.signature && msg.signature.signature) {
throw new Error("The transaction has been signed already");
}
// 签名
let txBuffer = Buffer.from(txMsgType.encode(msg).finish());
const prvKeyObj = ImportKey(prvKey, pass); // 私钥解密
if (prvKeyObj.pubKeyHex === undefined) {
// 当使用ImportKey方法从pem格式转object格式时,若其pubKeyHex为undefined则需在该object中补充pubKeyHex
// 否则签名将出错
prvKeyObj.pubKeyHex = ImportKey(pubKey).pubKeyHex;
}
const prvkeyPEM = GetKeyPEM(prvKeyObj);
const signature = Sign({ prvKey: prvkeyPEM, data: txBuffer, alg });
const signatureJsonObj = {
certId: {
creditCode,
certName,
},
tmLocal: getTimestamp(),
signature,
};
const err = signatureMsgType.verify(signatureJsonObj);
if (err) throw err;
msg.signature = signatureMsgType.create(signatureJsonObj);
txBuffer = Buffer.from(txMsgType.encode(msg).finish());
return txBuffer;
}
/**
* 对已签名的交易对象进行签名验证
* @param {String} pubKey pem格式的公钥
* @param {String} alg 使用的签名算法
* @returns {boolean} 验签是否成功
*/
verifySignature(pubKey, alg) {
const msg = _.cloneDeep(txMsgCollection.get(this));
const signature = _.cloneDeep(msg.signature);
if (!signature || !signature.signature) {
throw new Error("The transaction has not been signed yet");
}
msg.signature = null;
const msgBuffer = Buffer.from(txMsgType.encode(msg).finish());
const valid = VerifySign({
pubKey, sigValue: signature.signature, data: msgBuffer, alg,
});
return valid;
}
}
export default Transaction;