/* @licstart The following is the entire license notice for the JavaScript code in this page. Copyright (C) 2015, 2016 INRIA The JavaScript code in this page is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (GNU LGPL) as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any later version. The code is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU LGPL for more details. As additional permission under GNU LGPL version 2.1 section 7, you may distribute non-source (e.g., minimized or compacted) forms of that code without the copy of the GNU LGPL normally required by section 4, provided you include this license notice and a URL through which recipients can access the Corresponding Source. @licend The above is the entire license notice for the JavaScript code in this page. @author Marcello Stanisci */ "use strict"; /** * Updated list of exchanges base URLs that we have queried. */ var KNOWN_EXCHANGES = []; /** * The precision of the amount's fraction, when * it comes objectified. */ var FRACTION = 100000000; /** * Default 'start' URL argument used when querying for /history. */ var START = 0; /** * Fixed 'delta' URL argument used when querying for /history. */ var DELTA = -5 /** * Row id of the _oldest_ /history record that's shown on the * screen; will be used as the next 'delta' argument if scrolling * down occurs. */ var LAST = 0; /** * Flag controlling whether the scrolling of the page is enabled * or not; for example, after /track/transfer results are shown * on page, the semantics of "scrolling down towards older records" * disappears because former results aren't arranged regarding of * time. */ var SCROLL = true; /** * Maps error codes from https://git.taler.net/exchange.git/tree/src/include/taler_error_codes.h * to friendlier messages. */ function error_map(code, backup_text){ switch (Number(code)){ /* /history errors. */ case 2200: return "The timestamp used to query the history overflowed."; case 2201: return "Merchant had serious problems in its history database"; case 2202: return "The instance used to query the history is unknown"; /* /track/order errors. */ case 2307: /* Such errors should provide full proof to the user. */ return "One of the coin was untraceable"; case 2308: case 2408: return "Conflicting reports from the Exchange"; case 2301: return "Instance is unknown to track this order"; case 2303: case 2304: case 2305: case 1801: return "Merchant database failed, code: " + code; case 2306: return "One of the coin failed at getting its WTID"; /* /track/transfer errors. */ case 2410: return "Exchange charged a higher wire fee than what " + " it was originally advertised."; case 2405: return "Error from the exchange, no proof received!"; case 2404: case 2402: return "Database failure, could not store results, code: " + code; case 2406: return "Database failure, could not retrieve previous results"; case 2407: return "Database failure, internal logic error"; case 2409: return "Merchant could not pack the JSON response"; case 2403: return "Merchant failed to request /track/transfer to \ the exchange"; case 2400: return "Exchange timed out.."; case 2503: return "Proposal not found."; default: return backup_text ? backup_text : "Error code not given."; } } /** * Convert Taler-compliant amount to human-friendly string. * The amount may be both a string or object type. */ function amount_to_string(amount){ if (typeof amount === 'string' || amount instanceof String) { var split = amount.match(/([A-Z]+):([0-9]+)\.?([0-9]+)?/); if (!split) return "Bad amount given: " + amount; if (split[3]) return `${split[2]}.${split[3]} ${split[1]}`; return `${split[2]}.00 ${split[1]}`; } var number = Number(amount.value) + (Number(amount.fraction)/FRACTION); return `${number.toFixed(2)} ${amount.currency}`; } /** * Close the center-box that shows results from /track/transaction * Note that this function first *empties* the results, and then * closes the box. */ function close_popup(){ var ctx = document.getElementsByClassName("track-content")[0]; var tbody = xpath_get("table/tbody", ctx).snapshotItem(0); var tbody_children = xpath_get("table/tbody/tr", ctx); for(var i=1; i -1) return; var exchange_list = document.getElementById("exchange_url"); var option = document.createElement("option"); option.innerHTML = exchange; option.setAttribute("value", exchange); exchange_list.appendChild(option); KNOWN_EXCHANGES.push(exchange); /* Remove potential information bar. */ var info_bar = document.getElementById("information-bar"); info_bar.style.visibility = "hidden"; toggle_loader(); var qs = `/track/transfer?` + `exchange=${exchange}&` + `wtid=${wtid}&` + `instance=${get_instance()}`; var req = new XMLHttpRequest(); req.open("GET", qs, true); req.onload = function(){ if(4 == req.readyState){ switch(req.status){ case 200: /* Scroll is disabled after showing /track/transfer * results on the page; that's because those results * are not guaranteed to be sorted any how, so there * is little to no point in scrolling towards "older" * results: older than what? The scroll feature will * be re-enabled from the /history callback. */ SCROLL = false; clear_results(); var tracks = JSON.parse(req.responseText); cb(tracks.deposits_sums, tracks.execution_time, wtid); break; default: show_error(req.responseText, true); } } } req.send(); } /** * Fill the information bar on the top of the page with * error messages. The same orange bar is used for warnings. */ var show_warning; var show_error = show_warning = function(response_text){ toggle_loader(); close_popup(); /* Always dump whole error to the console. */ console.log(response_text); var msg; var hint = ""; try{ var parse = JSON.parse(response_text); console.log("Response was at least JSON"); /* handles undefined codes too. */ msg = error_map(parse['code'], response_text); if (parse['hint']) hint = parse['hint']; } catch (e) { msg = response_text; console.log("Must keep raw response"); } /* Get hold of the info bar. */ var info_bar = document.getElementById("information-bar"); info_bar.innerHTML = `

${msg} ${hint}

`; info_bar.style.visibility = "visible"; } /** * Call /track/order API offered by the frontend. It will make * results shown in a centered box that overlays the page. */ var track_order = function(order_id, cb){ /* Remove potential information bar. */ var info_bar = document.getElementById("information-bar"); info_bar.style.visibility = "hidden"; toggle_loader(); var req = new XMLHttpRequest(); var url = `/track/order?` + `order_id=${order_id}&` + `instance=${get_instance()}`; req.open("GET", url, true); req.onload = function(){ if (4 == req.readyState){ if ((200 == req.status) || (202 == req.status)) cb(JSON.parse(req.responseText), req.status); else show_error(req.responseText, true); return; } } req.send(); } /** * Fill the screen-centered box with data from /track/order. * This box will overlay the ordinary page, and disappear when * the user clicks the close button or Esc key. */ function fill_box(tracks, http_code) { toggle_loader(); if (http_code == 404){ alert("No tracks for that order."); return; } if (http_code == 202){ console.log("Pending order."); toggle_loader(); show_warning("This order is still waiting to be paid back"); return; } if(!tracks) show_error("/track/transfer returned EMPTY! Very bad."); console.log("Got invalid JSON"); if(0 == tracks.length || !tracks.length){ console.log(`Got no tracks and status == ${http_code}. ` + `Should not be here.`); return; } for(var i=0; i` + `${subject.substring(0, 20)}...` + `` + `` + `` + `${amount_to_string(entry.amount)}` + `` + `` + `${parse_date(entry.execution_time)}` + ``; table.appendChild(row); toggle_overlay(); } } /** * Helper function which abstracts the hard-to-remember * API offered by document.evaluate(). */ function xpath_get(xpath, ctx){ var ret = document.evaluate (xpath, ctx, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); return ret; } /** * Fill the page with orders rows. */ function fill_table(data, execution_time, wtid_marker){ var table = document.getElementById("history"); var tbody = xpath_get("tbody", table).snapshotItem(0); /* Make table visible, if it's hidden. */ xpath_get("tr[@class='headers']", tbody) .snapshotItem(0).style.visibility = "visible"; table.style.visibility = "visible"; for (var i=0; i ${entry.order_id}`; td_summary.className = "summary"; td_summary.innerHTML = entry.summary || "deposited"; td_amount.innerHTML = amount_to_string( entry.amount || entry.deposit_value); td_date.innerHTML = parse_date (entry.timestamp || execution_time); row.appendChild(td_order_id); row.appendChild(td_summary); row.appendChild(td_amount); row.appendChild(td_date); row_summary.appendChild(td_summary); tbody.appendChild(row); tbody.appendChild(row_summary); } if (wtid_marker){ // close popup showing wire transfer information close_popup(); // draw a line at the bottom, mentioning the WTID. var marker = make_marker(wtid_marker); tbody.appendChild(marker); } toggle_loader(); } /** * Make the spinning wheel appear/disappear. */ function toggle_loader(){ var loader = document.getElementsByClassName("loader")[0]; if (loader.style.visibility != "hidden") { loader.style.visibility = "hidden"; console.log("toggle_loader: visible>hidden"); } else { loader.style.visibility = "visible"; console.log("toggle_loader: hidden>visible"); } } /** * Issue a /track/order (/track/transfer) depending on * whether the user selected "order" ("transfer") on the * dedicated form. */ function track_cherry_pick(){ var form = document.getElementById("choices"); var types = xpath_get("input[@type='radio']", form); for(var i in [0, 1]){ if (!types.snapshotItem(i).checked) continue; var type = types.snapshotItem(i).value; if ("order" == type){ var order_id = xpath_get("input[@class='order']", form) .snapshotItem(0); track_order(order_id.value, fill_box); } else{ var data = xpath_get("input[@class='transfer']", form); var wtid = data.snapshotItem(0); var exchange = data.snapshotItem(1); track_transfer(exchange.value, wtid.value, fill_table); } } } /** * Make the /track/order form visible. */ function cherry_pick_form_order(form){ var input_order = xpath_get("input[@class='order']", form) .snapshotItem(0); input_order.style.display = ""; var input_transfer = xpath_get("input[@class='transfer']", form); for(var i in [0, 1]) input_transfer.snapshotItem(i).style.display = "none"; } /** * Make the /track/transaction form visible. */ function cherry_pick_form_transfer(form){ var input_order = xpath_get("input[@class='order']", form) .snapshotItem(0); input_order.style.display = "none"; var input_transfer = xpath_get("input[@class='transfer']", form); for(var i in [0, 1]) input_transfer.snapshotItem(i).style.display = ""; } /** * Retrieve the selected instance. */ function get_instance(){ var select = document.getElementById("instance"); return select.value; } /** * Remove tracks from the main page table, but * do NOT remove the table headers; it hides them though. */ var clear_results = function(){ var table = document.getElementById("history"); var tbody = xpath_get("tbody", table).snapshotItem(0); var tbody_children = xpath_get("tbody/*[position() > 1]", table); for(var i=0; i= document.body.offsetHeight) && SCROLL) get_history(true, fill_table); }); /** * Close the centered box that (typically) shows /track/transfer * results. */ document.onkeydown = function(e) { if(!e) e = event; if(e.keyCode == 27){ close_popup(); } } /** * export functions to be tested by AVA. */ if (typeof(module) != "undefined"){ module.exports.track_transfer = track_transfer; module.exports.track_order = track_order; clear_results = ()=>true; } var VALID_TT_INPUTS = 0; function validate_exchange_url(text_input) { var submit_button = document.getElementById ("submit-cherry-pick"); if (text_input.value.match (/^https:\/\/[a-zA-Z]+\.[a-zA-Z]+\//)) { console.log("Good value", text_input.value); VALID_TT_INPUTS |= 2; } else { console.log("Bad value", text_input.value); VALID_TT_INPUTS &= ~2; } if (3 == VALID_TT_INPUTS) submit_button.disabled = false; else submit_button.disabled = true; } function validate_wtid(text_input) { var submit_button = document.getElementById ("submit-cherry-pick"); if (text_input.value.match(/^[A-Za-z0-9]+$/)) { console.log("Good value", text_input.value); VALID_TT_INPUTS |= 1; } else { console.log("Bad value", text_input.value); VALID_TT_INPUTS &= ~1; } if (3 == VALID_TT_INPUTS) submit_button.disabled = false; else submit_button.disabled = true; } function validate_order_id(text_input) { var submit_button = document.getElementById ("submit-cherry-pick"); if (text_input.value.match(/^[A-Za-z0-9]+$/)) { console.log("Good value", text_input.value); submit_button.disabled = false; } else { console.log("Bad value", text_input.value); submit_button.disabled = true; } }