cosmic-lib
Version:
A JavaScript implementation of the CosmicLink protocol for Stellar
442 lines (364 loc) • 10.8 kB
JavaScript
;
/**
* Exposes some of the check routines used by cosmic-lib. Individual type-checks
* are also available for:
*
* > address, amount, asset, assetsArray, boolean, buffer, date, flags, hash,
* id, memo, network, price, sequence, signer, string, threshold, url, weight
*
* All checks are meant to be used on tdesc formatted values. Form example, in
* tdesc buffer values are not encoded as actual Buffer object but as something
* like: `{ type: "text", value: "Hello World!"}`.
*
* @example
* check.field("minTime", "2018-11")
* check.date("2018-11")
*
* @private
* @exports check
*/
const check = exports;
const misc = require("@cosmic-plus/jsutils/es5/misc");
const specs = require("./specs");
const status = require("./status");
/**
* Check that **tdesc** is valid.
*
* @example
* check.tdesc({
* memo: { type: "text", value: "Hello, World!" },
* network: "public",
* source: "tips*cosmic.link",
* operations: [{ type: "setOptions", homeDomain: "cosmic.link" }]
* })
*
* @param {Object} tdesc
*/
check.tdesc = function (conf, tdesc) {
let isValid = true;
for (let field in tdesc) {
try {
check.txField(conf, field, tdesc[field]);
} catch (error) {
isValid = false;
tdesc[field] = errDesc(error, tdesc[field]);
}
}
if (tdesc.operations.length > 100) {
isValid = false;
status.error(conf, "Too much operations (max 100)");
}
tdesc.operations.forEach(odesc => {
try {
check.odesc(conf, odesc);
} catch (e) {
isValid = false;
}
});
if (!isValid) {
const error = new Error("Invalid tdesc"); // TODO: check if this is really useful
error.tdesc = tdesc;
throw error;
}
};
/**
* Check that tdesc operation is valid (referred as **odesc**).
*
* @example
* check.odesc({ type: "payment", destination: "tips*cosmic.link", amount: "20" })
*
* @param {Object} odesc [description]
*/
check.odesc = function (conf, odesc) {
let isValid = true;
try {
check.operationType(conf, odesc.type);
} catch (error) {
isValid = false;
odesc.type = errDesc(error, odesc.type);
}
for (let field in odesc) {
try {
check.operationField(conf, odesc.type, field, odesc[field]);
} catch (error) {
isValid = false;
odesc[field] = errDesc(error, odesc[field]);
}
}
specs.operationMandatoryFields[odesc.type].forEach(field => {
if (odesc[field] === undefined) {
isValid = false;
const error = new Error("Missing mandatory field: " + field);
odesc[field] = errDesc(error);
status.error(conf, error.message);
}
});
if (!isValid) throw new Error("Invalid odesc");
};
/**
* Check that **field** is a valid transaction field and that its **value** is
* valid.
*
* @param {string} field
* @param {*} value
*/
check.txField = function (conf, field, value) {
if (field === "operations") return;
if (!specs.transactionOptionalFields.find(name => name === field)) {
status.error(conf, "Invalid transaction field: " + field, "throw");
}
check.field(conf, field, value);
};
/**
* Check that **type** is a valid Stellar Operation type.
*
* @param {String}
*/
check.operationType = function (conf, type) {
if (!specs.operationMandatoryFields[type]) {
status.error(conf, "Invalid operation: " + type, "throw");
}
};
/**
* Check that **field** is a valid **operation** field and that its **value** is
* valid.
*
* @param {String} operation
* @param {String} field
* @param {*} value
*/
check.operationField = function (conf, operation, field, value) {
if (field === "type") return;
if (!specs.isOperationField(operation, field)) {
status.error(conf, "Invalid field for ".concat(operation, ": ").concat(field), "throw");
}
check.field(conf, field, value);
};
function errDesc(error, value = "") {
return {
error: error,
value: value
};
}
/******************************************************************************/
/**
* Check that **field** **value** is a valid.
*
* @example
* check.field("memo", { type: "text", value: "Hello, World!" })
*
* @param {string} field The name of a Stellar Transaction parameter
* @param {*} value
*/
check.field = function (conf, field, value) {
if (value === "" && field !== "homeDomain" && field !== "value") {
status.error(conf, "Missing value for field: ".concat(field), "throw");
}
const type = specs.fieldType[field];
if (!type) status.error(conf, "Unknow field: " + field, "throw");
if (value) check.type(conf, type, value);
};
/**
* Check that **value** is of type **type**.
*
* @example
* check.type("date", "2018-11-28")
*
* @param {string} type
* @param {string} value
*/
check.type = function (conf, type, value) {
if (!specs.types.find(entry => entry === type)) {
throw new Error("Invalid type: " + type);
}
return check[type](conf, value);
};
/**
* Generic check for numbers. Check that **value** is a number or a string
* representing a number. **type** is for the customization of the message in
* case of error. **min** and **max** may be provided as additional restriction
* for `value`.
*
* @param {number|string} value
* @param {string} [type = 'number']
* @param {number|string} [min]
* @param {number|string} [max]
*/
check.number = function (conf, value, type = "number", min, max = "unlimited") {
const num = +value;
if (isNaN(num)) {
status.error(conf, "Invalid ".concat(type, " (should be a number): ").concat(value), "throw");
} else if (min && num < min || max && num > max) {
status.error(conf, "Invalid ".concat(type, " (should be between ").concat(min, " and ").concat(max, " ): ").concat(value), "throw");
}
};
/**
* Generic check for integers. Check that **value** is an integer or a string
* representing an integer. **type** is for the customization of the message in
* case of error. **min** and **max** may be provided as additional restriction for
* **value**.
*
* @param {number|string} value
* @param {string} [type = 'integer']
* @param {number|string} [min]
* @param {number|string} [max]
*/
check.integer = function (conf, value, type = "integer", min, max) {
check.number(conf, value, type, min, max);
if (parseInt(value) + "" !== value + "") {
status.error(conf, "Not an integer: ".concat(value), "throw");
}
};
/**
* Check that **value** is an UTF-8 string.
*
* *Note:* This use a weak (simplified) test that may not be accurate for small
* strings.
*
* @param {String} value
*/
check.utf8 = function (conf, value) {
if (typeof value !== "string" || !misc.isUtf8(value)) {
status.error(conf, "Invalid UTF8 string: ".concat(value), "throw");
}
};
/**
* Check that **value** is a base64 string.
*
* @param {String} value
*/
check.base64 = function (conf, value) {
if (typeof value !== "string" || !misc.isBase64(value)) {
status.error(conf, "Invalid base64 string: ".concat(value), "throw");
}
};
/******************************************************************************/
check.amount = function (conf, amount) {
check.number(conf, amount, "amount");
};
check.address = function (conf, address) {
if (address.length !== 56 && !address.match(/.*\*.*\..*/)) {
status.error(conf, "Invalid address: " + misc.shorter(address), "throw");
}
};
check.asset = function (conf, asset) {
const code = asset.code.toLowerCase();
if (!asset.issuer && code !== "xlm" && code !== "native") {
status.error(conf, "Missing issuer for asset: " + code, "throw");
}
};
check.assetsArray = function (conf, assetsArray) {
let isValid = true;
for (let index in assetsArray) {
try {
check.asset(conf, assetsArray[index]);
} catch (error) {
console.error(error);
isValid = false;
}
}
if (!isValid) throw new Error("Invalid assets array");
};
check.authorizeFlag = function (conf, flag) {
check.integer(conf, flag, "authorize flag", 0, 3);
};
check.balanceId = function (conf, balanceId) {
if (balanceId.length !== 72 || !balanceId.match(/^[0-9a-f]*$/)) {
status.error(conf, "Invalid balanceId: " + balanceId, "throw");
}
};
check.boolean = function (conf, boolean) {
if (typeof boolean !== "boolean") {
status.error(conf, "Invalid boolean: " + boolean, "throw");
}
};
check.buffer = function (conf, buffer) {
switch (buffer.type) {
case "text":
check.utf8(conf, buffer.value);
break;
case "base64":
check.base64(conf, buffer.value);
break;
default:
status.error(conf, "Invalid buffer type: " + buffer.type, "throw");
}
};
check.date = function (conf, date) {
if (isNaN(Date.parse(date))) {
status.error(conf, "Invalid date: " + date, "throw");
}
};
check.flags = function (conf, flags) {
check.integer(conf, flags, "flags", 0, 7);
};
check.hash = function (conf, hash) {
if (hash.length !== 64 || !hash.match(/^[0-9a-f]*$/)) {
status.error(conf, "Invalid hash: " + hash, "throw");
}
};
check.id = function (conf, id) {
if (!id.match(/^[0-9]*$/)) status.error(conf, "Invalid id: " + id, "throw");
};
check.memo = function (conf, memo) {
switch (memo.type) {
case "text":
check.utf8(conf, memo.value);
break;
case "base64":
check.base64(conf, memo.value);
break;
case "hash":
case "return":
check.hash(conf, memo.value);
break;
case "id":
check.id(conf, memo.value);
break;
default:
status.error(conf, "Invalid memo type: " + memo.type, "throw");
}
};
check.price = function (conf, price) {
if (typeof price === "object") {
try {
check.price(null, price.n);
check.price(null, price.d);
} catch (error) {
status.error(conf, "Invalid price: " + price, "throw");
}
} else {
check.number(conf, price, "price", 0);
}
};
check.signer = function (conf, signer) {
check.weight(conf, signer.weight);
switch (signer.type) {
case "key":
check.address(conf, signer.value);
break;
case "hash":
case "tx":
check.hash(conf, signer.value);
break;
default:
status.error(conf, "Invalid signer type: " + signer.type, "throw");
}
};
check.sequence = function (conf, sequence) {
check.number(conf, sequence, "sequence", 0);
};
check.threshold = function (conf, threshold) {
check.integer(conf, threshold, "threshold", 0, 255);
};
check.weight = function (conf, weight) {
check.integer(conf, weight, "weight", 0, 255);
};
/******************************************************************************/
/**
* Provide dummy aliases for every other type for convenience & backward
* compatibility.
*/
specs.types.forEach(type => {
if (!exports[type]) exports[type] = (conf, value) => value;
});