Updating UI

This commit is contained in:
sairaj mote 2022-12-20 23:42:53 +05:30
parent 9c59d01e11
commit 2cf02a5793
14 changed files with 26373 additions and 2549 deletions

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,6 @@ body {
} }
body { body {
color: rgba(var(--text-color), 1);
background: rgba(var(--background-color), 1);
}
body,
body * {
--accent-color: #2672ff; --accent-color: #2672ff;
--accent-color--light: rgba(38, 114, 255, 0.06); --accent-color--light: rgba(38, 114, 255, 0.06);
--text-color: 20, 20, 20; --text-color: 20, 20, 20;
@ -31,10 +26,11 @@ body * {
--yellow: rgb(220, 165, 0); --yellow: rgb(220, 165, 0);
--loan-color: rgb(255, 171, 93); --loan-color: rgb(255, 171, 93);
scrollbar-width: thin; scrollbar-width: thin;
color: rgba(var(--text-color), 1);
background: rgba(var(--background-color), 1);
} }
body[data-theme=dark], body[data-theme=dark] {
body[data-theme=dark] * {
--accent-color: rgb(170, 190, 255); --accent-color: rgb(170, 190, 255);
--accent-color--light: rgba(231, 239, 255, 0.06); --accent-color--light: rgba(231, 239, 255, 0.06);
--text-color: 200, 200, 200; --text-color: 200, 200, 200;
@ -81,26 +77,71 @@ button,
background-color: transparent; background-color: transparent;
overflow: hidden; overflow: hidden;
color: inherit; color: inherit;
cursor: pointer;
transition: transform 0.3s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
align-items: center;
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
padding: 0.8rem;
border-radius: 0.3rem;
justify-content: center;
}
button:focus-visible,
.button:focus-visible {
outline: var(--accent-color) solid medium;
}
button:not(:disabled),
.button:not(:disabled) {
cursor: pointer;
} }
.button { .button {
white-space: nowrap; background-color: rgba(var(--text-color), 0.02);
padding: 0.6rem 1rem; border: solid thin rgba(var(--text-color), 0.06);
border-radius: 0.3rem;
font-weight: 500;
font-size: 0.8rem;
background-color: rgba(var(--text-color), 0.06);
} }
.button--primary { .button--primary {
background-color: var(--accent-color);
color: rgba(var(--background-color), 1); color: rgba(var(--background-color), 1);
background-color: var(--accent-color);
}
.button--primary .icon {
fill: rgba(var(--background-color), 1);
}
.button--colored {
color: var(--accent-color);
}
.button--colored .icon {
fill: var(--accent-color);
}
.button--danger {
background-color: rgba(255, 115, 115, 0.062745098);
color: var(--danger-color);
}
.button--danger .icon {
fill: var(--danger-color);
}
.button--small {
padding: 0.4rem 0.6rem;
}
.button--outlined {
border: solid rgba(var(--text-color), 0.3) 0.1rem;
background-color: rgba(var(--foreground-color), 1);
}
.button--transparent {
background-color: transparent;
} }
button:disabled { button:disabled {
opacity: 0.5; opacity: 0.4;
cursor: not-allowed;
filter: saturate(0);
}
.cta {
text-transform: uppercase;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.05em;
padding: 0.8rem 1rem;
} }
a:-webkit-any-link:focus-visible { a:-webkit-any-link:focus-visible {
@ -115,17 +156,6 @@ a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid; outline: rgba(var(--text-color), 1) 0.1rem solid;
} }
sm-button {
--border-radius: 0.5rem;
--padding: 0.7rem 1rem;
}
sm-button[variant=primary] .icon {
fill: rgba(var(--background-color), 1);
}
sm-button[disabled] .icon {
fill: rgba(var(--text-color), 0.6);
}
ul { ul {
list-style: none; list-style: none;
} }
@ -143,7 +173,7 @@ ul {
pointer-events: none; pointer-events: none;
} }
.hide-completely { .hidden {
display: none !important; display: none !important;
} }
@ -335,23 +365,41 @@ ul {
fill: var(--accent-color); fill: var(--accent-color);
} }
#confirmation_popup { #confirmation_popup,
#prompt_popup {
flex-direction: column; flex-direction: column;
} }
#confirmation_popup h4 { #confirmation_popup h4,
font-weight: 500; #prompt_popup h4 {
margin-bottom: 0.5rem; font-size: 1.2rem;
margin-bottom: 1rem;
} }
#confirmation_popup sm-button { #confirmation_popup .flex,
margin: 0; #prompt_popup .flex {
}
#confirmation_popup .flex {
padding: 0;
margin-top: 1rem; margin-top: 1rem;
} }
#confirmation_popup .flex sm-button:first-of-type {
margin-right: 0.6rem; .popup__header {
margin-left: auto; position: relative;
display: grid;
gap: 0.5rem;
width: 100%;
padding: 0 1.5rem;
align-items: center;
}
.popup__header > * {
grid-row: 1;
}
.popup__header h3,
.popup__header h4 {
grid-column: 1/-1;
justify-self: center;
align-self: center;
}
.popup__header__close {
grid-column: 1;
margin-left: -1rem;
justify-self: flex-start;
} }
button:active, button:active,
@ -484,6 +532,7 @@ details[open] > summary .icon {
text-align: center; text-align: center;
} }
#loading sm-spinner { #loading sm-spinner {
justify-self: center;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@ -537,7 +586,15 @@ details[open] > summary .icon {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto;
}
#user_profile_button {
gap: 0.5rem;
background-color: rgba(var(--text-color), 0.06);
border-radius: 2rem;
font-size: 0.8rem;
padding: 0.6rem 0.8rem;
} }
#subpage_container { #subpage_container {
@ -601,13 +658,13 @@ details[open] > summary .icon {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
} }
strip-select { sm-chips {
--gap: 0; --gap: 0;
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
border-radius: 0.3rem; border-radius: 0.3rem;
} }
strip-option { sm-chip {
text-transform: uppercase; text-transform: uppercase;
font-weight: 500; font-weight: 500;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@ -616,10 +673,14 @@ strip-option {
--active-option-color: rgba(var(--background-color), 1); --active-option-color: rgba(var(--background-color), 1);
--active-option-background-color: var(--accent-color); --active-option-background-color: var(--accent-color);
} }
strip-option:first-of-type { sm-chip[selected] {
color: rgba(var(--background-color), 1);
--background: var(--accent-color);
}
sm-chip:first-of-type {
--border-radius: 0.3rem 0 0 0.3rem; --border-radius: 0.3rem 0 0 0.3rem;
} }
strip-option:last-of-type { sm-chip:last-of-type {
--border-radius: 0 0.3rem 0.3rem 0; --border-radius: 0 0.3rem 0.3rem 0;
} }
@ -1015,7 +1076,8 @@ strip-option:last-of-type {
#account_process__steps, #account_process__steps,
#transaction__steps { #transaction__steps {
display: grid; display: flex;
flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
@ -1140,7 +1202,7 @@ strip-option:last-of-type {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.loader-button-wrapper sm-button, .loader-button-wrapper button[type=submit],
.loader-button-wrapper slide-button { .loader-button-wrapper slide-button {
width: 100%; width: 100%;
z-index: 1; z-index: 1;
@ -1150,7 +1212,7 @@ strip-option:last-of-type {
-webkit-clip-path: circle(100%); -webkit-clip-path: circle(100%);
clip-path: circle(100%); clip-path: circle(100%);
} }
.loader-button-wrapper sm-button.clip, .loader-button-wrapper button[type=submit].clip,
.loader-button-wrapper slide-button.clip { .loader-button-wrapper slide-button.clip {
pointer-events: none; pointer-events: none;
-webkit-clip-path: circle(0); -webkit-clip-path: circle(0);
@ -1246,8 +1308,11 @@ strip-option:last-of-type {
} }
@media screen and (max-width: 640px) { @media screen and (max-width: 640px) {
sm-button { #user_profile_button {
--padding: 0.9rem 1.6rem; grid-area: 1/1/2/2;
}
theme-toggle {
grid-area: 1/2/2/3;
} }
#dashboard { #dashboard {
padding: 0 1.5rem; padding: 0 1.5rem;
@ -1298,8 +1363,14 @@ strip-option:last-of-type {
.activity-card--account .grid { .activity-card--account .grid {
text-align: right; text-align: right;
} }
.hide-on-mobile {
display: none;
}
} }
@media screen and (min-width: 640px) { @media screen and (min-width: 640px) {
sm-popup {
--width: 26rem;
}
.h1 { .h1 {
font-size: 2rem; font-size: 2rem;
} }
@ -1315,6 +1386,9 @@ strip-option:last-of-type {
#confirmation_popup { #confirmation_popup {
--width: 24rem; --width: 24rem;
} }
.popup__header {
padding: 1rem 1.5rem 0 1.5rem;
}
.page-layout { .page-layout {
grid-template-columns: 1fr 90vw 1fr; grid-template-columns: 1fr 90vw 1fr;
} }
@ -1349,6 +1423,9 @@ strip-option:last-of-type {
.nav-item__title { .nav-item__title {
display: none; display: none;
} }
#main_header {
grid-template-columns: 1fr auto auto;
}
#user_section { #user_section {
grid-area: user-section; grid-area: user-section;
background-color: rgba(var(--background-color), 1); background-color: rgba(var(--background-color), 1);

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -16,37 +16,31 @@ body {
} }
body { body {
&, --accent-color: #2672ff;
* { --accent-color--light: rgba(38, 114, 255, 0.06);
--accent-color: #2672ff; --text-color: 20, 20, 20;
--accent-color--light: rgba(38, 114, 255, 0.06); --foreground-color: 252, 253, 255;
--text-color: 20, 20, 20; --background-color: 241, 243, 248;
--foreground-color: 252, 253, 255; --danger-color: rgb(255, 75, 75);
--background-color: 241, 243, 248; --green: #1cad59;
--danger-color: rgb(255, 75, 75); --yellow: rgb(220, 165, 0);
--green: #1cad59; --loan-color: rgb(255, 171, 93);
--yellow: rgb(220, 165, 0); scrollbar-width: thin;
--loan-color: rgb(255, 171, 93);
scrollbar-width: thin;
}
color: rgba(var(--text-color), 1); color: rgba(var(--text-color), 1);
background: rgba(var(--background-color), 1); background: rgba(var(--background-color), 1);
} }
body[data-theme="dark"] { body[data-theme="dark"] {
&, --accent-color: rgb(170, 190, 255);
* { --accent-color--light: rgba(231, 239, 255, 0.06);
--accent-color: rgb(170, 190, 255); --text-color: 200, 200, 200;
--accent-color--light: rgba(231, 239, 255, 0.06); --foreground-color: 27, 28, 29;
--text-color: 200, 200, 200; --background-color: 21, 22, 22;
--foreground-color: 27, 28, 29; --danger-color: rgb(255, 106, 106);
--background-color: 21, 22, 22; --green: #00e676;
--danger-color: rgb(255, 106, 106); --yellow: rgb(255, 213, 5);
--green: #00e676; --loan-color: rgb(255, 232, 170);
--yellow: rgb(255, 213, 5);
--loan-color: rgb(255, 232, 170);
}
} }
p, p,
@ -83,45 +77,76 @@ button,
background-color: transparent; background-color: transparent;
overflow: hidden; overflow: hidden;
color: inherit; color: inherit;
cursor: pointer;
transition: transform 0.3s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} align-items: center;
.button { font-size: 0.9rem;
white-space: nowrap;
padding: 0.6rem 1rem;
border-radius: 0.3rem;
font-weight: 500; font-weight: 500;
font-size: 0.8rem; white-space: nowrap;
background-color: rgba(var(--text-color), 0.06); padding: 0.8rem;
&--primary { border-radius: 0.3rem;
background-color: var(--accent-color); justify-content: center;
color: rgba(var(--background-color), 1); &:focus-visible {
outline: var(--accent-color) solid medium;
}
&:not(:disabled) {
cursor: pointer;
} }
} }
button:disabled { .button {
opacity: 0.5; background-color: rgba(var(--text-color), 0.02);
} border: solid thin rgba(var(--text-color), 0.06);
&--primary {
color: rgba(var(--background-color), 1);
background-color: var(--accent-color);
a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
sm-button {
--border-radius: 0.5rem;
--padding: 0.7rem 1rem;
&[variant="primary"] {
.icon { .icon {
fill: rgba(var(--background-color), 1); fill: rgba(var(--background-color), 1);
} }
} }
&--colored {
&[disabled] { color: var(--accent-color);
.icon { .icon {
fill: rgba(var(--text-color), 0.6); fill: var(--accent-color);
} }
} }
&--danger {
background-color: #ff737310;
color: var(--danger-color);
.icon {
fill: var(--danger-color);
}
}
&--small {
padding: 0.4rem 0.6rem;
}
&--outlined {
border: solid rgba(var(--text-color), 0.3) 0.1rem;
background-color: rgba(var(--foreground-color), 1);
}
&--transparent {
background-color: transparent;
}
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
filter: saturate(0);
}
.cta {
text-transform: uppercase;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.05em;
padding: 0.8rem 1rem;
}
a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
} }
ul { ul {
@ -141,7 +166,7 @@ ul {
pointer-events: none; pointer-events: none;
} }
.hide-completely { .hidden {
display: none !important; display: none !important;
} }
@ -335,26 +360,39 @@ ul {
fill: var(--accent-color); fill: var(--accent-color);
} }
} }
#confirmation_popup { #confirmation_popup,
#prompt_popup {
flex-direction: column; flex-direction: column;
h4 { h4 {
font-weight: 500; font-size: 1.2rem;
margin-bottom: 0.5rem; margin-bottom: 1rem;
}
sm-button {
margin: 0;
} }
.flex { .flex {
padding: 0;
margin-top: 1rem; margin-top: 1rem;
}
}
sm-button:first-of-type { .popup__header {
margin-right: 0.6rem; position: relative;
margin-left: auto; display: grid;
} gap: 0.5rem;
width: 100%;
padding: 0 1.5rem;
align-items: center;
& > * {
grid-row: 1;
}
h3,
h4 {
grid-column: 1/-1;
justify-self: center;
align-self: center;
}
&__close {
grid-column: 1;
margin-left: -1rem;
justify-self: flex-start;
} }
} }
@ -481,6 +519,7 @@ details {
place-content: center; place-content: center;
text-align: center; text-align: center;
sm-spinner { sm-spinner {
justify-self: center;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
} }
@ -534,7 +573,14 @@ details {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto;
}
#user_profile_button {
gap: 0.5rem;
background-color: rgba(var(--text-color), 0.06);
border-radius: 2rem;
font-size: 0.8rem;
padding: 0.6rem 0.8rem;
} }
#subpage_container { #subpage_container {
grid-area: subpages; grid-area: subpages;
@ -592,12 +638,12 @@ details {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
} }
} }
strip-select { sm-chips {
--gap: 0; --gap: 0;
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
border-radius: 0.3rem; border-radius: 0.3rem;
} }
strip-option { sm-chip {
text-transform: uppercase; text-transform: uppercase;
font-weight: 500; font-weight: 500;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@ -605,6 +651,10 @@ strip-option {
--border-radius: 0; --border-radius: 0;
--active-option-color: rgba(var(--background-color), 1); --active-option-color: rgba(var(--background-color), 1);
--active-option-background-color: var(--accent-color); --active-option-background-color: var(--accent-color);
&[selected] {
color: rgba(var(--background-color), 1);
--background: var(--accent-color);
}
&:first-of-type { &:first-of-type {
--border-radius: 0.3rem 0 0 0.3rem; --border-radius: 0.3rem 0 0 0.3rem;
} }
@ -982,7 +1032,8 @@ strip-option {
} }
#account_process__steps, #account_process__steps,
#transaction__steps { #transaction__steps {
display: grid; display: flex;
flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.account-step { .account-step {
@ -1092,7 +1143,7 @@ strip-option {
position: relative; position: relative;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
sm-button, button[type="submit"],
slide-button { slide-button {
width: 100%; width: 100%;
z-index: 1; z-index: 1;
@ -1173,8 +1224,11 @@ strip-option {
padding-bottom: 3rem; padding-bottom: 3rem;
} }
@media screen and (max-width: 640px) { @media screen and (max-width: 640px) {
sm-button { #user_profile_button {
--padding: 0.9rem 1.6rem; grid-area: 1/1/2/2;
}
theme-toggle {
grid-area: 1/2/2/3;
} }
#dashboard { #dashboard {
padding: 0 1.5rem; padding: 0 1.5rem;
@ -1229,8 +1283,14 @@ strip-option {
text-align: right; text-align: right;
} }
} }
.hide-on-mobile {
display: none;
}
} }
@media screen and (min-width: 640px) { @media screen and (min-width: 640px) {
sm-popup {
--width: 26rem;
}
.h1 { .h1 {
font-size: 2rem; font-size: 2rem;
} }
@ -1249,6 +1309,9 @@ strip-option {
#confirmation_popup { #confirmation_popup {
--width: 24rem; --width: 24rem;
} }
.popup__header {
padding: 1rem 1.5rem 0 1.5rem;
}
.page-layout { .page-layout {
grid-template-columns: 1fr 90vw 1fr; grid-template-columns: 1fr 90vw 1fr;
} }
@ -1285,7 +1348,9 @@ strip-option {
display: none; display: none;
} }
} }
#main_header {
grid-template-columns: 1fr auto auto;
}
#user_section { #user_section {
grid-area: user-section; grid-area: user-section;
background-color: rgba(var(--background-color), 1); background-color: rgba(var(--background-color), 1);

