diff --git a/docs/index.html b/docs/index.html index fbb569d..ab02bbf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -17,8 +17,8 @@ - + @@ -91,7 +91,7 @@

Don't have FLO credentials?

Generate FLO credentials - clear local data + clear local data
@@ -1021,9 +1021,9 @@ showProcess('trade_button_wrapper') try { if (tradeType === 'buy') { - await buy(asset, quantity, price, proxy.userID, await proxy.secret) + await exchangeAPI.buy(asset, quantity, price, proxy.userID, await proxy.secret) } else { - await sell(asset, quantity, price, proxy.userID, await proxy.secret) + await exchangeAPI.sell(asset, quantity, price, proxy.userID, await proxy.secret) } getRef('trade_button_wrapper').append(getRef('success_template').content.cloneNode(true)) notify(`Placed ${tradeType} order`, 'success') @@ -1165,16 +1165,16 @@ if (type === 'deposit') { const privKey = getRef('get_private_key').value; if (asset === 'FLO') { - await depositFLO(quantity, proxy.userID, proxy.sinkID, privKey, proxySecret) + await exchangeAPI.depositFLO(quantity, proxy.userID, proxy.sinkID, privKey, proxySecret) } else { - await depositToken(asset, quantity, proxy.userID, proxy.sinkID, privKey, proxySecret) + await exchangeAPI.depositToken(asset, quantity, proxy.userID, proxy.sinkID, privKey, proxySecret) } showWalletResult('success', `Sent ${asset} deposit request`, 'This may take upto 30 mins to reflect in your wallet.') } else { if (asset === 'FLO') { - await withdrawFLO(quantity, proxy.userID, proxySecret) + await exchangeAPI.withdrawFLO(quantity, proxy.userID, proxySecret) } else { - await withdrawToken(asset, quantity, proxy.userID, proxySecret) + await exchangeAPI.withdrawToken(asset, quantity, proxy.userID, proxySecret) } showWalletResult('success', `Sent ${asset} withdraw request`, 'This may take upto 30 mins to reflect in your wallet.') } @@ -1317,7 +1317,7 @@ const target = e.target.closest('.order-card') const id = target.dataset.id const type = target.dataset.type - cancelOrder(type, id, proxy.userID, await proxy.secret) + exchangeAPI.cancelOrder(type, id, proxy.userID, await proxy.secret) .then(() => { notify('Order cancelled', 'success') target.animate([ @@ -1369,7 +1369,7 @@ if (res) { try { const proxy_secret = await proxy.secret; - const promises = [...selectedOrders].map(([id, type]) => cancelOrder(type, id, proxy.userID, proxy_secret)) + const promises = [...selectedOrders].map(([id, type]) => exchangeAPI.cancelOrder(type, id, proxy.userID, proxy_secret)) await Promise.all(promises) selectedOrders.clear() hideMyOrdersOptions() @@ -1439,7 +1439,7 @@ const ordersType = getRef('market_orders_category_selector').value if (ordersType === 'open') { try { - const [buyOrders, sellOrders] = await Promise.all([getBuyList(), getSellList()]) + const [buyOrders, sellOrders] = await Promise.all([exchangeAPI.getBuyList(), exchangeAPI.getSellList()]) const allOpenOrders = [...buyOrders, ...sellOrders].sort((a, b) => new Date(b.time_placed).getTime() - new Date(a.time_placed).getTime()) allOpenOrders.forEach(order => { const { floID, asset, quantity, minPrice = undefined, maxPrice = undefined, time_placed } = order @@ -1459,7 +1459,7 @@ } } else { try { - const marketTransactions = await getTradeList() + const marketTransactions = await exchangeAPI.getTradeList() marketTransactions.forEach(transaction => { const { seller, buyer, asset, quantity, unitValue, tx_time } = transaction const transactionDetails = { @@ -1604,7 +1604,7 @@ let floExchangeRate = 0 function updateRate(init = false) { - getRates().then(rates => { + exchangeAPI.getRates().then(rates => { console.debug(rates); if (init) { let assetList = getRef('get_asset'); @@ -1633,7 +1633,7 @@ console.info("init"); if (!proxy.userID) { getRef('home').classList.remove('signed-in'); - getLoginCode().then(response => { + exchangeAPI.getLoginCode().then(response => { getRef("login_form").classList.remove('hide-completely'); document.querySelectorAll(".user-content").forEach(elem => elem.classList.add('hide-completely')) getRef('sign_in_code').value = response.code; @@ -1666,7 +1666,7 @@ let accountDetails = {} async function account() { - getAccount(proxy.userID, await proxy.secret).then(acc => { + exchangeAPI.getAccount(proxy.userID, await proxy.secret).then(acc => { getRef("login_form").classList.add('hide-completely') getRef('home').classList.add('signed-in') getRef('user_popup_button').classList.remove('hide-completely') @@ -1706,7 +1706,7 @@ if (!privKey) privKey = getRef('get_registration_key').value.trim() if (privKey !== '') { - signUp(privKey, code, hash).then(result => { + exchangeAPI.signUp(privKey, code, hash).then(result => { console.info(result); notify("Account registered!", 'success') hidePopup() @@ -1719,7 +1719,7 @@ logout() { getConfirmation('Log out?', { cancelText: 'Stay', confirmText: 'Log out' }).then(async res => { if (res) { - logout(proxy.userID, await proxy.secret).then(result => { + exchangeAPI.logout(proxy.userID, await proxy.secret).then(result => { console.warn(result); proxy.clear(); location.reload(); @@ -1735,7 +1735,7 @@ hash = getRef('sign_in_hash').value; let rememberMe = getRef('remember_me').checked; let tmpKey = floCrypto.generateNewID(); - login(privKey, tmpKey.pubKey, code, hash).then(result => { + exchangeAPI.login(privKey, tmpKey.pubKey, code, hash).then(result => { console.log(result); proxy.secret = tmpKey.privKey; proxy.userID = floCrypto.getFloID(privKey); @@ -1756,7 +1756,7 @@ } window.addEventListener('load', e => { - refreshDataFromBlockchain().then(nodes => { + exchangeAPI.init().then(nodes => { console.log(nodes); refresh(true); }).catch(error => console.error(error)) diff --git a/docs/scripts/KBucket.js b/docs/scripts/KBucket.js deleted file mode 100644 index 247896a..0000000 --- a/docs/scripts/KBucket.js +++ /dev/null @@ -1,463 +0,0 @@ -'use strict'; - -(function(){ -/*Kademlia DHT K-bucket implementation as a binary tree.*/ -/** - * Implementation of a Kademlia DHT k-bucket used for storing - * contact (peer node) information. - * - * @extends EventEmitter - */ - function BuildKBucket(options = {}) { - /** - * `options`: - * `distance`: Function - * `function (firstId, secondId) { return distance }` An optional - * `distance` function that gets two `id` Uint8Arrays - * and return distance (as number) between them. - * `arbiter`: Function (Default: vectorClock arbiter) - * `function (incumbent, candidate) { return contact; }` An optional - * `arbiter` function that givent two `contact` objects with the same `id` - * returns the desired object to be used for updating the k-bucket. For - * more details, see [arbiter function](#arbiter-function). - * `localNodeId`: Uint8Array An optional Uint8Array representing the local node id. - * If not provided, a local node id will be created via `randomBytes(20)`. - * `metadata`: Object (Default: {}) Optional satellite data to include - * with the k-bucket. `metadata` property is guaranteed not be altered by, - * it is provided as an explicit container for users of k-bucket to store - * implementation-specific data. - * `numberOfNodesPerKBucket`: Integer (Default: 20) The number of nodes - * that a k-bucket can contain before being full or split. - * `numberOfNodesToPing`: Integer (Default: 3) The number of nodes to - * ping when a bucket that should not be split becomes full. KBucket will - * emit a `ping` event that contains `numberOfNodesToPing` nodes that have - * not been contacted the longest. - * - * @param {Object=} options optional - */ - - this.localNodeId = options.localNodeId || window.crypto.getRandomValues(new Uint8Array(20)) - this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20 - this.numberOfNodesToPing = options.numberOfNodesToPing || 3 - this.distance = options.distance || this.distance - // use an arbiter from options or vectorClock arbiter by default - this.arbiter = options.arbiter || this.arbiter - this.metadata = Object.assign({}, options.metadata) - - this.createNode = function() { - return { - contacts: [], - dontSplit: false, - left: null, - right: null - } - } - - this.ensureInt8 = function(name, val) { - if (!(val instanceof Uint8Array)) { - throw new TypeError(name + ' is not a Uint8Array') - } - } - - /** - * @param {Uint8Array} array1 - * @param {Uint8Array} array2 - * @return {Boolean} - */ - this.arrayEquals = function(array1, array2) { - if (array1 === array2) { - return true - } - if (array1.length !== array2.length) { - return false - } - for (let i = 0, length = array1.length; i < length; ++i) { - if (array1[i] !== array2[i]) { - return false - } - } - return true - } - - this.ensureInt8('option.localNodeId as parameter 1', this.localNodeId) - this.root = this.createNode() - - /** - * Default arbiter function for contacts with the same id. Uses - * contact.vectorClock to select which contact to update the k-bucket with. - * Contact with larger vectorClock field will be selected. If vectorClock is - * the same, candidat will be selected. - * - * @param {Object} incumbent Contact currently stored in the k-bucket. - * @param {Object} candidate Contact being added to the k-bucket. - * @return {Object} Contact to updated the k-bucket with. - */ - this.arbiter = function(incumbent, candidate) { - return incumbent.vectorClock > candidate.vectorClock ? incumbent : candidate - } - - /** - * Default distance function. Finds the XOR - * distance between firstId and secondId. - * - * @param {Uint8Array} firstId Uint8Array containing first id. - * @param {Uint8Array} secondId Uint8Array containing second id. - * @return {Number} Integer The XOR distance between firstId - * and secondId. - */ - this.distance = function(firstId, secondId) { - let distance = 0 - let i = 0 - const min = Math.min(firstId.length, secondId.length) - const max = Math.max(firstId.length, secondId.length) - for (; i < min; ++i) { - distance = distance * 256 + (firstId[i] ^ secondId[i]) - } - for (; i < max; ++i) distance = distance * 256 + 255 - return distance - } - - /** - * Adds a contact to the k-bucket. - * - * @param {Object} contact the contact object to add - */ - this.add = function(contact) { - this.ensureInt8('contact.id', (contact || {}).id) - - let bitIndex = 0 - let node = this.root - - while (node.contacts === null) { - // this is not a leaf node but an inner node with 'low' and 'high' - // branches; we will check the appropriate bit of the identifier and - // delegate to the appropriate node for further processing - node = this._determineNode(node, contact.id, bitIndex++) - } - - // check if the contact already exists - const index = this._indexOf(node, contact.id) - if (index >= 0) { - this._update(node, index, contact) - return this - } - - if (node.contacts.length < this.numberOfNodesPerKBucket) { - node.contacts.push(contact) - return this - } - - // the bucket is full - if (node.dontSplit) { - // we are not allowed to split the bucket - // we need to ping the first this.numberOfNodesToPing - // in order to determine if they are alive - // only if one of the pinged nodes does not respond, can the new contact - // be added (this prevents DoS flodding with new invalid contacts) - return this - } - - this._split(node, bitIndex) - return this.add(contact) - } - - /** - * Get the n closest contacts to the provided node id. "Closest" here means: - * closest according to the XOR metric of the contact node id. - * - * @param {Uint8Array} id Contact node id - * @param {Number=} n Integer (Default: Infinity) The maximum number of - * closest contacts to return - * @return {Array} Array Maximum of n closest contacts to the node id - */ - this.closest = function(id, n = Infinity) { - this.ensureInt8('id', id) - - if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { - throw new TypeError('n is not positive number') - } - - let contacts = [] - - for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { - const node = nodes.pop() - if (node.contacts === null) { - const detNode = this._determineNode(node, id, bitIndex++) - nodes.push(node.left === detNode ? node.right : node.left) - nodes.push(detNode) - } else { - contacts = contacts.concat(node.contacts) - } - } - - return contacts - .map(a => [this.distance(a.id, id), a]) - .sort((a, b) => a[0] - b[0]) - .slice(0, n) - .map(a => a[1]) - } - - /** - * Counts the total number of contacts in the tree. - * - * @return {Number} The number of contacts held in the tree - */ - this.count = function() { - // return this.toArray().length - let count = 0 - for (const nodes = [this.root]; nodes.length > 0;) { - const node = nodes.pop() - if (node.contacts === null) nodes.push(node.right, node.left) - else count += node.contacts.length - } - return count - } - - /** - * Determines whether the id at the bitIndex is 0 or 1. - * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise - * - * @param {Object} node internal object that has 2 leafs: left and right - * @param {Uint8Array} id Id to compare localNodeId with. - * @param {Number} bitIndex Integer (Default: 0) The bit index to which bit - * to check in the id Uint8Array. - * @return {Object} left leaf if id at bitIndex is 0, right leaf otherwise. - */ - this._determineNode = function(node, id, bitIndex) { - // *NOTE* remember that id is a Uint8Array and has granularity of - // bytes (8 bits), whereas the bitIndex is the bit index (not byte) - - // id's that are too short are put in low bucket (1 byte = 8 bits) - // (bitIndex >> 3) finds how many bytes the bitIndex describes - // bitIndex % 8 checks if we have extra bits beyond byte multiples - // if number of bytes is <= no. of bytes described by bitIndex and there - // are extra bits to consider, this means id has less bits than what - // bitIndex describes, id therefore is too short, and will be put in low - // bucket - const bytesDescribedByBitIndex = bitIndex >> 3 - const bitIndexWithinByte = bitIndex % 8 - if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { - return node.left - } - - const byteUnderConsideration = id[bytesDescribedByBitIndex] - - // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits - // where 255 is 11111111 and 0 is 00000000 - // in order to find out whether the bit at bitIndexWithinByte is set - // we construct (1 << (7 - bitIndexWithinByte)) which will consist - // of all bits being 0, with only one bit set to 1 - // for example, if bitIndexWithinByte is 3, we will construct 00010000 by - // (1 << (7 - 3)) -> (1 << 4) -> 16 - if (byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) { - return node.right - } - - return node.left - } - - /** - * Get a contact by its exact ID. - * If this is a leaf, loop through the bucket contents and return the correct - * contact if we have it or null if not. If this is an inner node, determine - * which branch of the tree to traverse and repeat. - * - * @param {Uint8Array} id The ID of the contact to fetch. - * @return {Object|Null} The contact if available, otherwise null - */ - this.get = function(id) { - this.ensureInt8('id', id) - - let bitIndex = 0 - - let node = this.root - while (node.contacts === null) { - node = this._determineNode(node, id, bitIndex++) - } - - // index of uses contact id for matching - const index = this._indexOf(node, id) - return index >= 0 ? node.contacts[index] : null - } - - /** - * Returns the index of the contact with provided - * id if it exists, returns -1 otherwise. - * - * @param {Object} node internal object that has 2 leafs: left and right - * @param {Uint8Array} id Contact node id. - * @return {Number} Integer Index of contact with provided id if it - * exists, -1 otherwise. - */ - this._indexOf = function(node, id) { - for (let i = 0; i < node.contacts.length; ++i) { - if (this.arrayEquals(node.contacts[i].id, id)) return i - } - - return -1 - } - - /** - * Removes contact with the provided id. - * - * @param {Uint8Array} id The ID of the contact to remove. - * @return {Object} The k-bucket itself. - */ - this.remove = function(id) { - this.ensureInt8('the id as parameter 1', id) - - let bitIndex = 0 - let node = this.root - - while (node.contacts === null) { - node = this._determineNode(node, id, bitIndex++) - } - - const index = this._indexOf(node, id) - if (index >= 0) { - const contact = node.contacts.splice(index, 1)[0] - } - - return this - } - - /** - * Splits the node, redistributes contacts to the new nodes, and marks the - * node that was split as an inner node of the binary tree of nodes by - * setting this.root.contacts = null - * - * @param {Object} node node for splitting - * @param {Number} bitIndex the bitIndex to which byte to check in the - * Uint8Array for navigating the binary tree - */ - this._split = function(node, bitIndex) { - node.left = this.createNode() - node.right = this.createNode() - - // redistribute existing contacts amongst the two newly created nodes - for (const contact of node.contacts) { - this._determineNode(node, contact.id, bitIndex).contacts.push(contact) - } - - node.contacts = null // mark as inner tree node - - // don't split the "far away" node - // we check where the local node would end up and mark the other one as - // "dontSplit" (i.e. "far away") - const detNode = this._determineNode(node, this.localNodeId, bitIndex) - const otherNode = node.left === detNode ? node.right : node.left - otherNode.dontSplit = true - } - - /** - * Returns all the contacts contained in the tree as an array. - * If this is a leaf, return a copy of the bucket. `slice` is used so that we - * don't accidentally leak an internal reference out that might be - * accidentally misused. If this is not a leaf, return the union of the low - * and high branches (themselves also as arrays). - * - * @return {Array} All of the contacts in the tree, as an array - */ - this.toArray = function() { - let result = [] - for (const nodes = [this.root]; nodes.length > 0;) { - const node = nodes.pop() - if (node.contacts === null) nodes.push(node.right, node.left) - else result = result.concat(node.contacts) - } - return result - } - - /** - * Updates the contact selected by the arbiter. - * If the selection is our old contact and the candidate is some new contact - * then the new contact is abandoned (not added). - * If the selection is our old contact and the candidate is our old contact - * then we are refreshing the contact and it is marked as most recently - * contacted (by being moved to the right/end of the bucket array). - * If the selection is our new contact, the old contact is removed and the new - * contact is marked as most recently contacted. - * - * @param {Object} node internal object that has 2 leafs: left and right - * @param {Number} index the index in the bucket where contact exists - * (index has already been computed in a previous - * calculation) - * @param {Object} contact The contact object to update. - */ - this._update = function(node, index, contact) { - // sanity check - if (!this.arrayEquals(node.contacts[index].id, contact.id)) { - throw new Error('wrong index for _update') - } - - const incumbent = node.contacts[index] - const selection = this.arbiter(incumbent, contact) - // if the selection is our old contact and the candidate is some new - // contact, then there is nothing to do - if (selection === incumbent && incumbent !== contact) return - - node.contacts.splice(index, 1) // remove old contact - node.contacts.push(selection) // add more recent contact version - - } -} - -function K_Bucket(masterID, backupList) { - const decodeID = function(floID) { - let k = bitjs.Base58.decode(floID); - k.shift(); - k.splice(-4, 4); - const decodedId = Crypto.util.bytesToHex(k); - const nodeIdBigInt = new BigInteger(decodedId, 16); - const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); - const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); - return nodeIdNewInt8Array; - }; - const _KB = new BuildKBucket({ - localNodeId: decodeID(masterID) - }); - backupList.forEach(id => _KB.add({ - id: decodeID(id), - floID: id - })); - const orderedList = backupList.map(sn => [_KB.distance(decodeID(masterID), decodeID(sn)), sn]) - .sort((a, b) => a[0] - b[0]) - .map(a => a[1]); - const self = this; - - Object.defineProperty(self, 'order', { - get: () => Array.from(orderedList) - }); - - self.closestNode = function(id, N = 1) { - let decodedId = decodeID(id); - let n = N || orderedList.length; - let cNodes = _KB.closest(decodedId, n) - .map(k => k.floID); - return (N == 1 ? cNodes[0] : cNodes); - }; - - self.isBefore = (source, target) => orderedList.indexOf(target) < orderedList.indexOf(source); - self.isAfter = (source, target) => orderedList.indexOf(target) > orderedList.indexOf(source); - self.isPrev = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) - 1; - self.isNext = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) + 1; - - self.prevNode = function(id, N = 1) { - let n = N || orderedList.length; - if (!orderedList.includes(id)) - throw Error(`${id} is not in KB list`); - let pNodes = orderedList.slice(0, orderedList.indexOf(id)).slice(-n); - return (N == 1 ? pNodes[0] : pNodes); - }; - - self.nextNode = function(id, N = 1) { - let n = N || orderedList.length; - if (!orderedList.includes(id)) - throw Error(`${id} is not in KB list`); - let nNodes = orderedList.slice(orderedList.indexOf(id) + 1).slice(0, n); - return (N == 1 ? nNodes[0] : nNodes); - }; - -}; -('object' === typeof module) ? module.exports = K_Bucket : window.K_Bucket = K_Bucket; -})(); \ No newline at end of file diff --git a/docs/scripts/exchangeAPI.js b/docs/scripts/exchangeAPI.js index b1ccbb0..63ce5af 100644 --- a/docs/scripts/exchangeAPI.js +++ b/docs/scripts/exchangeAPI.js @@ -1,645 +1,1110 @@ -//console.log(document.cookie.toString()); -const INVALID_SERVER_MSG = "INCORRECT_SERVER_ERROR"; -var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list +'use strict'; -function exchangeAPI(api, options) { - return new Promise((resolve, reject) => { - let curPos = exchangeAPI.curPos || 0; - if (curPos >= nodeList.length) - return resolve('No Nodes online'); - let url = "https://" + nodeURL[nodeList[curPos]]; - (options ? fetch(url + api, options) : fetch(url + api)) - .then(result => resolve(result)).catch(error => { - console.warn(nodeList[curPos], 'is offline'); - //try next node - exchangeAPI.curPos = curPos + 1; - exchangeAPI(api, options) - .then(result => resolve(result)) - .catch(error => reject(error)) - }); - }) -} +(function(EXPORTS) { + const exchangeAPI = EXPORTS; -function ResponseError(status, data) { - if (data === INVALID_SERVER_MSG) - location.reload(); - else if (this instanceof ResponseError) { - this.data = data; - this.status = status; - } else - return new ResponseError(status, data); -} + /*Kademlia DHT K-bucket implementation as a binary tree.*/ + /** + * Implementation of a Kademlia DHT k-bucket used for storing + * contact (peer node) information. + * + * @extends EventEmitter + */ + function BuildKBucket(options = {}) { + /** + * `options`: + * `distance`: Function + * `function (firstId, secondId) { return distance }` An optional + * `distance` function that gets two `id` Uint8Arrays + * and return distance (as number) between them. + * `arbiter`: Function (Default: vectorClock arbiter) + * `function (incumbent, candidate) { return contact; }` An optional + * `arbiter` function that givent two `contact` objects with the same `id` + * returns the desired object to be used for updating the k-bucket. For + * more details, see [arbiter function](#arbiter-function). + * `localNodeId`: Uint8Array An optional Uint8Array representing the local node id. + * If not provided, a local node id will be created via `randomBytes(20)`. + * `metadata`: Object (Default: {}) Optional satellite data to include + * with the k-bucket. `metadata` property is guaranteed not be altered by, + * it is provided as an explicit container for users of k-bucket to store + * implementation-specific data. + * `numberOfNodesPerKBucket`: Integer (Default: 20) The number of nodes + * that a k-bucket can contain before being full or split. + * `numberOfNodesToPing`: Integer (Default: 3) The number of nodes to + * ping when a bucket that should not be split becomes full. KBucket will + * emit a `ping` event that contains `numberOfNodesToPing` nodes that have + * not been contacted the longest. + * + * @param {Object=} options optional + */ -function responseParse(response, json_ = true) { - return new Promise((resolve, reject) => { - if (!response.ok) - response.text() - .then(result => reject(ResponseError(response.status, result))) - .catch(error => reject(error)); - else if (json_) - response.json() - .then(result => resolve(result)) - .catch(error => reject(error)); - else - response.text() - .then(result => resolve(result)) - .catch(error => reject(error)); - }); -} + this.localNodeId = options.localNodeId || window.crypto.getRandomValues(new Uint8Array(20)) + this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20 + this.numberOfNodesToPing = options.numberOfNodesToPing || 3 + this.distance = options.distance || this.distance + // use an arbiter from options or vectorClock arbiter by default + this.arbiter = options.arbiter || this.arbiter + this.metadata = Object.assign({}, options.metadata) -function getAccount(floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "get_account", - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/account', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} - -function getBuyList() { - return new Promise((resolve, reject) => { - exchangeAPI('/list-buyorders') - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} - -function getSellList() { - return new Promise((resolve, reject) => { - exchangeAPI('/list-sellorders') - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} - -function getTradeList() { - return new Promise((resolve, reject) => { - exchangeAPI('/list-trades') - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} - -function getRates(asset = null) { - return new Promise((resolve, reject) => { - exchangeAPI('/get-rates' + (asset ? "?asset=" + asset : "")) - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} - -function getBalance(floID = null, token = null) { - return new Promise((resolve, reject) => { - if (!floID && !token) - return reject("Need atleast one argument") - let queryStr = (floID ? "floID=" + floID : "") + - (floID && token ? "&" : "") + - (token ? "token=" + token : ""); - exchangeAPI('/get-balance?' + queryStr) - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }) -} - -function getTx(txid) { - return new Promise((resolve, reject) => { - if (!txid) - return reject('txid required'); - exchangeAPI('/get-transaction?txid=' + txid) - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }) -} - -function signRequest(request, signKey) { - if (typeof request !== "object") - throw Error("Request is not an object"); - let req_str = Object.keys(request).sort().map(r => r + ":" + request[r]).join("|"); - return floCrypto.signData(req_str, signKey); -} - -function getLoginCode() { - return new Promise((resolve, reject) => { - exchangeAPI('/get-login-code') - .then(result => responseParse(result) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }) -} - -/* -function signUp(privKey, code, hash) { - return new Promise((resolve, reject) => { - if (!code || !hash) - return reject("Login Code missing") - let request = { - pubKey: floCrypto.getPubKeyHex(privKey), - floID: floCrypto.getFloID(privKey), - code: code, - hash: hash, - timestamp: Date.now() - }; - request.sign = signRequest({ - type: "create_account", - random: code, - timestamp: request.timestamp - }, privKey); - console.debug(request); - - exchangeAPI("/signup", { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }); -} -*/ - -function login(privKey, proxyKey, code, hash) { - return new Promise((resolve, reject) => { - if (!code || !hash) - return reject("Login Code missing") - let request = { - proxyKey: proxyKey, - floID: floCrypto.getFloID(privKey), - pubKey: floCrypto.getPubKeyHex(privKey), - timestamp: Date.now(), - code: code, - hash: hash - }; - if (!privKey || !request.floID) - return reject("Invalid Private key"); - request.sign = signRequest({ - type: "login", - random: code, - proxyKey: proxyKey, - timestamp: request.timestamp - }, privKey); - console.debug(request); - - exchangeAPI("/login", { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)); - }) -} - -function logout(floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "logout", - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI("/logout", { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function buy(asset, quantity, max_price, floID, proxySecret) { - return new Promise((resolve, reject) => { - if (typeof quantity !== "number" || quantity <= 0) - return reject(`Invalid quantity (${quantity})`); - else if (typeof max_price !== "number" || max_price <= 0) - return reject(`Invalid max_price (${max_price})`); - let request = { - floID: floID, - asset: asset, - quantity: quantity, - max_price: max_price, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "buy_order", - asset: asset, - quantity: quantity, - max_price: max_price, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/buy', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) - -} - -function sell(asset, quantity, min_price, floID, proxySecret) { - return new Promise((resolve, reject) => { - if (typeof quantity !== "number" || quantity <= 0) - return reject(`Invalid quantity (${quantity})`); - else if (typeof min_price !== "number" || min_price <= 0) - return reject(`Invalid min_price (${min_price})`); - let request = { - floID: floID, - asset: asset, - quantity: quantity, - min_price: min_price, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "sell_order", - quantity: quantity, - asset: asset, - min_price: min_price, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/sell', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) - -} - -function cancelOrder(type, id, floID, proxySecret) { - return new Promise((resolve, reject) => { - if (type !== "buy" && type !== "sell") - return reject(`Invalid type (${type}): type should be sell (or) buy`); - let request = { - floID: floID, - orderType: type, - orderID: id, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "cancel_order", - order: type, - id: id, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/cancel', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -//receiver should be object eg {floID1: amount1, floID2: amount2 ...} -function transferToken(receiver, token, floID, proxySecret) { - return new Promise((resolve, reject) => { - if (typeof receiver !== 'object' || receiver === null) - return reject("Invalid receiver: parameter is not an object"); - let invalidIDs = [], - invalidAmt = []; - for (let f in receiver) { - if (!floCrypto.validateAddr(f)) - invalidIDs.push(f); - else if (typeof receiver[f] !== "number" || receiver[f] <= 0) - invalidAmt.push(receiver[f]) + this.createNode = function() { + return { + contacts: [], + dontSplit: false, + left: null, + right: null + } } - if (invalidIDs.length) - return reject(INVALID(`Invalid receiver (${invalidIDs})`)); - else if (invalidAmt.length) - return reject(`Invalid amount (${invalidAmt})`); - let request = { - floID: floID, - token: token, - receiver: receiver, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "transfer_token", - receiver: JSON.stringify(receiver), - token: token, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - exchangeAPI('/transfer-token', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function depositFLO(quantity, floID, sinkID, privKey, proxySecret = null) { - return new Promise((resolve, reject) => { - if (typeof quantity !== "number" || quantity <= floGlobals.fee) - return reject(`Invalid quantity (${quantity})`); - floBlockchainAPI.sendTx(floID, sinkID, quantity, privKey, 'Deposit FLO in market').then(txid => { - let request = { - floID: floID, - txid: txid, - timestamp: Date.now() - }; - if (!proxySecret) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(privKey); - request.sign = signRequest({ - type: "deposit_flo", - txid: txid, - timestamp: request.timestamp - }, proxySecret || privKey); - console.debug(request); - - exchangeAPI('/deposit-flo', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }).catch(error => reject(error)) - }) -} - -function withdrawFLO(quantity, floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - amount: quantity, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "withdraw_flo", - amount: quantity, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/withdraw-flo', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function depositToken(token, quantity, floID, sinkID, privKey, proxySecret = null) { - return new Promise((resolve, reject) => { - if (!floCrypto.verifyPrivKey(privKey, floID)) - return reject("Invalid Private Key"); - tokenAPI.sendToken(privKey, quantity, sinkID, 'Deposit Rupee in market', token).then(txid => { - let request = { - floID: floID, - txid: txid, - timestamp: Date.now() - }; - if (!proxySecret) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(privKey); - request.sign = signRequest({ - type: "deposit_token", - txid: txid, - timestamp: request.timestamp - }, proxySecret || privKey); - console.debug(request); - - exchangeAPI('/deposit-token', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }).catch(error => reject(error)) - }) -} - -function withdrawToken(token, quantity, floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - token: token, - amount: quantity, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "withdraw_token", - token: token, - amount: quantity, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/withdraw-token', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function addUserTag(tag_user, tag, floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - user: tag_user, - tag: tag, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "add_tag", - user: tag_user, - tag: tag, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/add-tag', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function removeUserTag(tag_user, tag, floID, proxySecret) { - return new Promise((resolve, reject) => { - let request = { - floID: floID, - user: tag_user, - tag: tag, - timestamp: Date.now() - }; - if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) - request.pubKey = floCrypto.getPubKeyHex(proxySecret); - request.sign = signRequest({ - type: "remove_tag", - user: tag_user, - tag: tag, - timestamp: request.timestamp - }, proxySecret); - console.debug(request); - - exchangeAPI('/remove-tag', { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }).then(result => responseParse(result, false) - .then(result => resolve(result)) - .catch(error => reject(error))) - .catch(error => reject(error)) - }) -} - -function refreshDataFromBlockchain() { - return new Promise((resolve, reject) => { - let nodes, lastTx; - try { - nodes = JSON.parse(localStorage.getItem('exchange-nodes')); - if (typeof nodes !== 'object' || nodes === null) - throw Error('nodes must be an object') - else - lastTx = parseInt(localStorage.getItem('exchange-lastTx')) || 0; - } catch (error) { - nodes = {}; - lastTx = 0; + this.ensureInt8 = function(name, val) { + if (!(val instanceof Uint8Array)) { + throw new TypeError(name + ' is not a Uint8Array') + } } - floBlockchainAPI.readData(floGlobals.adminID, { - ignoreOld: lastTx, - sentOnly: true, - pattern: floGlobals.application - }).then(result => { - result.data.reverse().forEach(data => { - var content = JSON.parse(data)[floGlobals.application]; - //Node List - if (content.Nodes) { - if (content.Nodes.remove) - for (let n of content.Nodes.remove) - delete nodes[n]; - if (content.Nodes.add) - for (let n in content.Nodes.add) - nodes[n] = content.Nodes.add[n]; + + /** + * @param {Uint8Array} array1 + * @param {Uint8Array} array2 + * @return {Boolean} + */ + this.arrayEquals = function(array1, array2) { + if (array1 === array2) { + return true + } + if (array1.length !== array2.length) { + return false + } + for (let i = 0, length = array1.length; i < length; ++i) { + if (array1[i] !== array2[i]) { + return false } - }); - localStorage.setItem('exchange-lastTx', result.totalTxs); - localStorage.setItem('exchange-nodes', JSON.stringify(nodes)); - nodeURL = nodes; - nodeKBucket = new K_Bucket(floGlobals.adminID, Object.keys(nodeURL)); - nodeList = nodeKBucket.order; - resolve(nodes); - }).catch(error => reject(error)); - }) -} + } + return true + } -function clearAllLocalData() { - localStorage.removeItem('exchange-nodes'); - localStorage.removeItem('exchange-lastTx'); - localStorage.removeItem('exchange-proxy_secret'); - localStorage.removeItem('exchange-user_ID'); - location.reload(); -} \ No newline at end of file + this.ensureInt8('option.localNodeId as parameter 1', this.localNodeId) + this.root = this.createNode() + + /** + * Default arbiter function for contacts with the same id. Uses + * contact.vectorClock to select which contact to update the k-bucket with. + * Contact with larger vectorClock field will be selected. If vectorClock is + * the same, candidat will be selected. + * + * @param {Object} incumbent Contact currently stored in the k-bucket. + * @param {Object} candidate Contact being added to the k-bucket. + * @return {Object} Contact to updated the k-bucket with. + */ + this.arbiter = function(incumbent, candidate) { + return incumbent.vectorClock > candidate.vectorClock ? incumbent : candidate + } + + /** + * Default distance function. Finds the XOR + * distance between firstId and secondId. + * + * @param {Uint8Array} firstId Uint8Array containing first id. + * @param {Uint8Array} secondId Uint8Array containing second id. + * @return {Number} Integer The XOR distance between firstId + * and secondId. + */ + this.distance = function(firstId, secondId) { + let distance = 0 + let i = 0 + const min = Math.min(firstId.length, secondId.length) + const max = Math.max(firstId.length, secondId.length) + for (; i < min; ++i) { + distance = distance * 256 + (firstId[i] ^ secondId[i]) + } + for (; i < max; ++i) distance = distance * 256 + 255 + return distance + } + + /** + * Adds a contact to the k-bucket. + * + * @param {Object} contact the contact object to add + */ + this.add = function(contact) { + this.ensureInt8('contact.id', (contact || {}).id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + // this is not a leaf node but an inner node with 'low' and 'high' + // branches; we will check the appropriate bit of the identifier and + // delegate to the appropriate node for further processing + node = this._determineNode(node, contact.id, bitIndex++) + } + + // check if the contact already exists + const index = this._indexOf(node, contact.id) + if (index >= 0) { + this._update(node, index, contact) + return this + } + + if (node.contacts.length < this.numberOfNodesPerKBucket) { + node.contacts.push(contact) + return this + } + + // the bucket is full + if (node.dontSplit) { + // we are not allowed to split the bucket + // we need to ping the first this.numberOfNodesToPing + // in order to determine if they are alive + // only if one of the pinged nodes does not respond, can the new contact + // be added (this prevents DoS flodding with new invalid contacts) + return this + } + + this._split(node, bitIndex) + return this.add(contact) + } + + /** + * Get the n closest contacts to the provided node id. "Closest" here means: + * closest according to the XOR metric of the contact node id. + * + * @param {Uint8Array} id Contact node id + * @param {Number=} n Integer (Default: Infinity) The maximum number of + * closest contacts to return + * @return {Array} Array Maximum of n closest contacts to the node id + */ + this.closest = function(id, n = Infinity) { + this.ensureInt8('id', id) + + if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { + throw new TypeError('n is not positive number') + } + + let contacts = [] + + for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { + const node = nodes.pop() + if (node.contacts === null) { + const detNode = this._determineNode(node, id, bitIndex++) + nodes.push(node.left === detNode ? node.right : node.left) + nodes.push(detNode) + } else { + contacts = contacts.concat(node.contacts) + } + } + + return contacts + .map(a => [this.distance(a.id, id), a]) + .sort((a, b) => a[0] - b[0]) + .slice(0, n) + .map(a => a[1]) + } + + /** + * Counts the total number of contacts in the tree. + * + * @return {Number} The number of contacts held in the tree + */ + this.count = function() { + // return this.toArray().length + let count = 0 + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) nodes.push(node.right, node.left) + else count += node.contacts.length + } + return count + } + + /** + * Determines whether the id at the bitIndex is 0 or 1. + * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Uint8Array} id Id to compare localNodeId with. + * @param {Number} bitIndex Integer (Default: 0) The bit index to which bit + * to check in the id Uint8Array. + * @return {Object} left leaf if id at bitIndex is 0, right leaf otherwise. + */ + this._determineNode = function(node, id, bitIndex) { + // *NOTE* remember that id is a Uint8Array and has granularity of + // bytes (8 bits), whereas the bitIndex is the bit index (not byte) + + // id's that are too short are put in low bucket (1 byte = 8 bits) + // (bitIndex >> 3) finds how many bytes the bitIndex describes + // bitIndex % 8 checks if we have extra bits beyond byte multiples + // if number of bytes is <= no. of bytes described by bitIndex and there + // are extra bits to consider, this means id has less bits than what + // bitIndex describes, id therefore is too short, and will be put in low + // bucket + const bytesDescribedByBitIndex = bitIndex >> 3 + const bitIndexWithinByte = bitIndex % 8 + if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { + return node.left + } + + const byteUnderConsideration = id[bytesDescribedByBitIndex] + + // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits + // where 255 is 11111111 and 0 is 00000000 + // in order to find out whether the bit at bitIndexWithinByte is set + // we construct (1 << (7 - bitIndexWithinByte)) which will consist + // of all bits being 0, with only one bit set to 1 + // for example, if bitIndexWithinByte is 3, we will construct 00010000 by + // (1 << (7 - 3)) -> (1 << 4) -> 16 + if (byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) { + return node.right + } + + return node.left + } + + /** + * Get a contact by its exact ID. + * If this is a leaf, loop through the bucket contents and return the correct + * contact if we have it or null if not. If this is an inner node, determine + * which branch of the tree to traverse and repeat. + * + * @param {Uint8Array} id The ID of the contact to fetch. + * @return {Object|Null} The contact if available, otherwise null + */ + this.get = function(id) { + this.ensureInt8('id', id) + + let bitIndex = 0 + + let node = this.root + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + // index of uses contact id for matching + const index = this._indexOf(node, id) + return index >= 0 ? node.contacts[index] : null + } + + /** + * Returns the index of the contact with provided + * id if it exists, returns -1 otherwise. + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Uint8Array} id Contact node id. + * @return {Number} Integer Index of contact with provided id if it + * exists, -1 otherwise. + */ + this._indexOf = function(node, id) { + for (let i = 0; i < node.contacts.length; ++i) { + if (this.arrayEquals(node.contacts[i].id, id)) return i + } + + return -1 + } + + /** + * Removes contact with the provided id. + * + * @param {Uint8Array} id The ID of the contact to remove. + * @return {Object} The k-bucket itself. + */ + this.remove = function(id) { + this.ensureInt8('the id as parameter 1', id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + const index = this._indexOf(node, id) + if (index >= 0) { + const contact = node.contacts.splice(index, 1)[0] + } + + return this + } + + /** + * Splits the node, redistributes contacts to the new nodes, and marks the + * node that was split as an inner node of the binary tree of nodes by + * setting this.root.contacts = null + * + * @param {Object} node node for splitting + * @param {Number} bitIndex the bitIndex to which byte to check in the + * Uint8Array for navigating the binary tree + */ + this._split = function(node, bitIndex) { + node.left = this.createNode() + node.right = this.createNode() + + // redistribute existing contacts amongst the two newly created nodes + for (const contact of node.contacts) { + this._determineNode(node, contact.id, bitIndex).contacts.push(contact) + } + + node.contacts = null // mark as inner tree node + + // don't split the "far away" node + // we check where the local node would end up and mark the other one as + // "dontSplit" (i.e. "far away") + const detNode = this._determineNode(node, this.localNodeId, bitIndex) + const otherNode = node.left === detNode ? node.right : node.left + otherNode.dontSplit = true + } + + /** + * Returns all the contacts contained in the tree as an array. + * If this is a leaf, return a copy of the bucket. `slice` is used so that we + * don't accidentally leak an internal reference out that might be + * accidentally misused. If this is not a leaf, return the union of the low + * and high branches (themselves also as arrays). + * + * @return {Array} All of the contacts in the tree, as an array + */ + this.toArray = function() { + let result = [] + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) nodes.push(node.right, node.left) + else result = result.concat(node.contacts) + } + return result + } + + /** + * Updates the contact selected by the arbiter. + * If the selection is our old contact and the candidate is some new contact + * then the new contact is abandoned (not added). + * If the selection is our old contact and the candidate is our old contact + * then we are refreshing the contact and it is marked as most recently + * contacted (by being moved to the right/end of the bucket array). + * If the selection is our new contact, the old contact is removed and the new + * contact is marked as most recently contacted. + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Number} index the index in the bucket where contact exists + * (index has already been computed in a previous + * calculation) + * @param {Object} contact The contact object to update. + */ + this._update = function(node, index, contact) { + // sanity check + if (!this.arrayEquals(node.contacts[index].id, contact.id)) { + throw new Error('wrong index for _update') + } + + const incumbent = node.contacts[index] + const selection = this.arbiter(incumbent, contact) + // if the selection is our old contact and the candidate is some new + // contact, then there is nothing to do + if (selection === incumbent && incumbent !== contact) return + + node.contacts.splice(index, 1) // remove old contact + node.contacts.push(selection) // add more recent contact version + + } + } + + exchangeAPI.K_Bucket = function K_Bucket(masterID, backupList) { + const decodeID = function(floID) { + let k = bitjs.Base58.decode(floID); + k.shift(); + k.splice(-4, 4); + const decodedId = Crypto.util.bytesToHex(k); + const nodeIdBigInt = new BigInteger(decodedId, 16); + const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }; + const _KB = new BuildKBucket({ + localNodeId: decodeID(masterID) + }); + backupList.forEach(id => _KB.add({ + id: decodeID(id), + floID: id + })); + const orderedList = backupList.map(sn => [_KB.distance(decodeID(masterID), decodeID(sn)), sn]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]); + const self = this; + + Object.defineProperty(self, 'order', { + get: () => Array.from(orderedList) + }); + + self.closestNode = function(id, N = 1) { + let decodedId = decodeID(id); + let n = N || orderedList.length; + let cNodes = _KB.closest(decodedId, n) + .map(k => k.floID); + return (N == 1 ? cNodes[0] : cNodes); + }; + + self.isBefore = (source, target) => orderedList.indexOf(target) < orderedList.indexOf(source); + self.isAfter = (source, target) => orderedList.indexOf(target) > orderedList.indexOf(source); + self.isPrev = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) - 1; + self.isNext = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) + 1; + + self.prevNode = function(id, N = 1) { + let n = N || orderedList.length; + if (!orderedList.includes(id)) + throw Error(`${id} is not in KB list`); + let pNodes = orderedList.slice(0, orderedList.indexOf(id)).slice(-n); + return (N == 1 ? pNodes[0] : pNodes); + }; + + self.nextNode = function(id, N = 1) { + let n = N || orderedList.length; + if (!orderedList.includes(id)) + throw Error(`${id} is not in KB list`); + let nNodes = orderedList.slice(orderedList.indexOf(id) + 1).slice(0, n); + return (N == 1 ? nNodes[0] : nNodes); + }; + + } + + const INVALID_SERVER_MSG = "INCORRECT_SERVER_ERROR"; + var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list + + function fetch_api(api, options) { + return new Promise((resolve, reject) => { + let curPos = fetch_api.curPos || 0; + if (curPos >= nodeList.length) + return resolve('No Nodes online'); + let url = "https://" + nodeURL[nodeList[curPos]]; + (options ? fetch(url + api, options) : fetch(url + api)) + .then(result => resolve(result)).catch(error => { + console.warn(nodeList[curPos], 'is offline'); + //try next node + fetch_api.curPos = curPos + 1; + fetch_api(api, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }); + }) + } + + function ResponseError(status, data) { + if (data === INVALID_SERVER_MSG) + location.reload(); + else if (this instanceof ResponseError) { + this.data = data; + this.status = status; + } else + return new ResponseError(status, data); + } + + function responseParse(response, json_ = true) { + return new Promise((resolve, reject) => { + if (!response.ok) + response.text() + .then(result => reject(ResponseError(response.status, result))) + .catch(error => reject(error)); + else if (json_) + response.json() + .then(result => resolve(result)) + .catch(error => reject(error)); + else + response.text() + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getAccount = function(floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "get_account", + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/account', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getBuyList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-buyorders') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getSellList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-sellorders') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getTradeList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-trades') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getRates = function(asset = null) { + return new Promise((resolve, reject) => { + fetch_api('/get-rates' + (asset ? "?asset=" + asset : "")) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getBalance = function(floID = null, token = null) { + return new Promise((resolve, reject) => { + if (!floID && !token) + return reject("Need atleast one argument") + let queryStr = (floID ? "floID=" + floID : "") + + (floID && token ? "&" : "") + + (token ? "token=" + token : ""); + fetch_api('/get-balance?' + queryStr) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + exchangeAPI.getTx = function(txid) { + return new Promise((resolve, reject) => { + if (!txid) + return reject('txid required'); + fetch_api('/get-transaction?txid=' + txid) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + function signRequest(request, signKey) { + if (typeof request !== "object") + throw Error("Request is not an object"); + let req_str = Object.keys(request).sort().map(r => r + ":" + request[r]).join("|"); + return floCrypto.signData(req_str, signKey); + } + + exchangeAPI.getLoginCode = function() { + return new Promise((resolve, reject) => { + fetch_api('/get-login-code') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + /* + exchangeAPI.signUp = function (privKey, code, hash) { + return new Promise((resolve, reject) => { + if (!code || !hash) + return reject("Login Code missing") + let request = { + pubKey: floCrypto.getPubKeyHex(privKey), + floID: floCrypto.getFloID(privKey), + code: code, + hash: hash, + timestamp: Date.now() + }; + request.sign = signRequest({ + type: "create_account", + random: code, + timestamp: request.timestamp + }, privKey); + console.debug(request); + + fetch_api("/signup", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + */ + + exchangeAPI.login = function(privKey, proxyKey, code, hash) { + return new Promise((resolve, reject) => { + if (!code || !hash) + return reject("Login Code missing") + let request = { + proxyKey: proxyKey, + floID: floCrypto.getFloID(privKey), + pubKey: floCrypto.getPubKeyHex(privKey), + timestamp: Date.now(), + code: code, + hash: hash + }; + if (!privKey || !request.floID) + return reject("Invalid Private key"); + request.sign = signRequest({ + type: "login", + random: code, + proxyKey: proxyKey, + timestamp: request.timestamp + }, privKey); + console.debug(request); + + fetch_api("/login", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + exchangeAPI.logout = function(floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "logout", + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api("/logout", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.buy = function(asset, quantity, max_price, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= 0) + return reject(`Invalid quantity (${quantity})`); + else if (typeof max_price !== "number" || max_price <= 0) + return reject(`Invalid max_price (${max_price})`); + let request = { + floID: floID, + asset: asset, + quantity: quantity, + max_price: max_price, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "buy_order", + asset: asset, + quantity: quantity, + max_price: max_price, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/buy', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + + } + + exchangeAPI.sell = function(asset, quantity, min_price, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= 0) + return reject(`Invalid quantity (${quantity})`); + else if (typeof min_price !== "number" || min_price <= 0) + return reject(`Invalid min_price (${min_price})`); + let request = { + floID: floID, + asset: asset, + quantity: quantity, + min_price: min_price, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "sell_order", + quantity: quantity, + asset: asset, + min_price: min_price, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/sell', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + + } + + exchangeAPI.cancelOrder = function(type, id, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (type !== "buy" && type !== "sell") + return reject(`Invalid type (${type}): type should be sell (or) buy`); + let request = { + floID: floID, + orderType: type, + orderID: id, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "cancel_order", + order: type, + id: id, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/cancel', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + //receiver should be object eg {floID1: amount1, floID2: amount2 ...} + exchangeAPI.transferToken = function(receiver, token, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof receiver !== 'object' || receiver === null) + return reject("Invalid receiver: parameter is not an object"); + let invalidIDs = [], + invalidAmt = []; + for (let f in receiver) { + if (!floCrypto.validateAddr(f)) + invalidIDs.push(f); + else if (typeof receiver[f] !== "number" || receiver[f] <= 0) + invalidAmt.push(receiver[f]) + } + if (invalidIDs.length) + return reject(INVALID(`Invalid receiver (${invalidIDs})`)); + else if (invalidAmt.length) + return reject(`Invalid amount (${invalidAmt})`); + let request = { + floID: floID, + token: token, + receiver: receiver, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "transfer_token", + receiver: JSON.stringify(receiver), + token: token, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/transfer-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.depositFLO = function(quantity, floID, sinkID, privKey, proxySecret = null) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= floGlobals.fee) + return reject(`Invalid quantity (${quantity})`); + floBlockchainAPI.sendTx(floID, sinkID, quantity, privKey, 'Deposit FLO in market').then(txid => { + let request = { + floID: floID, + txid: txid, + timestamp: Date.now() + }; + if (!proxySecret) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(privKey); + request.sign = signRequest({ + type: "deposit_flo", + txid: txid, + timestamp: request.timestamp + }, proxySecret || privKey); + console.debug(request); + + fetch_api('/deposit-flo', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + exchangeAPI.withdrawFLO = function(quantity, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + amount: quantity, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "withdraw_flo", + amount: quantity, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/withdraw-flo', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.depositToken = function(token, quantity, floID, sinkID, privKey, proxySecret = null) { + return new Promise((resolve, reject) => { + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key"); + tokenAPI.sendToken(privKey, quantity, sinkID, 'Deposit Rupee in market', token).then(txid => { + let request = { + floID: floID, + txid: txid, + timestamp: Date.now() + }; + if (!proxySecret) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(privKey); + request.sign = signRequest({ + type: "deposit_token", + txid: txid, + timestamp: request.timestamp + }, proxySecret || privKey); + console.debug(request); + + fetch_api('/deposit-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + exchangeAPI.withdrawToken = function(token, quantity, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + token: token, + amount: quantity, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "withdraw_token", + token: token, + amount: quantity, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/withdraw-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.addUserTag = function(tag_user, tag, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + user: tag_user, + tag: tag, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "add_tag", + user: tag_user, + tag: tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/add-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.removeUserTag = function(tag_user, tag, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + user: tag_user, + tag: tag, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "remove_tag", + user: tag_user, + tag: tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/remove-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.init = function refreshDataFromBlockchain(adminID = floGlobals.adminID, appName = floGlobals.application) { + return new Promise((resolve, reject) => { + let nodes, lastTx; + try { + nodes = JSON.parse(localStorage.getItem('exchange-nodes')); + if (typeof nodes !== 'object' || nodes === null) + throw Error('nodes must be an object') + else + lastTx = parseInt(localStorage.getItem('exchange-lastTx')) || 0; + } catch (error) { + nodes = {}; + lastTx = 0; + } + floBlockchainAPI.readData(adminID, { + ignoreOld: lastTx, + sentOnly: true, + pattern: appName + }).then(result => { + result.data.reverse().forEach(data => { + var content = JSON.parse(data)[appName]; + //Node List + if (content.Nodes) { + if (content.Nodes.remove) + for (let n of content.Nodes.remove) + delete nodes[n]; + if (content.Nodes.add) + for (let n in content.Nodes.add) + nodes[n] = content.Nodes.add[n]; + } + }); + localStorage.setItem('exchange-lastTx', result.totalTxs); + localStorage.setItem('exchange-nodes', JSON.stringify(nodes)); + nodeURL = nodes; + nodeKBucket = new K_Bucket(adminID, Object.keys(nodeURL)); + nodeList = nodeKBucket.order; + resolve(nodes); + }).catch(error => reject(error)); + }) + } + + exchangeAPI.clearAllLocalData = function() { + localStorage.removeItem('exchange-nodes'); + localStorage.removeItem('exchange-lastTx'); + localStorage.removeItem('exchange-proxy_secret'); + localStorage.removeItem('exchange-user_ID'); + location.reload(); + } + +})('object' === typeof module ? module.exports : window.exchangeAPI = {}); \ No newline at end of file diff --git a/src/backup/head.js b/src/backup/head.js index e6ada2d..c3dec3b 100644 --- a/src/backup/head.js +++ b/src/backup/head.js @@ -1,6 +1,6 @@ 'use strict'; -const K_Bucket = require('../../docs/scripts/KBucket'); +const K_Bucket = require('../../docs/scripts/exchangeAPI').K_Bucket; const slave = require('./slave'); const sync = require('./sync'); const WebSocket = require('ws');