cosmic-lib
Version:
A JavaScript implementation of the CosmicLink protocol for Stellar
574 lines (462 loc) • 16.9 kB
JavaScript
"use strict";
/**
* Contains methods to format a `transaction descriptor` into CSS/HTML for
* display in browser.
*
* @private
* @exports format
*/
require("core-js/modules/es.string.replace.js");
const format = exports;
const misc = require("@cosmic-plus/jsutils/es5/misc");
const nice = require("@cosmic-plus/jsutils/es5/nice");
const html = require("@cosmic-plus/domutils/es5/html");
const StellarSdk = require("@cosmic-plus/base/es5/stellar-sdk");
const config = require("./config");
const event = require("./event");
const resolve = require("./resolve");
const signersUtils = require("./signers-utils");
const specs = require("./specs");
/**
* Returns an HTML div describing `tdesc`.
*
* @param {Object} tdesc Transaction descriptor.
* @return {HTMLElement} Transaction HTML description.
*/
format.tdesc = function (conf, tdesc) {
const trNode = html.create("div", ".cosmiclib_transactionNode");
if (!tdesc) return trNode; // SEP-0007 verified domain.
if (conf.extra.originDomain) {
const domainNode = html.create("div");
html.append(trNode, domainNode);
conf.verifySep7().then(() => {
html.append(domainNode, "Request from: ".concat(conf.extra.originDomain));
domainNode.className = "cosmicLib_signed";
});
}
let infoNode;
for (let index in specs.transactionOptionalFields) {
const entry = specs.transactionOptionalFields[index];
if (entry === "horizon" && resolve.horizon(config, conf.network)) continue;
if (tdesc[entry]) {
if (!infoNode) infoNode = html.create("ul", ".cosmiclib_sideInfo");
const lineNode = html.create("li", {}, specs.fieldDesc[entry] + ": ", format.field(conf, entry, tdesc[entry]));
html.append(infoNode, lineNode);
}
}
if (infoNode) html.append(trNode, infoNode);
try {
for (let index in tdesc.operations) {
const operation = tdesc.operations[index];
const opNode = format.odesc(conf, operation);
opNode.index = index;
html.append(trNode, opNode);
}
} catch (error) {
console.error(error);
}
if (!tdesc.operations.length) {
if (Object.keys(tdesc).length === 1) {
html.append(trNode, html.create("div", null, "No transaction"));
} else {
html.append(trNode, html.create("div", ".cosmiclib_operation", "No operation"));
}
}
trNode.tdesc = tdesc;
return trNode;
};
/**
* Returns an HTML div describing `odesc`.
*
* @param {Object} odesc Operation in cosmic link JSON format.
* @return {HTMLElement} Operation HTML description.
*/
format.odesc = function (conf, odesc) {
const opNode = html.create("div", ".cosmiclib_operation");
opNode.odesc = odesc;
let retNode = opNode;
if (odesc.source) {
retNode = html.create("div", ".cosmiclib_sourcedOperation");
const sourceNode = html.create("div", ".cosmiclib_sideInfo", "Source: ");
const addressNode = format.address(conf, odesc.source);
html.append(sourceNode, addressNode);
html.append(retNode, sourceNode, opNode);
}
let meaning = operationMeaning(odesc);
while (meaning) {
if (meaning.substr(0, 1) === "{") {
const query = meaning.substr(1).replace(/}.*/, "");
meaning = meaning.replace(/^[^}]*}/, "");
if (query === "newline") {
if (meaning === "") break;
html.append(opNode, html.create("br"));
} else {
const fieldNode = format.field(conf, query, odesc[query]);
html.append(opNode, fieldNode);
}
} else {
const txt = meaning.replace(/{.*/, "");
meaning = meaning.replace(/^[^{]*/, "");
html.append(opNode, txt);
}
}
return retNode;
};
/**
* Returns a string describing `odesc`.
*
* @private
*/
function operationMeaning(odesc) {
let msg = "";
switch (odesc.type) {
case "accountMerge":
return "Merge account inside {destination}";
case "allowTrust":
if (odesc.authorize === 2) {
return "Allow liabilities for your asset {assetCode} to {trustor}";
} else if (odesc.authorize === 1) {
return "Allow usage of your asset {assetCode} to {trustor}";
} else {
return "Deny usage of your asset {assetCode} to {trustor}";
}
case "bumpSequence":
return "Set account sequence number to {bumpTo}";
case "changeTrust":
if (odesc.limit === "0") {
return "Refuse asset {asset}";
} else if (odesc.limit) {
return "Set holding limit as {limit} for asset {asset}";
} else {
return "Accept asset {asset}";
}
case "claimClaimableBalance":
return "Claim claimable balance {balanceId}";
case "createAccount":
return "Create account {destination} with {startingBalance} XLM";
case "createPassiveOffer":
case "createPassiveSellOffer":
return "Offer to passively sell {amount} {selling} at {price} {buying} / unit";
case "inflation":
return "Run inflation";
case "manageData":
if (odesc.value) {
if (odesc.value.type === "text") {
return "Set data entry '{name}' to: '{value}'";
} else {
return "Set data entry '{name}' to base64: '{value}'";
}
} else {
return "Delete data entry '{name}'";
}
case "manageOffer":
case "manageBuyOffer":
case "manageSellOffer":
if (odesc.amount === "0" || odesc.buyAmount === "0") {
return "Delete offer '{offerId}'";
} else {
if (odesc.offerId) {
msg += "Change offer '{offerId}' into:{newline}";
}
if (odesc.type === "manageBuyOffer") {
msg += "Offer to buy {buyAmount} {buying} at {price} {selling} / unit";
} else {
msg += "Offer to sell {amount} {selling} at {price} {buying} / unit";
}
return msg;
}
case "pathPaymentStrictReceive":
msg = "Send {destAmount} {destAsset} to {destination} for a maximum " + "of {sendMax} {sendAsset}";
if (odesc.path) msg += " using conversion path: {path}";
return msg;
case "pathPaymentStrictSend":
msg = "Send at least {destMin} {destAsset} to {destination} for " + "{sendAmount} {sendAsset}";
if (odesc.path) msg += " using conversion path: {path}";
return msg;
case "payment":
return "Send {amount} {asset} to {destination}";
case "setOptions":
if (odesc.inflationDest) {
msg += "Set inflation destination to: {inflationDest}{newline}";
}
if (odesc.clearFlags) msg += "Clear flag(s): {clearFlags}{newline}";
if (odesc.setFlags) msg += "Set flag(s): {setFlags}{newline}";
if (odesc.masterWeight) {
if (odesc.masterWeight === "0") {
msg += "Delete master key{newline}";
} else {
msg += "Set master key weight at: {masterWeight}{newline}";
}
}
["lowThreshold", "medThreshold", "highThreshold"].forEach(field => {
if (odesc[field]) msg += "Set " + field + " at: {" + field + "}{newline}";
});
if (odesc.signer) {
if (odesc.signer.type === "tx") {
if (odesc.signer.weight === "0") msg += "Remove pre-signed {signer}{newline}";else msg += "Pre-sign {signer}{newline}";
} else {
if (odesc.signer.weight === "0") msg += "Remove signer: {signer}{newline}";else msg += "Set signer: {signer}{newline}";
}
}
if (odesc.homeDomain) msg += "Set home domain: {homeDomain}{newline}";
if (odesc.homeDomain === "") msg += "Unset home domain";
if (!msg) msg = "Do nothing";
return msg;
default:
throw new Error("Unknow operation: " + odesc.type);
}
}
/**
* Returns an HTML div describing `signers`.
*
* @param {Object} signers Signers object as returned by @see{resolve.signers}.
* @return {HTMLElement} Signers HTML description
*/
format.signatures = function (conf, transaction) {
const signersNode = html.create("div", ".cosmiclib_signersNode");
signersUtils.for(conf, transaction).then(utils => {
if (utils.signersList.length < 2 && !utils.signatures.length) return;
utils.sources.forEach(accountId => {
if (accountId !== specs.neutralAccountId) {
const div = makeAccountSignersNode(conf, utils, accountId);
html.append(signersNode, div);
}
});
});
return signersNode;
};
function makeAccountSignersNode(conf, utils, accountId) {
const accountSignersNode = html.create("div");
const title = "Signers for " + misc.shorter(accountId);
const titleNode = html.create("span", ".cosmiclib_threshold", title);
const listNode = html.create("ul", ".cosmiclib_signers");
html.append(accountSignersNode, titleNode, listNode);
utils.signers[accountId].forEach(signer => {
const signerNode = format.signer(conf, signer);
const lineNode = html.create("li", null, signerNode);
if (utils.hasSigned(signer.key)) {
html.addClass(lineNode, "cosmiclib_signed");
listNode.insertBefore(lineNode, listNode.firstChild);
} else {
html.append(listNode, lineNode);
}
});
return accountSignersNode;
}
/**
* Retrieves the parent odesc (*Operation Descriptor*) of an HTML element, or
* returns `undefined` if **element** is not the child of an HTML formatted
* operation.
*
* @param {HTMLElement} element
* @return {Object} odesc
*/
format.parentOdesc = (conf, element) => parentProperty(element, "odesc");
/**
* Retrieves the parent operation index of an HTML element, or returns
* `undefined` if **element** is not the child of an HTML formatted operation.
*
* @param {HTMLElement} element
* @return {Number} operation index
*/
format.parentIndex = (conf, element) => parentProperty(element, "index");
/**
* Retrieves the parent tdesc of an HTML element, or returns `undefined`
* if **element** is not the child of an HTML formatted transaction.
*
* @param {HTMLElement} element
* @return {Object} tdesc
*/
format.parentTdesc = (conf, element) => parentProperty(element, "tdesc");
function parentProperty(element, property) {
while (element.parentNode) {
if (element.parentNode[property]) return element.parentNode[property];else element = element.parentNode;
}
}
/******************************************************************************/
/**
* Returns an HTML div describing `field` `value`.
*
* @param {string} field The field name of `value` as defined in `spec.js`.
* @param {*} value The value of `field`.
* @return {HTLMElement} `field` `value` HTML description
*/
format.field = function (conf, field, value) {
const type = specs.fieldType[field];
if (!type) throw new Error("Unknow field: " + field);
const domNode = format.type(conf, type, value);
domNode.field = field;
if (field !== type) html.addClass(domNode, "cosmiclib_" + field);
return domNode;
};
format.type = function (conf, type, value) {
if (typeof value === "object" && value.error) type = "error";
const formatter = process[type] || process.string;
const domNode = formatter(conf, value);
html.addClass(domNode, "cosmiclib_" + type);
const eventObject = {
conf: conf,
type: type,
value: value,
domNode: domNode
};
if (conf.constructor.name === "CosmicLink") eventObject.cosmicLink = conf;
if (event.defaultClickHandlers[type]) {
domNode.onclick = () => event.callClickHandler(conf, type, eventObject);
html.addClass(domNode, "cosmiclib_clickable");
}
return domNode;
}; /// Provide a format method for each data type.
specs.types.forEach(type => {
format[type] = (conf, value) => format.type(conf, type, value);
});
/******************************************************************************/
const process = {};
process.string = function (conf, string) {
if (typeof string !== "string") string = string + "";
return html.create("span", null, string);
};
process.error = function (conf, errDesc) {
const errorNode = html.create("span", ".cosmiclib_error");
errorNode.textContent = errDesc.value === "" ? "(undefined)" : errDesc.value.value || errDesc.value;
errorNode.title = errDesc.error.message;
return errorNode;
};
process.address = function (conf, address) {
const addressNode = html.create("span", {
title: "Resolving..."
}, misc.shorter(address), html.create("span", ".cosmiclib_loadingAnim"));
resolveAddressAndUpdate(conf, address, addressNode);
return addressNode;
};
async function resolveAddressAndUpdate(conf, address, addressNode) {
try {
const account = await resolve.address(conf, address);
addressNode.title = account.account_id;
if (account.memo) {
addressNode.title += "\nMemo (".concat(account.memo_type, "): ").concat(account.memo);
}
if (account.address) addressNode.textContent = account.address;else if (account.alias) addressNode.textContent = account.alias;
addressNode.extra = account;
} catch (error) {
addressNode.title = "Can't resolve address";
html.addClass(addressNode, "cosmiclib_error");
}
const animation = html.grab(".cosmiclib_loadingAnim", addressNode);
if (animation) html.destroy(animation);
}
process.amount = function (conf, amount, significant = 3, max = 7) {
// Hide non-significant numbers
if (typeof amount !== "number") amount = Number(amount);
const nicified = nice(amount, {
significant,
max
});
if (String(amount).length <= nicified.length) {
return html.create("span", null, nicified);
} else {
return html.create("span", {
className: "cosmiclib_clickable",
title: amount
}, html.create("span", ".cosmiclib_tilde", "~"), nicified);
}
};
process.asset = function (conf, asset) {
const assetNode = html.create("span", null, format.field(conf, "assetCode", asset.code));
if (asset.issuer) {
html.append(assetNode, " (", format.field(conf, "assetIssuer", asset.issuer), ")");
}
return assetNode;
};
process.assetsArray = function (conf, assetsArray) {
const assetsArrayNode = html.create("span");
for (let i = 0; i < assetsArray.length; i++) {
if (i !== 0) html.append(assetsArrayNode, ", ");
html.append(assetsArrayNode, format.asset(conf, assetsArray[i]));
}
return assetsArrayNode;
};
process.balanceId = process.hash;
process.buffer = function (conf, object) {
if (object.type === "base64") return format.hash(conf, object.value);else return format.string(conf, object.value);
};
process.date = function (conf, date) {
return html.create("span", {}, new Date(date).toLocaleString());
};
process.hash = function (conf, hash) {
return html.create("span", {
title: hash
}, misc.shorter(hash));
};
process.id = process.hash;
process.flags = function (conf, flags) {
let string = "";
if (flags >= 4) {
string = "immutable";
flags = flags - 4;
}
if (flags >= 2) {
if (string) string = ", " + string;
string = "revocable" + string;
flags = flags - 2;
}
if (+flags === 1) {
if (string) string = ", " + string;
string = "required" + string;
}
return html.create("span", {}, string);
};
process.memo = function (conf, memo) {
const typeNode = format.field(conf, "memoType", memo.type);
let valueNode;
switch (memo.type) {
case "text":
valueNode = format.field(conf, "memoText", memo.value);
break;
case "base64":
valueNode = format.field(conf, "memoBinary", memo.value);
break;
case "id":
valueNode = format.field(conf, "memoId", memo.value);
break;
case "hash":
valueNode = format.field(conf, "memoHash", memo.value);
break;
case "return":
valueNode = format.field(conf, "memoReturn", memo.value);
}
return html.create("span", {}, valueNode, " (", typeNode, ")");
};
process.price = function (conf, price) {
if (typeof price === "string") return process.amount(conf, price, 3, null);else return process.amount(conf, price.n / price.d, 3, null);
};
process.signer = function (conf, signer) {
const signerNode = html.create("span");
switch (signer.type) {
case "key":
case "ed25519_public_key":
{
const value1 = signer.value || signer.key;
html.append(signerNode, "Account ", format.field(conf, "signerKey", value1));
break;
}
case "tx":
{
const value2 = signer.value || signer.key;
html.append(signerNode, "transaction ", format.field(conf, "signerTx", value2));
break;
}
case "hash":
case "sha256hash":
{
const value3 = signer.value || StellarSdk.StrKey.decodeSha256Hash(signer.key).toString("hex");
html.append(signerNode, "key whose hash is ", format.field(conf, "signerHash", value3));
break;
}
}
if (signer.weight > 1) {
const weightNode = format.weight(conf, signer.weight);
html.append(signerNode, " (weight: ", weightNode, ")");
}
return signerNode;
};