View File

@ -6,8 +6,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RanchiMall Bank</title> <title>RanchiMall Bank</title>
<script src="components.js" defer></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js" <script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js"
integrity="sha512-Wt1bJGtlnMtGP0dqNFH1xlkLBNpEodaiQ8ZN5JLA5wpc1sUlk/O5uuOMNgvzddzkpvZ9GLyYNa8w2s7rqiTk5Q==" integrity="sha512-Wt1bJGtlnMtGP0dqNFH1xlkLBNpEodaiQ8ZN5JLA5wpc1sUlk/O5uuOMNgvzddzkpvZ9GLyYNa8w2s7rqiTk5Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
@ -15,6 +13,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
<script src="scripts/components.js" defer></script>
<script id="floGlobals"> <script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */ /* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = { const floGlobals = {
@ -43,21 +43,17 @@
</script> </script>
</head> </head>
<body onload="onLoadStartUp()" class="hide-completely"> <body onload="onLoadStartUp()" class="hidden">
<sm-notifications id="notification_drawer"></sm-notifications> <sm-notifications id="notification_drawer"></sm-notifications>
<audio id="notification_sound">
<source src="https://rmservices.duckdns.org/files/notification-sound.mp3" type="audio/mpeg">
<source src="https://rmservices.duckdns.org/files/notification-sound.ogg" type="audio/ogg">
</audio>
<sm-popup id="confirmation_popup"> <sm-popup id="confirmation_popup">
<h4 id="confirm_title"></h4> <h4 id="confirm_title"></h4>
<p id="confirm_message"></p> <p id="confirm_message"></p>
<div class="flex align-center"> <div class="flex align-center gap-0-5 margin-left-auto">
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button> <button class="button cancel-button">Cancel</button>
<sm-button variant="no-outline" class="submit-btn">OK</sm-button> <button class="button button--primary confirm-button">OK</button>
</div> </div>
</sm-popup> </sm-popup>
<article id="landing" class="page page-layout hide-completely"> <article id="landing" class="page page-layout hidden">
<header class="flex space-between"> <header class="flex space-between">
<div class="logo"> <div class="logo">
<svg class="main-logo" viewBox="0 0 27.25 32"> <svg class="main-logo" viewBox="0 0 27.25 32">
@ -82,7 +78,7 @@
</div> </div>
</div> </div>
</article> </article>
<article id="sign_in" class="page page-layout hide-completely"> <article id="sign_in" class="page page-layout hidden">
<header> <header>
<div class="logo"> <div class="logo">
<svg class="main-logo" viewBox="0 0 27.25 32"> <svg class="main-logo" viewBox="0 0 27.25 32">
@ -101,14 +97,14 @@
<sm-form> <sm-form>
<sm-input id="private_key_field" type="password" placeholder="FLO private key" <sm-input id="private_key_field" type="password" placeholder="FLO private key"
error-text="Private key is invalid" data-private-key required></sm-input> error-text="Private key is invalid" data-private-key required></sm-input>
<sm-button id="sign_in_button" variant="primary" disabled>Sign In</sm-button> <button id="sign_in_button" class="button button--primary" type="submit" disabled>Sign In</button>
</sm-form> </sm-form>
<p> <p>
New here? <a href="#/sign_up">get your FLO login credentials</a> New here? <a href="#/sign_up">get your FLO login credentials</a>
</p> </p>
</section> </section>
</article> </article>
<article id="sign_up" class="page page-layout hide-completely"> <article id="sign_up" class="page page-layout hidden">
<header> <header>
<div class="logo"> <div class="logo">
<svg class="main-logo" viewBox="0 0 27.25 32"> <svg class="main-logo" viewBox="0 0 27.25 32">
@ -134,7 +130,7 @@
<sm-copy id="generated_private_key"></sm-copy> <sm-copy id="generated_private_key"></sm-copy>
</div> </div>
</div> </div>
<sm-button id="sign_up_button" variant="primary">Sign in with these credentials</sm-button> <button id="sign_up_button" class="button button--primary">Sign in with these credentials</button>
<strong class="warning"> <strong class="warning">
Keep your private key secure and don't share with anyone. Keep your private key secure and don't share with anyone.
Once lost there is no way to recover private key. Once lost there is no way to recover private key.
@ -145,9 +141,9 @@
<sm-spinner></sm-spinner> <sm-spinner></sm-spinner>
<h4>Loading RanchiMall Bank</h4> <h4>Loading RanchiMall Bank</h4>
</article> </article>
<article id="home" class="page hide-completely"> <article id="home" class="page hidden">
<header id="main_header"> <header id="main_header">
<div class="logo"> <div class="logo hide-on-mobile">
<svg class="main-logo" viewBox="0 0 27.25 32"> <svg class="main-logo" viewBox="0 0 27.25 32">
<title>RanchiMall</title> <title>RanchiMall</title>
<path <path
@ -158,18 +154,26 @@
</div> </div>
</div> </div>
<theme-toggle></theme-toggle> <theme-toggle></theme-toggle>
<button class="icon-button hide-on-desktop" onclick="toggleUserSection()"> <button id="user_profile_button" class="icon-button" onclick="openPopup('profile_popup')">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" viewBox="0 0 24 24" width="24px" fill="#000000">
clip-rule="evenodd" /> <g>
<rect fill="none" height="24" width="24" />
</g>
<g>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4c1.93 0 3.5 1.57 3.5 3.5S13.93 13 12 13s-3.5-1.57-3.5-3.5S10.07 6 12 6zm0 14c-2.03 0-4.43-.82-6.14-2.88C7.55 15.8 9.68 15 12 15s4.45.8 6.14 2.12C16.43 19.18 14.03 20 12 20z" />
</g>
</svg> </svg>
<div id="user_profile_id" class="overflow-ellipsis"></div>
</button> </button>
</header> </header>
<nav id="main_nav"> <nav id="main_nav">
<a href="#/home/dashboard" class="nav-item nav-item--active user-option"> <a href="#/home/dashboard" class="nav-item nav-item--active user-option">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
<path fill="#000000">
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" /> <path d="M0 0h24v24H0z" fill="none" />
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" />
</svg> </svg>
<span class="nav-item__title"> <span class="nav-item__title">
Dashboard Dashboard
@ -185,31 +189,49 @@
</span> </span>
</a> </a>
<a id="notifications_page_button" href="#/home/notifications" class="nav-item user-option"> <a id="notifications_page_button" href="#/home/notifications" class="nav-item user-option">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path <path
d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" /> d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
</svg> </svg>
<span class="nav-item__title"> <span class="nav-item__title">
Notifications Notifications
</span> </span>
</a> </a>
<a href="#/home/history" class="nav-item user-option"> <a href="#/home/history" class="nav-item user-option">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
<path viewBox="0 0 24 24" width="24px" fill="#000000">
d="M5 12a1 1 0 102 0V6.414l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L5 6.414V12zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" /> <path d="M0,0h24v24H0V0z" fill="none" />
<g>
<path
d="M19.5,3.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2L7.5,3.5L6,2v14H3v3c0,1.66,1.34,3,3,3h12c1.66,0,3-1.34,3-3V2 L19.5,3.5z M19,19c0,0.55-0.45,1-1,1s-1-0.45-1-1v-3H8V5h11V19z" />
<rect height="2" width="6" x="9" y="7" />
<rect height="2" width="2" x="16" y="7" />
<rect height="2" width="6" x="9" y="10" />
<rect height="2" width="2" x="16" y="10" />
</g>
</svg> </svg>
<span class="nav-item__title"> <span class="nav-item__title">
History History
</span> </span>
</a> </a>
<a href="#/home/settings" class="nav-item"> <a href="#/home/settings" class="nav-item hide-on-desktop">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
<path fill-rule="evenodd" viewBox="0 0 24 24" width="24px" fill="#000000">
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" <g>
clip-rule="evenodd" /> <rect fill="none" height="24" width="24" />
</g>
<g>
<g>
<g>
<path
d="M11,5.08V2C6,2.5,2,6.81,2,12s4,9.5,9,10v-3.08c-3-0.48-6-3.4-6-6.92S8,5.56,11,5.08z M18.97,11H22c-0.47-5-4-8.53-9-9 v3.08C16,5.51,18.54,8,18.97,11z M13,18.92V22c5-0.47,8.53-4,9-9h-3.03C18.54,16,16,18.49,13,18.92z" />
</g>
</g>
</g>
</svg> </svg>
<span class="nav-item__title"> <span class="nav-item__title">
Settings Account
</span> </span>
</a> </a>
</nav> </nav>
@ -264,10 +286,10 @@
</svg> </svg>
Accounts Accounts
</h4> </h4>
<strip-select id="accounts_status_selector"> <sm-chips id="accounts_status_selector">
<strip-option value="active" selected>Active</strip-option> <sm-chip value="active" selected>Active</sm-chip>
<strip-option value="closed">Closed</strip-option> <sm-chip value="closed">Closed</sm-chip>
</strip-select> </sm-chips>
</div> </div>
<div id="user_accounts" class="observe-empty-state grid"></div> <div id="user_accounts" class="observe-empty-state grid"></div>
<div id="accounts_empty_state" class="empty-state grid justify-center gap-0-5"> <div id="accounts_empty_state" class="empty-state grid justify-center gap-0-5">
@ -302,7 +324,7 @@
</div> </div>
</section> </section>
</div> </div>
<div id="notifications" class="sub-page hide-completely"> <div id="notifications" class="sub-page hidden">
<div id="all_responses_list" class="observe-empty-state grid"></div> <div id="all_responses_list" class="observe-empty-state grid"></div>
<div class="empty-state grid gap-0-5 grid justify-center"> <div class="empty-state grid gap-0-5 grid justify-center">
<div class="empty-state__icon"> <div class="empty-state__icon">
@ -348,7 +370,7 @@
<p>Updates related to your transaction requests will appear here.</h4> <p>Updates related to your transaction requests will appear here.</h4>
</div> </div>
</div> </div>
<div id="history" class="sub-page hide-completely"> <div id="history" class="sub-page hidden">
<div id="all_requests_list" class="observe-empty-state grid"></div> <div id="all_requests_list" class="observe-empty-state grid"></div>
<div class="empty-state grid gap-0-5 grid justify-center"> <div class="empty-state grid gap-0-5 grid justify-center">
<div class="empty-state__icon"> <div class="empty-state__icon">
@ -396,19 +418,14 @@
<p>You transaction activities will appear here.</h4> <p>You transaction activities will appear here.</h4>
</div> </div>
</div> </div>
<div id="settings" class="sub-page hide-completely"> <div id="admin" class="sub-page hidden">
<h5>My FLO ID</h5>
<sm-copy id="user_flo_id"></sm-copy>
<sm-button id="sign_out_button" onclick="signOut()">Sign out</sm-button>
</div>
<div id="admin" class="sub-page hide-completely">
<div id="admin__header"> <div id="admin__header">
<h3 class="h3">Requests</h3> <h3 class="h3">Requests</h3>
<strip-select id="admin_request_selector"> <sm-chips id="admin_request_selector">
<strip-option value="pending" selected>Pending</strip-option> <sm-chip value="pending" selected>Pending</sm-chip>
<strip-option value="completed">Completed</strip-option> <sm-chip value="completed">Completed</sm-chip>
<strip-option value="failed">Failed</strip-option> <sm-chip value="failed">Failed</sm-chip>
</strip-select> </sm-chips>
</div> </div>
<div id="admin_requests_container" class="observe-empty-state"></div> <div id="admin_requests_container" class="observe-empty-state"></div>
<div class="empty-state grid gap-0-5 grid justify-center"> <div class="empty-state grid gap-0-5 grid justify-center">
@ -470,10 +487,19 @@
</div> </div>
<div class="user-option"> <div class="user-option">
<h4 class="flex align-center"> <h4 class="flex align-center">
<svg xmlns="http://www.w3.org/2000/svg" class="icon button__icon--left" viewBox="0 0 20 20" <svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg"
fill="currentColor"> enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" /> <g>
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" /> <rect fill="none" height="24" width="24" />
</g>
<g>
<g>
<g>
<path
d="M11,5.08V2C6,2.5,2,6.81,2,12s4,9.5,9,10v-3.08c-3-0.48-6-3.4-6-6.92S8,5.56,11,5.08z M18.97,11H22c-0.47-5-4-8.53-9-9 v3.08C16,5.51,18.54,8,18.97,11z M13,18.92V22c5-0.47,8.53-4,9-9h-3.03C18.54,16,16,18.49,13,18.92z" />
</g>
</g>
</g>
</svg> </svg>
Account summary Account summary
</h4> </h4>
@ -513,11 +539,11 @@
</div> </div>
<div> <div>
<h4 class="flex align-center space-between"> <h4 class="flex align-center space-between">
<svg xmlns="http://www.w3.org/2000/svg" class="icon button__icon--left" viewBox="0 0 20 20" <svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" height="24px"
fill="currentColor"> viewBox="0 0 24 24" width="24px" fill="#000000">
<path fill-rule="evenodd" <path d="M0 0h24v24H0z" fill="none" />
d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" <path
clip-rule="evenodd" /> d="M21 18v1c0 1.1-.9 2-2 2H5c-1.11 0-2-.9-2-2V5c0-1.1.89-2 2-2h14c1.1 0 2 .9 2 2v1h-9c-1.11 0-2 .9-2 2v8c0 1.1.89 2 2 2h9zm-9-2h10V8H12v8zm4-2.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" />
</svg> </svg>
My balance My balance
<button id="refresh_balance_button" class="button justify-right" onclick="refreshBalance(this)"> <button id="refresh_balance_button" class="button justify-right" onclick="refreshBalance(this)">
@ -548,7 +574,7 @@
</div> </div>
</section> </section>
</article> </article>
<article id="deposit" class="page hide-completely page-layout"> <article id="deposit" class="page hidden page-layout">
<a class="button icon-button justify-self-start" href="#/home/dashboard"> <a class="button icon-button justify-self-start" href="#/home/dashboard">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" <path fill-rule="evenodd"
@ -580,7 +606,7 @@
<div id="deposit_button_wrapper" class="loader-button-wrapper"></div> <div id="deposit_button_wrapper" class="loader-button-wrapper"></div>
</sm-form> </sm-form>
</article> </article>
<article id="loan" class="page hide-completely page-layout"> <article id="loan" class="page hidden page-layout">
<a class="button icon-button justify-self-start" href="#/home/dashboard"> <a class="button icon-button justify-self-start" href="#/home/dashboard">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" <path fill-rule="evenodd"
@ -610,7 +636,7 @@
<div id="loan_button_wrapper" class="loader-button-wrapper"></div> <div id="loan_button_wrapper" class="loader-button-wrapper"></div>
</sm-form> </sm-form>
</article> </article>
<article id="result" class="page hide-completely page-layout"> <article id="result" class="page hidden page-layout">
<section class="flex direction-column"> <section class="flex direction-column">
<div id="result__icon"></div> <div id="result__icon"></div>
<h4 id="result__title"></h4> <h4 id="result__title"></h4>
@ -618,7 +644,7 @@
<a id="result__back_button" href="#/home/dashboard" class="button">Done</a> <a id="result__back_button" href="#/home/dashboard" class="button">Done</a>
</section> </section>
</article> </article>
<article id="transaction" class="page hide-completely page-layout"> <article id="transaction" class="page hidden page-layout">
<div class="flex align-center page__header"> <div class="flex align-center page__header">
<button class="justify-self-start button__icon--left" onclick="history.back()"> <button class="justify-self-start button__icon--left" onclick="history.back()">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
@ -640,7 +666,7 @@
<div id="transaction__steps"></div> <div id="transaction__steps"></div>
</section> </section>
</article> </article>
<article id="account" class="page hide-completely page-layout"> <article id="account" class="page hidden page-layout">
<div class="flex align-center page__header"> <div class="flex align-center page__header">
<button class="justify-self-start button__icon--left" onclick="history.back()"> <button class="justify-self-start button__icon--left" onclick="history.back()">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
@ -665,13 +691,27 @@
<span id="account_detail__interest"></span> <span id="account_detail__interest"></span>
</div> </div>
</div> </div>
<sm-button id="account_action_button" onclick="processAccount()" variant="primary"></sm-button> <button id="account_action_button" onclick="processAccount()" class="button button--primary"></button>
</div> </div>
<div id="account_process"> <div id="account_process">
<div id="account_process__steps"></div> <div id="account_process__steps"></div>
</div> </div>
</section> </section>
</article> </article>
<sm-popup id="profile_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close justify-self-start" onclick="closePopup()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Profile</h3>
</header>
<div id="profile_popup__content" class="grid gap-3"></div>
</sm-popup>
<template id="admin_request_template"> <template id="admin_request_template">
<li class="activity-card activity-card--admin"> <li class="activity-card activity-card--admin">
<div class="activity-card__icon"></div> <div class="activity-card__icon"></div>
@ -735,30 +775,33 @@
</a> </a>
</details> </details>
</template> </template>
<script>
/*jshint esversion: 8 */
/**
* @yaireo/relative-time - javascript function to transform timestamp or date to local relative-time
*
* @version v1.0.0
* @homepage https://github.com/yairEO/relative-time
*/
!function (e, t) { var o = o || {}; "function" == typeof o && o.amd ? o([], t) : "object" == typeof exports && "object" == typeof module ? module.exports = t() : "object" == typeof exports ? exports.RelativeTime = t() : e.RelativeTime = t() }(this, (function () { const e = { year: 31536e6, month: 2628e6, day: 864e5, hour: 36e5, minute: 6e4, second: 1e3 }, t = "en", o = { numeric: "auto" }; function n(e) { e = { locale: (e = e || {}).locale || t, options: { ...o, ...e.options } }, this.rtf = new Intl.RelativeTimeFormat(e.locale, e.options) } return n.prototype = { from(t, o) { const n = t - (o || new Date); for (let t in e) if (Math.abs(n) > e[t] || "second" == t) return this.rtf.format(Math.round(n / e[t]), t) } }, n }));
const relativeTime = new RelativeTime({ style: 'narrow' });
</script>
<script id="ui_utils"> <script id="ui_utils">
// Global variables // Global variables
const domRefs = {}; const { html, render: renderElem } = uhtml;
let timerId; const domRefs = {}
const currentYear = new Date().getFullYear();
//Checks for internet connection status //Checks for internet connection status
if (!navigator.onLine) if (!navigator.onLine)
notify( floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
"There seems to be a problem connecting to the internet, Please check you internet connection.", window.addEventListener('offline', () => {
"error", floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
{ sound: true } })
); window.addEventListener('online', () => {
window.addEventListener("offline", () => { getRef('notification_drawer').remove(floGlobals.connectionErrorNotification)
notify( notify('We are back online.', 'success')
"There seems to be a problem connecting to the internet, Please check you internet connection.", })
"error",
{ pinned: true, sound: true }
);
});
window.addEventListener("online", () => {
getRef("notification_drawer").clearAll();
notify("We are back online.", "success");
});
// Use instead of document.getElementById // Use instead of document.getElementById
function getRef(elementId) { function getRef(elementId) {
@ -796,124 +839,93 @@
return elem return elem
} }
// Use when a function needs to be executed after user finishes changes let zIndex = 50
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
// Limits the rate of function execution
function throttle(func, delay) {
// If setTimeout is already scheduled, no need to do anything
if (timerId) {
return;
}
// Schedule a setTimeout after delay seconds
timerId = setTimeout(function () {
func();
// Once setTimeout function execution is finished, timerId = undefined so that in
// the next scroll event function execution can be scheduled by the setTimeout
timerId = undefined;
}, delay);
}
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.items.length == 0)
return "Underflow";
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
}
let popupStack = new Stack()
let zIndex = 10
// function required for popups or modals to appear // function required for popups or modals to appear
function showPopup(popupId, pinned) { function openPopup(popupId, pinned) {
zIndex++ zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`) getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
popupStack = getRef(popupId).show({ pinned, popupStack }) return getRef(popupId).show({ pinned })
return getRef(popupId);
} }
// hides the popup or modal // hides the popup or modal
function hidePopup() { function closePopup(options = {}) {
if (popupStack.peek() === undefined) if (popupStack.peek() === undefined)
return; return;
popupStack.peek().popup.hide() popupStack.peek().popup.hide(options)
} }
document.addEventListener('popupopened', e => {
switch (e.target.id) {
case 'profile_popup':
renderElem(getRef('profile_popup__content'), render.profile())
break;
}
})
document.addEventListener('popupclosed', e => {
switch (e.target.id) {
case 'profile_popup':
renderElem(getRef('profile_popup__content'), html``)
break;
}
zIndex--;
})
// displays a popup for asking permission. Use this instead of JS confirm // displays a popup for asking permission. Use this instead of JS confirm
const getConfirmation = (title, message, cancelText = 'Cancel', confirmText = 'OK') => { const getConfirmation = (title, options = {}) => {
return new Promise(resolve => { return new Promise(resolve => {
showPopup('confirmation_popup', true) const { message = '', cancelText = 'Cancel', confirmText = 'OK', danger = false } = options
getRef('confirm_title').textContent = title; getRef('confirm_title').innerText = title;
getRef('confirm_message').textContent = message; getRef('confirm_message').innerText = message;
let cancelButton = getRef('confirmation_popup').children[2].children[0], const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
submitButton = getRef('confirmation_popup').children[2].children[1] const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
submitButton.textContent = confirmText confirmButton.textContent = confirmText
cancelButton.textContent = cancelText cancelButton.textContent = cancelText
submitButton.onclick = () => { if (danger)
hidePopup() confirmButton.classList.add('button--danger')
resolve(true); else
confirmButton.classList.remove('button--danger')
const { opened, closed } = openPopup('confirmation_popup')
confirmButton.onclick = () => {
closePopup({ payload: true })
} }
cancelButton.onclick = () => { cancelButton.onclick = () => {
hidePopup() closePopup()
resolve(false);
} }
closed.then((payload) => {
confirmButton.onclick = null
cancelButton.onclick = null
if (payload)
resolve(true)
else
resolve(false)
})
}) })
} }
//Function for displaying toast notifications. pass in error for mode param if you want to show an error. //Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, options = {}) { function notify(message, mode, options = {}) {
const { pinned = false, sound = false } = options
let icon let icon
switch (mode) { switch (mode) {
case 'success': case 'success':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>` icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
break; break;
case 'error': case 'error':
icon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>` icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
options.pinned = true
break; break;
} }
getRef("notification_drawer").push(message, { pinned, icon });
if (navigator.onLine && sound) {
getRef("notification_sound").currentTime = 0;
getRef("notification_sound").play();
}
if (mode === 'error') { if (mode === 'error') {
console.error(message) console.error(message)
} }
return getRef("notification_drawer").push(message, { icon, ...options });
} }
function getFormattedTime(time, relative) { function getFormattedTime(timestamp, format) {
try { try {
if (String(time).indexOf('_')) timestamp = parseInt(timestamp)
time = String(time).split('_')[0] if (String(timestamp).length < 13)
const intTime = parseInt(time) timestamp *= 1000
if (String(intTime).length < 13) let [day, month, date, year] = new Date(timestamp).toString().split(' '),
time *= 1000 minutes = new Date(timestamp).getMinutes(),
let timeFrag = new Date(intTime).toString().split(' '), hours = new Date(timestamp).getHours(),
day = timeFrag[0],
month = timeFrag[1],
date = timeFrag[2],
year = timeFrag[3],
minutes = new Date(intTime).getMinutes(),
hours = new Date(intTime).getHours(),
currentTime = new Date().toString().split(' ') currentTime = new Date().toString().split(' ')
minutes = minutes < 10 ? `0${minutes}` : minutes minutes = minutes < 10 ? `0${minutes}` : minutes
@ -926,40 +938,30 @@
finalHours = `${hours}:${minutes}` finalHours = `${hours}:${minutes}`
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM` finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
if (relative) { switch (format) {
if (year == currentYear) { case 'date-only':
if (currentTime[1] === month) { return `${month} ${date}, ${year}`;
const dateDiff = (parseInt(currentTime[2]) - parseInt(date)) break;
if (dateDiff === 0) case 'time-only':
return `${finalHours}`; return finalHours;
else if (dateDiff === 1) case 'relative':
return `Yesterday`; return relativeTime.from(timestamp)
else if (dateDiff > 1 && dateDiff < 8) default:
return currentTime[0]; return `${month} ${date}, ${year} at ${finalHours}`;
else
return ` ${date} ${month}`;
}
else
return ` ${date} ${month}`;
}
else
return `${month} ${year}`;
} }
else
return `${month} ${date} ${year}, ${finalHours}`;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return time; return timestamp;
} }
} }
window.addEventListener('hashchange', e => showPage(window.location.hash)) window.addEventListener('hashchange', e => showPage(window.location.hash))
window.addEventListener("load", () => { window.addEventListener("load", () => {
document.body.classList.remove('hide-completely') document.body.classList.remove('hidden')
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex) document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
document.addEventListener('keyup', (e) => { document.addEventListener('keyup', (e) => {
if (e.code === 'Escape') { if (e.code === 'Escape') {
hidePopup() closePopup()
} }
}) })
document.addEventListener('copy', () => { document.addEventListener('copy', () => {
@ -1049,8 +1051,8 @@
showActionResult(params) showActionResult(params)
break; break;
} }
document.querySelector('.page:not(.hide-completely)')?.classList.add('hide-completely') document.querySelector('.page:not(.hidden)')?.classList.add('hidden')
getRef(pageId)?.classList.remove('hide-completely') getRef(pageId)?.classList.remove('hidden')
getRef(pageId)?.animate([ getRef(pageId)?.animate([
{ {
opacity: 0, opacity: 0,
@ -1108,8 +1110,8 @@
} }
break; break;
} }
document.querySelector('.sub-page:not(.hide-completely)')?.classList.add('hide-completely') document.querySelector('.sub-page:not(.hidden)')?.classList.add('hidden')
getRef(subPageId)?.classList.remove('hide-completely') getRef(subPageId)?.classList.remove('hidden')
getRef('main_nav').querySelectorAll('.nav-item--active').forEach(item => item.classList.remove('nav-item--active')) getRef('main_nav').querySelectorAll('.nav-item--active').forEach(item => item.classList.remove('nav-item--active'))
getRef('main_nav').querySelector(`.nav-item[href="#/${pageId}/${subPageId ? subPageId : ''}"]`).classList.add('nav-item--active') getRef('main_nav').querySelector(`.nav-item[href="#/${pageId}/${subPageId ? subPageId : ''}"]`).classList.add('nav-item--active')
getRef(subPageId)?.animate([ getRef(subPageId)?.animate([
@ -1338,7 +1340,7 @@
card.setAttribute('href', `#/transaction?requestID=${requestID}`) card.setAttribute('href', `#/transaction?requestID=${requestID}`)
card.querySelector('.activity-card__icon').innerHTML = icon card.querySelector('.activity-card__icon').innerHTML = icon
card.querySelector('.activity-card__title').textContent = action card.querySelector('.activity-card__title').textContent = action
card.querySelector('.activity-card__time').textContent = getFormattedTime(timestamp, true) card.querySelector('.activity-card__time').textContent = getFormattedTime(timestamp)
card.querySelector('.activity-card__amount').textContent = utils.formatAmount(amount) card.querySelector('.activity-card__amount').textContent = utils.formatAmount(amount)
card.querySelector('.activity-card__status').classList.add(status) card.querySelector('.activity-card__status').classList.add(status)
card.querySelector('.activity-card__status').textContent = status card.querySelector('.activity-card__status').textContent = status
@ -1376,7 +1378,7 @@
} else { } else {
card.querySelector('.activity-card__description').textContent = `Reason: ${reason}` card.querySelector('.activity-card__description').textContent = `Reason: ${reason}`
} }
card.querySelector('.activity-card__time').textContent = getFormattedTime(timestamp, true) card.querySelector('.activity-card__time').textContent = getFormattedTime(timestamp, 'relative')
return card return card
}, },
accountCard(accountDetails = {}) { accountCard(accountDetails = {}) {
@ -1407,6 +1409,17 @@
})) }))
} }
return elem return elem
},
profile() {
return html`
<div class="grid gap-1-5">
<div class="grid gap-0-5">
<b>My FLO address</b>
<sm-copy class="user-flo-id" clip-text value=${myFloID}></sm-copy>
</div>
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
</div>
`;
} }
} }
function getSignedIn() { function getSignedIn() {
@ -1429,7 +1442,7 @@
}) })
} }
function signOut() { function signOut() {
getConfirmation('Sign out?', 'You are about to sign out of the app, continue?', 'Stay', 'Leave') getConfirmation('Sign out?', { message: 'You are about to sign out of the app, continue?', cancelText: 'Stay', confirmText: 'Leave' })
.then(async (res) => { .then(async (res) => {
if (res) { if (res) {
await floDapps.clearCredentials() await floDapps.clearCredentials()
@ -1657,9 +1670,9 @@
renderAccountProgress(index) renderAccountProgress(index)
if (status === 'active' && !getAccountStatus(index).isPending) { if (status === 'active' && !getAccountStatus(index).isPending) {
getRef('account_action_button').classList.remove('hide-completely') getRef('account_action_button').classList.remove('hidden')
} else { } else {
getRef('account_action_button').classList.add('hide-completely') getRef('account_action_button').classList.add('hidden')
} }
getRef('account_action_button').textContent = (type === 'deposit') ? 'Withdraw' : 'Repay' getRef('account_action_button').textContent = (type === 'deposit') ? 'Withdraw' : 'Repay'
} }
@ -1669,12 +1682,12 @@
const { type, netAmt } = bank_app.getUserDetails(myFloID).accounts[index] const { type, netAmt } = bank_app.getUserDetails(myFloID).accounts[index]
let ans let ans
if (type === 'deposit') { if (type === 'deposit') {
ans = await getConfirmation('Withdraw deposit?', 'Are you sure to withdraw this deposit now?', 'No', 'Yes') ans = await getConfirmation('Withdraw deposit?', { message: 'Are you sure to withdraw this deposit now?', cancelText: 'No', confirmText: 'Yes' })
} else { } else {
ans = await getConfirmation('Repay loan?', 'Make sure you have required amount to repay the loan!', 'No', 'Yes') ans = await getConfirmation('Repay loan?', { message: 'Make sure you have required amount to repay the loan!', cancelText: 'No', confirmText: 'Yes' })
} }
if (ans) { if (ans) {
getRef('account_action_button').classList.add('hide-completely') getRef('account_action_button').classList.add('hidden')
const actionToDo = type === 'deposit' ? 'withdraw' : 'Repay' const actionToDo = type === 'deposit' ? 'withdraw' : 'Repay'
getRef('account_process__steps').append(render.accountProgressStep('loading', `Sending ${actionToDo} request`)) getRef('account_process__steps').append(render.accountProgressStep('loading', `Sending ${actionToDo} request`))
if (type === 'deposit') { if (type === 'deposit') {
@ -1715,9 +1728,9 @@
function updateChart() { function updateChart() {
const { depositTotal, loanTotal } = bank_app.getUserDetails(myFloID) const { depositTotal, loanTotal } = bank_app.getUserDetails(myFloID)
if (!depositTotal && !loanTotal) { if (!depositTotal && !loanTotal) {
getRef('account_chart_container').classList.add('hide-completely') getRef('account_chart_container').classList.add('hidden')
} else { } else {
getRef('account_chart_container').classList.remove('hide-completely') getRef('account_chart_container').classList.remove('hidden')
} }
if (accountChart && isChartVisible) { if (accountChart && isChartVisible) {
accountChart.data.datasets[0].data = [depositTotal, loanTotal] accountChart.data.datasets[0].data = [depositTotal, loanTotal]
@ -1775,11 +1788,11 @@
if (entry.isIntersecting) { if (entry.isIntersecting) {
const { depositTotal, loanTotal } = bank_app.getUserDetails(myFloID) const { depositTotal, loanTotal } = bank_app.getUserDetails(myFloID)
if (!depositTotal && !loanTotal) { if (!depositTotal && !loanTotal) {
getRef('account_chart_container').classList.add('hide-completely') getRef('account_chart_container').classList.add('hidden')
} else { } else {
accountChart = renderChart() accountChart = renderChart()
isChartVisible = true isChartVisible = true
getRef('account_chart_container').classList.remove('hide-completely') getRef('account_chart_container').classList.remove('hidden')
} }
} else if (!entry.isIntersecting && accountChart) { } else if (!entry.isIntersecting && accountChart) {
accountChart.destroy() accountChart.destroy()
@ -1855,8 +1868,8 @@
// Sets UX according to supported pointing device // Sets UX according to supported pointing device
const checkTouchSupport = (mql) => { const checkTouchSupport = (mql) => {
if (mql.matches) { if (mql.matches) {
getRef('deposit_button_wrapper').innerHTML = `<sm-button id="deposit_button" variant="primary" onclick="confirmDeposit()" disabled>Deposit</sm-button>` getRef('deposit_button_wrapper').innerHTML = `<button id="deposit_button" class="button button--primary" onclick="confirmDeposit()" disabled>Deposit</button>`
getRef('loan_button_wrapper').innerHTML = `<sm-button id="loan_button" variant="primary" onclick="confirmLoan()" disabled>Request loan</sm-button>` getRef('loan_button_wrapper').innerHTML = `<button id="loan_button" class="button button--primary" onclick="confirmLoan()" disabled>Request loan</button>`
getRef('deposit_form').addEventListener('submit', confirmDeposit) getRef('deposit_form').addEventListener('submit', confirmDeposit)
getRef('loan_form').addEventListener('submit', confirmLoan) getRef('loan_form').addEventListener('submit', confirmLoan)
@ -1881,7 +1894,7 @@
async function confirmDeposit() { async function confirmDeposit() {
const amount = parseFloat(getRef('get_deposit_amount').value) const amount = parseFloat(getRef('get_deposit_amount').value)
const confirm = await getConfirmation('Continue?', `Confirm deposit of ${utils.formatAmount(amount)}?`, 'Cancel', 'Confirm') const confirm = await getConfirmation('Continue?', { message: `Confirm deposit of ${utils.formatAmount(amount)}?`, cancelText: 'Cancel', confirmText: 'Confirm' })
if (confirm) { if (confirm) {
initDeposit() initDeposit()
} }
@ -1904,7 +1917,7 @@
async function confirmLoan(params) { async function confirmLoan(params) {
const amount = parseFloat(getRef('get_loan_amount').value) const amount = parseFloat(getRef('get_loan_amount').value)
const confirm = await getConfirmation('Continue?', `Confirm loan of ${utils.formatAmount(amount)}?`, 'Cancel', 'Confirm') const confirm = await getConfirmation('Continue?', { message: `Confirm loan of ${utils.formatAmount(amount)}?`, cancelText: 'Cancel', confirmText: 'Confirm' })
if (confirm) { if (confirm) {
initLoan() initLoan()
} }
@ -2017,14 +2030,14 @@
const { amount, rtype } = getRequestDetails(uid) const { amount, rtype } = getRequestDetails(uid)
if (isPending && (rtype === 'openDeposit' || rtype === 'openLoan')) { if (isPending && (rtype === 'openDeposit' || rtype === 'openLoan')) {
const action = rtype === 'openDeposit' ? 'Deposit' : 'Loan' const action = rtype === 'openDeposit' ? 'Deposit' : 'Loan'
getRef(`${type}_button_wrapper`).firstElementChild.classList.add('hide-completely') getRef(`${type}_button_wrapper`).firstElementChild.classList.add('hidden')
getRef(`${type}_button`).after(createElement('strong', { getRef(`${type}_button`).after(createElement('strong', {
className: 'warning', className: 'warning',
textContent: `${action} in process. You can't ${type === 'deposit' ? 'deposit' : 'request loan'} until that's completed.`, textContent: `${action} in process. You can't ${type === 'deposit' ? 'deposit' : 'request loan'} until that's completed.`,
})) }))
} }
} else { } else {
getRef(`${type}_button_wrapper`).firstElementChild?.classList.remove('hide-completely') getRef(`${type}_button_wrapper`).firstElementChild?.classList.remove('hidden')
} }
} }
let requestResponsePairs let requestResponsePairs
@ -2063,8 +2076,8 @@
delete floDapps.util.startUpFunctions.loadDataFromAppIDB; delete floDapps.util.startUpFunctions.loadDataFromAppIDB;
floDapps.launchStartUp().then(result => { floDapps.launchStartUp().then(result => {
console.log(result); console.log(result);
getRef('user_flo_id').value = myFloID
// alert(`Welcome FLO_ID: ${myFloID}`); // alert(`Welcome FLO_ID: ${myFloID}`);
getRef('user_profile_id').textContent = myFloID
//App functions.... //App functions....
bank_app.launchApp(reqCallback, responseCallback) bank_app.launchApp(reqCallback, responseCallback)
.then(result => { .then(result => {
@ -2075,12 +2088,12 @@
requestResponsePairs = requestResponse.mappedRequests requestResponsePairs = requestResponse.mappedRequests
if (floGlobals.adminID === myFloID) { if (floGlobals.adminID === myFloID) {
console.log('Logged in as admin') console.log('Logged in as admin')
document.querySelectorAll('.admin-option').forEach(option => option.classList.remove('hide-completely')) document.querySelectorAll('.admin-option').forEach(option => option.classList.remove('hidden'))
document.querySelectorAll('.user-option').forEach(option => option.classList.add('hide-completely')) document.querySelectorAll('.user-option').forEach(option => option.classList.add('hidden'))
} else { } else {
chartObserver.observe(getRef('account_chart_container')) chartObserver.observe(getRef('account_chart_container'))
document.querySelectorAll('.admin-option').forEach(option => option.classList.add('hide-completely')) document.querySelectorAll('.admin-option').forEach(option => option.classList.add('hidden'))
document.querySelectorAll('.user-option').forEach(option => option.classList.remove('hide-completely')) document.querySelectorAll('.user-option').forEach(option => option.classList.remove('hidden'))
} }
if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) { if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) {
window.location.hash = '' window.location.hash = ''

716
scripts/btcOperator.js Normal file
View File

@ -0,0 +1,716 @@
(function (EXPORTS) { //btcOperator v1.0.12
/* BTC Crypto and API Operator */
const btcOperator = EXPORTS;
//This library uses API provided by chain.so (https://chain.so/)
const URL = "https://chain.so/api/v2/";
const fetch_api = btcOperator.fetch = function (api) {
return new Promise((resolve, reject) => {
console.debug(URL + api);
fetch(URL + api).then(response => {
response.json()
.then(result => result.status === "success" ? resolve(result) : reject(result))
.catch(error => reject(error))
}).catch(error => reject(error))
})
};
const SATOSHI_IN_BTC = 1e8;
function get_fee_rate() {
return new Promise((resolve, reject) => {
fetch('https://api.blockchain.info/mempool/fees').then(response => {
if (response.ok)
response.json()
.then(result => resolve(parseFloat((result.regular / SATOSHI_IN_BTC).toFixed(8))))
.catch(error => reject(error));
else
reject(response);
}).catch(error => reject(error))
})
}
const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => {
let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: "rawtx=" + rawTxHex
}).then(response => {
response.text().then(resultText => {
let r = resultText.match(/<result>.*<\/result>/);
if (!r)
reject(resultText);
else {
r = r.pop().replace('<result>', '').replace('</result>', '');
if (r == '1') {
let txid = resultText.match(/<txid>.*<\/txid>/).pop().replace('<txid>', '').replace('</txid>', '');
resolve(txid);
} else if (r == '0') {
let error = resultText.match(/<response>.*<\/response>/).pop().replace('<response>', '').replace('</response>', '');
reject(decodeURIComponent(error.replace(/\+/g, " ")));
} else reject(resultText);
}
}).catch(error => reject(error))
}).catch(error => reject(error))
});
Object.defineProperties(btcOperator, {
newKeys: {
get: () => {
let r = coinjs.newKeys();
r.segwitAddress = coinjs.segwitAddress(r.pubkey).address;
r.bech32Address = coinjs.bech32Address(r.pubkey).address;
return r;
}
},
pubkey: {
value: key => key.length >= 66 ? key : (key.length == 64 ? coinjs.newPubkey(key) : coinjs.wif2pubkey(key).pubkey)
},
address: {
value: (key, prefix = undefined) => coinjs.pubkey2address(btcOperator.pubkey(key), prefix)
},
segwitAddress: {
value: key => coinjs.segwitAddress(btcOperator.pubkey(key)).address
},
bech32Address: {
value: key => coinjs.bech32Address(btcOperator.pubkey(key)).address
}
});
coinjs.compressed = true;
const verifyKey = btcOperator.verifyKey = function (addr, key) {
if (!addr || !key)
return undefined;
switch (coinjs.addressDecode(addr).type) {
case "standard":
return btcOperator.address(key) === addr;
case "multisig":
return btcOperator.segwitAddress(key) === addr;
case "bech32":
return btcOperator.bech32Address(key) === addr;
default:
return null;
}
}
const validateAddress = btcOperator.validateAddress = function (addr) {
if (!addr)
return undefined;
let type = coinjs.addressDecode(addr).type;
if (["standard", "multisig", "bech32"].includes(type))
return type;
else
return false;
}
btcOperator.multiSigAddress = function (pubKeys, minRequired) {
if (!Array.isArray(pubKeys))
throw "pubKeys must be an array of public keys";
else if (pubKeys.length < minRequired)
throw "minimum required should be less than the number of pubKeys";
return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired);
}
//convert from one blockchain to another blockchain (target version)
btcOperator.convert = {};
btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) {
let keyHex = decodeLegacy(source_wif).hex;
if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex))
return null;
else
return encodeLegacy(keyHex, target_version);
}
btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) {
let rawHex = decodeLegacy(source_addr).hex;
if (!rawHex)
return null;
else
return encodeLegacy(rawHex, target_version);
}
btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
let rawHex = decodeLegacy(source_addr).hex;
if (!rawHex)
return null;
else
return encodeBech32(rawHex, target_version, target_hrp);
}
btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
let rawHex = decodeBech32(source_addr).hex;
if (!rawHex)
return null;
else
return encodeBech32(rawHex, target_version, target_hrp);
}
btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) {
let rawHex = decodeBech32(source_addr).hex;
if (!rawHex)
return null;
else
return encodeLegacy(rawHex, target_version);
}
function decodeLegacy(source) {
var decode = coinjs.base58decode(source);
var raw = decode.slice(0, decode.length - 4),
checksum = decode.slice(decode.length - 4);
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
asBytes: true
}), {
asBytes: true
});
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
return null;
let version = raw.shift();
return {
version: version,
hex: Crypto.util.bytesToHex(raw)
}
}
function encodeLegacy(hex, version) {
var bytes = Crypto.util.hexToBytes(hex);
bytes.unshift(version);
var hash = Crypto.SHA256(Crypto.SHA256(bytes, {
asBytes: true
}), {
asBytes: true
});
var checksum = hash.slice(0, 4);
return coinjs.base58encode(bytes.concat(checksum));
}
function decodeBech32(source) {
let decode = coinjs.bech32_decode(source);
if (!decode)
return null;
var raw = decode.data;
let version = raw.shift();
raw = coinjs.bech32_convert(raw, 5, 8, false);
return {
hrp: decode.hrp,
version: version,
hex: Crypto.util.bytesToHex(raw)
}
}
function encodeBech32(hex, version, hrp) {
var bytes = Crypto.util.hexToBytes(hex);
bytes = coinjs.bech32_convert(bytes, 8, 5, true);
bytes.unshift(version)
return coinjs.bech32_encode(hrp, bytes);
}
//BTC blockchain APIs
btcOperator.getBalance = addr => new Promise((resolve, reject) => {
fetch_api(`get_address_balance/BTC/${addr}`)
.then(result => resolve(parseFloat(result.data.confirmed_balance)))
.catch(error => reject(error))
});
const BASE_TX_SIZE = 12,
BASE_INPUT_SIZE = 41,
LEGACY_INPUT_SIZE = 107,
BECH32_INPUT_SIZE = 27,
SEGWIT_INPUT_SIZE = 59,
MULTISIG_INPUT_SIZE_ES = 351,
BASE_OUTPUT_SIZE = 9,
LEGACY_OUTPUT_SIZE = 25,
BECH32_OUTPUT_SIZE = 23,
SEGWIT_OUTPUT_SIZE = 23;
function _redeemScript(addr, key) {
let decode = coinjs.addressDecode(addr);
switch (decode.type) {
case "standard":
return false;
case "multisig":
return key ? coinjs.segwitAddress(btcOperator.pubkey(key)).redeemscript : null;
case "bech32":
return decode.redeemscript;
default:
return null;
}
}
function _sizePerInput(addr, rs) {
switch (coinjs.addressDecode(addr).type) {
case "standard":
return BASE_INPUT_SIZE + LEGACY_INPUT_SIZE;
case "bech32":
return BASE_INPUT_SIZE + BECH32_INPUT_SIZE;
case "multisig":
switch (coinjs.script().decodeRedeemScript(rs).type) {
case "segwit__":
return BASE_INPUT_SIZE + SEGWIT_INPUT_SIZE;
case "multisig__":
return BASE_INPUT_SIZE + MULTISIG_INPUT_SIZE_ES;
default:
return null;
};
default:
return null;
}
}
function _sizePerOutput(addr) {
switch (coinjs.addressDecode(addr).type) {
case "standard":
return BASE_OUTPUT_SIZE + LEGACY_OUTPUT_SIZE;
case "bech32":
return BASE_OUTPUT_SIZE + BECH32_OUTPUT_SIZE;
case "multisig":
return BASE_OUTPUT_SIZE + SEGWIT_OUTPUT_SIZE;
default:
return null;
}
}
function validateTxParameters(parameters) {
let invalids = [];
//sender-ids
if (parameters.senders) {
if (!Array.isArray(parameters.senders))
parameters.senders = [parameters.senders];
parameters.senders.forEach(id => !validateAddress(id) ? invalids.push(id) : null);
if (invalids.length)
throw "Invalid senders:" + invalids;
}
if (parameters.privkeys) {
if (!Array.isArray(parameters.privkeys))
parameters.privkeys = [parameters.privkeys];
if (parameters.senders.length != parameters.privkeys.length)
throw "Array length for senders and privkeys should be equal";
parameters.senders.forEach((id, i) => {
let key = parameters.privkeys[i];
if (!verifyKey(id, key)) //verify private-key
invalids.push(id);
if (key.length === 64) //convert Hex to WIF if needed
parameters.privkeys[i] = coinjs.privkey2wif(key);
});
if (invalids.length)
throw "Invalid keys:" + invalids;
}
//receiver-ids (and change-id)
if (!Array.isArray(parameters.receivers))
parameters.receivers = [parameters.receivers];
parameters.receivers.forEach(id => !validateAddress(id) ? invalids.push(id) : null);
if (invalids.length)
throw "Invalid receivers:" + invalids;
if (parameters.change_addr && !validateAddress(parameters.change_addr))
throw "Invalid change_address:" + parameters.change_addr;
//fee and amounts
if ((typeof parameters.fee !== "number" || parameters.fee <= 0) && parameters.fee !== null) //fee = null (auto calc)
throw "Invalid fee:" + parameters.fee;
if (!Array.isArray(parameters.amounts))
parameters.amounts = [parameters.amounts];
if (parameters.receivers.length != parameters.amounts.length)
throw "Array length for receivers and amounts should be equal";
parameters.amounts.forEach(a => typeof a !== "number" || a <= 0 ? invalids.push(a) : null);
if (invalids.length)
throw "Invalid amounts:" + invalids;
//return
return parameters;
}
function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr) {
return new Promise((resolve, reject) => {
let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
const tx = coinjs.transaction();
let output_size = addOutputs(tx, receivers, amounts, change_addr);
addInputs(tx, senders, redeemScripts, total_amount, fee, output_size).then(result => {
if (result.change_amount > 0)
tx.outs[tx.outs.length - 1].value = parseInt(result.change_amount * SATOSHI_IN_BTC); //values are in satoshi
else
tx.outs.pop(); //remove the change output if no change_amount
result.output_size = output_size;
result.output_amount = total_amount;
result.total_size = BASE_TX_SIZE + output_size + result.input_size;
result.transaction = tx;
resolve(result);
}).catch(error => reject(error))
})
}
function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size) {
return new Promise((resolve, reject) => {
if (fee !== null) {
addUTXOs(tx, senders, redeemScripts, total_amount + fee, false).then(result => {
result.fee = fee;
resolve(result);
}).catch(error => reject(error))
} else {
get_fee_rate().then(fee_rate => {
let net_fee = BASE_TX_SIZE * fee_rate;
net_fee += (output_size * fee_rate);
addUTXOs(tx, senders, redeemScripts, total_amount + net_fee, fee_rate).then(result => {
result.fee = parseFloat((net_fee + (result.input_size * fee_rate)).toFixed(8));
result.fee_rate = fee_rate;
resolve(result);
}).catch(error => reject(error))
}).catch(error => reject(error))
}
})
}
function addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args = {}) {
return new Promise((resolve, reject) => {
required_amount = parseFloat(required_amount.toFixed(8));
if (typeof rec_args.n === "undefined") {
rec_args.n = 0;
rec_args.input_size = 0;
rec_args.input_amount = 0;
}
if (required_amount <= 0)
return resolve({
input_size: rec_args.input_size,
input_amount: rec_args.input_amount,
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
});
else if (rec_args.n >= senders.length)
return reject("Insufficient Balance");
let addr = senders[rec_args.n],
rs = redeemScripts[rec_args.n];
let size_per_input = _sizePerInput(addr, rs);
fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => {
let utxos = result.data.txs;
console.debug("add-utxo", addr, rs, required_amount, utxos);
for (let i = 0; i < utxos.length && required_amount > 0; i++) {
if (!utxos[i].confirmations) //ignore unconfirmed utxo
continue;
var script;
if (!rs || !rs.length) //legacy script
script = utxos[i].script_hex;
else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi))) {
//redeemScript for segwit/bech32
let s = coinjs.script();
s.writeBytes(Crypto.util.hexToBytes(rs));
s.writeOp(0);
s.writeBytes(coinjs.numToBytes((utxos[i].value * SATOSHI_IN_BTC).toFixed(0), 8));
script = Crypto.util.bytesToHex(s.buffer);
} else //redeemScript for multisig
script = rs;
tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee
//update track values
rec_args.input_size += size_per_input;
rec_args.input_amount += parseFloat(utxos[i].value);
required_amount -= parseFloat(utxos[i].value);
if (fee_rate) //automatic fee calculation (dynamic)
required_amount += size_per_input * fee_rate;
}
rec_args.n += 1;
addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args)
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
function addOutputs(tx, receivers, amounts, change_addr) {
let size = 0;
for (let i in receivers) {
tx.addoutput(receivers[i], amounts[i]);
size += _sizePerOutput(receivers[i]);
}
tx.addoutput(change_addr, 0);
size += _sizePerOutput(change_addr);
return size;
}
/*
function autoFeeCalc(tx) {
return new Promise((resolve, reject) => {
get_fee_rate().then(fee_rate => {
let tx_size = tx.size();
for (var i = 0; i < this.ins.length; i++)
switch (tx.extractScriptKey(i).type) {
case 'scriptpubkey':
tx_size += SIGN_SIZE;
break;
case 'segwit':
case 'multisig':
tx_size += SIGN_SIZE * 0.25;
break;
default:
console.warn('Unknown script-type');
tx_size += SIGN_SIZE;
}
resolve(tx_size * fee_rate);
}).catch(error => reject(error))
})
}
function editFee(tx, current_fee, target_fee, index = -1) {
//values are in satoshi
index = parseInt(index >= 0 ? index : tx.outs.length - index);
if (index < 0 || index >= tx.outs.length)
throw "Invalid index";
let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places
current_value = tx.outs[index].value; //could be BigInterger
if (edit_value < 0 && edit_value > current_value)
throw "Insufficient value at vout";
tx.outs[index].value = current_value instanceof BigInteger ?
current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value);
}
*/
btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee, change_addr = null) {
return new Promise((resolve, reject) => {
createSignedTx(senders, privkeys, receivers, amounts, fee, change_addr).then(result => {
debugger;
broadcastTx(result.transaction.serialize())
.then(txid => resolve(txid))
.catch(error => reject(error));
}).catch(error => reject(error))
})
}
const createSignedTx = btcOperator.createSignedTx = function (senders, privkeys, receivers, amounts, fee = null, change_addr = null) {
return new Promise((resolve, reject) => {
try {
({
senders,
privkeys,
receivers,
amounts
} = validateTxParameters({
senders,
privkeys,
receivers,
amounts,
fee,
change_addr
}));
} catch (e) {
return reject(e)
}
let redeemScripts = [],
wif_keys = [];
for (let i in senders) {
let rs = _redeemScript(senders[i], privkeys[i]); //get redeem-script (segwit/bech32)
redeemScripts.push(rs);
rs === false ? wif_keys.unshift(privkeys[i]) : wif_keys.push(privkeys[i]); //sorting private-keys (wif)
}
if (redeemScripts.includes(null)) //TODO: segwit
return reject("Unable to get redeem-script");
//create transaction
createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]).then(result => {
let tx = result.transaction;
console.debug("Unsigned:", tx.serialize());
new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/))); //Sign the tx using private key WIF
console.debug("Signed:", tx.serialize());
resolve(result);
}).catch(error => reject(error));
})
}
btcOperator.createTx = function (senders, receivers, amounts, fee = null, change_addr = null) {
return new Promise((resolve, reject) => {
try {
({
senders,
receivers,
amounts
} = validateTxParameters({
senders,
receivers,
amounts,
fee,
change_addr
}));
} catch (e) {
return reject(e)
}
let redeemScripts = senders.map(id => _redeemScript(id));
if (redeemScripts.includes(null)) //TODO: segwit
return reject("Unable to get redeem-script");
//create transaction
createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]).then(result => {
result.tx_hex = result.transaction.serialize();
delete result.transaction;
resolve(result);
}).catch(error => reject(error))
})
}
btcOperator.createMultiSigTx = function (sender, redeemScript, receivers, amounts, fee = null) {
return new Promise((resolve, reject) => {
//validate tx parameters
if (validateAddress(sender) !== "multisig")
return reject("Invalid sender (multisig):" + sender);
else {
let script = coinjs.script();
let decode = script.decodeRedeemScript(redeemScript);
if (!decode || decode.address !== sender)
return reject("Invalid redeem-script");
}
try {
({
receivers,
amounts
} = validateTxParameters({
receivers,
amounts,
fee
}));
} catch (e) {
return reject(e)
}
//create transaction
createTransaction([sender], [redeemScript], receivers, amounts, fee, sender).then(result => {
result.tx_hex = result.transaction.serialize();
delete result.transaction;
resolve(result);
}).catch(error => reject(error))
})
}
function deserializeTx(tx) {
if (typeof tx === 'string' || Array.isArray(tx)) {
try {
tx = coinjs.transaction().deserialize(tx);
} catch {
throw "Invalid transaction hex";
}
} else if (typeof tx !== 'object' || typeof tx.sign !== 'function')
throw "Invalid transaction object";
return tx;
}
btcOperator.signTx = function (tx, privkeys, sighashtype = 1) {
tx = deserializeTx(tx);
if (!Array.isArray(privkeys))
privkeys = [privkeys];
for (let i in privkeys)
if (privkeys[i].length === 64)
privkeys[i] = coinjs.privkey2wif(privkeys[i]);
new Set(privkeys).forEach(key => tx.sign(key, sighashtype)); //Sign the tx using private key WIF
return tx.serialize();
}
const checkSigned = btcOperator.checkSigned = function (tx, bool = true) {
tx = deserializeTx(tx);
let n = [];
for (let i in tx.ins) {
var s = tx.extractScriptKey(i);
if (s['type'] !== 'multisig')
n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2))
else {
var rs = coinjs.script().decodeRedeemScript(s.script);
let x = {
s: s['signatures'],
r: rs['signaturesRequired'],
t: rs['pubkeys'].length
};
if (x.r > x.t)
throw "signaturesRequired is more than publicKeys";
else if (x.s < x.r)
n.push(x);
else
n.push(true);
}
}
return bool ? !(n.filter(x => x !== true).length) : n;
}
btcOperator.checkIfSameTx = function (tx1, tx2) {
tx1 = deserializeTx(tx1);
tx2 = deserializeTx(tx2);
if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length)
return false;
for (let i = 0; i < tx1.ins.length; i++)
if (tx1.ins[i].outpoint.hash !== tx2.ins[i].outpoint.hash || tx1.ins[i].outpoint.index !== tx2.ins[i].outpoint.index)
return false;
for (let i = 0; i < tx2.ins.length; i++)
if (tx1.outs[i].value !== tx2.outs[i].value || Crypto.util.bytesToHex(tx1.outs[i].script.buffer) !== Crypto.util.bytesToHex(tx2.outs[i].script.buffer))
return false;
return true;
}
const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
fetch_api(`get_tx_outputs/BTC/${txid}/${i}`)
.then(result => resolve(result.data.outputs))
.catch(error => reject(error))
});
btcOperator.parseTransaction = function (tx) {
return new Promise((resolve, reject) => {
tx = deserializeTx(tx);
let result = {};
let promises = [];
//Parse Inputs
for (let i = 0; i < tx.ins.length; i++)
promises.push(getTxOutput(tx.ins[i].outpoint.hash, tx.ins[i].outpoint.index));
Promise.all(promises).then(inputs => {
result.inputs = inputs.map(inp => Object({
address: inp.address,
value: parseFloat(inp.value)
}));
let signed = checkSigned(tx, false);
result.inputs.forEach((inp, i) => inp.signed = signed[i]);
//Parse Outputs
result.outputs = tx.outs.map(out => {
var address;
switch (out.script.chunks[0]) {
case 0: //bech32
address = encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp);
break;
case 169: //multisig, segwit
address = encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig);
break;
case 118: //legacy
address = encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub);
}
return {
address,
value: parseFloat(out.value / SATOSHI_IN_BTC)
}
});
//Parse Totals
result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8));
result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8));
result.fee = parseFloat((result.total_input - result.total_output).toFixed(8));
resolve(result);
}).catch(error => reject(error))
})
}
btcOperator.transactionID = function (tx) {
tx = deserializeTx(tx);
let clone = coinjs.clone(tx);
clone.witness = null;
let raw_bytes = Crypto.util.hexToBytes(clone.serialize());
let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse();
return Crypto.util.bytesToHex(txid);
}
btcOperator.getTx = txid => new Promise((resolve, reject) => {
fetch_api(`tx/BTC/${txid}`)
.then(result => resolve(result.data))
.catch(error => reject(error))
});
btcOperator.getAddressData = addr => new Promise((resolve, reject) => {
fetch_api(`address/BTC/${addr}`)
.then(result => resolve(result.data))
.catch(error => reject(error))
});
btcOperator.getBlock = block => new Promise((resolve, reject) => {
fetch_api(`get_block/BTC/${block}`)
.then(result => resolve(result.data))
.catch(error => reject(error))
});
})('object' === typeof module ? module.exports : window.btcOperator = {});

