librarydjs
Version:
Node JS Module interface to publish messages to the Florincoin Blockchain.
487 lines (412 loc) • 16.6 kB
JavaScript
var florincoin = require('node-litecoin');
var request = require('request');
var jsonpatch = require('fast-json-patch');
var jsonpack = require('jsonpack/main');
var client;
function LibraryDJS(args){
this.client = new florincoin.Client({
host: args.host,
port: args.port,
user: args.username,
pass: args.password
});
}
// Accepts information in the following format: {"name": "Test Publisher", "address": "FLO Address"}
LibraryDJS.prototype.signPublisher = function(args, callback){
// Check if the callback is being passed in as args
if (!args || typeof args === "function"){
callback = args;
callback(generateResponseMessage(false, 'You must submit information in the following format: {"name": "Test Publisher", "address": "FLO Address"}'));
return;
}
if (!args.name){
callback(generateResponseMessage(false, 'You must include the publisher address and the publisher name as arguments. {"name": "Test Publisher", "address": "FLO Address"}'))
}
if (!args.address){
callback(generateResponseMessage(false, 'You must include the publisher address and the publisher name as arguments. {"name": "Test Publisher", "address": "FLO Address"}'))
}
// http://api.alexandria.io/#sign-publisher-announcement-message
// Publisher Name - Address - UNIX Timestamp
var toSign = args.name + "-" + args.address + "-" + Math.floor((new Date).getTime()/1000);
signMessage(args.address, toSign, function(data){
callback(data);
});
}
// Accepts information in the following format: {"name": "Test Publisher", "address": "FLO Address"}
LibraryDJS.prototype.signArtifact = function(args, callback){
// Check if the callback is being passed in as args
if (!args || typeof args === "function"){
callback = args;
callback(generateResponseMessage(false, 'You must submit information in the following format: {"ipfs": "IPFS Location Hash", "address": "FLO Address"}'));
return;
}
if (!args.ipfs){
callback(generateResponseMessage(false, 'You must include the publisher address and the publisher name as arguments. {"ipfs": "IPFS Location Hash", "address": "FLO Address"}'))
}
if (!args.address){
callback(generateResponseMessage(false, 'You must include the publisher address and the publisher name as arguments. {"ipfs": "IPFS Location Hash", "address": "FLO Address"}'))
}
// http://api.alexandria.io/#sign-publisher-announcement-message
// IPFS Location Hash - Address - UNIX Timestamp
var toSign = args.ipfs + "-" + args.address + "-" + Math.floor((new Date).getTime()/1000);
signMessage(args.address, toSign, function(signature){
callback(signature);
});
}
// Accepts information in the following format: {"alexandria-publisher":{"name":"Publisher Name","address":"FLO Address","emailmd5":"","bitmessage":""} }
LibraryDJS.prototype.announcePublisher = function(args, callback){
var errorString = 'You must submit information in the following format: {"alexandria-publisher":{"name":"Publisher Name","address":"FLO Address","emailmd5":"","bitmessage":""} }';
// Check if the callback is being passed in as args
if (!args || typeof args === "function"){
callback = args;
callback(generateResponseMessage(false, errorString));
return;
}
// Validate that information is being submitted correctly.
if (!args['alexandria-publisher']){
callback(generateResponseMessage(false, errorString))
return;
}
var publisher = args['alexandria-publisher'];
if (!publisher.name){
callback(generateResponseMessage(false, 'You must include the publisher name! ' + errorString));
return;
}
if (!publisher.address){
callback(generateResponseMessage(false, 'You must include the publisher address! ' + errorString));
return;
}
// http://api.alexandria.io/#sign-publisher-announcement-message
// Publisher Name - Address - UNIX Timestamp
var toSign = args.name + "-" + args.address + "-" + Math.floor((new Date).getTime()/1000);
signMessage(args.address, toSign, function(signature){
callback(signature);
});
}
// Accepts information in the format OIP-041
LibraryDJS.prototype.publishArtifact = function(oipArtifact, callback){
// Check if the callback is being passed in as args
if (!oipArtifact || typeof oipArtifact === "function"){
callback = oipArtifact;
callback(generateResponseMessage(false, 'You must submit information in the OIP-041 format'));
return;
}
// If the artifact verifies correctly it will contain no text, if it fails it will have an error.
var verify = verifyArtifact(oipArtifact);
// Check if there is an error
if (verify){
callback(verify);
return;
}
// http://api.alexandria.io/#sign-publisher-announcement-message
// IPFS - Address - UNIX Timestamp
var toSign = oipArtifact.artifact.storage.location + "-" + oipArtifact.artifact.publisher + "-" + oipArtifact.artifact.timestamp;
var libraryd = this;
// Sign the message
libraryd.signMessage(oipArtifact.artifact.publisher, toSign, function(res){
if (!res.success){
callback(res);
return;
}
// Attach signature
oipArtifact.signature = res.message;
// Above we remove the "oip-041" for ease of use, this adds it back in.
var reformattedOIP = { "oip-041": oipArtifact }
libraryd.sendToBlockChain(reformattedOIP, oipArtifact.artifact.publisher, function(response){
callback(response);
})
});
}
LibraryDJS.prototype.signMessage = function(address, toSign, callback){
try {
this.client.signMessage(address, toSign, function(err, signature) {
if (err){
callback(generateResponseMessage(false, "Error signing message: " + err));
console.log(generateResponseMessage(false, "Error signing message: " + err));
return;
}
// Return the signature
callback(generateResponseMessage(true, signature));
});
} catch (e) {
callback(generateResponseMessage(false, "Error signing publisher message: " + e));
console.log(generateResponseMessage(false, "Error signing publisher message: " + e));
}
}
// callback is (errorString, txIDs Array)
LibraryDJS.prototype.multiPart = function(txComment, address, callback) {
var txIDs = [];
var multiPartPrefix = "alexandria-media-multipart(";
var chop = this.chopString(txComment);
var part = 0;
var max = chop.length - 1;
// the first reference tx id is always 64 zeros
var reference = new Array(65).join("0");
var data = chop[part];
var preImage = part.toString() + "-" + max.toString() + "-" + address + "-" + reference + "-" + data;
var libraryd = this;
libraryd.signMessage(address, preImage, function(response){
if (!response.success){
callback(response);
return;
}
var txComment = multiPartPrefix + part.toString() + "," + max.toString() + "," + address + "," + reference + "," + response.message + "):" + data;
libraryd.client.sendToAddress(address, SEND_AMOUNT, "", "", txComment, function(err, txid) {
if (err){
callback(generateResponseMessage(false, "Unable to send funds to address: " + err))
console.log(err);
return;
}
txIDs[txIDs.length] = txid;
reference = txid;
libraryd.publishPart(chop, max, 0, reference, address, SEND_AMOUNT, multiPartPrefix, function(response){
callback(response);
})
});
})
}
var txIDs = [];
LibraryDJS.prototype.publishPart = function(chopPieces, numberOfPieces, lastPiecesCompleted, reference, address, amount, multiPartPrefix, callback){
// Increment the number of completed pieces
var part = lastPiecesCompleted + 1;
// Chop the next section of data to sign
var data = chopPieces[part];
var preImage = part.toString() + "-" + numberOfPieces.toString() + "-" + address + "-" + reference + "-" + data;
// Generate signature
var libraryd = this;
libraryd.signMessage(address, preImage, function(res){
if (!res.success){
callback(res)
console.log(res);
return;
}
var multiPart = multiPartPrefix + part.toString() + "," + numberOfPieces.toString() + "," + address + "," + reference + "," + res.message + "):" + data;
libraryd.client.sendToAddress(address, SEND_AMOUNT, "", "", multiPart, function(err, txid) {
if (err){
callback(generateResponseMessage(false, "Unable to send funds to address: " + err))
console.log(err);
return;
}
// Store the txid from the just sent transaction.
txIDs[txIDs.length] = txid;
// Check if we are done with publishing
if (part < numberOfPieces){
// Recurse back in.
libraryd.publishPart(chopPieces, numberOfPieces, part, reference, address, amount, multiPartPrefix, callback);
} else {
// We are done! Callback time.
callback(generateResponseMessage(true, txIDs));
// Clear out the txIDs
txIDs = [];
}
});
});
}
LibraryDJS.prototype.chopString = function(input) {
input = input.toString();
var chunks = [];
while (input.length > CHOP_MAX_LEN) {
chunks[chunks.length] = input.slice(0, CHOP_MAX_LEN);
input = input.slice(CHOP_MAX_LEN);
}
chunks[chunks.length] = input;
return chunks;
};
LibraryDJS.prototype.sendToBlockChain = function(txComment, address, callback){
// Make sure that it is a string and not JSON object.
if (typeof txComment != "string"){
// If JSON object then convert to string.
txComment = JSON.stringify(txComment);
}
// Check comment length.
if (txComment.length > (CHOP_MAX_LEN * 10)) {
callback(generateResponseMessage(false, "txComment is too large to fit within 10 multipart transactions. Try making it smaller!"));
}
else if (txComment.length > TXCOMMENT_MAX_LEN) {
this.multiPart(txComment, address, callback);
}
else {
this.client.sendToAddress(address, SEND_AMOUNT, "", "", txComment, function(err, txid) {
if (err){
callback(generateResponseMessage(false, "Unable to send funds to address: " + err))
console.log(err);
return;
}
callback(generateResponseMessage(true, [txid]));
});
}
}
LibraryDJS.prototype.generateEditDiff = function(originalArtifact, updatedArtifact, origTXID){
if (!originalArtifact || !updatedArtifact)
return generateResponseMessage(false, "You are missing either the original artifact or the updated artifact");
// Check if the original artifact is actually just the transaction ID for the original artifact
if (originalArtifact.length == TX_LENGTH)
return generateResponseMessage(false, "You just submit the original artifact JSON, not the TXID. You can get the Artifact JSON by using the getArtifact function.");
var oaVerify = this.verifyArtifact(originalArtifact);
var uaVerify = this.verifyArtifact(updatedArtifact);
if (oaVerify || uaVerify){
if (oaVerify){
return oaVerify;
}
if (uaVerify){
return uaVerify;
}
}
//console.log(JSON.stringify(originalArtifact));
//console.log(JSON.stringify(updatedArtifact));
// http://stackoverflow.com/a/8432188/1232109
var result = jsonpatch.compare(originalArtifact, updatedArtifact);
//console.log(result);
//console.log(jsonpack.pack(result));
//console.log(JSON.stringify(originalArtifact, true, 4));
//console.log(JSON.stringify(updatedArtifact, true, 4));
//console.log(JSON.stringify(squashPatch(result), true, 4));
var squashed = squashPatch(result);
//console.log(JSON.stringify(squashed));
var packed = jsonpack.pack(squashed);
//console.log(jsonpack.pack(squashed));
var oip041Edit = {
"oip-041":{
"edit":{
"txid": origTXID,
"timestamp":updatedArtifact['oip-041'].artifact.timestamp,
"patch": packed
}
}
}
return '{"success": true, "message": ' + JSON.stringify(oip041Edit) + '}';
}
LibraryDJS.prototype.getArtifact = function(txid, callback){
var baseURL = 'https://api.alexandria.io/alexandria/v1/search';
var options = {
method: 'POST',
headers: {},
url: baseURL,
body: JSON.stringify({
'protocol': 'media',
'search-on': 'txid',
'search-for': txid
})
};
try {
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
// Grab the result we want.
var artifacts = JSON.parse(body);
var artifact = artifacts.response[0];
var str = JSON.stringify(artifact);
if (artifacts.status == "success"){
// We return the message differently here as it hates returning JSON inside JSON for some reason...
callback('{"success": true, "message": ' + str + '}');
} else {
callback(generateResponseMessage(false, "Artifact could not be found."))
}
} else {
callback(generateResponseMessage(false, "Request failed with status: " + response.statusCode + ", with error: " + error));
}
});
} catch (e) {
// The request failed for some reason, catch to not crash the program.
callback(generateResponseMessage(false, "POST Request crashed, stack: " + e));
}
}
LibraryDJS.prototype.verifyArtifact = function(oipArtifact){
if (!oipArtifact || typeof oipArtifact === "function"){
return generateResponseMessage(false, 'You must submit information in the OIP-041 format');
}
if (typeof oipArtifact == "string"){
// Test to see if the artifact is valid JSON
try {
oipArtifact = JSON.parse(oipArtifact);
} catch (e) {
return generateResponseMessage(false, "Artifact is not valid JSON");
}
}
// Test that the artifact has all required fields.
// Test "oip-041" wrapping/version number
if (!oipArtifact["oip-041"]){
return generateResponseMessage(false, "Artifact is not contained in 'oip-041' or is using an unsupported oip schema version.");
}
oipArtifact = oipArtifact["oip-041"];
// Test required fields
if (!oipArtifact.artifact){
return generateResponseMessage(false, "You must submit artifact JSON inside oip-041!");
}
if (!oipArtifact.artifact.publisher){
return generateResponseMessage(false, "artifact.publisher is a required field");
}
if (!oipArtifact.artifact.timestamp){
return generateResponseMessage(false, "artifact.timestamp is a required field");
}
// Validate timestamp is a number
if (isNaN(oipArtifact.artifact.timestamp)){
return generateResponseMessage(false, "artifact.timestamp must be submitted as a number");
}
if (!oipArtifact.artifact.type){
return generateResponseMessage(false, "artifact.type is a required field");
}
if (!oipArtifact.artifact.info){
return generateResponseMessage(false, "artifact.info is a required fieldset");
}
if (!oipArtifact.artifact.info.title){
return generateResponseMessage(false, "artifact.info.title is a required field");
}
if (!oipArtifact.artifact.info.description){
return generateResponseMessage(false, "artifact.info.description is a required field");
}
if (!oipArtifact.artifact.info.year){
return generateResponseMessage(false, "artifact.info.year is a required field");
}
// Validate year is a number
if (isNaN(oipArtifact.artifact.info.year)){
return generateResponseMessage(false, "artifact.info.year must be submitted as a number");
}
if (!oipArtifact.artifact.storage){
return generateResponseMessage(false, "artifact.storage is a required fieldset");
}
if (!oipArtifact.artifact.storage.network){
return generateResponseMessage(false, "artifact.storage.network is a required field");
}
// Currently the storage location is a required field. This however can be "hidden" using LibraryD so that you can serve the IPFS file location via an API (for example, that detects payments)
if (!oipArtifact.artifact.storage.location){
return generateResponseMessage(false, "artifact.storage.location is a required field (Talk with Alexandria if you need to hide this field from being published in LibraryD)");
}
// Default return nothing.
return;
}
function squashPatch(patch){
var squashed = {
"add": [],
"replace": [],
"remove": []
}
for (var i = 0; i < patch.length; i++) {
// Store the operation
var operation = patch[i].op;
// Remove operation key from squashed patch
delete patch[i].op;
// Edit the path to be shorter, unless it is the signature.
patch[i].path = patch[i].path.replace("/oip-041/artifact", "");
// Check what the operation is, and move it to the right place
if (operation == "add")
squashed.add.push(patch[i]);
else if (operation == "replace")
squashed.replace.push(patch[i]);
else if (operation == "remove")
squashed.remove.push(patch[i]);
}
return squashed;
}
function generateResponseMessage(success, message) {
try {
return JSON.parse('{ "success": ' + success + (success ? ', "message": "' : ', "error": "') + message + '"}');
} catch(e) {
console.log(e);
return '{"success": false, "error": "Error generating response message"}';
}
}
const CHOP_MAX_LEN = 270;
const TXCOMMENT_MAX_LEN = 528;
const SEND_AMOUNT = 0.0001;
const TX_LENGTH = 64;
module.exports = LibraryDJS;