iota.lib.js
Version:
Javascript Library for IOTA
514 lines (392 loc) • 16 kB
JavaScript
var inputValidator = require("./inputValidator");
var makeRequest = require("./makeRequest");
var Curl = require("../crypto/curl/curl");
var Kerl = require("../crypto/kerl/kerl");
var Converter = require("../crypto/converter/converter");
var Signing = require("../crypto/signing/signing");
var CryptoJS = require("crypto-js");
var ascii = require("./asciiToTrytes");
var extractJson = require("./extractJson");
var BigNumber = require("bignumber.js");
/**
* Table of IOTA Units based off of the standard System of Units
**/
var unitMap = {
'i' : {val: new BigNumber(10).pow(0), dp: 0},
'Ki' : {val: new BigNumber(10).pow(3), dp: 3},
'Mi' : {val: new BigNumber(10).pow(6), dp: 6},
'Gi' : {val: new BigNumber(10).pow(9), dp: 9},
'Ti' : {val: new BigNumber(10).pow(12), dp: 12},
'Pi' : {val: new BigNumber(10).pow(15), dp: 15}// For the very, very rich
}
/**
* converts IOTA units
*
* @method convertUnits
* @param {string || int || float} value
* @param {string} fromUnit
* @param {string} toUnit
* @returns {integer} converted
**/
var convertUnits = function(value, fromUnit, toUnit) {
// Check if wrong unit provided
if (unitMap[fromUnit] === undefined || unitMap[toUnit] === undefined) {
throw new Error("Invalid unit provided");
}
var valueBn = new BigNumber(value);
if(valueBn.dp() > unitMap[fromUnit].dp) {
throw new Error("Input value exceeded max fromUnit precision.");
}
var valueRaw = valueBn.times(unitMap[fromUnit].val);
var valueScaled = valueRaw.dividedBy(unitMap[toUnit].val);
return valueScaled.toNumber();
}
/**
* Generates the 9-tryte checksum of an address
*
* @method addChecksum
* @param {string | list} inputValue
* @param {int} checksumLength
@ @param {bool} isAddress default is true
* @returns {string | list} address (with checksum)
**/
var addChecksum = function (inputValue, checksumLength, isAddress) {
var isString = inputValidator.isString(inputValue);
var isArray = inputValidator.isArray(inputValue);
if (!isString && !isArray) {
throw new Error('Invalid input');
}
// Check if isAddress param is explicity set to false
var addingChecksumForAddress = isAddress !== false;
var isPlainTritArray = inputValidator.isTritArray(
inputValue,
addingChecksumForAddress ? Curl.HASH_LENGTH : null
);
var isSingleInput = isString || isPlainTritArray;
var input = isSingleInput ? [inputValue] : inputValue;
var getChecksumTrits = function (trits) {
var kerl = new Kerl();
// Initialize kerl
kerl.initialize();
// Checksum trits
var checksumTrits = [];
// Absorb address trits
kerl.absorb(trits, 0, trits.length);
// Squeeze checksum trits
kerl.squeeze(checksumTrits, 0, Curl.HASH_LENGTH);
return checksumTrits.slice(-Curl.HASH_LENGTH / 9);
};
var withChecksum = function (value) {
var isTrytes = inputValidator.isTrytes(value, addingChecksumForAddress ? 81 : null);
var isTritArray = isPlainTritArray || inputValidator.isTritArray(value, addingChecksumForAddress ? Curl.HASH_LENGTH : null);
if (!isTrytes && !isTritArray) {
throw new Error('Invalid input');
}
var checksum = isTritArray ? getChecksumTrits(value) :
Converter.trytes(getChecksumTrits(Converter.trits(value)));
var fallbackLength = isTrytes ? 9 : 27;
var length = checksumLength || fallbackLength;
return value.concat(checksum.slice(-length));
};
var result = input.map(withChecksum);
return isSingleInput ? result[0] : result;
}
/**
* Removes the 9-tryte checksum of an address
*
* @method noChecksum
* @param {string | list} address
* @returns {string | list} address (without checksum)
**/
var noChecksum = function(address) {
var isSingleAddress = inputValidator.isString(address)
if (isSingleAddress && address.length === 81) {
return address
}
// If only single address, turn it into an array
if (isSingleAddress) address = new Array(address);
var addressesWithChecksum = [];
address.forEach(function(thisAddress) {
addressesWithChecksum.push(thisAddress.slice(0, 81))
})
// return either string or the list
if (isSingleAddress) {
return addressesWithChecksum[0];
} else {
return addressesWithChecksum;
}
}
/**
* Validates the checksum of an address
*
* @method isValidChecksum
* @param {string} addressWithChecksum
* @returns {bool}
**/
var isValidChecksum = function(addressWithChecksum) {
var addressWithoutChecksum = noChecksum(addressWithChecksum);
var newChecksum = addChecksum(addressWithoutChecksum);
return newChecksum === addressWithChecksum;
}
var transactionHash = function (transactionTrits) {
if (!inputValidator.isTritArray(transactionTrits, 2673 * 3)) {
throw new Error('Invalid transaction trits')
}
var hashTrits = []
var curl = new Curl()
// generate the correct transaction hash
curl.initialize()
curl.absorb(transactionTrits, 0, transactionTrits.length)
curl.squeeze(hashTrits, 0, 243)
return hashTrits
}
/**
* Converts transaction trytes of 2673 trytes into a transaction object
*
* @method transactionObject
* @param {string} trytes
* @param {string} hash - Transaction hash
* @returns {String} transactionObject
**/
var transactionObject = function(trytes, hash) {
if (!trytes) return;
// validity check
for (var i = 2279; i < 2295; i++) {
if (trytes.charAt(i) !== "9") {
return null;
}
}
var thisTransaction = {};
var transactionTrits = Converter.trits(trytes);
if (inputValidator.isHash(hash)) {
thisTransaction.hash = hash;
} else {
thisTransaction.hash = Converter.trytes(transactionHash(transactionTrits));
}
thisTransaction.signatureMessageFragment = trytes.slice(0, 2187);
thisTransaction.address = trytes.slice(2187, 2268);
thisTransaction.value = Converter.value(transactionTrits.slice(6804, 6885));
thisTransaction.obsoleteTag = trytes.slice(2295, 2322);
thisTransaction.timestamp = Converter.value(transactionTrits.slice(6966, 6993));
thisTransaction.currentIndex = Converter.value(transactionTrits.slice(6993, 7020));
thisTransaction.lastIndex = Converter.value(transactionTrits.slice(7020, 7047));
thisTransaction.bundle = trytes.slice(2349, 2430);
thisTransaction.trunkTransaction = trytes.slice(2430, 2511);
thisTransaction.branchTransaction = trytes.slice(2511, 2592);
thisTransaction.tag = trytes.slice(2592, 2619);
thisTransaction.attachmentTimestamp = Converter.value(transactionTrits.slice(7857, 7884));
thisTransaction.attachmentTimestampLowerBound = Converter.value(transactionTrits.slice(7884, 7911));
thisTransaction.attachmentTimestampUpperBound = Converter.value(transactionTrits.slice(7911, 7938));
thisTransaction.nonce = trytes.slice(2646, 2673);
return thisTransaction;
}
/**
* Converts a transaction object into trytes
*
* @method transactionTrytes
* @param {object} transactionTrytes
* @returns {String} trytes
**/
var transactionTrytes = function(transaction) {
var valueTrits = Converter.trits(transaction.value);
while (valueTrits.length < 81) {
valueTrits[valueTrits.length] = 0;
}
var timestampTrits = Converter.trits(transaction.timestamp);
while (timestampTrits.length < 27) {
timestampTrits[timestampTrits.length] = 0;
}
var currentIndexTrits = Converter.trits(transaction.currentIndex);
while (currentIndexTrits.length < 27) {
currentIndexTrits[currentIndexTrits.length] = 0;
}
var lastIndexTrits = Converter.trits(transaction.lastIndex);
while (lastIndexTrits.length < 27) {
lastIndexTrits[lastIndexTrits.length] = 0;
}
var attachmentTimestampTrits = Converter.trits(transaction.attachmentTimestamp || 0);
while (attachmentTimestampTrits.length < 27) {
attachmentTimestampTrits[attachmentTimestampTrits.length] = 0;
}
var attachmentTimestampLowerBoundTrits = Converter.trits(transaction.attachmentTimestampLowerBound || 0);
while (attachmentTimestampLowerBoundTrits.length < 27) {
attachmentTimestampLowerBoundTrits[attachmentTimestampLowerBoundTrits.length] = 0;
}
var attachmentTimestampUpperBoundTrits = Converter.trits(transaction.attachmentTimestampUpperBound || 0);
while (attachmentTimestampUpperBoundTrits.length < 27) {
attachmentTimestampUpperBoundTrits[attachmentTimestampUpperBoundTrits.length] = 0;
}
transaction.tag = transaction.tag || transaction.obsoleteTag;
return transaction.signatureMessageFragment
+ transaction.address
+ Converter.trytes(valueTrits)
+ transaction.obsoleteTag
+ Converter.trytes(timestampTrits)
+ Converter.trytes(currentIndexTrits)
+ Converter.trytes(lastIndexTrits)
+ transaction.bundle
+ transaction.trunkTransaction
+ transaction.branchTransaction
+ transaction.tag
+ Converter.trytes(attachmentTimestampTrits)
+ Converter.trytes(attachmentTimestampLowerBoundTrits)
+ Converter.trytes(attachmentTimestampUpperBoundTrits)
+ transaction.nonce;
}
var isTransactionHash = function (input, minWeightMagnitude) {
var isTxObject = inputValidator.isArrayOfTxObjects([input])
return (
minWeightMagnitude
? Converter.trits(isTxObject ? input.hash : input)
.slice(-minWeightMagnitude)
.every(function (trit) {
return trit === 0
})
: true
) && (
isTxObject
? input.hash === Converter.trytes(transactionHash(Converter.trits(transactionTrytes(input))))
: inputValidator.isHash(input)
)
}
/**
* Categorizes a list of transfers between sent and received
*
* @method categorizeTransfers
* @param {object} transfers Transfers (bundles)
* @param {list} addresses List of addresses that belong to the user
* @returns {String} trytes
**/
var categorizeTransfers = function(transfers, addresses) {
var categorized = {
'sent' : [],
'received' : []
}
// Iterate over all bundles and sort them between incoming and outgoing transfers
transfers.forEach(function(bundle) {
var spentAlreadyAdded = false;
// Iterate over every bundle entry
bundle.forEach(function(bundleEntry, bundleIndex) {
// If bundle address in the list of addresses associated with the seed
// add the bundle to the
if (addresses.indexOf(bundleEntry.address) > -1) {
// Check if it's a remainder address
var isRemainder = (bundleEntry.currentIndex === bundleEntry.lastIndex) && bundleEntry.lastIndex !== 0;
// check if sent transaction
if (bundleEntry.value < 0 && !spentAlreadyAdded && !isRemainder) {
categorized.sent.push(bundle);
// too make sure we do not add transactions twice
spentAlreadyAdded = true;
}
// check if received transaction, or 0 value (message)
// also make sure that this is not a 2nd tx for spent inputs
else if (bundleEntry.value >= 0 && !spentAlreadyAdded && !isRemainder) {
categorized.received.push(bundle);
}
}
})
})
return categorized;
}
/**
* Validates the signatures
*
* @method validateSignatures
* @param {array} signedBundle
* @param {string} inputAddress
* @returns {bool}
**/
var validateSignatures = function(signedBundle, inputAddress) {
var bundleHash;
var signatureFragments = [];
for (var i = 0; i < signedBundle.length; i++) {
if (signedBundle[i].address === inputAddress) {
bundleHash = signedBundle[i].bundle;
// if we reached remainder bundle
if (inputValidator.isNinesTrytes(signedBundle[i].signatureMessageFragment)) {
break;
}
signatureFragments.push(signedBundle[i].signatureMessageFragment)
}
}
if (!bundleHash) {
return false;
}
return Signing.validateSignatures(inputAddress, signatureFragments, bundleHash);
}
/**
* Checks is a Bundle is valid. Validates signatures and overall structure. Has to be tail tx first.
*
* @method isValidBundle
* @param {array} bundle
* @returns {bool} valid
**/
var isBundle = function(bundle) {
// If not correct bundle
if (!inputValidator.isArrayOfTxObjects(bundle)) return false;
var totalSum = 0, lastIndex, bundleHash = bundle[0].bundle;
// Prepare to absorb txs and get bundleHash
var bundleFromTxs = [];
var kerl = new Kerl();
kerl.initialize();
// Prepare for signature validation
var signaturesToValidate = [];
bundle.forEach(function(bundleTx, index) {
totalSum += bundleTx.value;
// currentIndex has to be equal to the index in the array
if (bundleTx.currentIndex !== index) return false;
// Get the transaction trytes
var thisTxTrytes = transactionTrytes(bundleTx);
// Absorb bundle hash + value + timestamp + lastIndex + currentIndex trytes.
var thisTxTrits = Converter.trits(thisTxTrytes.slice(2187, 2187 + 162));
kerl.absorb(thisTxTrits, 0, thisTxTrits.length);
// Check if input transaction
if (bundleTx.value < 0) {
var thisAddress = bundleTx.address;
var newSignatureToValidate = {
'address': thisAddress,
'signatureFragments': Array(bundleTx.signatureMessageFragment)
}
// Find the subsequent txs with the remaining signature fragment
for (var i = index; i < bundle.length - 1; i++) {
var newBundleTx = bundle[i + 1];
// Check if new tx is part of the signature fragment
if (newBundleTx.address === thisAddress && newBundleTx.value === 0) {
newSignatureToValidate.signatureFragments.push(newBundleTx.signatureMessageFragment);
}
}
signaturesToValidate.push(newSignatureToValidate);
}
});
// Check for total sum, if not equal 0 return error
if (totalSum !== 0) return false;
// get the bundle hash from the bundle transactions
kerl.squeeze(bundleFromTxs, 0, Curl.HASH_LENGTH);
var bundleFromTxs = Converter.trytes(bundleFromTxs);
// Check if bundle hash is the same as returned by tx object
if (bundleFromTxs !== bundleHash) return false;
// Last tx in the bundle should have currentIndex === lastIndex
if (bundle[bundle.length - 1].currentIndex !== bundle[bundle.length - 1].lastIndex) return false;
// Validate the signatures
for (var i = 0; i < signaturesToValidate.length; i++) {
var isValidSignature = Signing.validateSignatures(signaturesToValidate[i].address, signaturesToValidate[i].signatureFragments, bundleHash);
if (!isValidSignature) return false;
}
return true;
}
module.exports = {
convertUnits : convertUnits,
addChecksum : addChecksum,
noChecksum : noChecksum,
isValidChecksum : isValidChecksum,
transactionHash : transactionHash,
transactionObject : transactionObject,
transactionTrytes : transactionTrytes,
isTransactionHash : isTransactionHash,
categorizeTransfers : categorizeTransfers,
toTrytes : ascii.toTrytes,
fromTrytes : ascii.fromTrytes,
extractJson : extractJson,
validateSignatures : validateSignatures,
isBundle : isBundle
}