259
scripts/compactIDB.js Normal file
View File

@ -0,0 +1,259 @@
(function(EXPORTS) { //compactIDB v2.1.0
/* Compact IndexedDB operations */
'use strict';
const compactIDB = EXPORTS;
var defaultDB;
const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
if (!indexedDB) {
console.error("Your browser doesn't support a stable version of IndexedDB.");
return;
}
compactIDB.setDefaultDB = dbName => defaultDB = dbName;
Object.defineProperty(compactIDB, 'default', {
get: () => defaultDB,
set: dbName => defaultDB = dbName
});
function getDBversion(dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
resolve(db.version)
db.close()
}).catch(error => reject(error))
})
}
function upgradeDB(dbName, createList = null, deleteList = null) {
return new Promise((resolve, reject) => {
getDBversion(dbName).then(version => {
var idb = indexedDB.open(dbName, version + 1);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onupgradeneeded = (event) => {
let db = event.target.result;
if (createList instanceof Object) {
if (Array.isArray(createList)) {
let tmp = {}
createList.forEach(o => tmp[o] = {})
createList = tmp
}
for (let o in createList) {
let obs = db.createObjectStore(o, createList[o].options || {});
if (createList[o].indexes instanceof Object)
for (let i in createList[o].indexes)
obs.createIndex(i, i, createList[o].indexes || {});
}
}
if (Array.isArray(deleteList))
deleteList.forEach(o => db.deleteObjectStore(o));
resolve('Database upgraded')
}
idb.onsuccess = (event) => event.target.result.close();
}).catch(error => reject(error))
})
}
compactIDB.initDB = function(dbName, objectStores = {}) {
return new Promise((resolve, reject) => {
if (!(objectStores instanceof Object))
return reject('ObjectStores must be an object or array')
defaultDB = defaultDB || dbName;
var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onsuccess = (event) => {
var db = event.target.result;
let cList = Object.values(db.objectStoreNames);
var obs = {},
a_obs = {},
d_obs = [];
if (!Array.isArray(objectStores))
var obs = objectStores
else
objectStores.forEach(o => obs[o] = {})
let nList = Object.keys(obs)
for (let o of nList)
if (!cList.includes(o))
a_obs[o] = obs[o]
for (let o of cList)
if (!nList.includes(o))
d_obs.push(o)
if (!Object.keys(a_obs).length && !d_obs.length)
resolve("Initiated IndexedDB");
else
upgradeDB(dbName, a_obs, d_obs)
.then(result => resolve(result))
.catch(error => reject(error))
db.close();
}
});
}
const openDB = compactIDB.openDB = function(dbName = defaultDB) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onupgradeneeded = (event) => {
event.target.result.close();
deleteDB(dbName).then(_ => null).catch(_ => null).finally(_ => reject("Datebase not found"))
}
idb.onsuccess = (event) => resolve(event.target.result);
});
}
const deleteDB = compactIDB.deleteDB = function(dbName = defaultDB) {
return new Promise((resolve, reject) => {
var deleteReq = indexedDB.deleteDatabase(dbName);;
deleteReq.onerror = (event) => reject("Error deleting database!");
deleteReq.onsuccess = (event) => resolve("Database deleted successfully");
});
}
compactIDB.writeData = function(obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let writeReq = (key ? obs.put(data, key) : obs.put(data));
writeReq.onsuccess = (evt) => resolve(`Write data Successful`);
writeReq.onerror = (evt) => reject(
`Write data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.addData = function(obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let addReq = (key ? obs.add(data, key) : obs.add(data));
addReq.onsuccess = (evt) => resolve(`Add data successful`);
addReq.onerror = (evt) => reject(
`Add data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.removeData = function(obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let delReq = obs.delete(key);
delReq.onsuccess = (evt) => resolve(`Removed Data ${key}`);
delReq.onerror = (evt) => reject(
`Remove data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.clearData = function(obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let clearReq = obs.clear();
clearReq.onsuccess = (evt) => resolve(`Clear data Successful`);
clearReq.onerror = (evt) => reject(`Clear data Unsuccessful`);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.readData = function(obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
let getReq = obs.get(key);
getReq.onsuccess = (evt) => resolve(evt.target.result);
getReq.onerror = (evt) => reject(
`Read data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.readAllData = function(obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var tmpResult = {}
let curReq = obs.openCursor();
curReq.onsuccess = (evt) => {
var cursor = evt.target.result;
if (cursor) {
tmpResult[cursor.primaryKey] = cursor.value;
cursor.continue();
} else
resolve(tmpResult);
}
curReq.onerror = (evt) => reject(
`Read-All data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
/* compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var filteredResult = {}
let keyRange;
if(options.lowerKey!==null && options.upperKey!==null)
keyRange = IDBKeyRange.bound(options.lowerKey, options.upperKey);
else if(options.lowerKey!==null)
keyRange = IDBKeyRange.lowerBound(options.lowerKey);
else if (options.upperKey!==null)
keyRange = IDBKeyRange.upperBound(options.upperBound);
else if (options.atKey)
let curReq = obs.openCursor(keyRange, )
}).catch(error => reject(error))
})
}*/
compactIDB.searchData = function(obsName, options = {}, dbName = defaultDB) {
options.lowerKey = options.atKey || options.lowerKey || 0
options.upperKey = options.atKey || options.upperKey || false
options.patternEval = options.patternEval || ((k, v) => {
return true
})
options.limit = options.limit || false;
options.lastOnly = options.lastOnly || false
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var filteredResult = {}
let curReq = obs.openCursor(
options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey),
options.lastOnly ? "prev" : "next");
curReq.onsuccess = (evt) => {
var cursor = evt.target.result;
if (cursor) {
if (options.patternEval(cursor.primaryKey, cursor.value)) {
filteredResult[cursor.primaryKey] = cursor.value;
options.lastOnly ? resolve(filteredResult) : cursor.continue();
} else
cursor.continue();
} else
resolve(filteredResult);
}
curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`);
db.close();
}).catch(error => reject(error));
});
}
})(window.compactIDB = {});

