diff --git a/basicswap/static/js/dropdown.js b/basicswap/static/js/dropdown.js
new file mode 100644
index 0000000..6fdf12a
--- /dev/null
+++ b/basicswap/static/js/dropdown.js
@@ -0,0 +1,190 @@
+(function(window) {
+ 'use strict';
+
+ function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
+ targetEl.style.visibility = 'hidden';
+ targetEl.style.display = 'block';
+
+ const triggerRect = triggerEl.getBoundingClientRect();
+ const targetRect = targetEl.getBoundingClientRect();
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+
+ let top, left;
+
+ top = triggerRect.bottom + offsetDistance;
+ left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
+
+ switch (placement) {
+ case 'bottom-start':
+ left = triggerRect.left;
+ break;
+ case 'bottom-end':
+ left = triggerRect.right - targetRect.width;
+ break;
+ }
+
+ const viewport = {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+
+ if (left < 10) left = 10;
+ if (left + targetRect.width > viewport.width - 10) {
+ left = viewport.width - targetRect.width - 10;
+ }
+
+ targetEl.style.position = 'fixed';
+ targetEl.style.top = `${Math.round(top)}px`;
+ targetEl.style.left = `${Math.round(left)}px`;
+ targetEl.style.margin = '0';
+ targetEl.style.maxHeight = `${viewport.height - top - 10}px`;
+ targetEl.style.overflow = 'auto';
+ targetEl.style.visibility = 'visible';
+ }
+
+ class Dropdown {
+ constructor(targetEl, triggerEl, options = {}) {
+ this._targetEl = targetEl;
+ this._triggerEl = triggerEl;
+ this._options = {
+ placement: options.placement || 'bottom',
+ offset: options.offset || 5,
+ onShow: options.onShow || function() {},
+ onHide: options.onHide || function() {}
+ };
+ this._visible = false;
+ this._initialized = false;
+ this._handleScroll = this._handleScroll.bind(this);
+ this._handleResize = this._handleResize.bind(this);
+ this._handleOutsideClick = this._handleOutsideClick.bind(this);
+ this.init();
+ }
+
+ init() {
+ if (!this._initialized) {
+ this._targetEl.style.margin = '0';
+ this._targetEl.style.display = 'none';
+ this._targetEl.style.position = 'fixed';
+ this._targetEl.style.zIndex = '50';
+
+ this._setupEventListeners();
+ this._initialized = true;
+ }
+ }
+
+ _setupEventListeners() {
+ this._triggerEl.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.toggle();
+ });
+
+ document.addEventListener('click', this._handleOutsideClick);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') this.hide();
+ });
+ window.addEventListener('scroll', this._handleScroll, true);
+ window.addEventListener('resize', this._handleResize);
+ }
+
+ _handleScroll() {
+ if (this._visible) {
+ requestAnimationFrame(() => {
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+ });
+ }
+ }
+
+ _handleResize() {
+ if (this._visible) {
+ requestAnimationFrame(() => {
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+ });
+ }
+ }
+
+ _handleOutsideClick(e) {
+ if (this._visible &&
+ !this._targetEl.contains(e.target) &&
+ !this._triggerEl.contains(e.target)) {
+ this.hide();
+ }
+ }
+
+ show() {
+ if (!this._visible) {
+ this._targetEl.style.display = 'block';
+ this._targetEl.style.visibility = 'hidden';
+
+ requestAnimationFrame(() => {
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+
+ this._visible = true;
+ this._options.onShow();
+ });
+ }
+ }
+
+ hide() {
+ if (this._visible) {
+ this._targetEl.style.display = 'none';
+ this._visible = false;
+ this._options.onHide();
+ }
+ }
+
+ toggle() {
+ if (this._visible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ destroy() {
+ document.removeEventListener('click', this._handleOutsideClick);
+ window.removeEventListener('scroll', this._handleScroll, true);
+ window.removeEventListener('resize', this._handleResize);
+ this._initialized = false;
+ }
+ }
+
+ function initDropdowns() {
+ document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
+ const targetId = triggerEl.getAttribute('data-dropdown-toggle');
+ const targetEl = document.getElementById(targetId);
+
+ if (targetEl) {
+ const placement = triggerEl.getAttribute('data-dropdown-placement');
+ new Dropdown(targetEl, triggerEl, {
+ placement: placement || 'bottom-start'
+ });
+ }
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initDropdowns);
+ } else {
+ initDropdowns();
+ }
+
+ window.Dropdown = Dropdown;
+ window.initDropdowns = initDropdowns;
+
+})(window);
diff --git a/basicswap/static/js/libs/flowbite.js b/basicswap/static/js/libs/flowbite.js
deleted file mode 100644
index e2c52c2..0000000
--- a/basicswap/static/js/libs/flowbite.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Flowbite",[],e):"object"==typeof exports?exports.Flowbite=e():t.Flowbite=e()}(self,(function(){return function(){"use strict";var t={647:function(t,e,i){i.r(e)},853:function(t,e,i){i.r(e),i.d(e,{afterMain:function(){return w},afterRead:function(){return y},afterWrite:function(){return O},applyStyles:function(){return P},arrow:function(){return Q},auto:function(){return a},basePlacements:function(){return c},beforeMain:function(){return b},beforeRead:function(){return _},beforeWrite:function(){return L},bottom:function(){return o},clippingParents:function(){return u},computeStyles:function(){return it},createPopper:function(){return Pt},createPopperBase:function(){return Ht},createPopperLite:function(){return St},detectOverflow:function(){return mt},end:function(){return l},eventListeners:function(){return ot},flip:function(){return yt},hide:function(){return wt},left:function(){return s},main:function(){return E},modifierPhases:function(){return k},offset:function(){return Lt},placements:function(){return g},popper:function(){return h},popperGenerator:function(){return Tt},popperOffsets:function(){return It},preventOverflow:function(){return Ot},read:function(){return m},reference:function(){return f},right:function(){return r},start:function(){return d},top:function(){return n},variationPlacements:function(){return v},viewport:function(){return p},write:function(){return I}});var n="top",o="bottom",r="right",s="left",a="auto",c=[n,o,r,s],d="start",l="end",u="clippingParents",p="viewport",h="popper",f="reference",v=c.reduce((function(t,e){return t.concat([e+"-"+d,e+"-"+l])}),[]),g=[].concat(c,[a]).reduce((function(t,e){return t.concat([e,e+"-"+d,e+"-"+l])}),[]),_="beforeRead",m="read",y="afterRead",b="beforeMain",E="main",w="afterMain",L="beforeWrite",I="write",O="afterWrite",k=[_,m,y,b,E,w,L,I,O];function x(t){return t?(t.nodeName||"").toLowerCase():null}function A(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function C(t){return t instanceof A(t).Element||t instanceof Element}function T(t){return t instanceof A(t).HTMLElement||t instanceof HTMLElement}function H(t){return"undefined"!=typeof ShadowRoot&&(t instanceof A(t).ShadowRoot||t instanceof ShadowRoot)}var P={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},o=e.elements[t];T(o)&&x(o)&&(Object.assign(o.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?o.removeAttribute(t):o.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],o=e.attributes[t]||{},r=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});T(n)&&x(n)&&(Object.assign(n.style,r),Object.keys(o).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function S(t){return t.split("-")[0]}var j=Math.max,D=Math.min,z=Math.round;function M(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function q(){return!/^((?!chrome|android).)*safari/i.test(M())}function V(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),o=1,r=1;e&&T(t)&&(o=t.offsetWidth>0&&z(n.width)/t.offsetWidth||1,r=t.offsetHeight>0&&z(n.height)/t.offsetHeight||1);var s=(C(t)?A(t):window).visualViewport,a=!q()&&i,c=(n.left+(a&&s?s.offsetLeft:0))/o,d=(n.top+(a&&s?s.offsetTop:0))/r,l=n.width/o,u=n.height/r;return{width:l,height:u,top:d,right:c+l,bottom:d+u,left:c,x:c,y:d}}function B(t){var e=V(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function R(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&H(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return A(t).getComputedStyle(t)}function F(t){return["table","td","th"].indexOf(x(t))>=0}function K(t){return((C(t)?t.ownerDocument:t.document)||window.document).documentElement}function N(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(H(t)?t.host:null)||K(t)}function U(t){return T(t)&&"fixed"!==W(t).position?t.offsetParent:null}function X(t){for(var e=A(t),i=U(t);i&&F(i)&&"static"===W(i).position;)i=U(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(M());if(/Trident/i.test(M())&&T(t)&&"fixed"===W(t).position)return null;var i=N(t);for(H(i)&&(i=i.host);T(i)&&["html","body"].indexOf(x(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Y(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function G(t,e,i){return j(t,D(e,i))}function $(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function J(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Q={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,d=t.options,l=i.elements.arrow,u=i.modifiersData.popperOffsets,p=S(i.placement),h=Y(p),f=[s,r].indexOf(p)>=0?"height":"width";if(l&&u){var v=function(t,e){return $("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:J(t,c))}(d.padding,i),g=B(l),_="y"===h?n:s,m="y"===h?o:r,y=i.rects.reference[f]+i.rects.reference[h]-u[h]-i.rects.popper[f],b=u[h]-i.rects.reference[h],E=X(l),w=E?"y"===h?E.clientHeight||0:E.clientWidth||0:0,L=y/2-b/2,I=v[_],O=w-g[f]-v[m],k=w/2-g[f]/2+L,x=G(I,k,O),A=h;i.modifiersData[a]=((e={})[A]=x,e.centerOffset=x-k,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&R(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,c=t.placement,d=t.variation,u=t.offsets,p=t.position,h=t.gpuAcceleration,f=t.adaptive,v=t.roundOffsets,g=t.isFixed,_=u.x,m=void 0===_?0:_,y=u.y,b=void 0===y?0:y,E="function"==typeof v?v({x:m,y:b}):{x:m,y:b};m=E.x,b=E.y;var w=u.hasOwnProperty("x"),L=u.hasOwnProperty("y"),I=s,O=n,k=window;if(f){var x=X(i),C="clientHeight",T="clientWidth";if(x===A(i)&&"static"!==W(x=K(i)).position&&"absolute"===p&&(C="scrollHeight",T="scrollWidth"),c===n||(c===s||c===r)&&d===l)O=o,b-=(g&&x===k&&k.visualViewport?k.visualViewport.height:x[C])-a.height,b*=h?1:-1;if(c===s||(c===n||c===o)&&d===l)I=r,m-=(g&&x===k&&k.visualViewport?k.visualViewport.width:x[T])-a.width,m*=h?1:-1}var H,P=Object.assign({position:p},f&&tt),S=!0===v?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:z(e*n)/n||0,y:z(i*n)/n||0}}({x:m,y:b}):{x:m,y:b};return m=S.x,b=S.y,h?Object.assign({},P,((H={})[O]=L?"0":"",H[I]=w?"0":"",H.transform=(k.devicePixelRatio||1)<=1?"translate("+m+"px, "+b+"px)":"translate3d("+m+"px, "+b+"px, 0)",H)):Object.assign({},P,((e={})[O]=L?b+"px":"",e[I]=w?m+"px":"",e.transform="",e))}var it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,o=void 0===n||n,r=i.adaptive,s=void 0===r||r,a=i.roundOffsets,c=void 0===a||a,d={placement:S(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},d,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:c})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},d,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},nt={passive:!0};var ot={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,o=n.scroll,r=void 0===o||o,s=n.resize,a=void 0===s||s,c=A(e.elements.popper),d=[].concat(e.scrollParents.reference,e.scrollParents.popper);return r&&d.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&c.addEventListener("resize",i.update,nt),function(){r&&d.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&c.removeEventListener("resize",i.update,nt)}},data:{}},rt={left:"right",right:"left",bottom:"top",top:"bottom"};function st(t){return t.replace(/left|right|bottom|top/g,(function(t){return rt[t]}))}var at={start:"end",end:"start"};function ct(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function dt(t){var e=A(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function lt(t){return V(K(t)).left+dt(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+o+n)}function pt(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:T(t)&&ut(t)?t:pt(N(t))}function ht(t,e){var i;void 0===e&&(e=[]);var n=pt(t),o=n===(null==(i=t.ownerDocument)?void 0:i.body),r=A(n),s=o?[r].concat(r.visualViewport||[],ut(n)?n:[]):n,a=e.concat(s);return o?a:a.concat(ht(N(s)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function vt(t,e,i){return e===p?ft(function(t,e){var i=A(t),n=K(t),o=i.visualViewport,r=n.clientWidth,s=n.clientHeight,a=0,c=0;if(o){r=o.width,s=o.height;var d=q();(d||!d&&"fixed"===e)&&(a=o.offsetLeft,c=o.offsetTop)}return{width:r,height:s,x:a+lt(t),y:c}}(t,i)):C(e)?function(t,e){var i=V(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=K(t),n=dt(t),o=null==(e=t.ownerDocument)?void 0:e.body,r=j(i.scrollWidth,i.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=j(i.scrollHeight,i.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),a=-n.scrollLeft+lt(t),c=-n.scrollTop;return"rtl"===W(o||i).direction&&(a+=j(i.clientWidth,o?o.clientWidth:0)-r),{width:r,height:s,x:a,y:c}}(K(t)))}function gt(t,e,i,n){var o="clippingParents"===e?function(t){var e=ht(N(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&T(t)?X(t):t;return C(i)?e.filter((function(t){return C(t)&&R(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),r=[].concat(o,[i]),s=r[0],a=r.reduce((function(e,i){var o=vt(t,i,n);return e.top=j(o.top,e.top),e.right=D(o.right,e.right),e.bottom=D(o.bottom,e.bottom),e.left=j(o.left,e.left),e}),vt(t,s,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function _t(t){var e,i=t.reference,a=t.element,c=t.placement,u=c?S(c):null,p=c?Z(c):null,h=i.x+i.width/2-a.width/2,f=i.y+i.height/2-a.height/2;switch(u){case n:e={x:h,y:i.y-a.height};break;case o:e={x:h,y:i.y+i.height};break;case r:e={x:i.x+i.width,y:f};break;case s:e={x:i.x-a.width,y:f};break;default:e={x:i.x,y:i.y}}var v=u?Y(u):null;if(null!=v){var g="y"===v?"height":"width";switch(p){case d:e[v]=e[v]-(i[g]/2-a[g]/2);break;case l:e[v]=e[v]+(i[g]/2-a[g]/2)}}return e}function mt(t,e){void 0===e&&(e={});var i=e,s=i.placement,a=void 0===s?t.placement:s,d=i.strategy,l=void 0===d?t.strategy:d,v=i.boundary,g=void 0===v?u:v,_=i.rootBoundary,m=void 0===_?p:_,y=i.elementContext,b=void 0===y?h:y,E=i.altBoundary,w=void 0!==E&&E,L=i.padding,I=void 0===L?0:L,O=$("number"!=typeof I?I:J(I,c)),k=b===h?f:h,x=t.rects.popper,A=t.elements[w?k:b],T=gt(C(A)?A:A.contextElement||K(t.elements.popper),g,m,l),H=V(t.elements.reference),P=_t({reference:H,element:x,strategy:"absolute",placement:a}),S=ft(Object.assign({},x,P)),j=b===h?S:H,D={top:T.top-j.top+O.top,bottom:j.bottom-T.bottom+O.bottom,left:T.left-j.left+O.left,right:j.right-T.right+O.right},z=t.modifiersData.offset;if(b===h&&z){var M=z[a];Object.keys(D).forEach((function(t){var e=[r,o].indexOf(t)>=0?1:-1,i=[n,o].indexOf(t)>=0?"y":"x";D[t]+=M[i]*e}))}return D}var yt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,l=t.name;if(!e.modifiersData[l]._skip){for(var u=i.mainAxis,p=void 0===u||u,h=i.altAxis,f=void 0===h||h,_=i.fallbackPlacements,m=i.padding,y=i.boundary,b=i.rootBoundary,E=i.altBoundary,w=i.flipVariations,L=void 0===w||w,I=i.allowedAutoPlacements,O=e.options.placement,k=S(O),x=_||(k===O||!L?[st(O)]:function(t){if(S(t)===a)return[];var e=st(t);return[ct(t),e,ct(e)]}(O)),A=[O].concat(x).reduce((function(t,i){return t.concat(S(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,o=i.boundary,r=i.rootBoundary,s=i.padding,a=i.flipVariations,d=i.allowedAutoPlacements,l=void 0===d?g:d,u=Z(n),p=u?a?v:v.filter((function(t){return Z(t)===u})):c,h=p.filter((function(t){return l.indexOf(t)>=0}));0===h.length&&(h=p);var f=h.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:o,rootBoundary:r,padding:s})[S(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}(e,{placement:i,boundary:y,rootBoundary:b,padding:m,flipVariations:L,allowedAutoPlacements:I}):i)}),[]),C=e.rects.reference,T=e.rects.popper,H=new Map,P=!0,j=A[0],D=0;D=0,B=V?"width":"height",R=mt(e,{placement:z,boundary:y,rootBoundary:b,altBoundary:E,padding:m}),W=V?q?r:s:q?o:n;C[B]>T[B]&&(W=st(W));var F=st(W),K=[];if(p&&K.push(R[M]<=0),f&&K.push(R[W]<=0,R[F]<=0),K.every((function(t){return t}))){j=z,P=!1;break}H.set(z,K)}if(P)for(var N=function(t){var e=A.find((function(e){var i=H.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return j=e,"break"},U=L?3:1;U>0;U--){if("break"===N(U))break}e.placement!==j&&(e.modifiersData[l]._skip=!0,e.placement=j,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function bt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Et(t){return[n,r,o,s].some((function(e){return t[e]>=0}))}var wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,o=e.rects.popper,r=e.modifiersData.preventOverflow,s=mt(e,{elementContext:"reference"}),a=mt(e,{altBoundary:!0}),c=bt(s,n),d=bt(a,o,r),l=Et(c),u=Et(d);e.modifiersData[i]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:l,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":l,"data-popper-escaped":u})}};var Lt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,o=t.name,a=i.offset,c=void 0===a?[0,0]:a,d=g.reduce((function(t,i){return t[i]=function(t,e,i){var o=S(t),a=[s,n].indexOf(o)>=0?-1:1,c="function"==typeof i?i(Object.assign({},e,{placement:t})):i,d=c[0],l=c[1];return d=d||0,l=(l||0)*a,[s,r].indexOf(o)>=0?{x:l,y:d}:{x:d,y:l}}(i,e.rects,c),t}),{}),l=d[e.placement],u=l.x,p=l.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=p),e.modifiersData[o]=d}};var It={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=_t({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var Ot={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,c=i.mainAxis,l=void 0===c||c,u=i.altAxis,p=void 0!==u&&u,h=i.boundary,f=i.rootBoundary,v=i.altBoundary,g=i.padding,_=i.tether,m=void 0===_||_,y=i.tetherOffset,b=void 0===y?0:y,E=mt(e,{boundary:h,rootBoundary:f,padding:g,altBoundary:v}),w=S(e.placement),L=Z(e.placement),I=!L,O=Y(w),k="x"===O?"y":"x",x=e.modifiersData.popperOffsets,A=e.rects.reference,C=e.rects.popper,T="function"==typeof b?b(Object.assign({},e.rects,{placement:e.placement})):b,H="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),P=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,z={x:0,y:0};if(x){if(l){var M,q="y"===O?n:s,V="y"===O?o:r,R="y"===O?"height":"width",W=x[O],F=W+E[q],K=W-E[V],N=m?-C[R]/2:0,U=L===d?A[R]:C[R],$=L===d?-C[R]:-A[R],J=e.elements.arrow,Q=m&&J?B(J):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[q],it=tt[V],nt=G(0,A[R],Q[R]),ot=I?A[R]/2-N-nt-et-H.mainAxis:U-nt-et-H.mainAxis,rt=I?-A[R]/2+N+nt+it+H.mainAxis:$+nt+it+H.mainAxis,st=e.elements.arrow&&X(e.elements.arrow),at=st?"y"===O?st.clientTop||0:st.clientLeft||0:0,ct=null!=(M=null==P?void 0:P[O])?M:0,dt=W+rt-ct,lt=G(m?D(F,W+ot-ct-at):F,W,m?j(K,dt):K);x[O]=lt,z[O]=lt-W}if(p){var ut,pt="x"===O?n:s,ht="x"===O?o:r,ft=x[k],vt="y"===k?"height":"width",gt=ft+E[pt],_t=ft-E[ht],yt=-1!==[n,s].indexOf(w),bt=null!=(ut=null==P?void 0:P[k])?ut:0,Et=yt?gt:ft-A[vt]-C[vt]-bt+H.altAxis,wt=yt?ft+A[vt]+C[vt]-bt-H.altAxis:_t,Lt=m&&yt?function(t,e,i){var n=G(t,e,i);return n>i?i:n}(Et,ft,wt):G(m?Et:gt,ft,m?wt:_t);x[k]=Lt,z[k]=Lt-ft}e.modifiersData[a]=z}},requiresIfExists:["offset"]};function kt(t,e,i){void 0===i&&(i=!1);var n,o,r=T(e),s=T(e)&&function(t){var e=t.getBoundingClientRect(),i=z(e.width)/t.offsetWidth||1,n=z(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=K(e),c=V(t,s,i),d={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(r||!r&&!i)&&(("body"!==x(e)||ut(a))&&(d=(n=e)!==A(n)&&T(n)?{scrollLeft:(o=n).scrollLeft,scrollTop:o.scrollTop}:dt(n)),T(e)?((l=V(e,!0)).x+=e.clientLeft,l.y+=e.clientTop):a&&(l.x=lt(a))),{x:c.left+d.scrollLeft-l.x,y:c.top+d.scrollTop-l.y,width:c.width,height:c.height}}function xt(t){var e=new Map,i=new Set,n=[];function o(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&o(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||o(t)})),n}var At={placement:"bottom",modifiers:[],strategy:"absolute"};function Ct(){for(var t=arguments.length,e=new Array(t),i=0;it._options.maxValue&&(i.value=t._options.maxValue.toString()),null!==t._options.minValue&&parseInt(i.value)=this._options.maxValue||(this._targetEl.value=(this.getCurrentValue()+1).toString(),this._options.onIncrement(this))},t.prototype.decrement=function(){null!==this._options.minValue&&this.getCurrentValue()<=this._options.minValue||(this._targetEl.value=(this.getCurrentValue()-1).toString(),this._options.onDecrement(this))},t.prototype.updateOnIncrement=function(t){this._options.onIncrement=t},t.prototype.updateOnDecrement=function(t){this._options.onDecrement=t},t}();function c(){document.querySelectorAll("[data-input-counter]").forEach((function(t){var e=t.id,i=document.querySelector('[data-input-counter-increment="'+e+'"]'),n=document.querySelector('[data-input-counter-decrement="'+e+'"]'),r=t.getAttribute("data-input-counter-min"),s=t.getAttribute("data-input-counter-max");t?o.default.instanceExists("InputCounter",t.getAttribute("id"))||new a(t,i||null,n||null,{minValue:r?parseInt(r):null,maxValue:s?parseInt(s):null}):console.error('The target element with id "'.concat(e,'" does not exist. Please check the data-input-counter attribute.'))}))}e.initInputCounters=c,"undefined"!=typeof window&&(window.InputCounter=a,window.initInputCounters=c),e.default=a},16:function(t,e,i){var n=this&&this.__assign||function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i {
+ this.priceUpdatePaused = false;
+ if (!this.isConnected()) {
+ this.connect();
+ }
+ this.startHealthCheck();
+ }, 1000);
},
startHealthCheck() {
@@ -217,7 +224,6 @@ const WebSocketManager = {
performHealthCheck() {
if (!this.isConnected()) {
- console.warn('Health check: Connection lost, attempting reconnect');
this.handleReconnect();
return;
}
@@ -225,13 +231,11 @@ const WebSocketManager = {
const now = Date.now();
const lastCheck = this.connectionState.lastHealthCheck;
if (lastCheck && (now - lastCheck) > 60000) {
- console.warn('Health check: Connection stale, refreshing');
this.handleReconnect();
return;
}
this.connectionState.lastHealthCheck = now;
- console.log('Health check passed');
},
connect() {
@@ -248,7 +252,6 @@ const WebSocketManager = {
const wsPort = config.port || window.ws_port || '11700';
if (!wsPort) {
- console.error('WebSocket port not configured');
this.connectionState.isConnecting = false;
return false;
}
@@ -258,7 +261,6 @@ const WebSocketManager = {
this.connectionState.connectTimeout = setTimeout(() => {
if (this.connectionState.isConnecting) {
- console.log('⏳ Connection attempt timed out');
this.cleanup();
this.handleReconnect();
}
@@ -266,7 +268,6 @@ const WebSocketManager = {
return true;
} catch (error) {
- console.error('Error creating WebSocket:', error);
this.connectionState.isConnecting = false;
this.handleReconnect();
return false;
@@ -277,7 +278,6 @@ const WebSocketManager = {
if (!this.ws) return;
this.handlers.open = () => {
- console.log('🟢 WebSocket connected successfully');
this.connectionState.isConnecting = false;
this.reconnectAttempts = 0;
clearTimeout(this.connectionState.connectTimeout);
@@ -291,18 +291,15 @@ const WebSocketManager = {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
- console.error('Error processing WebSocket message:', error);
updateConnectionStatus('error');
}
};
this.handlers.error = (error) => {
- console.error('WebSocket error:', error);
updateConnectionStatus('error');
};
this.handlers.close = (event) => {
- console.log('🔴 WebSocket closed:', event.code, event.reason);
this.connectionState.isConnecting = false;
window.ws = null;
updateConnectionStatus('disconnected');
@@ -320,7 +317,6 @@ const WebSocketManager = {
handleMessage(message) {
if (this.messageQueue.length >= this.maxQueueSize) {
- console.warn('Message queue full, dropping oldest message');
this.messageQueue.shift();
}
@@ -359,7 +355,6 @@ const WebSocketManager = {
this.messageQueue = [];
} catch (error) {
- console.error('Error processing message queue:', error);
} finally {
this.processingQueue = false;
}
@@ -372,8 +367,6 @@ const WebSocketManager = {
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
-
const delay = Math.min(
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
30000
@@ -385,9 +378,7 @@ const WebSocketManager = {
}
}, delay);
} else {
- console.error('Max reconnection attempts reached');
updateConnectionStatus('error');
-
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
@@ -396,8 +387,6 @@ const WebSocketManager = {
},
cleanup() {
- console.log('Cleaning up WebSocket resources');
-
clearTimeout(this.debounceTimeout);
clearTimeout(this.reconnectTimeout);
clearTimeout(this.connectionState.connectTimeout);
@@ -862,7 +851,7 @@ function continueInitialization() {
//console.log('Initialization completed');
}
-function initializeFlowbiteTooltips() {
+function initializeTooltips() {
if (typeof Tooltip === 'undefined') {
console.warn('Tooltip is not defined. Make sure the required library is loaded.');
return;
@@ -1074,6 +1063,11 @@ function getEmptyPriceData() {
}
async function fetchLatestPrices() {
+ if (WebSocketManager.isPageHidden || WebSocketManager.priceUpdatePaused) {
+ const cachedData = CacheManager.get('prices_coingecko');
+ return cachedData?.value || getEmptyPriceData();
+ }
+
const PRICES_CACHE_KEY = 'prices_coingecko';
const minRequestInterval = 60000;
const currentTime = Date.now();
@@ -1081,7 +1075,6 @@ async function fetchLatestPrices() {
if (!window.isManualRefresh) {
const lastRequestTime = window.lastPriceRequest || 0;
if (currentTime - lastRequestTime < minRequestInterval) {
- console.log('Request too soon, using cache');
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
return cachedData.value;
@@ -1089,10 +1082,10 @@ async function fetchLatestPrices() {
}
}
window.lastPriceRequest = currentTime;
+
if (!window.isManualRefresh) {
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData && cachedData.remainingTime > 60000) {
- console.log('Using cached price data');
latestPrices = cachedData.value;
Object.entries(cachedData.value).forEach(([coin, prices]) => {
if (prices.usd) {
@@ -1102,11 +1095,12 @@ async function fetchLatestPrices() {
return cachedData.value;
}
}
+
try {
- console.log('Initiating fresh price data fetch...');
const existingCache = CacheManager.get(PRICES_CACHE_KEY, true);
const fallbackData = existingCache ? existingCache.value : null;
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
+
const response = await fetch('/json/readurl', {
method: 'POST',
headers: {
@@ -1121,19 +1115,21 @@ async function fetchLatestPrices() {
}
})
});
+
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
}
+
const data = await response.json();
+
if (data.Error) {
if (fallbackData) {
- console.log('Using fallback data due to API error');
return fallbackData;
}
throw new Error(data.Error);
}
+
if (data && Object.keys(data).length > 0) {
- console.log('Processing fresh price data');
latestPrices = data;
CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
@@ -1144,10 +1140,11 @@ async function fetchLatestPrices() {
});
return data;
}
+
if (fallbackData) {
- console.log('Using fallback data due to empty response');
return fallbackData;
}
+
const fallbackPrices = {};
Object.keys(getEmptyPriceData()).forEach(coin => {
const fallbackValue = tableRateModule.getFallbackValue(coin);
@@ -1155,14 +1152,13 @@ async function fetchLatestPrices() {
fallbackPrices[coin] = { usd: fallbackValue, btc: null };
}
});
+
if (Object.keys(fallbackPrices).length > 0) {
return fallbackPrices;
}
- console.warn('No price data received');
+
return null;
} catch (error) {
- console.error('Price Fetch Error:', error);
-
const fallbackPrices = {};
Object.keys(getEmptyPriceData()).forEach(coin => {
const fallbackValue = tableRateModule.getFallbackValue(coin);
@@ -1556,7 +1552,7 @@ async function updateOffersTable() {
offersBody.appendChild(fragment);
requestAnimationFrame(() => {
- initializeFlowbiteTooltips();
+ initializeTooltips();
updateRowTimes();
updatePaginationControls(totalPages);
@@ -2014,14 +2010,12 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
Grey: Less than 5 minutes left or expired
-
@@ -2030,28 +2024,24 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
${isRevoked ? 'Offer Revoked' : (offer.is_own_offer ? 'Edit Offer' : `Buy ${coinFrom}`)}
-
${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)}
diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js
index 7ff1628..96534e7 100644
--- a/basicswap/static/js/pricechart.js
+++ b/basicswap/static/js/pricechart.js
@@ -35,7 +35,19 @@ const config = {
sixMonths: { days: 180, interval: 'daily' },
day: { days: 1, interval: 'hourly' }
},
- currentResolution: 'year'
+ currentResolution: 'year',
+ requestTimeout: 60000, // 60 sec
+ retryDelays: [5000, 15000, 30000],
+ rateLimits: {
+ coingecko: {
+ requestsPerMinute: 50,
+ minInterval: 1200 // 1.2 sec
+ },
+ cryptocompare: {
+ requestsPerMinute: 30,
+ minInterval: 2000 // 2 sec
+ }
+ }
};
// UTILS
@@ -82,8 +94,13 @@ const api = {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/json/readurl');
xhr.setRequestHeader('Content-Type', 'application/json');
- xhr.timeout = 30000;
- xhr.ontimeout = () => reject(new AppError('Request timed out'));
+ xhr.timeout = config.requestTimeout;
+
+ xhr.ontimeout = () => {
+ logger.warn(`Request timed out for ${url}`);
+ reject(new AppError('Request timed out'));
+ };
+
xhr.onload = () => {
logger.log(`Response for ${url}:`, xhr.responseText);
if (xhr.status === 200) {
@@ -104,7 +121,12 @@ const api = {
reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
}
};
- xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError'));
+
+ xhr.onerror = () => {
+ logger.error(`Network error occurred for ${url}`);
+ reject(new AppError('Network error occurred', 'NetworkError'));
+ };
+
xhr.send(JSON.stringify({
url: url,
headers: headers
@@ -123,6 +145,12 @@ const api = {
try {
return await api.makePostRequest(url, headers);
} catch (error) {
+ logger.error(`CryptoCompare request failed for ${coin}:`, error);
+ const cachedData = cache.get(`coinData_${coin}`);
+ if (cachedData) {
+ logger.info(`Using cached data for ${coin}`);
+ return cachedData.value;
+ }
return { error: error.message };
}
});
@@ -282,11 +310,11 @@ const api = {
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
- coingecko: 30000,
- cryptocompare: 2000
+ coingecko: config.rateLimits.coingecko.minInterval,
+ cryptocompare: config.rateLimits.cryptocompare.minInterval
},
requestQueue: {},
- retryDelays: [2000, 5000, 10000],
+ retryDelays: config.retryDelays,
canMakeRequest: function(apiName) {
const now = Date.now();
@@ -328,6 +356,19 @@ const rateLimiter = {
await new Promise(resolve => setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
+
+ if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
+ retryCount < this.retryDelays.length) {
+ const delay = this.retryDelays[retryCount];
+ logger.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
+ apiName,
+ retryCount,
+ error: error.message
+ });
+ await new Promise(resolve => setTimeout(resolve, delay));
+ return this.queueRequest(apiName, requestFn, retryCount + 1);
+ }
+
throw error;
}
};
@@ -336,10 +377,12 @@ const rateLimiter = {
return await this.requestQueue[apiName];
} catch (error) {
- if (error.message.includes('429')) {
+ if (error.message.includes('429') ||
+ error.message.includes('timeout') ||
+ error.name === 'NetworkError') {
const cachedData = cache.get(`coinData_${apiName}`);
if (cachedData) {
- console.log('Rate limit reached, using cached data');
+ console.log('Using cached data due to request failure');
return cachedData.value;
}
}
@@ -439,8 +482,8 @@ displayCoinData: (coin, data) => {
}
updateUI(false);
} catch (error) {
- logger.error('Failed to parse cache item:', error.message);
- localStorage.removeItem(key);
+ logger.error(`Failed to display data for ${coin}:`, error.message);
+ updateUI(true); // Show error state in UI
}
},
@@ -664,6 +707,8 @@ const chartModule = {
family: "'Inter', sans-serif"
},
color: 'rgba(156, 163, 175, 1)',
+ maxRotation: 0,
+ minRotation: 0,
callback: function(value) {
const date = new Date(value);
if (config.currentResolution === 'day') {
@@ -855,74 +900,73 @@ const chartModule = {
return hourlyData;
},
- updateChart: async (coinSymbol, forceRefresh = false) => {
- try {
- chartModule.showChartLoader();
- chartModule.loadStartTime = Date.now();
- const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
- let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
- let data;
- if (cachedData && Object.keys(cachedData.value).length > 0) {
- data = cachedData.value;
- //console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
- } else {
- //console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
- const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
- data = allData[coinSymbol];
- if (!data || Object.keys(data).length === 0) {
- throw new Error(`No data returned for ${coinSymbol}`);
+ updateChart: async (coinSymbol, forceRefresh = false) => {
+ try {
+ const currentChartData = chartModule.chart?.data.datasets[0].data || [];
+ if (currentChartData.length === 0) {
+ chartModule.showChartLoader();
+ }
+ chartModule.loadStartTime = Date.now();
+ const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
+ let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
+ let data;
+ if (cachedData && Object.keys(cachedData.value).length > 0) {
+ data = cachedData.value;
+ console.log(`Using cached data for ${coinSymbol}`);
+ } else {
+ try {
+ const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
+ data = allData[coinSymbol];
+ if (!data || Object.keys(data).length === 0) {
+ throw new Error(`No data returned for ${coinSymbol}`);
+ }
+ cache.set(cacheKey, data, config.cacheTTL);
+ } catch (error) {
+ if (error.message.includes('429') && currentChartData.length > 0) {
+ console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
+ return;
+ }
+ const expiredCache = localStorage.getItem(cacheKey);
+ if (expiredCache) {
+ try {
+ const parsedCache = JSON.parse(expiredCache);
+ data = parsedCache.value;
+ console.log(`Using expired cache data for ${coinSymbol}`);
+ } catch (cacheError) {
+ throw error;
+ }
+ } else {
+ throw error;
+ }
+ }
+ }
+ const chartData = chartModule.prepareChartData(coinSymbol, data);
+ if (chartData.length > 0 && chartModule.chart) {
+ chartModule.chart.data.datasets[0].data = chartData;
+ chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
+ if (coinSymbol === 'WOW') {
+ chartModule.chart.options.scales.x.time.unit = 'hour';
+ } else {
+ const resolution = config.resolutions[config.currentResolution];
+ chartModule.chart.options.scales.x.time.unit =
+ resolution.interval === 'hourly' ? 'hour' :
+ config.currentResolution === 'year' ? 'month' : 'day';
+ }
+ chartModule.chart.update('active');
+ chartModule.currentCoin = coinSymbol;
+ const loadTime = Date.now() - chartModule.loadStartTime;
+ ui.updateLoadTimeAndCache(loadTime, cachedData);
+ }
+ } catch (error) {
+ console.error(`Error updating chart for ${coinSymbol}:`, error);
+ if (!(chartModule.chart?.data.datasets[0].data.length > 0)) {
+ chartModule.chart.data.datasets[0].data = [];
+ chartModule.chart.update('active');
+ }
+ } finally {
+ chartModule.hideChartLoader();
}
- //console.log(`Caching new data for ${cacheKey}`);
- cache.set(cacheKey, data, config.cacheTTL);
- cachedData = null;
- }
-
- const chartData = chartModule.prepareChartData(coinSymbol, data);
- //console.log(`Prepared chart data for ${coinSymbol}:`, chartData.slice(0, 5));
-
- if (chartData.length === 0) {
- throw new Error(`No valid chart data for ${coinSymbol}`);
- }
-
- if (chartModule.chart) {
- chartModule.chart.data.datasets[0].data = chartData;
- chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
-
- if (coinSymbol === 'WOW') {
- chartModule.chart.options.scales.x.time.unit = 'hour';
- chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24;
- } else {
- const resolution = config.resolutions[config.currentResolution];
- chartModule.chart.options.scales.x.time.unit = resolution.interval === 'hourly' ? 'hour' : 'day';
- if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') {
- chartModule.chart.options.scales.x.time.unit = 'month';
- }
- if (config.currentResolution === 'year') {
- chartModule.chart.options.scales.x.ticks.maxTicksLimit = 12;
- } else if (config.currentResolution === 'sixMonths') {
- chartModule.chart.options.scales.x.ticks.maxTicksLimit = 6;
- } else if (config.currentResolution === 'day') {
- chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24;
- }
- }
-
- chartModule.chart.update('active');
- } else {
- //console.error('Chart object not initialized');
- throw new Error('Chart object not initialized');
- }
-
- chartModule.currentCoin = coinSymbol;
- const loadTime = Date.now() - chartModule.loadStartTime;
- ui.updateLoadTimeAndCache(loadTime, cachedData);
-
- } catch (error) {
- //console.error(`Error updating chart for ${coinSymbol}:`, error);
- ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`);
- } finally {
- chartModule.hideChartLoader();
- }
- },
+ },
showChartLoader: () => {
const loader = document.getElementById('chart-loader');
@@ -994,8 +1038,8 @@ const app = {
disabled: 'Auto-refresh: disabled',
justRefreshed: 'Just refreshed',
},
- cacheTTL: 5 * 60 * 1000, // 5 minutes
- minimumRefreshInterval: 60 * 1000, // 1 minute
+ cacheTTL: 5 * 60 * 1000, // 5 min
+ minimumRefreshInterval: 60 * 1000, // 1 min
init: () => {
console.log('Initializing app...');
@@ -1203,142 +1247,138 @@ setupEventListeners: () => {
app.updateNextRefreshTime();
},
refreshAllData: async () => {
- if (app.isRefreshing) {
- console.log('Refresh already in progress, skipping...');
- return;
+ if (app.isRefreshing) {
+ console.log('Refresh already in progress, skipping...');
+ return;
+ }
+
+ const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0;
+ const timeSinceLastRequest = Date.now() - lastGeckoRequest;
+ const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest);
+
+ if (waitTime > 0) {
+ const seconds = Math.ceil(waitTime / 1000);
+ ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
+
+ let remainingTime = seconds;
+ const countdownInterval = setInterval(() => {
+ remainingTime--;
+ if (remainingTime > 0) {
+ ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
+ } else {
+ clearInterval(countdownInterval);
+ ui.hideErrorMessage();
+ }
+ }, 1000);
+
+ return;
+ }
+
+ console.log('Starting refresh of all data...');
+ app.isRefreshing = true;
+ ui.showLoader();
+ chartModule.showChartLoader();
+
+ try {
+ ui.hideErrorMessage();
+ cache.clear();
+
+ const btcUpdateSuccess = await app.updateBTCPrice();
+ if (!btcUpdateSuccess) {
+ console.warn('BTC price update failed, continuing with cached or default value');
}
+ await new Promise(resolve => setTimeout(resolve, 1000));
- const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0;
- const timeSinceLastRequest = Date.now() - lastGeckoRequest;
- const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest);
-
- if (waitTime > 0) {
- const seconds = Math.ceil(waitTime / 1000);
- ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
-
-
- let remainingTime = seconds;
- const countdownInterval = setInterval(() => {
- remainingTime--;
- if (remainingTime > 0) {
- ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
- } else {
- clearInterval(countdownInterval);
- ui.hideErrorMessage();
- }
- }, 1000);
-
- return;
+ const allCoinData = await api.fetchCoinGeckoDataXHR();
+ if (allCoinData.error) {
+ throw new Error(`CoinGecko API Error: ${allCoinData.error}`);
}
- console.log('Starting refresh of all data...');
- app.isRefreshing = true;
- ui.showLoader();
- chartModule.showChartLoader();
+ const failedCoins = [];
- try {
- ui.hideErrorMessage();
- cache.clear();
+ for (const coin of config.coins) {
+ const symbol = coin.symbol.toLowerCase();
+ const coinData = allCoinData[symbol];
- const btcUpdateSuccess = await app.updateBTCPrice();
- if (!btcUpdateSuccess) {
- console.warn('BTC price update failed, continuing with cached or default value');
- }
-
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- const allCoinData = await api.fetchCoinGeckoDataXHR();
- if (allCoinData.error) {
- throw new Error(`CoinGecko API Error: ${allCoinData.error}`);
- }
-
- const failedCoins = [];
-
- for (const coin of config.coins) {
- const symbol = coin.symbol.toLowerCase();
- const coinData = allCoinData[symbol];
-
- try {
- if (!coinData) {
- throw new Error(`No data received`);
- }
-
- coinData.displayName = coin.displayName || coin.symbol;
- ui.displayCoinData(coin.symbol, coinData);
-
- const cacheKey = `coinData_${coin.symbol}`;
- cache.set(cacheKey, coinData);
-
- } catch (coinError) {
- console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
- failedCoins.push(coin.symbol);
+ try {
+ if (!coinData) {
+ throw new Error(`No data received`);
}
+
+ coinData.displayName = coin.displayName || coin.symbol;
+ ui.displayCoinData(coin.symbol, coinData);
+
+ const cacheKey = `coinData_${coin.symbol}`;
+ cache.set(cacheKey, coinData);
+
+ } catch (coinError) {
+ console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
+ failedCoins.push(coin.symbol);
}
+ }
- await new Promise(resolve => setTimeout(resolve, 1000));
+ await new Promise(resolve => setTimeout(resolve, 1000));
- if (chartModule.currentCoin) {
- try {
- await chartModule.updateChart(chartModule.currentCoin, true);
- } catch (chartError) {
- console.error('Chart update failed:', chartError);
-
- }
+ if (chartModule.currentCoin) {
+ try {
+ await chartModule.updateChart(chartModule.currentCoin, true);
+ } catch (chartError) {
+ console.error('Chart update failed:', chartError);
}
+ }
- app.lastRefreshedTime = new Date();
- localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
- ui.updateLastRefreshedTime();
+ app.lastRefreshedTime = new Date();
+ localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
+ ui.updateLastRefreshedTime();
- if (failedCoins.length > 0) {
- const failureMessage = failedCoins.length === config.coins.length
- ? 'Failed to update any coin data'
- : `Failed to update some coins: ${failedCoins.join(', ')}`;
+ if (failedCoins.length > 0) {
+ const failureMessage = failedCoins.length === config.coins.length
+ ? 'Failed to update any coin data'
+ : `Failed to update some coins: ${failedCoins.join(', ')}`;
- let countdown = 5;
- ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
+ let countdown = 5;
+ ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
- const countdownInterval = setInterval(() => {
- countdown--;
- if (countdown > 0) {
- ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
- } else {
- clearInterval(countdownInterval);
- ui.hideErrorMessage();
- }
- }, 1000);
- }
-
- console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
-
- } catch (error) {
- console.error('Critical error during refresh:', error);
-
-
- let countdown = 10;
- ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
-
const countdownInterval = setInterval(() => {
countdown--;
if (countdown > 0) {
- ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
+ ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
} else {
clearInterval(countdownInterval);
ui.hideErrorMessage();
}
}, 1000);
-
- } finally {
- ui.hideLoader();
- chartModule.hideChartLoader();
- app.isRefreshing = false;
-
- if (app.isAutoRefreshEnabled) {
- app.scheduleNextRefresh();
- }
}
- },
+
+ console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
+
+ } catch (error) {
+ console.error('Critical error during refresh:', error);
+
+ let countdown = 10;
+ ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
+
+ const countdownInterval = setInterval(() => {
+ countdown--;
+ if (countdown > 0) {
+ ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
+ } else {
+ clearInterval(countdownInterval);
+ ui.hideErrorMessage();
+ }
+ }, 1000);
+
+ } finally {
+ ui.hideLoader();
+ chartModule.hideChartLoader();
+ app.isRefreshing = false;
+
+ if (app.isAutoRefreshEnabled) {
+ app.scheduleNextRefresh();
+ }
+ }
+},
updateNextRefreshTime: () => {
console.log('Updating next refresh time display');
diff --git a/basicswap/static/js/tabs.js b/basicswap/static/js/tabs.js
new file mode 100644
index 0000000..a8e9c76
--- /dev/null
+++ b/basicswap/static/js/tabs.js
@@ -0,0 +1,115 @@
+(function(window) {
+ 'use strict';
+
+ class Tabs {
+ constructor(tabsEl, items = [], options = {}) {
+ this._tabsEl = tabsEl;
+ this._items = items;
+ this._activeTab = options.defaultTabId ? this.getTab(options.defaultTabId) : null;
+ this._options = {
+ defaultTabId: options.defaultTabId || null,
+ activeClasses: options.activeClasses || 'text-blue-600 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500',
+ inactiveClasses: options.inactiveClasses || 'dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300',
+ onShow: options.onShow || function() {}
+ };
+ this._initialized = false;
+ this.init();
+ }
+
+ init() {
+ if (this._items.length && !this._initialized) {
+ if (!this._activeTab) {
+ this.setActiveTab(this._items[0]);
+ }
+
+ this.show(this._activeTab.id, true);
+
+ this._items.forEach(tab => {
+ tab.triggerEl.addEventListener('click', () => {
+ this.show(tab.id);
+ });
+ });
+
+ this._initialized = true;
+ }
+ }
+
+ show(tabId, force = false) {
+ const tab = this.getTab(tabId);
+
+ if ((tab !== this._activeTab) || force) {
+ this._items.forEach(t => {
+ if (t !== tab) {
+ t.triggerEl.classList.remove(...this._options.activeClasses.split(' '));
+ t.triggerEl.classList.add(...this._options.inactiveClasses.split(' '));
+ t.targetEl.classList.add('hidden');
+ t.triggerEl.setAttribute('aria-selected', false);
+ }
+ });
+
+ tab.triggerEl.classList.add(...this._options.activeClasses.split(' '));
+ tab.triggerEl.classList.remove(...this._options.inactiveClasses.split(' '));
+ tab.triggerEl.setAttribute('aria-selected', true);
+ tab.targetEl.classList.remove('hidden');
+
+ this.setActiveTab(tab);
+ this._options.onShow(this, tab);
+ }
+ }
+
+ getTab(id) {
+ return this._items.find(t => t.id === id);
+ }
+
+ getActiveTab() {
+ return this._activeTab;
+ }
+
+ setActiveTab(tab) {
+ this._activeTab = tab;
+ }
+ }
+
+ function initTabs() {
+ document.querySelectorAll('[data-tabs-toggle]').forEach(tabsEl => {
+ const items = [];
+ let defaultTabId = null;
+
+ tabsEl.querySelectorAll('[role="tab"]').forEach(triggerEl => {
+ const isActive = triggerEl.getAttribute('aria-selected') === 'true';
+ const tab = {
+ id: triggerEl.getAttribute('data-tabs-target'),
+ triggerEl: triggerEl,
+ targetEl: document.querySelector(triggerEl.getAttribute('data-tabs-target'))
+ };
+ items.push(tab);
+
+ if (isActive) {
+ defaultTabId = tab.id;
+ }
+ });
+
+ new Tabs(tabsEl, items, {
+ defaultTabId: defaultTabId
+ });
+ });
+ }
+
+ const style = document.createElement('style');
+ style.textContent = `
+ [data-tabs-toggle] [role="tab"] {
+ cursor: pointer;
+ }
+ `;
+ document.head.appendChild(style);
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initTabs);
+ } else {
+ initTabs();
+ }
+
+ window.Tabs = Tabs;
+ window.initTabs = initTabs;
+
+})(window);
diff --git a/basicswap/static/js/tooltips.js b/basicswap/static/js/tooltips.js
new file mode 100644
index 0000000..d528994
--- /dev/null
+++ b/basicswap/static/js/tooltips.js
@@ -0,0 +1,309 @@
+(function(window) {
+ 'use strict';
+
+ const tooltipContainer = document.createElement('div');
+ tooltipContainer.className = 'tooltip-container';
+
+ const style = document.createElement('style');
+ style.textContent = `
+ [role="tooltip"] {
+ position: absolute;
+ z-index: 9999;
+ transition: opacity 0.2s ease-in-out;
+ pointer-events: auto;
+ opacity: 0;
+ visibility: hidden;
+ }
+
+ .tooltip-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 0;
+ overflow: visible;
+ pointer-events: none;
+ z-index: 9999;
+ }
+ `;
+
+ function ensureContainerExists() {
+ if (!document.body.contains(tooltipContainer)) {
+ document.body.appendChild(tooltipContainer);
+ }
+ }
+
+
+ function rafThrottle(callback) {
+ let requestId = null;
+ let lastArgs = null;
+
+ const later = (context) => {
+ requestId = null;
+ callback.apply(context, lastArgs);
+ };
+
+ return function(...args) {
+ lastArgs = args;
+ if (requestId === null) {
+ requestId = requestAnimationFrame(() => later(this));
+ }
+ };
+ }
+
+ function positionElement(targetEl, triggerEl, placement = 'top', offsetDistance = 8) {
+ const triggerRect = triggerEl.getBoundingClientRect();
+ const targetRect = targetEl.getBoundingClientRect();
+ let top, left;
+
+ switch (placement) {
+ case 'top':
+ top = triggerRect.top - targetRect.height - offsetDistance;
+ left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
+ break;
+ case 'bottom':
+ top = triggerRect.bottom + offsetDistance;
+ left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
+ break;
+ case 'left':
+ top = triggerRect.top + (triggerRect.height - targetRect.height) / 2;
+ left = triggerRect.left - targetRect.width - offsetDistance;
+ break;
+ case 'right':
+ top = triggerRect.top + (triggerRect.height - targetRect.height) / 2;
+ left = triggerRect.right + offsetDistance;
+ break;
+ }
+
+ const viewport = {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+
+ if (left < 0) left = 0;
+ if (top < 0) top = 0;
+ if (left + targetRect.width > viewport.width)
+ left = viewport.width - targetRect.width;
+ if (top + targetRect.height > viewport.height)
+ top = viewport.height - targetRect.height;
+
+ targetEl.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
+ }
+
+ const tooltips = new WeakMap();
+
+ class Tooltip {
+ constructor(targetEl, triggerEl, options = {}) {
+ ensureContainerExists();
+
+ this._targetEl = targetEl;
+ this._triggerEl = triggerEl;
+ this._options = {
+ placement: options.placement || 'top',
+ triggerType: options.triggerType || 'hover',
+ offset: options.offset || 8,
+ onShow: options.onShow || function() {},
+ onHide: options.onHide || function() {}
+ };
+ this._visible = false;
+ this._initialized = false;
+ this._hideTimeout = null;
+ this._showTimeout = null;
+
+ if (this._targetEl.parentNode !== tooltipContainer) {
+ tooltipContainer.appendChild(this._targetEl);
+ }
+
+ this._targetEl.style.visibility = 'hidden';
+ this._targetEl.style.opacity = '0';
+
+ this._showHandler = this.show.bind(this);
+ this._hideHandler = this._handleHide.bind(this);
+ this._updatePosition = rafThrottle(() => {
+ if (this._visible) {
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+ }
+ });
+
+ this.init();
+ }
+
+ init() {
+ if (!this._initialized) {
+ this._setupEventListeners();
+ this._initialized = true;
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+ }
+ }
+
+ _setupEventListeners() {
+ this._triggerEl.addEventListener('mouseenter', this._showHandler);
+ this._triggerEl.addEventListener('mouseleave', this._hideHandler);
+ this._triggerEl.addEventListener('focus', this._showHandler);
+ this._triggerEl.addEventListener('blur', this._hideHandler);
+
+ this._targetEl.addEventListener('mouseenter', () => {
+ clearTimeout(this._hideTimeout);
+ clearTimeout(this._showTimeout);
+ this._visible = true;
+ this._targetEl.style.visibility = 'visible';
+ this._targetEl.style.opacity = '1';
+ });
+
+ this._targetEl.addEventListener('mouseleave', this._hideHandler);
+
+ if (this._options.triggerType === 'click') {
+ this._triggerEl.addEventListener('click', this._showHandler);
+ }
+
+ window.addEventListener('scroll', this._updatePosition, { passive: true });
+ document.addEventListener('scroll', this._updatePosition, { passive: true, capture: true });
+ window.addEventListener('resize', this._updatePosition, { passive: true });
+
+ let rafId;
+ const smoothUpdate = () => {
+ if (this._visible) {
+ this._updatePosition();
+ rafId = requestAnimationFrame(smoothUpdate);
+ }
+ };
+
+ this._startSmoothUpdate = () => {
+ if (!rafId) rafId = requestAnimationFrame(smoothUpdate);
+ };
+
+ this._stopSmoothUpdate = () => {
+ if (rafId) {
+ cancelAnimationFrame(rafId);
+ rafId = null;
+ }
+ };
+ }
+
+ _handleHide() {
+ clearTimeout(this._hideTimeout);
+ clearTimeout(this._showTimeout);
+
+ this._hideTimeout = setTimeout(() => {
+ if (this._visible) {
+ this.hide();
+ }
+ }, 100);
+ }
+
+ show() {
+ clearTimeout(this._hideTimeout);
+ clearTimeout(this._showTimeout);
+
+ this._showTimeout = setTimeout(() => {
+ if (!this._visible) {
+ positionElement(
+ this._targetEl,
+ this._triggerEl,
+ this._options.placement,
+ this._options.offset
+ );
+
+ this._targetEl.style.visibility = 'visible';
+ this._targetEl.style.opacity = '1';
+ this._visible = true;
+ this._startSmoothUpdate();
+ this._options.onShow();
+ }
+ }, 20);
+ }
+
+ hide() {
+ this._targetEl.style.opacity = '0';
+ this._targetEl.style.visibility = 'hidden';
+ this._visible = false;
+ this._stopSmoothUpdate();
+ this._options.onHide();
+ }
+
+ destroy() {
+ clearTimeout(this._hideTimeout);
+ clearTimeout(this._showTimeout);
+ this._stopSmoothUpdate();
+
+ this._triggerEl.removeEventListener('mouseenter', this._showHandler);
+ this._triggerEl.removeEventListener('mouseleave', this._hideHandler);
+ this._triggerEl.removeEventListener('focus', this._showHandler);
+ this._triggerEl.removeEventListener('blur', this._hideHandler);
+ this._targetEl.removeEventListener('mouseenter', this._showHandler);
+ this._targetEl.removeEventListener('mouseleave', this._hideHandler);
+
+ if (this._options.triggerType === 'click') {
+ this._triggerEl.removeEventListener('click', this._showHandler);
+ }
+
+ window.removeEventListener('scroll', this._updatePosition);
+ document.removeEventListener('scroll', this._updatePosition, true);
+ window.removeEventListener('resize', this._updatePosition);
+
+ this._targetEl.style.visibility = '';
+ this._targetEl.style.opacity = '';
+ this._targetEl.style.transform = '';
+
+ if (this._targetEl.parentNode === tooltipContainer) {
+ document.body.appendChild(this._targetEl);
+ }
+
+ this._initialized = false;
+ }
+
+ toggle() {
+ if (this._visible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+ }
+
+ document.head.appendChild(style);
+
+ function initTooltips() {
+ ensureContainerExists();
+
+ document.querySelectorAll('[data-tooltip-target]').forEach(triggerEl => {
+ if (tooltips.has(triggerEl)) return;
+
+ const targetId = triggerEl.getAttribute('data-tooltip-target');
+ const targetEl = document.getElementById(targetId);
+
+ if (targetEl) {
+ const placement = triggerEl.getAttribute('data-tooltip-placement');
+ const triggerType = triggerEl.getAttribute('data-tooltip-trigger');
+
+ const tooltip = new Tooltip(targetEl, triggerEl, {
+ placement: placement || 'top',
+ triggerType: triggerType || 'hover',
+ offset: 8
+ });
+
+ tooltips.set(triggerEl, tooltip);
+ }
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initTooltips);
+ } else {
+ initTooltips();
+ }
+
+ window.Tooltip = Tooltip;
+ window.initTooltips = initTooltips;
+
+})(window);
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html
index 33ab064..b1132df 100644
--- a/basicswap/templates/header.html
+++ b/basicswap/templates/header.html
@@ -8,13 +8,18 @@
{% endif %}
+
-
-
-
+
+
+
+
+
+