@woleet/woleet-widget
Version:
Woleet verification widget
665 lines (576 loc) • 21.4 kB
JavaScript
;
(function (root) {
/**
* @param [hash]
* @constructor
*/
function Widget(hash) {
// Check parameter
if (hash && typeof hash !== 'string') throw new Error('Invalid parameter type'); // Init state
var state = {
state: 'initial',
hash: null
};
/**
* @typedef {{
* target:function(),
* addClass: function(string[]|string):$,
* removeClass: function(string[]|string):$,
* show: function():$,
* hide: function():$,
* text: function(text: string, add?: boolean):$,
* html: function(text: string, add?: boolean):$,
* link: function(link: string):$,
* clear: function():$,
* style: function(props: object),
* attr: function(attr: string, val: *),
* on: function(type: string, listener:function, capture: boolean),
* toDom: function()
* }} $
*/
var defineProperty = function defineProperty(target) {
return function (name, value) {
return Object.defineProperty(target, name, {
enumerable: false,
value: value
});
};
};
/**
* @description "virtual" DOM element Object
* @param {Element|string} element
* @private
*/
var $ = function $(element) {
if (!element instanceof Element) {
throw new TypeError();
}
var target = element;
/**
* @type {$}
*/
var self = this;
var rts = function rts() {
return self;
};
var def = defineProperty(this);
def('target', function () {
return target;
});
def('attr', function (attr, val) {
return rts(val ? target.setAttribute(attr, val) : target.removeAttribute(attr));
});
def('removeClass', function (e) {
return rts(Array.isArray(e) ? e.forEach(function (e) {
return target.classList.remove(e);
}) : target.classList.remove(e));
});
def('addClass', function (e) {
return rts(Array.isArray(e) ? e.forEach(function (e) {
return target.classList.add(e);
}) : target.classList.add(e));
});
def('text', function (text, add) {
return rts(add ? target.innerText += text : target.innerText = text);
});
def('html', function (text, add) {
return rts(add ? target.innerHTML += text : target.innerHTML = text);
});
def('link', function (url) {
return rts(self.text(url).attr('href', url));
});
def('clear', function () {
return rts(self.text(''), self.attr('href', null));
});
def('show', function () {
return self.removeClass('hidden');
});
def('hide', function () {
return self.addClass('hidden');
});
def('style', function (props) {
if (Array.isArray(props)) {
return props.map(function (p) {
return target.style[p];
});
} else if (typeof props === 'string') {
return target.style[props];
} else {
for (var prop in props) {
target.style[prop] = props[prop];
}
}
});
def('on', function (type, listener, capture) {
return rts(target.addEventListener(type, listener, capture));
});
def('toDom', function () {
var root = self.target();
for (var e in self) {
var elt = self[e];
try {
if (Array.isArray(elt)) elt.forEach(function (e) {
return root.appendChild(e.toDom());
});else root.appendChild(elt.toDom());
} catch (err) {
console.warn(e, target, self[e], err);
}
}
return root;
});
};
/**
* @description "virtual" DOM element factory
* @param {String} [e] element type
* @param {String|Array<String>} [c] class/classes
*/
var $touch = function $touch() {
var e = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div';
var c = arguments[1];
var d = new $(document.createElement(e));
if (c) d.addClass(c);
return d;
}; // Build the "virtual" widget
var widget = $touch('div', 'widget');
var head = widget.head = $touch('div', 'head');
head.logo = $touch('div', 'woleet-logo');
head.x = $touch('div', 'floatright');
head.x.receipt = $touch('button', ['receipt-button', 'clickable']).on('click', forceReceipt).text('Drop proof');
head.x.reset = $touch('div', ['reset', 'mini-button', 'clickable']).on('click', reset);
head.x.cancel = $touch('div', ['cancel', 'mini-button', 'clickable']).on('click', cancelHash);
var body = widget.body = $touch('div', 'body');
var hashZone = body.hashZone = $touch('div', 'hashZone');
hashZone.hashProgessContainer = $touch('div', 'progressBarContainer');
hashZone.percentage = $touch('span').text('Hashing... 0%');
var progressBar = hashZone.hashProgessContainer.progressBar = $touch('div', 'progressBar');
var infoZone = body.infoZone = $touch('div', 'infoZone');
infoZone.items = [];
defineProperty(infoZone)('addItem', function () {
var _this = this;
var item = $touch('div', 'infoZoneItem');
item.mainTextZone = $touch('div', ['text', 'small']);
item.subTextZone = $touch('div', ['text', 'x-small']);
item.byTextZone = $touch('span', ['text', 'x-small']).text('by ');
item.signTextZone = $touch('a', ['link', 'x-small']);
item.identityTextZone = $touch('div', ['text', 'x-small']);
item.warnTextZone = $touch('div', ['text', 'x-small', 'warn']);
item.on('click', function () {
return selectItem(item, _this.items);
});
this.items.push(item);
return item;
});
defineProperty(infoZone)('flush', function () {
this.html('');
this.items = [];
return this;
});
var dropZone = body.dropZone = $touch('div', 'dropZoneContainer');
dropZone.inputContainer = $touch('div');
dropZone.inputContainer.mainTextZone = $touch('div', ['text', 'small']);
dropZone.inputContainer.subTextZone = $touch('div', ['text', 'x-small']);
dropZone.inputContainer.input = $touch('input', ['dropZone', 'clickable']).attr('type', 'file').on('change', setInputFile);
var foot = widget.foot = $touch('div', 'foot');
foot.expand = $touch('div', ['expand', 'expand-button', 'clickable']).on('click', expandList);
init(); // Simulate a drop if hash is provided
if (hash) setInputFile.call({
files: [hash]
});
function init() {
resetText();
dropZone.inputContainer.mainTextZone.text('Drop the file to verify');
infoZone.removeClass(['ok', 'error', 'info']);
infoZone.hide();
dropZone.show();
hashZone.hide();
head.x.reset.hide();
head.x.cancel.hide();
head.x.receipt.hide();
foot.hide();
}
function resetText() {
dropZone.inputContainer.mainTextZone.clear();
dropZone.inputContainer.subTextZone.clear();
infoZone.flush();
}
/**
* @param {Date} date
*/
function formatDate(date) {
var options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
};
return date.toLocaleDateString('en', options);
}
var hasher = new woleet.file.Hasher();
function setInputFile() {
var file = this.files[0];
if (!file) return; // Set default vue
if (state.state === 'done') setVue('init');
var setProgress = function setProgress(e) {
var p = e.progress * 100;
if (p !== p) p = 0;
p = p.toFixed(0);
progressBar.style({
width: p + '%'
});
hashZone.percentage.text('Hashing... ' + p + '%');
}; // We need a receipt to verify the hash|file
if (state.state === 'needReceipt') {
return parseReceiptFile(file).then(function (receipt) {
return woleet.verify.DAB(state.hash, receipt, setProgress);
}).then(function (res) {
if (res.code !== 'verified') {
throw new Error(res.code);
}
setVue('ok', res);
state.state = 'done';
}).catch(function (err) {
setVue('error', err);
});
} // We just entered a new hash|file to verify
else {
var woleetDAB = function woleetDAB(hash) {
setVue('pending', 'Verifying proof(s)... ');
return woleet.verify.WoleetDAB(hash, setProgress).then(function (results) {
state.state = 'done';
addResults(results.sort(function (b, a) {
return a.timestamp > b.timestamp ? -1 : a.timestamp < b.timestamp ? 1 : 0;
}));
}).catch(function (err) {
// As we use cross-domain, it is difficult to know where the error come from,
// so we guess that the Woleet API isn't available and set state to need-receipt
// if the error came from network
if (err.hasOwnProperty('code') || err.message === 'need-receipt') {
state.state = 'needReceipt';
setVue('need-receipt');
} else {
setVue('error', err);
}
});
};
if (typeof file === 'string') {
state.hash = file;
return woleetDAB(state.hash);
} else {
setVue('hashing');
return new Promise(function (resolve, reject) {
hasher.start(file);
hasher.on('progress', setProgress);
hasher.on('error', function (err) {
return setVue('error', err.error || err.message);
});
hasher.on('result', function (r) {
state.hash = r.result;
setProgress({
progress: 0
});
resolve(woleetDAB(state.hash));
});
});
}
}
}
function forceReceipt() {
resetText();
foot.hide();
state.state = 'needReceipt';
setVue('need-receipt');
dropZone.inputContainer.mainTextZone.text('Drop the proof receipt');
dropZone.inputContainer.subTextZone.clear();
}
function getDetailedMessage(error) {
var detail = {};
switch (error) {
case 'need-receipt':
detail.main = 'No public proof found at Woleet';
detail.sub = 'No public proof receipt found at Woleet: you must provide one to verify this file';
break;
case 'file_matched_but_anchor_not_yet_processed':
detail.main = 'The proof is not yet verifiable';
detail.sub = 'A public proof receipt has been found at Woleet, but is not yet verifiable (try again later)';
break;
case 'target_hash_mismatch':
detail.main = 'The proof mismatches the file';
detail.sub = 'The proof receipt\'s target hash attribute doesn\'t match the file hash';
break;
case 'unable_to_parse_json':
detail.main = 'The proof cannot be parsed';
detail.sub = 'The file you provided is not a valid Chainpoint 1 or 2 proof receipt';
break;
case 'merkle_root_mismatch':
detail.main = 'The proof is corrupted';
detail.sub = 'The proof receipt\'s merkle root attribute does not match the proof result';
break;
case 'non_sha256_target_proof_element':
detail.main = 'The proof is corrupted';
detail.sub = 'An attribute in the proof (parent or left or right) in not a SHA256 hash';
break;
case 'invalid_parent_in_proof_element':
detail.main = 'The proof is corrupted';
detail.sub = 'A parent in the proof is not the SHA256 of its children';
break;
case 'invalid_receipt_format':
detail.main = 'The proof is corrupted';
detail.sub = 'The proof receipt does not conform to the Chainpoint 1 or 2 format';
break;
case 'invalid_target_proof':
detail.main = 'The proof is corrupted';
detail.sub = 'The proof receipt\'s merkle proof attribute is invalid';
break;
case 'op_return_mismatches_merkle_root':
detail.main = 'The receipt is corrupted';
detail.sub = 'The transaction\'s OP_RETURN mismatches the proof receipt\'s merkle root';
break;
case 'http_error':
detail.main = 'Unexpected HTTP error';
detail.sub = 'An unexpected HTTP error occurred during the verification process';
break;
case 'tx_not_found':
detail.main = 'The transaction cannot be found';
detail.sub = 'The proof receipt\'s transaction doesn\'t exist in the blockchain';
break;
case 'tx_not_confirmed':
detail.main = 'The transaction is not confirmed';
detail.sub = 'The proof receipt\'s transaction is not yet confirmed by the blockchain';
break;
case 'invalid_receipt_signature':
detail.main = 'The signature is invalid';
detail.sub = 'The proof receipt\'s signature is not valid';
break;
case 'invalid_receipt_signature_format':
detail.main = 'The signature is corrupted';
detail.sub = 'The proof receipt does not conform to the Chainpoint 1 or 2 format with signature extension';
break;
case 'file_too_big_to_be_hashed_without_worker':
detail.main = 'Cannot hash without worker';
detail.sub = 'The file is too big to be hashed without worker';
break;
default:
console.trace('unexpected case', error);
detail.main = error;
detail.sub = 'Unexpected case';
break;
}
return detail;
}
function addResults(results) {
resetText();
if (results.length) {
if (results.length > 1) {
foot.expand.text(results.length + '+');
foot.expand.removeClass('up-arrow');
foot.show();
}
results.forEach(function (res, i) {
if (res.code === 'verified') {
addVue('ok', res, i);
} else {
addVue('error', res.code, i);
}
});
infoZone.toDom();
} else {
state.state = 'needReceipt';
setVue('need-receipt');
}
}
function addVue(vue, message, index) {
infoZone.show();
var item = infoZone.addItem();
item.addClass(index ? 'reduced' : 'expanded');
dropZone.hide();
hashZone.hide();
head.x.cancel.hide();
switch (vue) {
case 'ok':
var sig = message.receipt.signature;
var pubKey = sig ? sig.pubKey : null; // Timestamp
if (!message.confirmations) {
item.mainTextZone.text('Proof not yet verifiable');
item.subTextZone.text('The proof receipt\'s transaction is not yet confirmed (try again later)');
item.addClass('info');
} else {
var date = formatDate(message.timestamp);
item.mainTextZone.text((pubKey ? 'Signed' : 'Timestamped') + " on " + date);
item.addClass('ok');
} // Identity
var idStatus = message.identityVerificationStatus;
var identity = idStatus ? Object.assign({}, idStatus.signedIdentity, idStatus.identity) : null;
if (idStatus && idStatus.code === 'verified' && identity) {
item.byTextZone.addClass('link');
if (sig.identityURL) item.signTextZone.link(sig.identityURL + "?pubKey=" + pubKey + "&leftData=random" + (sig.signedIdentity ? "&signedIdentity=" + encodeURIComponent(sig.signedIdentity) : ''));
if (identity && identity.commonName) {
item.signTextZone.text("" + identity.commonName);
item.identityTextZone.html("" + (identity.organization || '') + (identity.organization && identity.organizationalUnit ? ' - ' : '') + (identity.organizationalUnit || '') + "<br>\n " + (identity.locality || '') + (identity.locality && identity.country ? ' - ' : '') + (identity.country || ''));
} else {
item.signTextZone.text("" + sig.identityURL);
}
item.byTextZone.show();
} else if (pubKey) {
item.signTextZone.text("" + pubKey);
item.byTextZone.show();
} else {
item.byTextZone.hide();
}
if (idStatus && idStatus.code && idStatus.code !== 'verified') {
item.warnTextZone.text("Cannot verify identity (" + idStatus.code + ")");
}
dropZone.attr('disabled', true);
head.x.receipt.show();
break;
case 'error':
if (state.state === 'needReceipt') head.x.receipt.show();
var error = message instanceof Error ? message.message : message;
var detail = getDetailedMessage(error);
item.mainTextZone.text(detail.main);
item.subTextZone.text(detail.sub);
item.byTextZone.hide();
item.warnTextZone.hide();
item.addClass(error !== 'file_matched_but_anchor_not_yet_processed' ? 'error' : 'info');
break;
}
head.x.reset.show();
}
function setVue(vue, message) {
switch (vue) {
case 'ok':
resetText();
addVue('ok', message, 0);
infoZone.toDom();
break;
case 'need-receipt':
resetText();
infoZone.hide();
dropZone.show();
hashZone.hide();
head.x.cancel.hide();
head.x.receipt.hide();
dropZone.inputContainer.mainTextZone.text('No public proof found at Woleet');
dropZone.inputContainer.subTextZone.text('Please provide a proof receipt');
head.x.reset.show();
break;
case 'error':
resetText();
addVue('error', message, 0);
infoZone.toDom();
break;
case 'hashing':
resetText();
head.x.cancel.show();
head.x.receipt.hide();
infoZone.hide();
dropZone.hide();
hashZone.show();
break;
case 'pending':
resetText();
infoZone.hide();
dropZone.show();
hashZone.hide();
head.x.cancel.hide();
head.x.receipt.hide();
dropZone.inputContainer.mainTextZone.text(message);
head.x.reset.show();
break;
case 'init':
default:
init();
break;
}
}
function parseReceiptFile(receipt) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onloadend = function (e) {
try {
resolve(JSON.parse(e.target.result));
} catch (err) {
reject(new Error('unable_to_parse_json'));
}
};
reader.readAsText(receipt);
});
}
var expanded = false;
var selected = null;
function selectItem(item, items) {
if (!expanded) return;
if (selected === item) {
// Clicked on item for the second time
return expandList();
}
selected = item;
items.forEach(function (item) {
item.removeClass('expanded');
item.addClass('reduced');
});
item.addClass('expanded');
item.removeClass('reduced');
}
function expandList() {
var items = infoZone.items;
if (expanded) {
body.removeClass('expanded');
items.forEach(function (item) {
return item.removeClass('clickable');
});
foot.expand.text(items.length + "+");
foot.expand.removeClass("up-arrow");
expanded = false;
} else {
body.addClass('expanded');
items.forEach(function (item) {
return item.addClass('clickable');
});
foot.expand.text('');
foot.expand.addClass('up-arrow');
expanded = true;
}
}
function reset() {
setVue('init');
hasher.cancel();
state.hash = null;
state.state = 'initial';
}
function cancelHash() {
reset();
}
this.setInputFile = function (file) {
if (state.state === 'needReceipt') throw new Error('Current state is needReceipt');
var ctx = {
files: [file]
};
return setInputFile.call(ctx);
};
this.setReceipt = function (file) {
forceReceipt();
if (state.state !== 'needReceipt') throw new Error('Current state must be needReceipt');
var ctx = {
files: [file]
};
return setInputFile.call(ctx);
};
this.reset = reset;
this.cancelHash = cancelHash;
this.toDom = function () {
return widget.toDom();
};
}
document.addEventListener("DOMContentLoaded", function () {
var widgets = document.getElementsByClassName("woleet-widget");
for (var i = 0; i < widgets.length; i++) {
var e = widgets[i];
var hash = e.getAttribute("data-hash");
var widget = new Widget(hash);
e.appendChild(widget.toDom());
}
});
root.Widget = Widget;
})(window);
//# sourceMappingURL=woleet-widget.js.map