154
scripts/components.js Normal file

File diff suppressed because one or more lines are too long

580
scripts/floBlockchainAPI.js Normal file
View File

@ -0,0 +1,580 @@
(function (EXPORTS) { //floBlockchainAPI v2.3.3d
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict';
const floBlockchainAPI = EXPORTS;
const DEFAULT = {
blockchain: floGlobals.blockchain,
apiURL: {
FLO: ['https://flosight.duckdns.org/'],
FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/']
},
sendAmt: 0.001,
fee: 0.0005,
minChangeAmt: 0.0005,
receiverID: floGlobals.adminID
};
Object.defineProperties(floBlockchainAPI, {
sendAmt: {
get: () => DEFAULT.sendAmt,
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
},
fee: {
get: () => DEFAULT.fee,
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
},
defaultReceiver: {
get: () => DEFAULT.receiverID,
set: floID => DEFAULT.receiverID = floID
},
blockchain: {
get: () => DEFAULT.blockchain
}
});
if (floGlobals.sendAmt) floBlockchainAPI.sendAmt = floGlobals.sendAmt;
if (floGlobals.fee) floBlockchainAPI.fee = floGlobals.fee;
Object.defineProperties(floGlobals, {
sendAmt: {
get: () => DEFAULT.sendAmt,
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
},
fee: {
get: () => DEFAULT.fee,
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
}
});
const allServerList = new Set(floGlobals.apiURL && floGlobals.apiURL[DEFAULT.blockchain] ? floGlobals.apiURL[DEFAULT.blockchain] : DEFAULT.apiURL[DEFAULT.blockchain]);
var serverList = Array.from(allServerList);
var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_flosight) {
return new Promise((resolve, reject) => {
let i = serverList.indexOf(rm_flosight)
if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
.then(result => resolve(result))
.catch(error => reject(error));
})
}
function fetch_api(apicall, ic = true) {
return new Promise((resolve, reject) => {
if (serverList.length === 0) {
if (ic) {
serverList = Array.from(allServerList);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
.then(result => resolve(result))
.catch(error => reject(error));
} else
reject("No floSight server working");
} else {
let flosight = serverList[curPos];
fetch(flosight + apicall).then(response => {
if (response.ok)
response.json().then(data => resolve(data));
else {
fetch_retry(apicall, flosight)
.then(result => resolve(result))
.catch(error => reject(error));
}
}).catch(error => {
fetch_retry(apicall, flosight)
.then(result => resolve(result))
.catch(error => reject(error));
})
}
})
}
Object.defineProperties(floBlockchainAPI, {
serverList: {
get: () => Array.from(serverList)
},
current_server: {
get: () => serverList[curPos]
}
});
//Promised function to get data from API
const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall) {
return new Promise((resolve, reject) => {
//console.log(apicall);
fetch_api(apicall)
.then(result => resolve(result))
.catch(error => reject(error));
});
}
//Get balance for the given Address
const getBalance = floBlockchainAPI.getBalance = function (addr) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addr/${addr}/balance`)
.then(balance => resolve(parseFloat(balance)))
.catch(error => reject(error));
});
}
//Send Tx to blockchain
const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
else if (!floCrypto.validateFloID(senderAddr))
return reject(`Invalid address : ${senderAddr}`);
else if (!floCrypto.validateFloID(receiverAddr))
return reject(`Invalid address : ${receiverAddr}`);
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
return reject("Invalid Private key!");
else if (typeof sendAmt !== 'number' || sendAmt <= 0)
return reject(`Invalid sendAmt : ${sendAmt}`);
getBalance(senderAddr).then(balance => {
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
//get unconfirmed tx list
promisedAPI(`api/addr/${senderAddr}`).then(result => {
readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => {
let unconfirmedSpent = {};
for (let tx of result.items)
if (tx.confirmations == 0)
for (let vin of tx.vin)
if (vin.addr === senderAddr) {
if (Array.isArray(unconfirmedSpent[vin.txid]))
unconfirmedSpent[vin.txid].push(vin.vout);
else
unconfirmedSpent[vin.txid] = [vin.vout];
}
//get utxos list
promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
continue; //A transaction has already used the utxo, but is unconfirmed.
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
trx.addoutput(receiverAddr, sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
});
}
//Write Data into blockchain
floBlockchainAPI.writeData = function (senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {}) {
let strict_utxo = options.strict_utxo === false ? false : true,
sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt;
return new Promise((resolve, reject) => {
if (typeof data != "string")
data = JSON.stringify(data);
sendTx(senderAddr, receiverAddr, sendAmt, privKey, data, strict_utxo)
.then(txid => resolve(txid))
.catch(error => reject(error));
});
}
//merge all UTXOs of a given floID into a single UTXO
floBlockchainAPI.mergeUTXOs = function (floID, privKey, floData = '') {
return new Promise((resolve, reject) => {
if (!floCrypto.validateFloID(floID))
return reject(`Invalid floID`);
if (!floCrypto.verifyPrivKey(privKey, floID))
return reject("Invalid Private Key");
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
var trx = bitjs.transaction();
var utxoAmt = 0.0;
var fee = DEFAULT.fee;
promisedAPI(`api/addr/${floID}/utxo`).then(utxos => {
for (var i = utxos.length - 1; i >= 0; i--)
if (utxos[i].confirmations) {
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
}
trx.addoutput(floID, utxoAmt - fee);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
/**Write data into blockchain from (and/or) to multiple floID
* @param {Array} senderPrivKeys List of sender private-keys
* @param {string} data FLO data of the txn
* @param {Array} receivers List of receivers
* @param {boolean} preserveRatio (optional) preserve ratio or equal contribution
* @return {Promise}
*/
floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], preserveRatio = true) {
return new Promise((resolve, reject) => {
if (!Array.isArray(senderPrivKeys))
return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array");
if (!preserveRatio) {
let tmp = {};
let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length;
senderPrivKeys.forEach(key => tmp[key] = amount);
senderPrivKeys = tmp;
}
if (!Array.isArray(receivers))
return reject("Invalid receivers: Receivers must be Array");
else {
let tmp = {};
let amount = DEFAULT.sendAmt;
receivers.forEach(floID => tmp[floID] = amount);
receivers = tmp
}
if (typeof data != "string")
data = JSON.stringify(data);
sendTxMultiple(senderPrivKeys, receivers, data)
.then(txid => resolve(txid))
.catch(error => reject(error))
})
}
/**Send Tx from (and/or) to multiple floID
* @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent)
* @param {Object} receivers List of receivers with respective amount to be sent
* @param {string} floData FLO data of the txn
* @return {Promise}
*/
const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function (senderPrivKeys, receivers, floData = '') {
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
let senders = {},
preserveRatio;
//check for argument validations
try {
let invalids = {
InvalidSenderPrivKeys: [],
InvalidSenderAmountFor: [],
InvalidReceiverIDs: [],
InvalidReceiveAmountFor: []
}
let inputVal = 0,
outputVal = 0;
//Validate sender privatekeys (and send amount if passed)
//conversion when only privateKeys are passed (preserveRatio mode)
if (Array.isArray(senderPrivKeys)) {
senderPrivKeys.forEach(key => {
try {
if (!key)
invalids.InvalidSenderPrivKeys.push(key);
else {
let floID = floCrypto.getFloID(key);
senders[floID] = {
wif: key
}
}
} catch (error) {
invalids.InvalidSenderPrivKeys.push(key)
}
})
preserveRatio = true;
}
//conversion when privatekeys are passed with send amount
else {
for (let key in senderPrivKeys) {
try {
if (!key)
invalids.InvalidSenderPrivKeys.push(key);
else {
if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[key] <= 0)
invalids.InvalidSenderAmountFor.push(key);
else
inputVal += senderPrivKeys[key];
let floID = floCrypto.getFloID(key);
senders[floID] = {
wif: key,
coins: senderPrivKeys[key]
}
}
} catch (error) {
invalids.InvalidSenderPrivKeys.push(key)
}
}
preserveRatio = false;
}
//Validate the receiver IDs and receive amount
for (let floID in receivers) {
if (!floCrypto.validateFloID(floID))
invalids.InvalidReceiverIDs.push(floID);
if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0)
invalids.InvalidReceiveAmountFor.push(floID);
else
outputVal += receivers[floID];
}
//Reject if any invalids are found
for (let i in invalids)
if (!invalids[i].length)
delete invalids[i];
if (Object.keys(invalids).length)
return reject(invalids);
//Reject if given inputVal and outputVal are not equal
if (!preserveRatio && inputVal != outputVal)
return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`);
} catch (error) {
return reject(error)
}
//Get balance of senders
let promises = [];
for (let floID in senders)
promises.push(getBalance(floID));
Promise.all(promises).then(results => {
let totalBalance = 0,
totalFee = DEFAULT.fee,
balance = {};
//Divide fee among sender if not for preserveRatio
if (!preserveRatio)
var dividedFee = totalFee / Object.keys(senders).length;
//Check if balance of each sender is sufficient enough
let insufficient = [];
for (let floID in senders) {
balance[floID] = parseFloat(results.shift());
if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= totalFee) ||
(!preserveRatio && balance[floID] < senders[floID].coins + dividedFee))
insufficient.push(floID);
totalBalance += balance[floID];
}
if (insufficient.length)
return reject({
InsufficientBalance: insufficient
})
//Calculate totalSentAmount and check if totalBalance is sufficient
let totalSendAmt = totalFee;
for (let floID in receivers)
totalSendAmt += receivers[floID];
if (totalBalance < totalSendAmt)
return reject("Insufficient total Balance");
//Get the UTXOs of the senders
let promises = [];
for (let floID in senders)
promises.push(promisedAPI(`api/addr/${floID}/utxo`));
Promise.all(promises).then(results => {
let wifSeq = [];
var trx = bitjs.transaction();
for (let floID in senders) {
let utxos = results.shift();
let sendAmt;
if (preserveRatio) {
let ratio = (balance[floID] / totalBalance);
sendAmt = totalSendAmt * ratio;
} else
sendAmt = senders[floID].coins + dividedFee;
let wif = senders[floID].wif;
let utxoAmt = 0.0;
for (let i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt); i--) {
if (utxos[i].confirmations) {
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
wifSeq.push(wif);
utxoAmt += utxos[i].amount;
}
}
if (utxoAmt < sendAmt)
return reject("Insufficient balance:" + floID);
let change = (utxoAmt - sendAmt);
if (change > 0)
trx.addoutput(floID, change);
}
for (let floID in receivers)
trx.addoutput(floID, receivers[floID]);
trx.addflodata(floData.replace(/\n/g, ' '));
for (let i = 0; i < wifSeq.length; i++)
trx.signinput(i, wifSeq[i], 1);
var signedTxHash = trx.serialize();
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
//Broadcast signed Tx in blockchain using API
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => {
if (signedTxHash.length < 1)
return reject("Empty Signature");
var url = serverList[curPos] + 'api/tx/send';
fetch(url, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: `{"rawtx":"${signedTxHash}"}`
}).then(response => {
if (response.ok)
response.json().then(data => resolve(data.txid.result));
else
response.text().then(data => resolve(data));
}).catch(error => reject(error));
})
}
floBlockchainAPI.getTx = function (txid) {
return new Promise((resolve, reject) => {
promisedAPI(`api/tx/${txid}`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
//Read Txs of Address between from and to
const readTxs = floBlockchainAPI.readTxs = function (addr, from, to) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=${from}&to=${to}`)
.then(response => resolve(response))
.catch(error => reject(error))
});
}
//Read All Txs of Address (newest first)
floBlockchainAPI.readAllTxs = function (addr) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${response.totalItems}0`)
.then(response => resolve(response.items))
.catch(error => reject(error));
}).catch(error => reject(error))
});
}
/*Read flo Data from txs of given Address
options can be used to filter data
limit : maximum number of filtered data (default = 1000, negative = no limit)
ignoreOld : ignore old txs (default = 0)
sentOnly : filters only sent data
receivedOnly: filters only received data
pattern : filters data that with JSON pattern
filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'})
tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details)
sender : flo-id(s) of sender
receiver : flo-id(s) of receiver
*/
floBlockchainAPI.readData = function (addr, options = {}) {
options.limit = options.limit || 0;
options.ignoreOld = options.ignoreOld || 0;
if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers];
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
var newItems = response.totalItems - options.ignoreOld;
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${newItems * 2}`).then(response => {
if (options.limit <= 0)
options.limit = response.items.length;
var filteredData = [];
let numToRead = response.totalItems - options.ignoreOld,
unconfirmedCount = 0;
for (let i = 0; i < numToRead && filteredData.length < options.limit; i++) {
if (!response.items[i].confirmations) { //unconfirmed transactions
unconfirmedCount++;
if (numToRead < response.items[i].length)
numToRead++;
continue;
}
if (options.pattern) {
try {
let jsonContent = JSON.parse(response.items[i].floData);
if (!Object.keys(jsonContent).includes(options.pattern))
continue;
} catch (error) {
continue;
}
}
if (options.sentOnly) {
let flag = false;
for (let vin of response.items[i].vin)
if (vin.addr === addr) {
flag = true;
break;
}
if (!flag) continue;
}
if (Array.isArray(options.senders)) {
let flag = false;
for (let vin of response.items[i].vin)
if (options.senders.includes(vin.addr)) {
flag = true;
break;
}
if (!flag) continue;
}
if (options.receivedOnly) {
let flag = false;
for (let vout of response.items[i].vout)
if (vout.scriptPubKey.addresses[0] === addr) {
flag = true;
break;
}
if (!flag) continue;
}
if (Array.isArray(options.receivers)) {
let flag = false;
for (let vout of response.items[i].vout)
if (options.receivers.includes(vout.scriptPubKey.addresses[0])) {
flag = true;
break;
}
if (!flag) continue;
}
if (options.filter && !options.filter(response.items[i].floData))
continue;
if (options.tx) {
let d = {}
d.txid = response.items[i].txid;
d.time = response.items[i].time;
d.blockheight = response.items[i].blockheight;
d.senders = new Set(response.items[i].vin.map(v => v.addr));
d.receivers = new Set(response.items[i].vout.map(v => v.scriptPubKey.addresses[0]));
d.data = response.items[i].floData;
filteredData.push(d);
} else
filteredData.push(response.items[i].floData);
}
resolve({
totalTxs: response.totalItems - unconfirmedCount,
data: filteredData
});
}).catch(error => {
reject(error);
});
}).catch(error => {
reject(error);
});
});
}
})('object' === typeof module ? module.exports : window.floBlockchainAPI = {});

1057
scripts/floCloudAPI.js Normal file

File diff suppressed because it is too large Load Diff

442
scripts/floCrypto.js Normal file
View File

@ -0,0 +1,442 @@
(function (EXPORTS) { //floCrypto v2.3.3e
/* FLO Crypto Operators */
'use strict';
const floCrypto = EXPORTS;
const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);
const ecparams = EllipticCurve.getSECCurveByName("secp256k1");
const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`;
const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4"));
coinjs.compressed = true; //defaulting coinjs compressed to true;
function calculateY(x) {
let exp = exponent1();
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
}
function getUncompressedPublicKey(compressedPublicKey) {
// Fetch x from compressedPublicKey
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
const prefix = pubKeyBytes.shift() // remove prefix
let prefix_modulus = prefix % 2;
pubKeyBytes.unshift(0) // add prefix 0
let x = new BigInteger(pubKeyBytes)
let xDecimalValue = x.toString()
// Fetch y
let y = calculateY(x);
let yDecimalValue = y.toString();
// verify y value
let resultBigInt = y.mod(BigInteger("2"));
let check = resultBigInt.toString() % 2;
if (prefix_modulus !== check)
yDecimalValue = y.negate().mod(p).toString();
return {
x: xDecimalValue,
y: yDecimalValue
};
}
function getSenderPublicKeyString() {
let privateKey = ellipticCurveEncryption.senderRandom();
var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
return {
privateKey: privateKey,
senderPublicKeyString: senderPublicKeyString
}
}
function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) {
let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex);
var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey);
return senderDerivedKey;
}
function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) {
return ellipticCurveEncryption.receiverSharedKeyDerivation(
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey);
}
function getReceiverPublicKeyString(privateKey) {
return ellipticCurveEncryption.receiverPublicString(privateKey);
}
function wifToDecimal(pk_wif, isPubKeyCompressed = false) {
let pk = Bitcoin.Base58.decode(pk_wif)
pk.shift()
pk.splice(-4, 4)
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
if (isPubKeyCompressed == true) pk.pop()
pk.unshift(0)
let privateKeyDecimal = BigInteger(pk).toString()
let privateKeyHex = Crypto.util.bytesToHex(pk)
return {
privateKeyDecimal: privateKeyDecimal,
privateKeyHex: privateKeyHex
}
}
//generate a random Interger within range
floCrypto.randInt = function (min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(securedMathRandom() * (max - min + 1)) + min;
}
//generate a random String within length (options : alphaNumeric chars only)
floCrypto.randString = function (length, alphaNumeric = true) {
var result = '';
var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' :
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():';
for (var i = 0; i < length; i++)
result += characters.charAt(Math.floor(securedMathRandom() * characters.length));
return result;
}
//Encrypt Data using public-key
floCrypto.encryptData = function (data, receiverPublicKeyHex) {
var senderECKeyData = getSenderPublicKeyString();
var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey);
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
let secret = Crypto.AES.encrypt(data, senderKey);
return {
secret: secret,
senderPublicKeyString: senderECKeyData.senderPublicKeyString
};
}
//Decrypt Data using private-key
floCrypto.decryptData = function (data, privateKeyHex) {
var receiverECKeyData = {};
if (typeof privateKeyHex !== "string") throw new Error("No private key found.");
let privateKey = wifToDecimal(privateKeyHex, true);
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key.");
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey);
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey);
return decryptMsg;
}
//Sign data using private-key
floCrypto.signData = function (data, privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
var messageHash = Crypto.SHA256(data);
var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv);
var sighex = Crypto.util.bytesToHex(messageSign);
return sighex;
}
//Verify signatue of the data using public-key
floCrypto.verifySign = function (data, signatureHex, publicKeyHex) {
var msgHash = Crypto.SHA256(data);
var sigBytes = Crypto.util.hexToBytes(signatureHex);
var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex);
var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint);
return verify;
}
//Generates a new flo ID and returns private-key, public-key and floID
const generateNewID = floCrypto.generateNewID = function () {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat()
}
}
Object.defineProperties(floCrypto, {
newID: {
get: () => generateNewID()
},
tmpID: {
get: () => {
let bytes = Crypto.util.randomBytes(20);
bytes.unshift(bitjs.pub);
var hash = Crypto.SHA256(Crypto.SHA256(bytes, {
asBytes: true
}), {
asBytes: true
});
var checksum = hash.slice(0, 4);
return bitjs.Base58.encode(bytes.concat(checksum));
}
}
});
//Returns public-key from private-key
floCrypto.getPubKeyHex = function (privateKeyHex) {
if (!privateKeyHex)
return null;
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return null;
key.setCompressed(true);
return key.getPubKeyHex();
}
//Returns flo-ID from public-key or private-key
floCrypto.getFloID = function (keyHex) {
if (!keyHex)
return null;
try {
var key = new Bitcoin.ECKey(keyHex);
if (key.priv == null)
key.setPub(keyHex);
return key.getBitcoinAddress();
} catch {
return null;
}
}
floCrypto.getAddress = function (privateKeyHex, strict = false) {
if (!privateKeyHex)
return;
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return null;
key.setCompressed(true);
let pubKey = key.getPubKeyHex(),
version = bitjs.Base58.decode(privateKeyHex)[0];
switch (version) {
case coinjs.priv: //BTC
return coinjs.bech32Address(pubKey).address;
case bitjs.priv: //FLO
return bitjs.pubkey2address(pubKey);
default:
return strict ? false : bitjs.pubkey2address(pubKey); //default to FLO address (if strict=false)
}
}
//Verify the private-key for the given public-key or flo-ID
floCrypto.verifyPrivKey = function (privateKeyHex, pubKey_floID, isfloID = true) {
if (!privateKeyHex || !pubKey_floID)
return false;
try {
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return false;
key.setCompressed(true);
if (isfloID && pubKey_floID == key.getBitcoinAddress())
return true;
else if (!isfloID && pubKey_floID == key.getPubKeyHex())
return true;
else
return false;
} catch {
return null;
}
}
//Check if the given flo-id is valid or not
floCrypto.validateFloID = function (floID) {
if (!floID)
return false;
try {
let addr = new Bitcoin.Address(floID);
return true;
} catch {
return false;
}
}
//Check if the given address (any blockchain) is valid or not
floCrypto.validateAddr = function (address, std = true, bech = true) {
let raw = decodeAddress(address);
if (!raw)
return false;
if (typeof raw.version !== 'undefined') { //legacy or segwit
if (std == false)
return false;
else if (std === true || (!Array.isArray(std) && std === raw.version) || (Array.isArray(std) && std.includes(raw.version)))
return true;
else
return false;
} else if (typeof raw.bech_version !== 'undefined') { //bech32
if (bech === false)
return false;
else if (bech === true || (!Array.isArray(bech) && bech === raw.bech_version) || (Array.isArray(bech) && bech.includes(raw.bech_version)))
return true;
else
return false;
} else //unknown
return false;
}
//Check the public-key for the address (any blockchain)
floCrypto.verifyPubKey = function (pubKeyHex, address) {
let raw = decodeAddress(address),
pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), {
asBytes: true
})));
return raw ? pub_hash === raw.hex : false;
}
//Convert the given address (any blockchain) to equivalent floID
floCrypto.toFloID = function (address, options = null) {
if (!address)
return;
let raw = decodeAddress(address);
if (!raw)
return;
else if (options) {
if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version)))
return;
if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version)))
return;
}
raw.bytes.unshift(bitjs.pub);
let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, {
asBytes: true
}), {
asBytes: true
});
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4)));
}
//Checks if the given addresses (any blockchain) are same (w.r.t keys)
floCrypto.isSameAddr = function (addr1, addr2) {
if (!addr1 || !addr2)
return;
let raw1 = decodeAddress(addr1),
raw2 = decodeAddress(addr2);
if (!raw1 || !raw2)
return false;
else
return raw1.hex === raw2.hex;
}
const decodeAddress = floCrypto.decodeAddr = function (address) {
if (!address)
return;
else if (address.length == 33 || address.length == 34) { //legacy encoding
let decode = bitjs.Base58.decode(address);
let bytes = decode.slice(0, decode.length - 4);
let checksum = decode.slice(decode.length - 4),
hash = Crypto.SHA256(Crypto.SHA256(bytes, {
asBytes: true
}), {
asBytes: true
});
return (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) ? null : {
version: bytes.shift(),
hex: Crypto.util.bytesToHex(bytes),
bytes
}
} else if (address.length == 42) { //bech encoding
let decode = coinjs.bech32_decode(address);
if (decode) {
let bytes = decode.data;
let bech_version = bytes.shift();
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
return {
bech_version,
hrp: decode.hrp,
hex: Crypto.util.bytesToHex(bytes),
bytes
}
} else
return null;
}
}
//Split the str using shamir's Secret and Returns the shares
floCrypto.createShamirsSecretShares = function (str, total_shares, threshold_limit) {
try {
if (str.length > 0) {
var strHex = shamirSecretShare.str2hex(str);
var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit);
return shares;
}
return false;
} catch {
return false
}
}
//Returns the retrived secret by combining the shamirs shares
const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function (sharesArray) {
try {
if (sharesArray.length > 0) {
var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length));
comb = shamirSecretShare.hex2str(comb);
return comb;
}
return false;
} catch {
return false;
}
}
//Verifies the shares and str
floCrypto.verifyShamirsSecret = function (sharesArray, str) {
if (!str)
return null;
else if (retrieveShamirSecret(sharesArray) === str)
return true;
else
return false;
}
const validateASCII = floCrypto.validateASCII = function (string, bool = true) {
if (typeof string !== "string")
return null;
if (bool) {
let x;
for (let i = 0; i < string.length; i++) {
x = string.charCodeAt(i);
if (x < 32 || x > 127)
return false;
}
return true;
} else {
let x, invalids = {};
for (let i = 0; i < string.length; i++) {
x = string.charCodeAt(i);
if (x < 32 || x > 127)
if (x in invalids)
invalids[string[i]].push(i)
else
invalids[string[i]] = [i];
}
if (Object.keys(invalids).length)
return invalids;
else
return true;
}
}
floCrypto.convertToASCII = function (string, mode = 'soft-remove') {
let chars = validateASCII(string, false);
if (chars === true)
return string;
else if (chars === null)
return null;
let convertor, result = string,
refAlt = {};
ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2));
mode = mode.toLowerCase();
if (mode === "hard-unicode")
convertor = (c) => `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`;
else if (mode === "soft-unicode")
convertor = (c) => refAlt[c] || `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`;
else if (mode === "hard-remove")
convertor = c => "";
else if (mode === "soft-remove")
convertor = c => refAlt[c] || "";
else
return null;
for (let c in chars)
result = result.replaceAll(c, convertor(c));
return result;
}
floCrypto.revertUnicode = function (string) {
return string.replace(/\\u[\dA-F]{4}/gi,
m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16)));
}
})('object' === typeof module ? module.exports : window.floCrypto = {});

