diff --git a/index.html b/index.html index 9db8a29..f4f3fcb 100644 --- a/index.html +++ b/index.html @@ -2,22 +2,22 @@ - - - Document - - - + + + FLOBNB Economic System + + + + rel="stylesheet" /> - +

@@ -53,17 +53,16 @@ - Section 1 - Section 2 + Overview + User info + Subadmin Input
-

Section Heading

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. -

+

Overview

+

Details of the overall tokens issued

first option @@ -73,27 +72,39 @@
-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Total Amount issued

+

+ -

-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

customer tokens

+

+ -

-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Producer tokens

+

+ -

-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Investor tokens

+

+ - +

+
+
+

Referrer tokens

+

+ - +

+
+
+

Ranchimall's Ownership

+

+ -

@@ -101,10 +112,8 @@
-

Section Heading

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. -

+

User info

+

Lorem ipsum dolor sit amet consectetur adipisicing elit.

first option @@ -114,283 +123,602 @@
-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Total FLOBNB

+

+ -

-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Blockchain FLOBNB

+

+ -

-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. -

-
-
-

Card

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam, officia. +

Exchange FLOBNB

+

+ -

+
+
+
+

Subadmin input

+

Lorem ipsum dolor sit amet consectetur adipisicing elit.

+
+ + first option + second option + third option + +
+ +
+

Flobnb Details

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Esse, sint. Aperiam ipsum expedita + tenetur + quasi, repellat ea. Laboriosam, earum ratione!

+ + + + + + + + + + + + + + + + Submit + + + Select file + +
+
+ + - - - - - TEST_MODE - (use console) - - + + + + + } + } + return obj1; + }; - - ecparams: EllipticCurve.getSECCurveByName("secp256k1"), + - + + - - - + + + .catch((error) => reject(error)); + }); + }, + + getNextGeneralData: function (type, vectorClock = null, options = {}) { + var fk = floCloudAPI.util.filterKey(type, options); + vectorClock = vectorClock || this.getNextGeneralData[fk] || "0"; + var filteredResult = {}; + if (floGlobals.generalData[fk]) { + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock) + filteredResult[d] = JSON.parse( + JSON.stringify(floGlobals.generalData[fk][d]) + ); + } else if (options.comment) { + let comment = options.comment; + delete options.comment; + let fk = floCloudAPI.util.filterKey(type, options); + for (let d in floGlobals.generalData[fk]) + if ( + d > vectorClock && + floGlobals.generalData[fk][d].comment == comment + ) + filteredResult[d] = JSON.parse( + JSON.stringify(floGlobals.generalData[fk][d]) + ); + } + if (options.decrypt) { + let decryptionKey = + options.decrypt === true ? myPrivKey : options.decrypt; + if (!Array.isArray(decryptionKey)) decryptionKey = [decryptionKey]; + for (let f in filteredResult) { + let data = filteredResult[f]; + try { + if ( + data.message instanceof Object && + "secret" in data.message + ) { + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(data.message, key); + data.message = JSON.parse(tmp); + break; + } catch (error) { } + } + } + } catch (error) { } + } + } + this.getNextGeneralData[fk] = Object.keys(filteredResult) + .sort() + .pop(); + return filteredResult; + }, + + syncData: { + oldDevice: function () { + return new Promise((resolve, reject) => { + let sync = { + contacts: floGlobals.contacts, + pubKeys: floGlobals.pubKeys, + messages: floGlobals.messages, + }; + let message = Crypto.AES.encrypt(JSON.stringify(sync), myPrivKey); + let options = { + receiverID: myFloID, + application: "floDapps", + }; + floCloudAPI + .sendApplicationData(message, "syncData", options) + .then((result) => resolve(result)) + .catch((error) => reject(error)); + }); + }, + + newDevice() { + return new Promise((resolve, reject) => { + var options = { + receiverID: myFloID, + senderIDs: myFloID, + application: "floDapps", + mostRecent: true, + }; + floCloudAPI + .requestApplicationData("syncData", options) + .then((response) => { + let vc = Object.keys(response).sort().pop(); + let sync = JSON.parse( + Crypto.AES.decrypt(response[vc].message, myPrivKey) + ); + let promises = []; + let store = (key, val, obs) => + promises.push( + compactIDB.writeData(obs, val, key, `floDapps#${floID}`) + ); + ["contacts", "pubKeys", "messages"].forEach((c) => { + for (let i in sync[c]) { + store(i, sync[c][i], c); + floGlobals[c][i] = sync[c][i]; + } + }); + Promise.all(promises) + .then((results) => resolve("Sync data successful")) + .catch((error) => reject(error)); + }) + .catch((error) => reject(error)); + }); + }, + }, + }; + - + + \ No newline at end of file diff --git a/js/components.min.js b/js/components.min.js index 7078968..c731beb 100644 --- a/js/components.min.js +++ b/js/components.min.js @@ -6,4 +6,7 @@ const smMenu=document.createElement("template");smMenu.innerHTML='\n\n
\n',customElements.define("sm-notifications",class extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"}).append(smNotifications.content.cloneNode(!0)),this.notificationPanel=this.shadowRoot.querySelector(".notification-panel"),this.animationOptions={duration:300,fill:"forwards",easing:"cubic-bezier(0.175, 0.885, 0.32, 1.275)"},this.push=this.push.bind(this),this.createNotification=this.createNotification.bind(this),this.removeNotification=this.removeNotification.bind(this),this.clearAll=this.clearAll.bind(this)}randString(n){let t="";const i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let o=0;o${o}\n

