hdc
Version:
NPM module implementing HDC Messages Format
301 lines (283 loc) • 10 kB
JavaScript
var async = require('async');
var sha1 = require('sha1');
var _ = require('underscore');
module.exports = function Amendment(rawAmend){
this.version = null;
this.currency = null;
this.number = null;
this.generated = null;
this.dividend = null;
this.coinBase = null;
this.coinList = null;
this.coinAlgo = null;
this.nextVotes = null;
this.previousHash = null;
this.membersRoot = null;
this.membersCount = 0;
this.membersChanges = [];
this.votersRoot = null;
this.votersCount = 0;
this.votersChanges = [];
this.hash = null;
this.error = null;
this.parse = function(rawAmend) {
if(!rawAmend){
this.error = "No amendment given";
return false;
}
else{
this.error = "";
this.hash = sha1(unix2dos(rawAmend)).toUpperCase();
var obj = this;
var captures = [
{prop: "version", regexp: /Version: (.*)/},
{prop: "currency", regexp: /Currency: (.*)/},
{prop: "number", regexp: /Number: (.*)/},
{prop: "generated", regexp: /GeneratedOn: (.*)/},
{prop: "dividend", regexp: /UniversalDividend: (.*)/},
{prop: "coinBase", regexp: /CoinBase: (.*)/},
{prop: "coinList", regexp: /CoinList: (.*)/},
{prop: "coinAlgo", regexp: /CoinAlgo: (.*)/},
{prop: "nextVotes", regexp: /NextRequiredVotes: (.*)/},
{prop: "previousHash", regexp: /PreviousHash: (.*)/},
{prop: "membersRoot", regexp: /MembersRoot: (.*)/},
{prop: "membersCount", regexp: /MembersCount: (.*)/},
{prop: "membersChanges", regexp: /MembersChanges:\n([\s\S]*)VotersRoot/},
{prop: "votersRoot", regexp: /VotersRoot: (.*)/},
{prop: "votersCount", regexp: /VotersCount: (.*)/},
{prop: "votersChanges", regexp: /VotersChanges:\n([\s\S]*)/},
];
var crlfCleaned = rawAmend.replace(/\r\n/g, "\n");
if(crlfCleaned.match(/\n$/)){
captures.forEach(function (cap) {
if(cap.prop != "membersChanges" && cap.prop != "votersChanges")
simpleLineExtraction(obj, crlfCleaned, cap);
else{
this.error = multipleLinesExtraction(obj, crlfCleaned, cap);
if(this.error)
return false;
}
});
return true;
}
else{
this.error = "Bad document structure: no new line character at the end of the document.";
return false;
}
}
};
this.verify = function(currency){
var err = null;
var codes = {
'VERSION': 150,
'CURRENCY': 151,
'NUMBER': 152,
'GENERATEDON': 153,
'UD': 154,
'NEXT_VOTES': 156,
'PREV_HASH': 157,
'MEMBERS_ROOT': 160,
'MEMBERS_COUNT': 161,
'MEMBERS_CHANGES': 162,
'VOTERS_ROOT': 160,
'VOTERS_COUNT': 161,
'VOTERS_CHANGES': 162,
'COIN_BASE': 173,
'COIN_LIST': 174,
'COIN_SUM': 175
}
if(this.error){
err = {code: 0, message: this.error};
}
if(!err){
// Version
if(!this.version || !this.version.match(/^1$/))
err = {code: codes['VERSION'], message: "Version unknown"};
}
if(!err){
// Currency
if(!this.currency || !this.currency.match("^"+ currency + "$"))
err = {code: codes['CURRENCY'], message: "Currency '"+ this.currency +"' not managed"};
}
if(!err){
// Number
if(!this.number || !this.number.match(/^\d+$/))
err = {code: codes['NUMBER'], message: "Incorrect Number field"};
}
if(!err){
// GeneratedOn
if(!this.generated || !this.generated.match(/^\d+$/))
err = {code: codes['GENERATEDON'], message: "GeneratedOn field must be a positive or zero integer"};
}
if(!err){
// Universal Dividend
if(this.dividend && !this.dividend.match(/^\d+$/))
err = {code: codes['UD'], message: "UniversalDividend must be a positive or zero integer"};
// Coin Base
if(this.dividend && (!this.coinBase || !this.coinBase.match(/^\d+$/)))
err = {code: codes['COIN_BASE'], message: "CoinBase must be a positive or zero integer"};
// Coin List
if(this.dividend && (!this.coinList || !this.coinList.match(/^(\d+ )*\d+$/)))
err = {code: codes['COIN_LIST'], message: "CoinList must be a space separated list of positive or zero integers"};
else if(this.dividend) {
var dividendSum = 0;
var power = parseInt(this.coinBase);
this.coinList.split(" ").forEach(function(c){
dividendSum += parseInt(c) * Math.pow(2, power++);
});
if (parseInt(this.dividend) != dividendSum) {
err = {code: codes['COIN_SUM'], message: "CoinList sum '" + dividendSum + "' does not match UniversalDividend '" + this.dividend + "'"};
}
}
}
if(!err){
// NextRequiredVotes
if(this.nextVotes && !this.nextVotes.match(/^\d+$/))
err = {code: codes['NEXT_VOTES'], message: "NextRequiredVotes must be a positive or zero integer"};
}
if(!err){
// Previous hash
var isRoot = parseInt(this.number, 10) === 0;
if(!isRoot && (!this.previousHash || !this.previousHash.match(/^[A-Z\d]{40}$/)))
err = {code: codes['PREV_HASH'], message: "PreviousHash must be provided for non-root amendment and match an uppercase SHA1 hash"};
else if(isRoot && this.previousHash)
err = {code: codes['PREV_HASH'], message: "PreviousHash must not be provided for root amendment"};
}
if(!err){
// VotersRoot
if(this.previousHash && (!this.votersRoot || !this.votersRoot.match(/^[A-Z\d]{40}$/)))
err = {code: codes['VOTERS_ROOT'], message: "VotersRoot must be provided and match an uppercase SHA1 hash"};
}
if(!err){
// VotersCount
if(this.previousHash && (!this.votersCount || !this.votersCount.match(/^\d+$/)))
err = {code: codes['VOTERS_COUNT'], message: "VotersCount must be a positive or zero integer"};
}
if(!err){
// MembersRoot
if(!this.membersRoot || !this.membersRoot.match(/^[A-Z\d]{40}$/))
err = {code: codes['MEMBERS_ROOT'], message: "MembersRoot must be provided and match an uppercase SHA1 hash"};
}
if(!err){
// MembersCount
if(!this.membersCount || !this.membersCount.match(/^\d+$/))
err = {code: codes['MEMBERS_COUNT'], message: "MembersCount must be a positive or zero integer"};
}
if(err){
this.error = err.message;
this.errorCode = err.code;
return false;
}
return true;
};
this.getNewMembers = function() {
var members = [];
for (var i = 0; i < this.membersChanges.length; i++) {
var matches = this.membersChanges[i].match(/^\+([\w\d]{40})$/);
if(matches){
members.push(matches[1]);
}
}
return members;
};
this.getLeavingMembers = function() {
var members = [];
for (var i = 0; i < this.membersChanges.length; i++) {
var matches = this.membersChanges[i].match(/^\-([\w\d]{40})$/);
if(matches){
members.push(matches[1]);
}
}
return members;
};
this.getNewVoters = function() {
var voters = [];
for (var i = 0; i < this.votersChanges.length; i++) {
var matches = this.votersChanges[i].match(/^\+([\w\d]{40})$/);
if(matches){
voters.push(matches[1]);
}
}
return voters;
};
this.getLeavingVoters = function() {
var voters = [];
for (var i = 0; i < this.votersChanges.length; i++) {
var matches = this.votersChanges[i].match(/^\-([\w\d]{40})$/);
if(matches){
voters.push(matches[1]);
}
}
return voters;
};
this.getRaw = function() {
var raw = "";
raw += "Version: " + this.version + "\n";
raw += "Currency: " + this.currency + "\n";
raw += "Number: " + this.number + "\n";
raw += "GeneratedOn: " + this.generated + "\n";
if(this.dividend){
raw += "UniversalDividend: " + this.dividend + "\n";
raw += "CoinAlgo: " + this.coinAlgo + "\n";
raw += "CoinBase: " + this.coinBase + "\n";
raw += "CoinList: " + this.coinList + "\n";
}
raw += "NextRequiredVotes: " + this.nextVotes + "\n";
if(this.previousHash){
raw += "PreviousHash: " + this.previousHash + "\n";
}
if(this.membersRoot){
raw += "MembersRoot: " + this.membersRoot + "\n";
raw += "MembersCount: " + this.membersCount + "\n";
raw += "MembersChanges:\n";
for(var i = 0; i < this.membersChanges.length; i++){
raw += this.membersChanges[i] + "\n";
}
}
raw += "VotersRoot: " + this.votersRoot + "\n";
raw += "VotersCount: " + this.votersCount + "\n";
raw += "VotersChanges:\n";
for(var j = 0; j < this.votersChanges.length; j++){
raw += this.votersChanges[j] + "\n";
}
return unix2dos(raw);
};
this.parse(rawAmend);
};
function simpleLineExtraction(am, wholeAmend, cap) {
var fieldValue = wholeAmend.match(cap.regexp);
if(fieldValue && fieldValue.length === 2){
am[cap.prop] = fieldValue[1];
}
return;
}
function multipleLinesExtraction(am, wholeAmend, cap) {
var fieldValue = wholeAmend.match(cap.regexp);
am[cap.prop] = [];
if(fieldValue && fieldValue.length == 2){
var lines = fieldValue[1].split(/\n/);
if(lines[lines.length - 1].match(/^$/)){
for (var i = 0; i < lines.length - 1; i++) {
var line = lines[i];
var fprChange = line.match(/([+-][A-Z\d]{40})/);
if(fprChange && fprChange.length == 2){
am[cap.prop].push(fprChange[1]);
}
else{
return "Wrong structure for line: '" + line + "'";
}
}
}
else return "Wrong structure for line: '" + line + "'";
}
return;
}
function trim(str){
return str.replace(/^\s+|\s+$/g, '');
}
function unix2dos(str){
return dos2unix(str).replace(/\n/g, '\r\n');
}
function dos2unix(str){
return str.replace(/\r\n/g, '\n');
}