770
scripts/floDapps.js Normal file
View File

@ -0,0 +1,770 @@
(function(EXPORTS) { //floDapps v2.3.2d
/* General functions for FLO Dapps*/
'use strict';
const floDapps = EXPORTS;
const DEFAULT = {
root: "floDapps",
application: floGlobals.application,
adminID: floGlobals.adminID
};
Object.defineProperties(floDapps, {
application: {
get: () => DEFAULT.application
},
adminID: {
get: () => DEFAULT.adminID
},
root: {
get: () => DEFAULT.root
}
});
var user_priv_raw, aes_key, user_priv_wrap; //private variable inside capsule
const raw_user = {
get private() {
if (!user_priv_raw)
throw "User not logged in";
return Crypto.AES.decrypt(user_priv_raw, aes_key);
}
}
var user_id, user_public, user_private;
const user = floDapps.user = {
get id() {
if (!user_id)
throw "User not logged in";
return user_id;
},
get public() {
if (!user_public)
throw "User not logged in";
return user_public;
},
get private() {
if (!user_private)
throw "User not logged in";
else if (user_private instanceof Function)
return user_private();
else
return Crypto.AES.decrypt(user_private, aes_key);
},
sign(message) {
return floCrypto.signData(message, raw_user.private);
},
decrypt(data) {
return floCrypto.decryptData(data, raw_user.private);
},
encipher(message) {
return Crypto.AES.encrypt(message, raw_user.private);
},
decipher(data) {
return Crypto.AES.decrypt(data, raw_user.private);
},
get db_name() {
return "floDapps#" + floCrypto.toFloID(user.id);
},
lock() {
user_private = user_priv_wrap;
},
async unlock() {
if (await user.private === raw_user.private)
user_private = user_priv_raw;
},
get_contact(id) {
if (!user.contacts)
throw "Contacts not available";
else if (user.contacts[id])
return user.contacts[id];
else {
let id_raw = floCrypto.decodeAddr(id).hex;
for (let i in user.contacts)
if (floCrypto.decodeAddr(i).hex == id_raw)
return user.contacts[i];
}
},
get_pubKey(id) {
if (!user.pubKeys)
throw "Contacts not available";
else if (user.pubKeys[id])
return user.pubKeys[id];
else {
let id_raw = floCrypto.decodeAddr(id).hex;
for (let i in user.pubKeys)
if (floCrypto.decodeAddr(i).hex == id_raw)
return user.pubKeys[i];
}
},
clear() {
user_id = user_public = user_private = undefined;
user_priv_raw = aes_key = undefined;
delete user.contacts;
delete user.pubKeys;
delete user.messages;
}
};
Object.defineProperties(window, {
myFloID: {
get: () => {
try {
return user.id;
} catch {
return;
}
}
},
myUserID: {
get: () => {
try {
return user.id;
} catch {
return;
}
}
},
myPubKey: {
get: () => {
try {
return user.public;
} catch {
return;
}
}
},
myPrivKey: {
get: () => {
try {
return user.private;
} catch {
return;
}
}
}
});
var subAdmins, settings
Object.defineProperties(floGlobals, {
subAdmins: {
get: () => subAdmins
},
settings: {
get: () => settings
},
contacts: {
get: () => user.contacts
},
pubKeys: {
get: () => user.pubKeys
},
messages: {
get: () => user.messages
}
})
function initIndexedDB() {
return new Promise((resolve, reject) => {
var obs_g = {
//general
lastTx: {},
//supernode (cloud list)
supernodes: {
indexes: {
uri: null,
pubKey: null
}
}
}
var obs_a = {
//login credentials
credentials: {},
//for Dapps
subAdmins: {},
settings: {},
appObjects: {},
generalData: {},
lastVC: {}
}
//add other given objectStores
initIndexedDB.appObs = initIndexedDB.appObs || {}
for (let o in initIndexedDB.appObs)
if (!(o in obs_a))
obs_a[o] = initIndexedDB.appObs[o]
Promise.all([
compactIDB.initDB(DEFAULT.application, obs_a),
compactIDB.initDB(DEFAULT.root, obs_g)
]).then(result => {
compactIDB.setDefaultDB(DEFAULT.application)
resolve("IndexedDB App Storage Initated Successfully")
}).catch(error => reject(error));
})
}
function initUserDB() {
return new Promise((resolve, reject) => {
var obs = {
contacts: {},
pubKeys: {},
messages: {}
}
compactIDB.initDB(user.db_name, obs).then(result => {
resolve("UserDB Initated Successfully")
}).catch(error => reject('Init userDB failed'));
})
}
function loadUserDB() {
return new Promise((resolve, reject) => {
var loadData = ["contacts", "pubKeys", "messages"]
var promises = []
for (var i = 0; i < loadData.length; i++)
promises[i] = compactIDB.readAllData(loadData[i], user.db_name)
Promise.all(promises).then(results => {
for (var i = 0; i < loadData.length; i++)
user[loadData[i]] = results[i]
resolve("Loaded Data from userDB")
}).catch(error => reject('Load userDB failed'))
})
}
const startUpFunctions = [];
startUpFunctions.push(function readSupernodeListFromAPI() {
return new Promise((resolve, reject) => {
compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => {
floBlockchainAPI.readData(floCloudAPI.SNStorageID, {
ignoreOld: lastTx,
sentOnly: true,
pattern: "SuperNodeStorage"
}).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i]).SuperNodeStorage;
for (let sn in content.removeNodes)
compactIDB.removeData("supernodes", sn, DEFAULT.root);
for (let sn in content.newNodes)
compactIDB.writeData("supernodes", content.newNodes[sn], sn, DEFAULT.root);
}
compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root);
compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
floCloudAPI.init(nodes)
.then(result => resolve("Loaded Supernode list\n" + result))
.catch(error => reject(error))
})
})
}).catch(error => reject(error))
})
});
startUpFunctions.push(function readAppConfigFromAPI() {
return new Promise((resolve, reject) => {
compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => {
floBlockchainAPI.readData(DEFAULT.adminID, {
ignoreOld: lastTx,
sentOnly: true,
pattern: DEFAULT.application
}).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i])[DEFAULT.application];
if (!content || typeof content !== "object")
continue;
if (Array.isArray(content.removeSubAdmin))
for (var j = 0; j < content.removeSubAdmin.length; j++)
compactIDB.removeData("subAdmins", content.removeSubAdmin[j]);
if (Array.isArray(content.addSubAdmin))
for (var k = 0; k < content.addSubAdmin.length; k++)
compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]);
if (content.settings)
for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l)
}
compactIDB.writeData("lastTx", result.totalTxs, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
compactIDB.readAllData("subAdmins").then(result => {
subAdmins = Object.keys(result);
compactIDB.readAllData("settings").then(result => {
settings = result;
resolve("Read app configuration from blockchain");
})
})
})
}).catch(error => reject(error))
})
});
startUpFunctions.push(function loadDataFromAppIDB() {
return new Promise((resolve, reject) => {
var loadData = ["appObjects", "generalData", "lastVC"]
var promises = []
for (var i = 0; i < loadData.length; i++)
promises[i] = compactIDB.readAllData(loadData[i])
Promise.all(promises).then(results => {
for (var i = 0; i < loadData.length; i++)
floGlobals[loadData[i]] = results[i]
resolve("Loaded Data from app IDB")
}).catch(error => reject(error))
})
});
var keyInput = type => new Promise((resolve, reject) => {
let inputVal = prompt(`Enter ${type}: `)
if (inputVal === null)
reject(null)
else
resolve(inputVal)
});
function getCredentials() {
const readSharesFromIDB = indexArr => new Promise((resolve, reject) => {
var promises = []
for (var i = 0; i < indexArr.length; i++)
promises.push(compactIDB.readData('credentials', indexArr[i]))
Promise.all(promises).then(shares => {
var secret = floCrypto.retrieveShamirSecret(shares)
if (secret)
resolve(secret)
else
reject("Shares are insufficient or incorrect")
}).catch(error => {
clearCredentials();
location.reload();
})
});
const writeSharesToIDB = (shares, i = 0, resultIndexes = []) => new Promise(resolve => {
if (i >= shares.length)
return resolve(resultIndexes)
var n = floCrypto.randInt(0, 100000)
compactIDB.addData("credentials", shares[i], n).then(res => {
resultIndexes.push(n)
writeSharesToIDB(shares, i + 1, resultIndexes)
.then(result => resolve(result))
}).catch(error => {
writeSharesToIDB(shares, i, resultIndexes)
.then(result => resolve(result))
})
});
const getPrivateKeyCredentials = () => new Promise((resolve, reject) => {
var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`)
if (indexArr) {
readSharesFromIDB(JSON.parse(indexArr))
.then(result => resolve(result))
.catch(error => reject(error))
} else {
var privKey;
keyInput("PRIVATE_KEY").then(result => {
if (!result)
return reject("Empty Private Key")
var floID = floCrypto.getFloID(result)
if (!floID || !floCrypto.validateFloID(floID))
return reject("Invalid Private Key")
privKey = result;
}).catch(error => {
console.log(error, "Generating Random Keys")
privKey = floCrypto.generateNewID().privKey
}).finally(_ => {
if (!privKey)
return;
var threshold = floCrypto.randInt(10, 20)
var shares = floCrypto.createShamirsSecretShares(privKey, threshold, threshold)
writeSharesToIDB(shares).then(resultIndexes => {
//store index keys in localStorage
localStorage.setItem(`${DEFAULT.application}#privKey`, JSON.stringify(resultIndexes))
//also add a dummy privatekey to the IDB
var randomPrivKey = floCrypto.generateNewID().privKey
var randomThreshold = floCrypto.randInt(10, 20)
var randomShares = floCrypto.createShamirsSecretShares(randomPrivKey, randomThreshold, randomThreshold)
writeSharesToIDB(randomShares)
//resolve private Key
resolve(privKey)
})
})
}
});
const checkIfPinRequired = key => new Promise((resolve, reject) => {
if (key.length == 52)
resolve(key)
else {
keyInput("PIN/Password").then(pwd => {
try {
let privKey = Crypto.AES.decrypt(key, pwd);
resolve(privKey)
} catch (error) {
reject("Access Denied: Incorrect PIN/Password")
}
}).catch(error => reject("Access Denied: PIN/Password required"))
}
});
return new Promise((resolve, reject) => {
getPrivateKeyCredentials().then(key => {
checkIfPinRequired(key).then(privKey => {
try {
user_public = floCrypto.getPubKeyHex(privKey);
user_id = floCrypto.getAddress(privKey);
floCloudAPI.user(user_id, privKey); //Set user for floCloudAPI
user_priv_wrap = () => checkIfPinRequired(key);
let n = floCrypto.randInt(12, 20);
aes_key = floCrypto.randString(n);
user_priv_raw = Crypto.AES.encrypt(privKey, aes_key);
user_private = user_priv_wrap;
resolve('Login Credentials loaded successful')
} catch (error) {
console.log(error)
reject("Corrupted Private Key")
}
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
var startUpLog = (status, log) => status ? console.log(log) : console.error(log);
const callStartUpFunction = i => new Promise((resolve, reject) => {
startUpFunctions[i]().then(result => {
callStartUpFunction.completed += 1;
startUpLog(true, `${result}\nCompleted ${callStartUpFunction.completed}/${callStartUpFunction.total} Startup functions`)
resolve(true)
}).catch(error => {
callStartUpFunction.failed += 1;
startUpLog(false, `${error}\nFailed ${callStartUpFunction.failed}/${callStartUpFunction.total} Startup functions`)
reject(false)
})
});
var _midFunction;
const midStartUp = () => new Promise((res, rej) => {
if (_midFunction instanceof Function) {
_midFunction()
.then(r => res("Mid startup function completed"))
.catch(e => rej("Mid startup function failed"))
} else
res("No mid startup function")
});
const callAndLog = p => new Promise((res, rej) => {
p.then(r => {
startUpLog(true, r)
res(r)
}).catch(e => {
startUpLog(false, e)
rej(e)
})
});
floDapps.launchStartUp = function() {
return new Promise((resolve, reject) => {
initIndexedDB().then(log => {
console.log(log)
callStartUpFunction.total = startUpFunctions.length;
callStartUpFunction.completed = 0;
callStartUpFunction.failed = 0;
let p1 = new Promise((res, rej) => {
Promise.all(startUpFunctions.map((f, i) => callStartUpFunction(i))).then(r => {
callAndLog(midStartUp())
.then(r => res(true))
.catch(e => rej(false))
})
});
let p2 = new Promise((res, rej) => {
callAndLog(getCredentials()).then(r => {
callAndLog(initUserDB()).then(r => {
callAndLog(loadUserDB())
.then(r => res(true))
.catch(e => rej(false))
}).catch(e => rej(false))
}).catch(e => rej(false))
})
Promise.all([p1, p2])
.then(r => resolve('App Startup finished successful'))
.catch(e => reject('App Startup failed'))
}).catch(error => {
startUpLog(false, error);
reject("App database initiation failed")
})
})
}
floDapps.addStartUpFunction = fn => fn instanceof Function && !startUpFunctions.includes(fn) ? startUpFunctions.push(fn) : false;
floDapps.setMidStartup = fn => fn instanceof Function ? _midFunction = fn : false;
floDapps.setCustomStartupLogger = fn => fn instanceof Function ? startUpLog = fn : false;
floDapps.setCustomPrivKeyInput = fn => fn instanceof Function ? keyInput = fn : false;
floDapps.setAppObjectStores = appObs => initIndexedDB.appObs = appObs;
floDapps.storeContact = function(floID, name) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject("Invalid floID!")
compactIDB.writeData("contacts", name, floID, user.db_name).then(result => {
user.contacts[floID] = name;
resolve("Contact stored")
}).catch(error => reject(error))
});
}
floDapps.storePubKey = function(floID, pubKey) {
return new Promise((resolve, reject) => {
if (floID in user.pubKeys)
return resolve("pubKey already stored")
if (!floCrypto.validateAddr(floID))
return reject("Invalid floID!")
if (!floCrypto.verifyPubKey(pubKey, floID))
return reject("Incorrect pubKey")
compactIDB.writeData("pubKeys", pubKey, floID, user.db_name).then(result => {
user.pubKeys[floID] = pubKey;
resolve("pubKey stored")
}).catch(error => reject(error))
});
}
floDapps.sendMessage = function(floID, message) {
return new Promise((resolve, reject) => {
let options = {
receiverID: floID,
application: DEFAULT.root,
comment: DEFAULT.application
}
if (floID in user.pubKeys)
message = floCrypto.encryptData(JSON.stringify(message), user.pubKeys[floID])
floCloudAPI.sendApplicationData(message, "Message", options)
.then(result => resolve(result))
.catch(error => reject(error))
})
}
floDapps.requestInbox = function(callback) {
return new Promise((resolve, reject) => {
let lastVC = Object.keys(user.messages).sort().pop()
let options = {
receiverID: user.id,
application: DEFAULT.root,
lowerVectorClock: lastVC + 1
}
let privKey = raw_user.private;
options.callback = (d, e) => {
for (let v in d) {
try {
if (d[v].message instanceof Object && "secret" in d[v].message)
d[v].message = floCrypto.decryptData(d[v].message, privKey)
} catch (error) {}
compactIDB.writeData("messages", d[v], v, user.db_name)
user.messages[v] = d[v]
}
if (callback instanceof Function)
callback(d, e)
}
floCloudAPI.requestApplicationData("Message", options)
.then(result => resolve(result))
.catch(error => reject(error))
})
}
floDapps.manageAppConfig = function(adminPrivKey, addList, rmList, settings) {
return new Promise((resolve, reject) => {
if (!Array.isArray(addList) || !addList.length) addList = undefined;
if (!Array.isArray(rmList) || !rmList.length) rmList = undefined;
if (!settings || typeof settings !== "object" || !Object.keys(settings).length) settings = undefined;
if (!addList && !rmList && !settings)
return reject("No configuration change")
var floData = {
[DEFAULT.application]: {
addSubAdmin: addList,
removeSubAdmin: rmList,
settings: settings
}
}
var floID = floCrypto.getFloID(adminPrivKey)
if (floID != DEFAULT.adminID)
reject('Access Denied for Admin privilege')
else
floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey)
.then(result => resolve(['Updated App Configuration', result]))
.catch(error => reject(error))
})
}
const clearCredentials = floDapps.clearCredentials = function() {
return new Promise((resolve, reject) => {
compactIDB.clearData('credentials', DEFAULT.application).then(result => {
localStorage.removeItem(`${DEFAULT.application}#privKey`);
user.clear();
resolve("privKey credentials deleted!")
}).catch(error => reject(error))
})
}
floDapps.deleteUserData = function(credentials = false) {
return new Promise((resolve, reject) => {
let p = []
p.push(compactIDB.deleteDB(user.db_name))
if (credentials)
p.push(clearCredentials())
Promise.all(p)
.then(result => resolve('User database(local) deleted'))
.catch(error => reject(error))
})
}
floDapps.deleteAppData = function() {
return new Promise((resolve, reject) => {
compactIDB.deleteDB(DEFAULT.application).then(result => {
localStorage.removeItem(`${DEFAULT.application}#privKey`)
user.clear();
compactIDB.removeData('lastTx', `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root)
.then(result => resolve("App database(local) deleted"))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
floDapps.securePrivKey = function(pwd) {
return new Promise(async (resolve, reject) => {
let indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`)
if (!indexArr)
return reject("PrivKey not found");
indexArr = JSON.parse(indexArr)
let encryptedKey = Crypto.AES.encrypt(await user.private, pwd);
let threshold = indexArr.length;
let shares = floCrypto.createShamirsSecretShares(encryptedKey, threshold, threshold)
let promises = [];
let overwriteFn = (share, index) =>
compactIDB.writeData("credentials", share, index, DEFAULT.application);
for (var i = 0; i < threshold; i++)
promises.push(overwriteFn(shares[i], indexArr[i]));
Promise.all(promises)
.then(results => resolve("Private Key Secured"))
.catch(error => reject(error))
})
}
floDapps.verifyPin = function(pin = null) {
const readSharesFromIDB = function(indexArr) {
return new Promise((resolve, reject) => {
var promises = []
for (var i = 0; i < indexArr.length; i++)
promises.push(compactIDB.readData('credentials', indexArr[i]))
Promise.all(promises).then(shares => {
var secret = floCrypto.retrieveShamirSecret(shares)
console.info(shares, secret)
if (secret)
resolve(secret)
else
reject("Shares are insufficient or incorrect")
}).catch(error => {
clearCredentials();
location.reload();
})
})
}
return new Promise((resolve, reject) => {
var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`)
console.info(indexArr)
if (!indexArr)
reject('No login credentials found')
readSharesFromIDB(JSON.parse(indexArr)).then(key => {
if (key.length == 52) {
if (pin === null)
resolve("Private key not secured")
else
reject("Private key not secured")
} else {
if (pin === null)
return reject("PIN/Password required")
try {
let privKey = Crypto.AES.decrypt(key, pin);
resolve("PIN/Password verified")
} catch (error) {
reject("Incorrect PIN/Password")
}
}
}).catch(error => reject(error))
})
}
const getNextGeneralData = floDapps.getNextGeneralData = function(type, vectorClock = null, options = {}) {
var fk = floCloudAPI.util.filterKey(type, options)
vectorClock = vectorClock || 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) ? raw_user.private : 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) {}
}
}
getNextGeneralData[fk] = Object.keys(filteredResult).sort().pop();
return filteredResult;
}
const syncData = floDapps.syncData = {};
syncData.oldDevice = () => new Promise((resolve, reject) => {
let sync = {
contacts: user.contacts,
pubKeys: user.pubKeys,
messages: user.messages
}
let message = Crypto.AES.encrypt(JSON.stringify(sync), raw_user.private)
let options = {
receiverID: user.id,
application: DEFAULT.root
}
floCloudAPI.sendApplicationData(message, "syncData", options)
.then(result => resolve(result))
.catch(error => reject(error))
});
syncData.newDevice = () => new Promise((resolve, reject) => {
var options = {
receiverID: user.id,
senderID: user.id,
application: DEFAULT.root,
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, raw_user.private))
let promises = []
let store = (key, val, obs) => promises.push(compactIDB.writeData(obs, val, key, user.db_name));
["contacts", "pubKeys", "messages"].forEach(c => {
for (let i in sync[c]) {
store(i, sync[c][i], c)
user[c][i] = sync[c][i]
}
})
Promise.all(promises)
.then(results => resolve("Sync data successful"))
.catch(error => reject(error))
}).catch(error => reject(error))
});
})('object' === typeof module ? module.exports : window.floDapps = {});

12536
scripts/index.html Normal file

File diff suppressed because one or more lines are too long

9356
scripts/lib.js Normal file

File diff suppressed because it is too large Load Diff