${n}

\n `,i&&(e.classList.add("pinned"),a+='\n \n '),e.innerHTML=a,e}push(n,t={}){const i=this.createNotification(n,t);return this.notificationPanel.append(i),i.animate([{transform:"translateY(1rem)",opacity:"0"},{transform:"none",opacity:"1"}],this.animationOptions),i.id}removeNotification(n){n.animate([{transform:"none",opacity:"1"},{transform:"translateY(0.5rem)",opacity:"0"}],this.animationOptions).onfinish=(()=>{n.remove()})}clearAll(){Array.from(this.notificationPanel.children).forEach(n=>{this.removeNotification(n)})}connectedCallback(){this.notificationPanel.addEventListener("click",n=>{n.target.closest(".close")&&this.removeNotification(n.target.closest(".notification"))});const n=new MutationObserver(n=>{n.forEach(n=>{"childList"===n.type&&n.addedNodes.length&&!n.addedNodes[0].classList.contains("pinned")&&setTimeout(()=>{this.removeNotification(n.addedNodes[0])},5e3)})});n.observe(this.notificationPanel,{childList:!0})}}); const smPopup=document.createElement("template");smPopup.innerHTML='\n\n\n',customElements.define("sm-popup",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smPopup.content.cloneNode(!0)),this.allowClosing=!1,this.isOpen=!1,this.pinned=!1,this.popupStack,this.offset,this.touchStartY=0,this.touchEndY=0,this.touchStartTime=0,this.touchEndTime=0,this.touchEndAnimataion,this.popupContainer=this.shadowRoot.querySelector(".popup-container"),this.popup=this.shadowRoot.querySelector(".popup"),this.popupBodySlot=this.shadowRoot.querySelector(".popup-body slot"),this.popupHeader=this.shadowRoot.querySelector(".popup-top"),this.resumeScrolling=this.resumeScrolling.bind(this),this.show=this.show.bind(this),this.hide=this.hide.bind(this),this.handleTouchStart=this.handleTouchStart.bind(this),this.handleTouchMove=this.handleTouchMove.bind(this),this.handleTouchEnd=this.handleTouchEnd.bind(this),this.movePopup=this.movePopup.bind(this)}static get observedAttributes(){return["open"]}get open(){return this.isOpen}resumeScrolling(){const t=document.body.style.top;window.scrollTo(0,-1*parseInt(t||"0")),setTimeout(()=>{document.body.style.overflow="auto",document.body.style.top="initial"},300)}show(t={}){const{pinned:e=!1,popupStack:n}=t;return n&&(this.popupStack=n),this.popupStack&&!this.hasAttribute("open")&&(this.popupStack.push({popup:this,permission:e}),this.popupStack.items.length>1&&this.popupStack.items[this.popupStack.items.length-2].popup.classList.add("stacked"),this.dispatchEvent(new CustomEvent("popupopened",{bubbles:!0,detail:{popup:this,popupStack:this.popupStack}})),this.setAttribute("open",""),this.pinned=e,this.isOpen=!0),this.popupContainer.classList.remove("hide"),this.popup.style.transform="none",document.body.style.overflow="hidden",document.body.style.top=`-${window.scrollY}px`,this.popupStack}hide(){window.innerWidth<640?this.popup.style.transform="translateY(100%)":this.popup.style.transform="translateY(3rem)",this.popupContainer.classList.add("hide"),this.removeAttribute("open"),void 0!==this.popupStack?(this.popupStack.pop(),this.popupStack.items.length?this.popupStack.items[this.popupStack.items.length-1].popup.classList.remove("stacked"):this.resumeScrolling()):this.resumeScrolling(),this.forms.length&&setTimeout(()=>{this.forms.forEach(t=>t.reset())},300),setTimeout(()=>{this.dispatchEvent(new CustomEvent("popupclosed",{bubbles:!0,detail:{popup:this,popupStack:this.popupStack}})),this.isOpen=!1},300)}handleTouchStart(t){this.touchStartY=t.changedTouches[0].clientY,this.popup.style.transition="transform 0.1s",this.touchStartTime=t.timeStamp}handleTouchMove(t){this.touchStartYthis.movePopup()))}handleTouchEnd(t){if(this.touchEndTime=t.timeStamp,cancelAnimationFrame(this.touchEndAnimataion),this.touchEndY=t.changedTouches[0].clientY,this.popup.style.transition="transform 0.3s",this.threshold=.3*this.popup.getBoundingClientRect().height,this.touchEndTime-this.touchStartTime>200)if(this.touchEndY-this.touchStartY>this.threshold){if(this.pinned)return void this.show();this.hide()}else this.show();else if(this.touchEndY>this.touchStartY){if(this.pinned)return void this.show();this.hide()}}movePopup(){this.popup.style.transform=`translateY(${this.offset}px)`}connectedCallback(){this.popupBodySlot.addEventListener("slotchange",()=>{this.forms=this.querySelectorAll("sm-form")}),this.popupContainer.addEventListener("mousedown",t=>{t.target!==this.popupContainer||this.pinned||(this.pinned?this.show():this.hide())});const t=new ResizeObserver(t=>{for(let e of t)if(e.contentBoxSize){Array.isArray(e.contentBoxSize)?e.contentBoxSize[0]:e.contentBoxSize;this.threshold=.3*e.blockSize.height}else this.threshold=.3*e.contentRect.height});t.observe(this),this.popupHeader.addEventListener("touchstart",t=>{this.handleTouchStart(t)},{passive:!0}),this.popupHeader.addEventListener("touchmove",t=>{this.handleTouchMove(t)},{passive:!0}),this.popupHeader.addEventListener("touchend",t=>{this.handleTouchEnd(t)},{passive:!0})}disconnectedCallback(){this.popupHeader.removeEventListener("touchstart",this.handleTouchStart,{passive:!0}),this.popupHeader.removeEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.popupHeader.removeEventListener("touchend",this.handleTouchEnd,{passive:!0}),resizeObserver.unobserve()}attributeChangedCallback(t,e,n){"open"===t&&this.hasAttribute("open")&&this.show()}}); const smTabHeader=document.createElement("template");smTabHeader.innerHTML='\n\n
\n
\n \n
\n
\n
\n',customElements.define("sm-tab-header",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smTabHeader.content.cloneNode(!0)),this.prevTab,this.allTabs,this.activeTab,this.indicator=this.shadowRoot.querySelector(".indicator"),this.tabSlot=this.shadowRoot.querySelector("slot"),this.tabHeader=this.shadowRoot.querySelector(".tab-header"),this.changeTab=this.changeTab.bind(this),this.handleClick=this.handleClick.bind(this),this.handlePanelChange=this.handlePanelChange.bind(this)}fireEvent(t){this.dispatchEvent(new CustomEvent(`switchedtab${this.target}`,{bubbles:!0,detail:{index:parseInt(t)}}))}moveIndiactor(t){this.indicator.setAttribute("style",`width: ${t.width}px; transform: translateX(${t.left-this.tabHeader.getBoundingClientRect().left+this.tabHeader.scrollLeft}px)`)}changeTab(t){t!==this.prevTab&&t.closest("sm-tab")&&(this.prevTab&&this.prevTab.classList.remove("active"),t.classList.add("active"),t.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"}),this.moveIndiactor(t.getBoundingClientRect()),this.prevTab=t,this.activeTab=t)}handleClick(t){t.target.closest("sm-tab")&&(this.changeTab(t.target),this.fireEvent(t.target.dataset.index))}handlePanelChange(t){this.changeTab(this.allTabs[t.detail.index])}connectedCallback(){if(!this.hasAttribute("target")||""===this.getAttribute("target").value)return;this.target=this.getAttribute("target"),this.tabSlot.addEventListener("slotchange",()=>{this.allTabs=this.tabSlot.assignedElements(),this.allTabs.forEach((t,n)=>{t.dataset.index=n})}),this.addEventListener("click",this.handleClick),document.addEventListener(`switchedpanel${this.target}`,this.handlePanelChange);let t=new ResizeObserver(t=>{t.forEach(t=>{if(this.prevTab){let t=this.activeTab.getBoundingClientRect();this.moveIndiactor(t)}})});t.observe(this);let n=new IntersectionObserver(t=>{t.forEach(t=>{if(t.isIntersecting)if(this.indicator.style.transition="none",this.activeTab){let t=this.activeTab.getBoundingClientRect();this.moveIndiactor(t)}else{this.allTabs[0].classList.add("active");let t=this.allTabs[0].getBoundingClientRect();this.moveIndiactor(t),this.fireEvent(0),this.prevTab=this.tabSlot.assignedElements()[0],this.activeTab=this.prevTab}})},{threshold:1});n.observe(this)}disconnectedCallback(){this.removeEventListener("click",this.handleClick),document.removeEventListener(`switchedpanel${this.target}`,this.handlePanelChange)}});const smTab=document.createElement("template");smTab.innerHTML='\n\n
\n\n
\n',customElements.define("sm-tab",class extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"}).append(smTab.content.cloneNode(!0))}});const smTabPanels=document.createElement("template");smTabPanels.innerHTML='\n\n
\n Nothing to see here.\n
\n',customElements.define("sm-tab-panels",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smTabPanels.content.cloneNode(!0)),this.isTransitioning=!1,this.panelContainer=this.shadowRoot.querySelector(".panel-container"),this.panelSlot=this.shadowRoot.querySelector("slot"),this.handleTabChange=this.handleTabChange.bind(this)}handleTabChange(t){this.isTransitioning=!0,this.panelContainer.scrollTo({left:this.allPanels[t.detail.index].getBoundingClientRect().left-this.panelContainer.getBoundingClientRect().left+this.panelContainer.scrollLeft,behavior:"smooth"}),setTimeout(()=>{this.isTransitioning=!1},300)}fireEvent(t){this.dispatchEvent(new CustomEvent(`switchedpanel${this.id}`,{bubbles:!0,detail:{index:parseInt(t)}}))}connectedCallback(){this.panelSlot.addEventListener("slotchange",()=>{this.allPanels=this.panelSlot.assignedElements(),this.allPanels.forEach((n,e)=>{n.dataset.index=e,t.observe(n)})}),document.addEventListener(`switchedtab${this.id}`,this.handleTabChange);const t=new IntersectionObserver(t=>{t.forEach(t=>{!this.isTransitioning&&t.isIntersecting&&this.fireEvent(t.target.dataset.index)})},{threshold:.6})}disconnectedCallback(){intersectionObserver.disconnect(),document.removeEventListener(`switchedtab${this.id}`,this.handleTabChange)}}); -const themeToggle=document.createElement("template");themeToggle.innerHTML='\n \n \n';class ThemeToggle extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(themeToggle.content.cloneNode(!0)),this.isChecked=!1,this.hasTheme="light",this.toggleState=this.toggleState.bind(this),this.fireEvent=this.fireEvent.bind(this),this.handleThemeChange=this.handleThemeChange.bind(this)}static get observedAttributes(){return["checked"]}daylight(){this.hasTheme="light",document.body.dataset.theme="light",this.setAttribute("aria-checked","false")}nightlight(){this.hasTheme="dark",document.body.dataset.theme="dark",this.setAttribute("aria-checked","true")}toggleState(){this.toggleAttribute("checked"),this.fireEvent()}handleKeyDown(e){"Space"===e.code&&this.toggleState()}handleThemeChange(e){e.detail.theme!==this.hasTheme&&("dark"===e.detail.theme?this.setAttribute("checked",""):this.removeAttribute("checked"))}fireEvent(){this.dispatchEvent(new CustomEvent("themechange",{bubbles:!0,composed:!0,detail:{theme:this.hasTheme}}))}connectedCallback(){this.setAttribute("role","switch"),this.setAttribute("aria-label","theme toggle"),"dark"===localStorage.theme?(this.nightlight(),this.setAttribute("checked","")):"light"===localStorage.theme?(this.daylight(),this.removeAttribute("checked")):window.matchMedia("(prefers-color-scheme: dark)").matches?(this.nightlight(),this.setAttribute("checked","")):(this.daylight(),this.removeAttribute("checked")),this.addEventListener("click",this.toggleState),this.addEventListener("keydown",this.handleKeyDown),document.addEventListener("themechange",this.handleThemeChange)}disconnectedCallback(){this.removeEventListener("click",this.toggleState),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("themechange",this.handleThemeChange)}attributeChangedCallback(e,t,n){"checked"===e&&(this.hasAttribute("checked")?(this.nightlight(),localStorage.setItem("theme","dark")):(this.daylight(),localStorage.setItem("theme","light")))}}window.customElements.define("theme-toggle",ThemeToggle); \ No newline at end of file +const themeToggle=document.createElement("template");themeToggle.innerHTML='\n \n \n';class ThemeToggle extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(themeToggle.content.cloneNode(!0)),this.isChecked=!1,this.hasTheme="light",this.toggleState=this.toggleState.bind(this),this.fireEvent=this.fireEvent.bind(this),this.handleThemeChange=this.handleThemeChange.bind(this)}static get observedAttributes(){return["checked"]}daylight(){this.hasTheme="light",document.body.dataset.theme="light",this.setAttribute("aria-checked","false")}nightlight(){this.hasTheme="dark",document.body.dataset.theme="dark",this.setAttribute("aria-checked","true")}toggleState(){this.toggleAttribute("checked"),this.fireEvent()}handleKeyDown(e){"Space"===e.code&&this.toggleState()}handleThemeChange(e){e.detail.theme!==this.hasTheme&&("dark"===e.detail.theme?this.setAttribute("checked",""):this.removeAttribute("checked"))}fireEvent(){this.dispatchEvent(new CustomEvent("themechange",{bubbles:!0,composed:!0,detail:{theme:this.hasTheme}}))}connectedCallback(){this.setAttribute("role","switch"),this.setAttribute("aria-label","theme toggle"),"dark"===localStorage.theme?(this.nightlight(),this.setAttribute("checked","")):"light"===localStorage.theme?(this.daylight(),this.removeAttribute("checked")):window.matchMedia("(prefers-color-scheme: dark)").matches?(this.nightlight(),this.setAttribute("checked","")):(this.daylight(),this.removeAttribute("checked")),this.addEventListener("click",this.toggleState),this.addEventListener("keydown",this.handleKeyDown),document.addEventListener("themechange",this.handleThemeChange)}disconnectedCallback(){this.removeEventListener("click",this.toggleState),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("themechange",this.handleThemeChange)}attributeChangedCallback(e,t,n){"checked"===e&&(this.hasAttribute("checked")?(this.nightlight(),localStorage.setItem("theme","dark")):(this.daylight(),localStorage.setItem("theme","light")))}}window.customElements.define("theme-toggle",ThemeToggle); +const smCheckbox=document.createElement("template");smCheckbox.innerHTML='\n\n',customElements.define("sm-checkbox",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCheckbox.content.cloneNode(!0)),this.defaultState,this.checkbox=this.shadowRoot.querySelector(".checkbox"),this.reset=this.reset.bind(this),this.dispatch=this.dispatch.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleClick=this.handleClick.bind(this)}static get observedAttributes(){return["value","disabled","checked"]}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get checked(){return this.hasAttribute("checked")}set checked(e){e?this.setAttribute("checked",""):this.removeAttribute("checked")}set value(e){this.setAttribute("value",e)}get value(){return this.getAttribute("value")}focusIn(){this.focus()}reset(){this.value=this.defaultState}dispatch(){this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0}))}handleKeyDown(e){" "===e.key&&(e.preventDefault(),this.click())}handleClick(e){this.toggleAttribute("checked")}connectedCallback(){this.hasAttribute("disabled")||this.setAttribute("tabindex","0"),this.setAttribute("role","checkbox"),this.defaultState=this.hasAttribute("checked"),this.hasAttribute("checked")||this.setAttribute("aria-checked","false"),this.addEventListener("keydown",this.handleKeyDown),this.addEventListener("click",this.handleClick)}attributeChangedCallback(e,t,n){t!==n&&("checked"===e?(this.setAttribute("aria-checked",this.hasAttribute("checked")),this.dispatch()):"disabled"===e&&(this.hasAttribute("disabled")?this.removeAttribute("tabindex"):this.setAttribute("tabindex","0")))}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown),this.removeEventListener("change",this.handleClick)}}); +const smCopy=document.createElement("template");smCopy.innerHTML='\n\n
\n

\n \n
\n',customElements.define("sm-copy",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCopy.content.cloneNode(!0)),this.copyContent=this.shadowRoot.querySelector(".copy-content"),this.copyButton=this.shadowRoot.querySelector(".copy-button"),this.copy=this.copy.bind(this)}static get observedAttributes(){return["value"]}set value(n){this.setAttribute("value",n)}get value(){return this.getAttribute("value")}fireEvent(){this.dispatchEvent(new CustomEvent("copy",{composed:!0,bubbles:!0,cancelable:!0}))}copy(){navigator.clipboard.writeText(this.copyContent.textContent).then(n=>this.fireEvent()).catch(n=>console.error(n))}connectedCallback(){this.copyButton.addEventListener("click",this.copy)}attributeChangedCallback(n,t,o){"value"===n&&(this.copyContent.textContent=o)}disconnectedCallback(){this.copyButton.removeEventListener("click",this.copy)}}); +const fileInput=document.createElement("template");fileInput.innerHTML='\n \t\n\t
    \n \t\n',customElements.define("file-input",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(fileInput.content.cloneNode(!0)),this.input=this.shadowRoot.querySelector("input"),this.fileInput=this.shadowRoot.querySelector(".file-input"),this.filesPreviewWrapper=this.shadowRoot.querySelector(".files-preview-wrapper"),this.reflectedAttributes=["accept","multiple","capture","type"],this.reset=this.reset.bind(this),this.formatBytes=this.formatBytes.bind(this),this.createFilePreview=this.createFilePreview.bind(this),this.handleChange=this.handleChange.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}static get observedAttributes(){return["accept","multiple","capture","type"]}get files(){return this.input.files}set accept(t){this.setAttribute("accept",t)}set multiple(t){t?this.setAttribute("multiple",""):this.removeAttribute("multiple")}set capture(t){this.setAttribute("capture",t)}set value(t){this.input.value=t}get isValid(){return""!==this.input.value}reset(){this.input.value="",this.filesPreviewWrapper.innerHTML=""}formatBytes(t,e=2){if(0===t)return"0 Bytes";const n=0>e?0:e,i=Math.floor(Math.log(t)/Math.log(1024));return parseFloat((t/Math.pow(1024,i)).toFixed(n))+" "+["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][i]}createFilePreview(t){const e=document.createElement("li"),{name:n,size:i}=t;return e.className="file-preview",e.innerHTML=`\n\t\t\t
    ${n}
    \n
    ${this.formatBytes(i)}
    \n\t\t`,e}handleChange(t){this.filesPreviewWrapper.innerHTML="";const e=document.createDocumentFragment();Array.from(t.target.files).forEach(t=>{e.append(this.createFilePreview(t))}),this.filesPreviewWrapper.append(e)}handleKeyDown(t){"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),this.input.click())}connectedCallback(){this.setAttribute("role","button"),this.setAttribute("aria-label","File upload"),this.input.addEventListener("change",this.handleChange),this.fileInput.addEventListener("keydown",this.handleKeyDown)}attributeChangedCallback(t){this.reflectedAttributes.includes(t)&&(this.hasAttribute(t)?this.input.setAttribute(t,this.getAttribute(t)?this.getAttribute(t):""):this.input.removeAttribute(t))}disconnectedCallback(){this.input.removeEventListener("change",this.handleChange),this.fileInput.removeEventListener("keydown",this.handleKeyDown)}}); \ No newline at end of file diff --git a/js/exchangeAPI.js b/js/exchangeAPI.js new file mode 100644 index 0000000..b1ccbb0 --- /dev/null +++ b/js/exchangeAPI.js @@ -0,0 +1,645 @@ +//console.log(document.cookie.toString()); +const INVALID_SERVER_MSG = "INCORRECT_SERVER_ERROR"; +var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list + +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 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)); + }); +} + +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]) + } + 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; + } + 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]; + } + }); + 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)); + }) +} + +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 diff --git a/js/floExchangeAPI.js b/js/floExchangeAPI.js new file mode 100644 index 0000000..8fac499 --- /dev/null +++ b/js/floExchangeAPI.js @@ -0,0 +1,1110 @@ +'use strict'; + +(function(EXPORTS) { + const exchangeAPI = EXPORTS; + + /*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 + + } + } + + const K_Bucket = 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"); + floTokenAPI.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.floExchangeAPI = {}); \ No newline at end of file diff --git a/js/floTokenAPI.js b/js/floTokenAPI.js new file mode 100644 index 0000000..3233279 --- /dev/null +++ b/js/floTokenAPI.js @@ -0,0 +1,53 @@ +'use strict'; + +/* Token Operator to send/receive tokens from blockchain using API calls*/ +(function(GLOBAL) { + const floTokenAPI = GLOBAL.floTokenAPI = { + fetch_api: function(apicall) { + return new Promise((resolve, reject) => { + console.log(floGlobals.tokenURL + apicall); + fetch(floGlobals.tokenURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else + reject(response) + }).catch(error => reject(error)) + }) + }, + getBalance: function(floID, token = floGlobals.currency) { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) + .then(result => resolve(result.balance || 0)) + .catch(error => reject(error)) + }) + }, + getTx: function(txID) { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then(res => { + if (res.result === "error") + reject(res.description); + else if (!res.parsedFloData) + reject("Data piece (parsedFloData) missing"); + else if (!res.transactionDetails) + reject("Data piece (transactionDetails) missing"); + else + resolve(res); + }).catch(error => reject(error)) + }) + }, + sendToken: function(privKey, amount, receiverID, message = "", token = floGlobals.currency) { + return new Promise((resolve, reject) => { + let senderID = floCrypto.getFloID(privKey); + if (typeof amount !== "number" || amount <= 0) + return reject("Invalid amount"); + this.getBalance(senderID, token).then(bal => { + if (amount > bal) + return reject("Insufficiant token balance"); + floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + } +})(typeof global !== "undefined" ? global : window); \ No newline at end of file