Dash Budget Proposal Generator (#18)

* GovObject Proposal Form

* update proposal_name --> name

* jquery ui date picker

* cleanup + copyToClipboard

* remove websockets

* aesthetic/UX changes

* fix start/end epoch times

* remove "dash-cli" prefix from gobject commands

* bugfix: proposal name double-quote serialization

* add .gitignore

* Refactor UX

* ProposalGenerator / UI Controller

* Proposal form is disabled on create

* bugfix: prepare command string

* add proposal validation

* refactor setFormEditable

* simplify url regex

* first commit with drop down list instead of datepicker for payment start & end dates

* first commit with drop down list instead of datepicker for payment start & end dates

* automatic default adjustment of end date to one month after start date whenever start date is changed

* clean readme

* added parameter bufferdays to easily change number of days before next superblock before we automatically select start date of next budget cycle

* included dropdown lists to be uneditable/editable with the "Create Proposal"/"Edit Proposal"-buttons

* replaced end date dropdown with a time span in month dropdown

* adapted default superblock for start date dropdown

* fill $("#time").val once again for the time parameter in gobject cmd

* first code to check feeTxid of govobject prepare and count confirmations

* first code to check feeTxid of govobject prepare and count confirmations

* disable/enable fee Transaction input field

* checking new block for feeTxid

* listener for blocks and 6 confirmations

* listener for blocks and 6 confirmations

* listener for blocks and 6 confirmations

* progress bar while waiting for fee tx confirmations

* added error messages

* Add epoch time to serialization and relax address validation (#4)

- address validation is breaking in Ubuntu Firefox. disabled until future fix

* bugfix: convert javascript time to unix time (/1000)

* refactor: tx listener and superblock date selection fields (#6)

* adds images, css dir, reformats proposal form

* adds dash logo, styles header, sync with design

* refactor start and end epoch calculation

* refactor js dependencies

* comment out social links for now, issue #8

* adds favicon

* move proposol header and description to right side of form

* include Roboto font

* refactor js dependencies

* refactor transaction listener

* implements tabs as step progression

* clean up tabs

* progress buttons

* progress bar

* implement transaction listener progress bar

* save progress on getting submit proposal command and progressing UI confirmations

* confirmations counted, transitions to final step

* fee transaction id appends to submit command, step four complete

* implement network selector

* bugfix: provider

* adds css proposalBlock style to rest of steps, raises validation errors on step 2 while inputting txID

* adds disconnect event for socket io

* enables new and edit buttons, hides steps in progress if new proposal clicked

* mainnet/testnet toggle is hidden upon clicking Create Proposal, reappeears upon New Proposal

* calculate total proposal amount

* bugfix and refactor total amount calculation

* adjust payment cycle selectors

* various adjustments

* moves network toggle buttons, popup on tx errors (#16)

* replace dash_logo, sharpen Governance Tools subtitle (#17)

* moves network toggle buttons, popup on tx errors

* replace dash_logo, sharpen Governance Tools subtitle

* include proposal maturity constants

* updated proposal validation

* update bitcore-lib-dash to latest

* adjust start_epoch and end_epoch calculation

* various design / label improvements

* add api prefix as parameter for socket.io

* update DASH fee amount

* update default network
This commit is contained in:
snogcel 2017-02-03 09:56:33 -07:00 committed by GitHub
parent b2588253ae
commit dff6605da4
33 changed files with 63941 additions and 3 deletions

25
js/formHandler.js Executable file
View file

@ -0,0 +1,25 @@
function setFormEditable(edit) {
$('.createProposal input').each(function() {
$(this).attr("disabled", edit);
});
$('.createProposal select').each(function() {
$(this).attr("disabled", edit);
});
if (edit === true) {
$('#btnPrepare').addClass('hidden');
$('#btnEdit').removeClass('hidden');
$('#btnEdit').removeAttr('disabled');
$('#btnNew').removeClass('hidden');
$('#btnNew').removeAttr('disabled');
} else {
$('#btnPrepare').removeClass('hidden');
$('#btnEdit').addClass('hidden');
$('#btnNew').addClass('hidden');
}
}
var copyToClipboard = function(id) {
document.getElementById(id).select();
document.execCommand('copy');
};

199
js/paymentCycle.js Executable file
View file

@ -0,0 +1,199 @@
/***
* Payment Cycle Generator
*
* @param gov
* @constructor
*/
function PaymentCycle(gov, provider, prefix) {
var self = this;
this.network = gov.network;
this.provider = provider;
this.prefix = prefix;
this.paymentCycle = 16616;
this.proposalMaturity = 1662; // ~(60*24*3)/2.6 = about three days
this.budgetCycles = 24;
this.selectedStartIndex = 0;
this.selectedPeriods = 1;
if (this.network == 'testnet') this.paymentCycle = 23;
if (this.network == 'testnet') this.proposalMaturity = 24; // a little more than one hour
if (this.network == 'testnet') this.budgetCycles = 99;
this.blockHeight = 0;
this.startDate = [];
this.endDate = [];
this.Messages = {
paymentCycle: {
payment: "Payment",
payments: "Payments",
months: "Months",
month: "Month",
days: "Days",
day: "Day",
hours: "Hours",
hour: "Hour",
minutes: "Minutes",
minute: "Minute",
seconds: "Seconds",
second: "Second"
}
};
this.getInfo(function(err, res) {
self.blockHeight = res.info.blocks;
console.log("current blockheight: " + self.blockHeight);
self.updateDropdowns();
});
}
PaymentCycle.prototype.getNextSuperblock = function(block) {
return (Math.floor((block/this.paymentCycle)) * this.paymentCycle + this.paymentCycle);
};
PaymentCycle.prototype.getBlockTimestamp = function(block) {
var blocks = block - this.blockHeight;
var now = Math.floor(Date.now());
return (now + (blocks * (155 * 1000))); // 155 seconds per block x 1000 = ms per block
};
PaymentCycle.prototype.getTimeDifference = function(opts, start, end) {
var precision = opts.precision;
var millisec = end - start;
var seconds = (millisec / 1000).toFixed(precision);
var minutes = (millisec / (1000 * 60)).toFixed(precision);
var hours = (millisec / (1000 * 60 * 60)).toFixed(precision);
var days = (millisec / (1000 * 60 * 60 * 24)).toFixed(precision);
var months = (millisec / (1000 * 60 * 60 * 24 * 30)).toFixed(precision);
if (seconds < 60) {
if (seconds <= 1) return seconds + " " + this.Messages.paymentCycle.second; // singular
return seconds + " " + this.Messages.paymentCycle.seconds;
} else if (minutes < 60) {
if (minutes <= 1) return minutes + " " + this.Messages.paymentCycle.minute; // singular
return minutes + " " + this.Messages.paymentCycle.minutes;
} else if (hours < 24) {
if (hours <= 1) return hours + " " + this.Messages.paymentCycle.hour; // singular
return hours + " " + this.Messages.paymentCycle.hours;
} else if (days < 30) {
if (days <= 1) return days + " " + this.Messages.paymentCycle.day; // singular
return days + " " + this.Messages.paymentCycle.days;
} else {
if (months <= 1) return months + " " + this.Messages.paymentCycle.month; // singular
return months + " " + this.Messages.paymentCycle.months;
}
};
PaymentCycle.prototype.updateDropdowns = function() {
var self = this;
var blockHeight = this.blockHeight;
var now = Math.floor(Date.now());
for (i = 0; i < this.budgetCycles + 1; i++) {
var superblock = this.getNextSuperblock(blockHeight);
var timestamp = this.getBlockTimestamp(superblock);
var before = this.getBlockTimestamp((superblock-(this.paymentCycle/2))); // set start_epoch to halfway before superblock
var after = this.getBlockTimestamp((superblock+(this.paymentCycle/2))); // set end_epoch to halfway after superblock
var votingDeadline = this.getBlockTimestamp((superblock-this.proposalMaturity)); // if superblock is within ~3 days skip to the next one
var label = new Date(timestamp).toLocaleDateString();
if (this.network == 'testnet') label += " @ " + new Date(timestamp).toLocaleTimeString();
var superblockDate = {
superblock: superblock,
timestamp: timestamp,
before: before,
after: after,
label: label
};
// include superblock if proposal maturity date is later than now
if (votingDeadline > now) {
this.startDate.push(superblockDate);
this.endDate.push(superblockDate);
}
blockHeight = superblock;
}
// this.endDate.shift(); // remove first element of endDate
// this.startDate.pop(); // remove last element of startDate to keep length even
var opts = {
precision: 2
}; // 2 unit of precision for eta formatting
// calculate the amount of time between start and stop, show: e.g. 5 Months or 5 Hours
var start_epoch = $("#start_epoch");
start_epoch.find('option').remove();
$.each(this.startDate, function(index) {
var eta = self.getTimeDifference(opts, now, this.timestamp);
var time = this.timestamp - now;
var option = $("<option />").val((Math.floor(this.before / 1000))).text(this.label).attr('data-index', index).attr('data-time', time).attr('data-eta', eta).attr('data-block', this.superblock);
start_epoch.append(option);
});
self.updateEndEpoch();
};
PaymentCycle.prototype.updateEndEpoch = function() {
var self = this;
var opts = {
precision: null
}; // 0 units of precision for eta formatting
var end_epoch = $("#end_epoch");
end_epoch.find('option').remove();
var i = 1;
var payments = self.Messages.paymentCycle.payment;
$.each(this.endDate, function(index) {
if(index >= self.selectedStartIndex) {
if (i > 1) payments = self.Messages.paymentCycle.payments;
var eta = self.getTimeDifference(opts, self.startDate[self.selectedStartIndex].timestamp, this.timestamp);
var time = this.timestamp - self.startDate[self.selectedStartIndex].timestamp;
var option = $("<option />").val((Math.floor(this.after / 1000))).text((i+" "+payments)).attr('data-index', index).attr('data-label', this.label).attr('data-time', time).attr('data-eta', eta).attr('data-block', this.superblock);
end_epoch.append(option);
i++;
}
});
};
PaymentCycle.prototype.getInfo = function(cb) {
$.getJSON(this.provider + this.prefix + "/status?q=getinfo", function( data ) {
cb(null, data);
});
};

129
js/proposalGenerator.js Executable file
View file

@ -0,0 +1,129 @@
function ProposalGenerator(gov) {
this._mode = 'proposal';
if(!gov.network) gov.network = 'livenet';
this.gov = gov;
// proposal basic fields
this.gov.name = $('#name').val();
this.gov.url = $('#url').val();
this.gov.payment_address = $('#payment_address').val();
this.gov.payment_amount = $('#payment_amount').val();
this.gov.start_epoch = $('#start_epoch').val();
this.gov.end_epoch = $('#end_epoch').val();
// hidden elements
this.gov.type = parseInt($('#type').val());
}
ProposalGenerator.prototype.validate = function() {
try {
var gov = this.gov.serialize();
}
catch (e) {
switch(e.message) {
case 'Invalid Name':
console.log("error: invalid name");
$('#name').addClass('validationError');
$('#name').val("Invalid name. Please enter a 40 character alphanumeric name without spaces.");
break;
case 'Invalid URL':
console.log("Error: invalid url");
$('#url').addClass('validationError');
$('#url').val("There is a formatting error in your URL. Did you forget the leading 'http://'?");
break;
case 'Invalid Payment Amount':
console.log("Error: invalid payment amount");
$('#payment_amount').addClass('validationError');
// $('#payment_amount').val("Invalid payment amount.");
break;
case 'Invalid Timespan':
console.log("Error: invalid timespan");
$('#start_epoch, #end_epoch').addClass('validationError');
break;
case 'Invalid Start Date':
console.log("Error: invalid start date");
$('#start_epoch').addClass('validationError');
break;
case 'Invalid End Date':
console.log("Error: invalid end date");
$('#end_epoch').addClass('validationError');
break;
case 'Invalid Address':
console.log("Error: invalid address");
$('#payment_address').addClass('validationError');
$('#payment_address').val("Invalid payment address.");
break;
default:
console.log(e);
break;
}
return false;
}
return true;
};
ProposalGenerator.prototype.walletCommands = function() {
var gov = this.gov;
var prepCommand = "gobject prepare "+$('#parentHash').val() + " " + $('#revision').val() +" " + $('#time').val() +" " + gov.serialize();
console.log(prepCommand);
$("textarea#prepareProposal").val(prepCommand);
if(this._mode == 'proposal') {
setFormEditable(true);
$('.walletCommands#walletCommandsHeader').removeClass('hidden');
$('.walletCommands#walletCommandsPrepare').removeClass('hidden');
$('.walletCommands#walletCommandsTx').removeClass('hidden');
//$('.walletCommands#walletCommandsProgress').removeClass('hidden');
//$('.walletCommands#walletCommandsSubmit').removeClass('hidden');
this._mode = 'command';
}
};
ProposalGenerator.prototype.createProposal = function() {
$('#feeTxid').val("");
$('#submitProposal').val("");
if(this._mode == 'command') {
setFormEditable(false);
$('.walletCommands#walletCommandsHeader').addClass('hidden');
$('.walletCommands#walletCommandsPrepare').addClass('hidden');
$('.walletCommands#walletCommandsTx').addClass('hidden');
$('.walletCommands#walletCommandsProgress').addClass('hidden');
$('.walletCommands#walletCommandsSubmit').addClass('hidden');
this._mode = 'proposal';
}
};
ProposalGenerator.prototype.resetProposal = function() {
$('.createProposal input').each(function() {
$(this).val('');
});
$("#btnEdit").val('Edit Proposal');
$("#btnNew").val('New Proposal');
$("#btnPrepare").val('Create Proposal');
setFormEditable(false);
$('.walletCommands#walletCommandsHeader').addClass('hidden');
$('.walletCommands#walletCommandsPrepare').addClass('hidden');
$('.walletCommands#walletCommandsTx').addClass('hidden');
$('.walletCommands#walletCommandsProgress').addClass('hidden');
$('.walletCommands#walletCommandsSubmit').addClass('hidden');
this._mode = 'proposal';
};

93
js/txListener.js Executable file
View file

@ -0,0 +1,93 @@
function TXListener(socket, provider, prefix, transaction) {
this.socket = socket;
this.provider = provider;
this.prefix = prefix;
this.transaction = transaction;
this.blockheight = null;
this.confirmations = null;
}
TXListener.prototype.initSocket = function(cb) {
var self = this;
var socket = this.socket;
var confirmations = 0;
socket.on('block', function(data) {
console.log('block: '+ data);
self.getBlock(data, function(err, res) {
if (err) console.log("error fetching block: " + data);
//self.confirmations = (res.height - self.blockheight) + 1; // compare blockHeight against transaction blockHeight
confirmations++;
if (confirmations >= 6) {
cb();
};
$("#progressbar").progressbar({value: ((100 / 6) * confirmations)});
console.log('confirmations: ' + confirmations);
});
});
};
TXListener.prototype.getTx = function(cb) {
var txid = this.transaction;
var opts = {
type: "GET",
route: "/tx/"+txid,
data: {
format: "json"
}
};
this._fetch(opts, cb);
};
TXListener.prototype.getBlock = function(hash, cb) {
var opts = {
type: "GET",
route: "/block/"+hash,
data: {
format: "json"
}
};
this._fetch(opts, cb);
};
TXListener.prototype._fetch = function(opts,cb) {
var self = this;
var provider = opts.provider || self.provider;
var prefix = opts.prefix || self.prefix;
if(opts.type && opts.route && opts.data) {
jQuery.ajax({
type: opts.type,
url: provider + prefix + opts.route,
data: JSON.stringify(opts.data),
contentType: "application/json; charset=utf-8",
crossDomain: true,
dataType: "json",
success: function (data, status, jqXHR) {
cb(null, data);
},
error: function (jqXHR, status, error) {
var err = jqXHR.status;
//var err = eval("(" + jqXHR.responseText + ")");
cb(err, null);
}
});
} else {
cb('missing parameter',null);
}
};