This commit is contained in:
sairaj mote 2023-01-06 02:42:54 +05:30
parent 40bcdb20e8
commit 7bbc85937b
12 changed files with 3309 additions and 2296 deletions

View File

@ -2,7 +2,12 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: "Roboto", sans-serif; font-family: "Inter", sans-serif;
}
*:focus-visible {
outline: var(--accent-color) solid medium;
outline-offset: 0;
transition: outline-offset 0.3s;
} }
:root { :root {
@ -15,17 +20,20 @@ body {
} }
body { body {
--accent-color: #4d77ff; --accent-color: #234ebf;
--text-color: 20, 20, 20; --accent-color-rgb: 35, 78, 191;
--foreground-color: 252, 253, 255; --blue-accent-1: #f5f8ff;
--background-color: 241, 243, 248; --blue-accent-2: #f2f6ff;
--danger-color: rgb(255, 75, 75); --text-color: 58, 58, 58;
--foreground-color: 255, 255, 255;
--background-color: 243, 245, 250;
--danger-color: #ff6161;
--green: #1cad59; --green: #1cad59;
--yellow: rgb(220, 165, 0); --yellow: rgb(220, 165, 0);
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-gutter: stable; scrollbar-gutter: stable;
color: rgba(var(--text-color), 1); color: rgba(var(--text-color), 1);
background-color: rgba(var(--background-color), 1); background-color: rgba(var(--foreground-color), 1);
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative; position: relative;
display: flex; display: flex;
@ -34,6 +42,9 @@ body {
body[data-theme=dark] { body[data-theme=dark] {
--accent-color: #a0b6ff; --accent-color: #a0b6ff;
--accent-color-rgb: 160, 182, 255;
--blue-accent-1: #f5f8ff10;
--blue-accent-2: #f2f6ff10;
--text-color: 220, 220, 220; --text-color: 220, 220, 220;
--foreground-color: 27, 28, 29; --foreground-color: 27, 28, 29;
--background-color: 21, 22, 22; --background-color: 21, 22, 22;
@ -66,18 +77,8 @@ a {
text-decoration: none; text-decoration: none;
color: var(--accent-color); color: var(--accent-color);
} }
a:focus-visible {
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset;
}
a.button {
padding: 0.4rem 0.6rem;
border-radius: 0.3rem;
font-size: 0.9rem;
font-weight: 500;
color: inherit;
}
.button,
button { button {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -93,20 +94,21 @@ button {
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
padding: 0.8rem; padding: 0.8rem;
border-radius: 0.3rem; border-radius: 0.5rem;
justify-content: center; justify-content: center;
color: inherit; color: inherit;
min-width: -webkit-max-content;
min-width: -moz-max-content;
min-width: max-content;
} }
button:focus-visible { .button:not(:disabled),
outline: var(--accent-color) solid medium;
}
button:not(:disabled) { button:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
.button { .button {
color: var(--accent-color); color: var(--accent-color);
background-color: rgba(var(--text-color), 0.06); background-color: var(--blue-accent-1);
} }
.button .icon { .button .icon {
fill: var(--accent-color); fill: var(--accent-color);
@ -152,18 +154,6 @@ button:disabled {
opacity: 0.5; opacity: 0.5;
} }
a:-webkit-any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
a:-moz-any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
details summary { details summary {
display: flex; display: flex;
-webkit-user-select: none; -webkit-user-select: none;
@ -172,7 +162,6 @@ details summary {
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: var(--accent-color);
} }
details[open] summary { details[open] summary {
@ -192,18 +181,8 @@ sm-textarea button .icon {
fill: var(--accent-color); fill: var(--accent-color);
} }
sm-button { sm-copy {
--padding: 0.8rem; font-size: 0.9rem;
}
sm-button[variant=primary] .icon {
fill: rgba(var(--background-color), 1);
}
sm-button[disabled] .icon {
fill: rgba(var(--text-color), 0.6);
}
sm-button.danger {
--background: var(--danger-color);
color: rgba(var(--background-color), 1);
} }
sm-spinner { sm-spinner {
@ -225,26 +204,26 @@ sm-option {
} }
sm-chips { sm-chips {
--gap: 0; --gap: 0.3rem;
background-color: rgba(var(--text-color), 0.06);
border-radius: 0.3rem;
padding: 0.3rem;
} }
sm-chip { sm-chip {
font-size: 0.8rem; position: relative;
--border-radius: 0.2rem; font-size: 0.9rem;
--border-radius: 0.5rem;
--padding: 0.5rem 0.8rem;
--background: rgba(var(--text-color), 0.06);
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
font-weight: 500;
}
sm-chip[selected] {
--background: var(--accent-color);
color: rgba(var(--foreground-color), 1);
} }
sm-copy { ul {
font-size: 0.9rem;
}
ul,
ol {
list-style: none; list-style: none;
} }
@ -440,6 +419,10 @@ ol li::before {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.margin-right-auto {
margin-right: auto;
}
.margin-left-0-5 { .margin-left-0-5 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
@ -593,16 +576,16 @@ ol li::before {
height: 100%; height: 100%;
} }
.page__header { .page__header {
position: -webkit-sticky;
position: sticky;
top: 0;
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
min-height: 5rem; background-color: rgba(var(--foreground-color), 1);
} z-index: 1;
.page__header .grid {
margin-top: auto;
}
.page__header h1 {
font-size: 2rem;
} }
#confirmation_popup, #confirmation_popup,
@ -729,13 +712,36 @@ ol li::before {
#main_header { #main_header {
display: grid; display: grid;
gap: 1.5rem; gap: 1rem;
padding: 1rem 1rem; padding: 1rem;
align-items: center; align-items: center;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: 1fr auto auto auto;
min-height: 4rem; min-height: 4rem;
} }
#user_profile_button {
background-color: var(--blue-accent-1);
border-radius: 2rem;
font-size: 0.8rem;
padding: 0.6rem 0.8rem;
font-weight: 700;
}
#notifications_wrapper {
position: relative;
}
#notifications_panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
background-color: rgba(var(--foreground-color), 1);
padding: 1rem;
}
.logged-in-user-id { .logged-in-user-id {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
max-width: -webkit-fit-content; max-width: -webkit-fit-content;
@ -753,61 +759,11 @@ ol li::before {
height: 100%; height: 100%;
width: 100%; width: 100%;
transition: background-color 0.3s; transition: background-color 0.3s;
background-color: rgba(var(--background-color), 1);
} }
#main_card.nav-hidden .inner-page { #main_card.nav-hidden .inner-page {
grid-area: 2/1/3/-1; grid-area: 2/1/3/-1;
} }
#main_navbar {
display: flex;
background-color: rgba(var(--foreground-color), 0.8);
}
#main_navbar ul {
display: flex;
height: 100%;
width: 100%;
}
#main_navbar ul li {
width: 100%;
}
.nav-item {
position: relative;
display: flex;
flex: 1;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem 0.3rem;
color: var(--text-color);
font-size: 0.8rem;
font-weight: 500;
gap: 0.5rem;
}
.nav-item .icon {
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.nav-item__title {
transition: opacity 0.2s, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.nav-item--active {
color: var(--accent-color);
}
.nav-item--active .icon {
fill: var(--accent-color);
}
.nav-item__indicator {
position: absolute;
bottom: 0;
width: 2rem;
height: 0.3rem;
background: var(--accent-color);
border-radius: 1rem 1rem 0 0;
z-index: 1;
}
.badge { .badge {
display: flex; display: flex;
align-items: center; align-items: center;
@ -864,9 +820,45 @@ ol li::before {
} }
#home { #home {
position: relative;
display: grid;
padding: 0;
gap: 3rem;
overflow-y: visible;
align-content: initial;
}
#home > * {
align-content: flex-start;
}
#wallet_section {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
gap: 1.5rem;
background-color: rgba(var(--accent-color-rgb), 0.02);
}
#wallet_section sm-chips {
display: flex;
justify-content: center;
}
#wallet_section sm-chip::part(chip) {
width: 8rem;
justify-content: center;
}
#wallet_section > * {
width: 100%;
}
#wallet_section::before {
content: "";
position: absolute;
top: -10vmax;
left: 0;
width: 100%;
height: 10vmax;
background-color: inherit;
pointer-events: none;
} }
#user, #user,
@ -896,41 +888,30 @@ ol li::before {
#wallet_cards_wrapper { #wallet_cards_wrapper {
display: grid; display: grid;
gap: 0.5rem; grid-template-columns: 6rem 1fr;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); background-color: rgba(var(--text-color), 0.06);
align-items: flex-start; border-radius: 0.5rem;
padding: 0 0.2rem;
}
#wallet_cards_wrapper sm-input {
--background: transparent;
}
#wallet_cards_wrapper sm-select {
margin: 0.2rem 0;
--background: rgba(var(--foreground-color), 1);
}
#wallet_cards_wrapper sm-select[open] {
z-index: 10;
} }
.balance-card { .balance-card {
display: grid; background-color: rgba(var(--accent-color-rgb), 0.06);
overflow: hidden; border-radius: 0.5rem;
border-radius: 0.8rem; padding: 0.8rem;
background-color: rgba(var(--foreground-color), 1); font-size: 0.9rem;
} }
.balance-card > * { .balance-card > * {
padding: 1rem; font-weight: 500;
gap: 1.5rem;
}
.balance-card > :last-child:not(:only-child) {
border-top: thin solid rgba(var(--text-color), 0.2);
}
.balance-card .asset-icon {
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
border-radius: 1.1rem;
margin-right: 0.8rem;
fill: rgba(var(--background-color), 1);
}
.balance-card .asset-icon--rupee {
background-color: var(--green);
}
.balance-card .asset-icon--flo {
fill: white;
background-color: #4d77ff;
}
.balance-card .asset-icon--btc {
background-color: rgb(255, 173, 8);
} }
#rupee_balance span:first-of-type, #rupee_balance span:first-of-type,
@ -945,9 +926,11 @@ ol li::before {
} }
.actions-wrapper { .actions-wrapper {
display: grid; display: flex;
gap: 0.5rem; gap: 0.5rem;
grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr)); }
.actions-wrapper > * {
flex: 1;
} }
.wallet-action, .wallet-action,
@ -955,12 +938,17 @@ ol li::before {
position: relative; position: relative;
color: inherit; color: inherit;
font-weight: 500; font-weight: 500;
font-size: 0.9rem;
} }
.wallet-action .icon:first-of-type, .wallet-action .icon:first-of-type,
.integrated-action-button .icon:first-of-type { .integrated-action-button .icon:first-of-type {
height: 1.5rem; height: 3rem;
width: 1.5rem; width: 3rem;
padding: 0.95rem;
overflow: visible;
fill: var(--accent-color); fill: var(--accent-color);
border-radius: 2rem;
background-color: var(--blue-accent-2);
} }
.wallet-action .badge, .wallet-action .badge,
.integrated-action-button .badge { .integrated-action-button .badge {
@ -969,20 +957,13 @@ ol li::before {
} }
.wallet-action { .wallet-action {
gap: 0.5rem;
flex-direction: column; flex-direction: column;
text-align: center;
align-items: center; align-items: center;
font-size: 0.8rem; font-size: 0.8rem;
white-space: initial; white-space: initial;
padding: 0.8rem;
border: solid thin rgba(var(--text-color), 0.1);
border-radius: 0.5rem; border-radius: 0.5rem;
} padding: 0;
.wallet-action:hover {
border: solid thin rgba(var(--text-color), 0.5);
}
.wallet-action .icon {
margin-bottom: 0.8rem;
} }
.integrated-action-button { .integrated-action-button {
@ -992,8 +973,8 @@ ol li::before {
.integrated-action-button .icon:first-of-type { .integrated-action-button .icon:first-of-type {
margin-right: 1rem; margin-right: 1rem;
} }
.integrated-action-button .icon:last-of-type { .integrated-action-button span:last-of-type {
margin-left: 0.5rem; font-size: 1rem;
} }
.cashier-status { .cashier-status {
@ -1049,53 +1030,48 @@ ol li::before {
position: relative; position: relative;
} }
#recent_contacts {
gap: 0;
}
#saved_ids_list { #saved_ids_list {
display: grid; display: grid;
gap: 0.5rem; gap: 1rem;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.saved-id { .saved-id {
align-items: center;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 0 0.8rem; gap: 0 1rem;
border-radius: 0.5rem; padding: 0.5rem 0;
padding: 0.5rem;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
background-color: rgba(var(--foreground-color), 1); border-radius: 0.5rem;
} }
.saved-id.highlight { .saved-id.highlight {
box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset; box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset;
} }
.saved-id .edit-saved { .saved-id .edit-saved {
grid-area: 1/1/3/2;
padding: 0.3rem; padding: 0.3rem;
position: relative; position: relative;
} }
.saved-id .edit-saved .icon {
position: absolute;
right: 0;
bottom: 0;
border-radius: 0.5rem;
padding: 0.2rem;
background-color: rgba(var(--background-color), 1);
}
.saved-id__initials { .saved-id__initials {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 2.4rem; height: 2.5rem;
width: 2.4rem; width: 2.5rem;
font-size: 1.2rem; font-size: 1.2rem;
text-transform: uppercase; text-transform: uppercase;
color: rgba(var(--background-color), 1); color: var(--accent-color);
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
background-color: var(--accent-color); background-color: rgba(var(--text-color), 0.03);
justify-self: flex-start;
border-radius: 2rem; border-radius: 2rem;
grid-area: 1/1/3/2;
} }
.saved-id__title { .saved-id__title {
align-self: flex-end; align-self: flex-end;
@ -1107,6 +1083,11 @@ ol li::before {
.saved-id__flo-id { .saved-id__flo-id {
font-size: 0.8rem; font-size: 0.8rem;
} }
.saved-id__copy {
padding: 0.3rem 0.5rem;
border-radius: 0.3rem;
background-color: rgba(var(--text-color), 0.03);
}
#saved_ids_tip { #saved_ids_tip {
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
@ -1187,25 +1168,15 @@ ol li::before {
font-size: 0.8rem; font-size: 0.8rem;
} }
#history .page__header { #history_applied_filters {
margin-bottom: 0;
}
#history_applied_filters_wrapper {
position: -webkit-sticky;
position: sticky;
top: 0;
padding: 0.2rem 0 0.5rem 0; padding: 0.2rem 0 0.5rem 0;
background-color: rgba(var(--background-color), 1);
z-index: 1;
transition: background-color 0.3s;
} }
.applied-filter { .applied-filter {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.5rem 0.6rem 0.5rem 0.8rem; padding: 0.5rem 0.6rem 0.5rem 0.8rem;
border-radius: 1rem; border-radius: 0.5rem;
border: solid thin rgba(var(--text-color), 0.2); border: solid thin rgba(var(--text-color), 0.2);
text-transform: uppercase; text-transform: uppercase;
font-size: 0.8rem; font-size: 0.8rem;
@ -1219,6 +1190,17 @@ ol li::before {
margin-left: 0.3rem; margin-left: 0.3rem;
} }
fieldset {
border: none;
padding: 0;
margin: 0;
}
fieldset legend {
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.category-chip { .category-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -1228,11 +1210,6 @@ ol li::before {
font-size: 0.8rem; font-size: 0.8rem;
border: solid thin rgba(var(--text-color), 0.2); border: solid thin rgba(var(--text-color), 0.2);
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.category-chip:focus {
outline: solid var(--accent-color);
} }
.category-chip input { .category-chip input {
display: none; display: none;
@ -1266,6 +1243,7 @@ ol li::before {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
margin-top: 1.5rem; margin-top: 1.5rem;
padding-bottom: 3rem;
} }
.transaction { .transaction {
@ -1276,16 +1254,16 @@ ol li::before {
.transaction__amount { .transaction__amount {
white-space: nowrap; white-space: nowrap;
} }
.transaction.sent .icon { .transaction.sent .icon--tx-type {
fill: rgba(var(--text-color), 0.8); fill: var(--danger-color);
} }
.transaction.sent .transaction__amount { .transaction.sent .transaction__amount {
color: rgba(var(--text-color), 0.8); color: var(--danger-color);
} }
.transaction.sent .transaction__amount::before { .transaction.sent .transaction__amount::before {
content: "- "; content: "- ";
} }
.transaction.received .icon { .transaction.received .icon--tx-type {
fill: var(--green); fill: var(--green);
} }
.transaction.received .transaction__amount { .transaction.received .transaction__amount {
@ -1295,14 +1273,30 @@ ol li::before {
content: "+ "; content: "+ ";
} }
.transaction__icon { .transaction__icon {
display: flex; position: relative;
grid-area: icon;
display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
grid-area: 1/1/3/2;
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem; border-radius: 2rem;
grid-area: 1/1/3/2;
}
.transaction__icon > * {
grid-area: 1/1;
}
.transaction__icon .icon--tx-type {
position: absolute;
background-color: inherit;
right: -0.3rem;
bottom: -0.3rem;
border-radius: 1rem;
padding: 0.2rem;
height: 1.5rem;
width: 1.5rem;
border: solid medium rgba(var(--foreground-color), 1);
} }
.transaction__receiver { .transaction__receiver {
font-weight: 500; font-weight: 500;
@ -1313,7 +1307,7 @@ ol li::before {
} }
.transaction__amount { .transaction__amount {
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 500;
grid-area: 1/3/3/4; grid-area: 1/3/3/4;
} }
@ -1323,13 +1317,10 @@ ol li::before {
align-items: center; align-items: center;
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid"; grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid";
} }
.btc-tx:not(:last-of-type) {
padding-bottom: 1rem;
}
.btc-tx__amount { .btc-tx__amount {
white-space: nowrap; white-space: nowrap;
} }
.btc-tx.out .icon { .btc-tx.out .icon--tx-type {
fill: var(--danger-color); fill: var(--danger-color);
} }
.btc-tx.out .btc-tx__amount { .btc-tx.out .btc-tx__amount {
@ -1338,7 +1329,7 @@ ol li::before {
.btc-tx.out .btc-tx__amount::before { .btc-tx.out .btc-tx__amount::before {
content: "- "; content: "- ";
} }
.btc-tx.in .icon { .btc-tx.in .icon--tx-type {
fill: var(--green); fill: var(--green);
} }
.btc-tx.in .btc-tx__amount { .btc-tx.in .btc-tx__amount {
@ -1354,8 +1345,9 @@ ol li::before {
fill: var(--yellow); fill: var(--yellow);
} }
.btc-tx__icon { .btc-tx__icon {
position: relative;
grid-area: icon; grid-area: icon;
display: flex; display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 2.5rem; width: 2.5rem;
@ -1363,23 +1355,37 @@ ol li::before {
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem; border-radius: 2rem;
} }
.btc-tx__icon > * {
grid-area: 1/1;
}
.btc-tx__icon .icon--tx-type {
position: absolute;
background-color: inherit;
right: -0.3rem;
bottom: -0.3rem;
border-radius: 1rem;
padding: 0.2rem;
height: 1.5rem;
width: 1.5rem;
border: solid medium rgba(var(--foreground-color), 1);
}
.btc-tx__receiver { .btc-tx__receiver {
grid-area: receiver; grid-area: receiver;
font-weight: 500; font-weight: 500;
} }
.btc-tx__time { .btc-tx__time {
grid-area: time; grid-area: time;
font-size: 0.9rem; font-size: 0.8rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
.btc-tx__amount { .btc-tx__amount {
grid-area: amount; grid-area: amount;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 500;
} }
.btc-tx__id { .btc-tx__id {
grid-area: txid; grid-area: txid;
font-size: 0.9rem; font-size: 0.8rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
.btc-tx .unconfirmed-wrapper { .btc-tx .unconfirmed-wrapper {
@ -1411,12 +1417,6 @@ ol li::before {
background-color: rgba(var(--foreground-color), 1); background-color: rgba(var(--foreground-color), 1);
} }
#add_address_button {
border-radius: 0.5rem;
background-color: transparent;
border: solid 0.1rem var(--accent-color);
}
.user-action-result__icon { .user-action-result__icon {
justify-self: center; justify-self: center;
height: 4rem; height: 4rem;
@ -1561,6 +1561,9 @@ ol li::before {
color: var(--accent-color); color: var(--accent-color);
background-color: rgba(var(--foreground-color), 1); background-color: rgba(var(--foreground-color), 1);
} }
.payment-request--pending {
padding: 0;
}
.wallet-request { .wallet-request {
display: grid; display: grid;
@ -1652,13 +1655,14 @@ ol li::before {
} }
@media screen and (max-width: 40rem) { @media screen and (max-width: 40rem) {
.inner-page { #home {
padding-bottom: 7rem; padding-bottom: 3rem;
} }
#main_navbar.hide-away { #home > * {
bottom: 0; padding: 0 1rem;
left: 0; }
right: 0; .integrated-action-button .icon:last-of-type {
margin-left: auto;
} }
.nav-item--active .icon { .nav-item--active .icon {
transform: translateY(50%); transform: translateY(50%);
@ -1670,6 +1674,9 @@ ol li::before {
.integrated-action-button .icon:last-of-type { .integrated-action-button .icon:last-of-type {
margin-left: auto; margin-left: auto;
} }
.hide-on-mobile {
display: none;
}
} }
@media screen and (min-width: 40rem) { @media screen and (min-width: 40rem) {
sm-popup { sm-popup {
@ -1682,51 +1689,39 @@ ol li::before {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#main_card { .inner-page {
display: grid; padding: 0 14vw;
grid-template-columns: 10rem 1fr;
grid-template-rows: auto 1fr;
grid-template-areas: "header header" "navbar content";
} }
#main_header { #main_header {
grid-area: header; padding: 1.5rem 3vw;
padding: 1rem 1.5rem;
background-color: rgba(var(--foreground-color), 1);
}
#main_navbar {
height: 100%;
grid-area: navbar;
}
#main_navbar ul {
flex-direction: column;
gap: 0.3rem;
padding: 0.3rem;
}
.nav-item {
padding: 1rem;
padding-right: 2rem;
border-radius: 0.5rem;
flex-direction: row;
justify-content: start;
gap: 0.5rem;
font-size: 0.9rem;
opacity: 0.8;
}
.nav-item--active {
opacity: 1;
}
.nav-item__indicator {
width: 0.25rem;
height: 50%;
left: 0;
border-radius: 0 1rem 1rem 0;
bottom: auto;
} }
.card { .card {
padding: 1.5rem; padding: 1.5rem;
} }
.balance-card > * { #home {
padding: 1.5rem; grid-template-columns: 26rem 1fr;
gap: 4vw;
padding-right: 3vw;
}
#profile_popup {
--width: 32rem;
}
#notifications_panel {
position: absolute;
top: 100%;
left: auto;
bottom: auto;
right: 0;
border-radius: 0.5rem;
z-index: 10;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
border: solid 1px rgba(var(--text-color), 0.1);
background-color: rgba(var(--foreground-color), 1);
width: max(24rem, 100%);
}
#wallet_section {
padding: 0 3vw;
grid-row: 1/3;
} }
#contact > * { #contact > * {
padding: 1rem 12vw; padding: 1rem 12vw;
@ -1735,10 +1730,16 @@ ol li::before {
padding: 0.5rem 12vw; padding: 0.5rem 12vw;
} }
#saved_ids_list { #saved_ids_list {
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
} }
.inner-page { .hide-on-desktop {
padding: 1.5rem 14vw; display: none;
}
}
@media screen and (min-width: 68rem) {
#home {
grid-template-columns: 26rem 1fr 1fr;
} }
} }
@media (any-hover: hover) { @media (any-hover: hover) {
@ -1753,20 +1754,24 @@ ol li::before {
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgba(var(--text-color), 0.5); background: rgba(var(--text-color), 0.5);
} }
.interact:not([disabled]) { .button--primary:not([disabled]),
.button--danger:not([disabled]) {
transition: filter 0.3s;
}
.button--primary:not([disabled]):hover,
.button--danger:not([disabled]):hover {
filter: brightness(1.3);
}
.interact:not([disabled]),
button:not([disabled], .button--primary, .button--danger),
.button:not([disabled], .button--primary, .button--danger) {
transition: background-color 0.3s; transition: background-color 0.3s;
} }
.interact:not([disabled]):hover { .interact:not([disabled]):hover,
button:not([disabled], .button--primary, .button--danger):hover,
.button:not([disabled], .button--primary, .button--danger):hover {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
} }
button:not([disabled]),
.button:not([disabled]) {
transition: background-color 0.3s, filter 0.3s;
}
button:not([disabled]):hover,
.button:not([disabled]):hover {
filter: contrast(2);
}
} }
@supports (overflow: overlay) { @supports (overflow: overlay) {
body { body {

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,12 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: "Roboto", sans-serif; font-family: "Inter", sans-serif;
&:focus-visible {
outline: var(--accent-color) solid medium;
outline-offset: 0;
transition: outline-offset 0.3s;
}
} }
:root { :root {
font-size: clamp(1rem, 1.2vmax, 1.2rem); font-size: clamp(1rem, 1.2vmax, 1.2rem);
@ -14,17 +19,20 @@ body {
} }
body { body {
--accent-color: #4d77ff; --accent-color: #234ebf;
--text-color: 20, 20, 20; --accent-color-rgb: 35, 78, 191;
--foreground-color: 252, 253, 255; --blue-accent-1: #f5f8ff;
--background-color: 241, 243, 248; --blue-accent-2: #f2f6ff;
--danger-color: rgb(255, 75, 75); --text-color: 58, 58, 58;
--foreground-color: 255, 255, 255;
--background-color: 243, 245, 250;
--danger-color: #ff6161;
--green: #1cad59; --green: #1cad59;
--yellow: rgb(220, 165, 0); --yellow: rgb(220, 165, 0);
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-gutter: stable; scrollbar-gutter: stable;
color: rgba(var(--text-color), 1); color: rgba(var(--text-color), 1);
background-color: rgba(var(--background-color), 1); background-color: rgba(var(--foreground-color), 1);
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative; position: relative;
display: flex; display: flex;
@ -33,6 +41,9 @@ body {
body[data-theme="dark"] { body[data-theme="dark"] {
--accent-color: #a0b6ff; --accent-color: #a0b6ff;
--accent-color-rgb: 160, 182, 255;
--blue-accent-1: #f5f8ff10;
--blue-accent-2: #f2f6ff10;
--text-color: 220, 220, 220; --text-color: 220, 220, 220;
--foreground-color: 27, 28, 29; --foreground-color: 27, 28, 29;
--background-color: 21, 22, 22; --background-color: 21, 22, 22;
@ -62,18 +73,8 @@ strong {
a { a {
text-decoration: none; text-decoration: none;
color: var(--accent-color); color: var(--accent-color);
&:focus-visible {
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset;
} }
} .button,
a.button {
padding: 0.4rem 0.6rem;
border-radius: 0.3rem;
font-size: 0.9rem;
font-weight: 500;
color: inherit;
}
button { button {
user-select: none; user-select: none;
position: relative; position: relative;
@ -87,19 +88,17 @@ button {
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
padding: 0.8rem; padding: 0.8rem;
border-radius: 0.3rem; border-radius: 0.5rem;
justify-content: center; justify-content: center;
color: inherit; color: inherit;
&:focus-visible { min-width: max-content;
outline: var(--accent-color) solid medium;
}
&:not(:disabled) { &:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
} }
.button { .button {
color: var(--accent-color); color: var(--accent-color);
background-color: rgba(var(--text-color), 0.06); background-color: var(--blue-accent-1);
.icon { .icon {
fill: var(--accent-color); fill: var(--accent-color);
} }
@ -143,17 +142,12 @@ button:disabled {
opacity: 0.5; opacity: 0.5;
} }
a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
details summary { details summary {
display: flex; display: flex;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: var(--accent-color);
} }
details[open] { details[open] {
@ -175,23 +169,8 @@ sm-textarea {
} }
} }
} }
sm-button { sm-copy {
--padding: 0.8rem; font-size: 0.9rem;
&[variant="primary"] {
.icon {
fill: rgba(var(--background-color), 1);
}
}
&[disabled] {
.icon {
fill: rgba(var(--text-color), 0.6);
}
}
&.danger {
--background: var(--danger-color);
color: rgba(var(--background-color), 1);
}
} }
sm-spinner { sm-spinner {
--size: 1rem; --size: 1rem;
@ -208,21 +187,23 @@ sm-option {
font-size: 0.9rem; font-size: 0.9rem;
} }
sm-chips { sm-chips {
--gap: 0; --gap: 0.3rem;
background-color: rgba(var(--text-color), 0.06);
border-radius: 0.3rem;
padding: 0.3rem;
} }
sm-chip { sm-chip {
font-size: 0.8rem; position: relative;
--border-radius: 0.2rem;
user-select: none;
}
sm-copy {
font-size: 0.9rem; font-size: 0.9rem;
--border-radius: 0.5rem;
--padding: 0.5rem 0.8rem;
--background: rgba(var(--text-color), 0.06);
user-select: none;
font-weight: 500;
&[selected] {
--background: var(--accent-color);
color: rgba(var(--foreground-color), 1);
} }
ul, }
ol { ul {
list-style: none; list-style: none;
} }
ol { ol {
@ -403,7 +384,9 @@ ol {
.margin-right-0-5 { .margin-right-0-5 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.margin-right-auto {
margin-right: auto;
}
.margin-left-0-5 { .margin-left-0-5 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
@ -540,16 +523,15 @@ ol {
.page { .page {
height: 100%; height: 100%;
&__header { &__header {
position: sticky;
top: 0;
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
min-height: 5rem; background-color: rgba(var(--foreground-color), 1);
.grid { z-index: 1;
margin-top: auto;
}
h1 {
font-size: 2rem;
}
} }
} }
#confirmation_popup, #confirmation_popup,
@ -671,12 +653,32 @@ ol {
} }
#main_header { #main_header {
display: grid; display: grid;
gap: 1.5rem; gap: 1rem;
padding: 1rem 1rem; padding: 1rem;
align-items: center; align-items: center;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: 1fr auto auto auto;
min-height: 4rem; min-height: 4rem;
} }
#user_profile_button {
background-color: var(--blue-accent-1);
border-radius: 2rem;
font-size: 0.8rem;
padding: 0.6rem 0.8rem;
font-weight: 700;
}
#notifications_wrapper {
position: relative;
}
#notifications_panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
background-color: rgba(var(--foreground-color), 1);
padding: 1rem;
}
.logged-in-user-id { .logged-in-user-id {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
max-width: fit-content; max-width: fit-content;
@ -691,62 +693,12 @@ ol {
height: 100%; height: 100%;
width: 100%; width: 100%;
transition: background-color 0.3s; transition: background-color 0.3s;
background-color: rgba(var(--background-color), 1);
&.nav-hidden { &.nav-hidden {
.inner-page { .inner-page {
grid-area: 2/1/3/-1; grid-area: 2/1/3/-1;
} }
} }
} }
#main_navbar {
display: flex;
background-color: rgba(var(--foreground-color), 0.8);
ul {
display: flex;
height: 100%;
width: 100%;
li {
width: 100%;
}
}
}
.nav-item {
position: relative;
display: flex;
flex: 1;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem 0.3rem;
color: var(--text-color);
font-size: 0.8rem;
font-weight: 500;
gap: 0.5rem;
.icon {
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&__title {
transition: opacity 0.2s,
transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&--active {
color: var(--accent-color);
.icon {
fill: var(--accent-color);
}
}
&__indicator {
position: absolute;
bottom: 0;
width: 2rem;
height: 0.3rem;
background: var(--accent-color);
border-radius: 1rem 1rem 0 0;
z-index: 1;
}
}
.badge { .badge {
display: flex; display: flex;
align-items: center; align-items: center;
@ -801,9 +753,44 @@ ol {
overflow-y: auto; overflow-y: auto;
} }
#home { #home {
position: relative;
display: grid;
padding: 0;
gap: 3rem;
overflow-y: visible;
align-content: initial;
& > * {
align-content: flex-start;
}
}
#wallet_section {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
gap: 1.5rem;
background-color: rgba(var(--accent-color-rgb), 0.02);
sm-chips {
display: flex;
justify-content: center;
}
sm-chip::part(chip) {
width: 8rem;
justify-content: center;
}
> * {
width: 100%;
}
&::before {
content: "";
position: absolute;
top: -10vmax;
left: 0;
width: 100%;
height: 10vmax;
background-color: inherit;
pointer-events: none;
}
} }
#user, #user,
#cashier { #cashier {
@ -830,40 +817,28 @@ ol {
} }
#wallet_cards_wrapper { #wallet_cards_wrapper {
display: grid; display: grid;
gap: 0.5rem; grid-template-columns: 6rem 1fr;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); background-color: rgba(var(--text-color), 0.06);
align-items: flex-start; border-radius: 0.5rem;
padding: 0 0.2rem;
sm-input {
--background: transparent;
}
sm-select {
margin: 0.2rem 0;
--background: rgba(var(--foreground-color), 1);
&[open] {
z-index: 10;
}
}
} }
.balance-card { .balance-card {
display: grid; background-color: rgba(var(--accent-color-rgb), 0.06);
overflow: hidden; border-radius: 0.5rem;
border-radius: 0.8rem; padding: 0.8rem;
background-color: rgba(var(--foreground-color), 1); font-size: 0.9rem;
& > * { & > * {
padding: 1rem; font-weight: 500;
gap: 1.5rem;
}
& > :last-child:not(:only-child) {
border-top: thin solid rgba(var(--text-color), 0.2);
}
.asset-icon {
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
border-radius: 1.1rem;
margin-right: 0.8rem;
fill: rgba(var(--background-color), 1);
}
.asset-icon--rupee {
background-color: var(--green);
}
.asset-icon--flo {
fill: white;
background-color: #4d77ff;
}
.asset-icon--btc {
background-color: rgba(255, 173, 8);
} }
} }
#rupee_balance, #rupee_balance,
@ -877,19 +852,26 @@ ol {
} }
} }
.actions-wrapper { .actions-wrapper {
display: grid; display: flex;
gap: 0.5rem; gap: 0.5rem;
grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr)); > * {
flex: 1;
}
} }
.wallet-action, .wallet-action,
.integrated-action-button { .integrated-action-button {
position: relative; position: relative;
color: inherit; color: inherit;
font-weight: 500; font-weight: 500;
font-size: 0.9rem;
.icon:first-of-type { .icon:first-of-type {
height: 1.5rem; height: 3rem;
width: 1.5rem; width: 3rem;
padding: 0.95rem;
overflow: visible;
fill: var(--accent-color); fill: var(--accent-color);
border-radius: 2rem;
background-color: var(--blue-accent-2);
} }
.badge { .badge {
left: 0; left: 0;
@ -897,20 +879,13 @@ ol {
} }
} }
.wallet-action { .wallet-action {
gap: 0.5rem;
flex-direction: column; flex-direction: column;
text-align: center;
align-items: center; align-items: center;
font-size: 0.8rem; font-size: 0.8rem;
white-space: initial; white-space: initial;
padding: 0.8rem;
border: solid thin rgba(var(--text-color), 0.1);
border-radius: 0.5rem; border-radius: 0.5rem;
&:hover { padding: 0;
border: solid thin rgba(var(--text-color), 0.5);
}
.icon {
margin-bottom: 0.8rem;
}
} }
.integrated-action-button { .integrated-action-button {
padding: 0; padding: 0;
@ -918,8 +893,8 @@ ol {
.icon:first-of-type { .icon:first-of-type {
margin-right: 1rem; margin-right: 1rem;
} }
.icon:last-of-type { span:last-of-type {
margin-left: 0.5rem; font-size: 1rem;
} }
} }
.cashier-status { .cashier-status {
@ -970,49 +945,43 @@ ol {
#contacts { #contacts {
position: relative; position: relative;
} }
#recent_contacts {
gap: 0;
}
#saved_ids_list { #saved_ids_list {
display: grid; display: grid;
gap: 0.5rem; gap: 1rem;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.saved-id { .saved-id {
align-items: center;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 0 0.8rem; gap: 0 1rem;
border-radius: 0.5rem; padding: 0.5rem 0;
padding: 0.5rem;
user-select: none; user-select: none;
background-color: rgba(var(--foreground-color), 1); border-radius: 0.5rem;
&.highlight { &.highlight {
box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset; box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset;
} }
.edit-saved { .edit-saved {
grid-area: 1/1/3/2;
padding: 0.3rem; padding: 0.3rem;
position: relative; position: relative;
.icon {
position: absolute;
right: 0;
bottom: 0;
border-radius: 0.5rem;
padding: 0.2rem;
background-color: rgba(var(--background-color), 1);
}
} }
&__initials { &__initials {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 2.4rem; height: 2.5rem;
width: 2.4rem; width: 2.5rem;
font-size: 1.2rem; font-size: 1.2rem;
text-transform: uppercase; text-transform: uppercase;
color: rgba(var(--background-color), 1); color: var(--accent-color);
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
background-color: var(--accent-color); background-color: rgba(var(--text-color), 0.03);
justify-self: flex-start;
border-radius: 2rem; border-radius: 2rem;
grid-area: 1/1/3/2;
} }
&__title { &__title {
align-self: flex-end; align-self: flex-end;
@ -1024,6 +993,11 @@ ol {
&__flo-id { &__flo-id {
font-size: 0.8rem; font-size: 0.8rem;
} }
&__copy {
padding: 0.3rem 0.5rem;
border-radius: 0.3rem;
background-color: rgba(var(--text-color), 0.03);
}
} }
#saved_ids_tip { #saved_ids_tip {
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
@ -1101,25 +1075,14 @@ ol {
font-size: 0.8rem; font-size: 0.8rem;
} }
} }
#history_applied_filters {
#history {
.page__header {
margin-bottom: 0;
}
}
#history_applied_filters_wrapper {
position: sticky;
top: 0;
padding: 0.2rem 0 0.5rem 0; padding: 0.2rem 0 0.5rem 0;
background-color: rgba(var(--background-color), 1);
z-index: 1;
transition: background-color 0.3s;
} }
.applied-filter { .applied-filter {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.5rem 0.6rem 0.5rem 0.8rem; padding: 0.5rem 0.6rem 0.5rem 0.8rem;
border-radius: 1rem; border-radius: 0.5rem;
border: solid thin rgba(var(--text-color), 0.2); border: solid thin rgba(var(--text-color), 0.2);
text-transform: uppercase; text-transform: uppercase;
font-size: 0.8rem; font-size: 0.8rem;
@ -1132,6 +1095,16 @@ ol {
margin-left: 0.3rem; margin-left: 0.3rem;
} }
} }
fieldset {
border: none;
padding: 0;
margin: 0;
legend {
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
}
.category-chip { .category-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -1141,11 +1114,6 @@ ol {
font-size: 0.8rem; font-size: 0.8rem;
border: solid thin rgba(var(--text-color), 0.2); border: solid thin rgba(var(--text-color), 0.2);
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
&:focus {
outline: solid var(--accent-color);
}
input { input {
display: none; display: none;
} }
@ -1178,6 +1146,7 @@ ol {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
margin-top: 1.5rem; margin-top: 1.5rem;
padding-bottom: 3rem;
} }
.transaction { .transaction {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
@ -1187,18 +1156,18 @@ ol {
white-space: nowrap; white-space: nowrap;
} }
&.sent { &.sent {
.icon { .icon--tx-type {
fill: rgba(var(--text-color), 0.8); fill: var(--danger-color);
} }
.transaction__amount { .transaction__amount {
color: rgba(var(--text-color), 0.8); color: var(--danger-color);
&::before { &::before {
content: "- "; content: "- ";
} }
} }
} }
&.received { &.received {
.icon { .icon--tx-type {
fill: var(--green); fill: var(--green);
} }
.transaction__amount { .transaction__amount {
@ -1209,14 +1178,30 @@ ol {
} }
} }
&__icon { &__icon {
display: flex; position: relative;
grid-area: icon;
display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
grid-area: 1/1/3/2;
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem; border-radius: 2rem;
grid-area: 1/1/3/2;
& > * {
grid-area: 1/1;
}
.icon--tx-type {
position: absolute;
background-color: inherit;
right: -0.3rem;
bottom: -0.3rem;
border-radius: 1rem;
padding: 0.2rem;
height: 1.5rem;
width: 1.5rem;
border: solid medium rgba(var(--foreground-color), 1);
}
} }
&__receiver { &__receiver {
font-weight: 500; font-weight: 500;
@ -1227,7 +1212,7 @@ ol {
} }
&__amount { &__amount {
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 500;
grid-area: 1/3/3/4; grid-area: 1/3/3/4;
} }
} }
@ -1236,14 +1221,11 @@ ol {
gap: 0.5rem 1rem; gap: 0.5rem 1rem;
align-items: center; align-items: center;
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid"; grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid";
&:not(:last-of-type) {
padding-bottom: 1rem;
}
&__amount { &__amount {
white-space: nowrap; white-space: nowrap;
} }
&.out { &.out {
.icon { .icon--tx-type {
fill: var(--danger-color); fill: var(--danger-color);
} }
.btc-tx__amount { .btc-tx__amount {
@ -1254,7 +1236,7 @@ ol {
} }
} }
&.in { &.in {
.icon { .icon--tx-type {
fill: var(--green); fill: var(--green);
} }
.btc-tx__amount { .btc-tx__amount {
@ -1271,14 +1253,29 @@ ol {
} }
} }
&__icon { &__icon {
position: relative;
grid-area: icon; grid-area: icon;
display: flex; display: grid;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem; border-radius: 2rem;
& > * {
grid-area: 1/1;
}
.icon--tx-type {
position: absolute;
background-color: inherit;
right: -0.3rem;
bottom: -0.3rem;
border-radius: 1rem;
padding: 0.2rem;
height: 1.5rem;
width: 1.5rem;
border: solid medium rgba(var(--foreground-color), 1);
}
} }
&__receiver { &__receiver {
grid-area: receiver; grid-area: receiver;
@ -1286,17 +1283,17 @@ ol {
} }
&__time { &__time {
grid-area: time; grid-area: time;
font-size: 0.9rem; font-size: 0.8rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
&__amount { &__amount {
grid-area: amount; grid-area: amount;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 500;
} }
&__id { &__id {
grid-area: txid; grid-area: txid;
font-size: 0.9rem; font-size: 0.8rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
.unconfirmed-wrapper { .unconfirmed-wrapper {
@ -1326,11 +1323,6 @@ ol {
border-radius: 3rem; border-radius: 3rem;
background-color: rgba(var(--foreground-color), 1); background-color: rgba(var(--foreground-color), 1);
} }
#add_address_button {
border-radius: 0.5rem;
background-color: transparent;
border: solid 0.1rem var(--accent-color);
}
.user-action-result__icon { .user-action-result__icon {
justify-self: center; justify-self: center;
height: 4rem; height: 4rem;
@ -1448,6 +1440,9 @@ ol {
color: var(--accent-color); color: var(--accent-color);
background-color: rgba(var(--foreground-color), 1); background-color: rgba(var(--foreground-color), 1);
} }
&--pending {
padding: 0;
}
} }
.wallet-request { .wallet-request {
display: grid; display: grid;
@ -1545,14 +1540,15 @@ ol {
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
} }
@media screen and (max-width: 40rem) { @media screen and (max-width: 40rem) {
.inner-page { #home {
padding-bottom: 7rem; padding-bottom: 3rem;
> * {
padding: 0 1rem;
} }
#main_navbar { }
&.hide-away { .integrated-action-button {
bottom: 0; .icon:last-of-type {
left: 0; margin-left: auto;
right: 0;
} }
} }
.nav-item { .nav-item {
@ -1571,6 +1567,9 @@ ol {
margin-left: auto; margin-left: auto;
} }
} }
.hide-on-mobile {
display: none;
}
} }
@media screen and (min-width: 40rem) { @media screen and (min-width: 40rem) {
sm-popup { sm-popup {
@ -1583,55 +1582,39 @@ ol {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#main_card { .inner-page {
display: grid; padding: 0 14vw;
grid-template-columns: 10rem 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"header header"
"navbar content";
} }
#main_header { #main_header {
grid-area: header; padding: 1.5rem 3vw;
padding: 1rem 1.5rem;
background-color: rgba(var(--foreground-color), 1);
}
#main_navbar {
height: 100%;
grid-area: navbar;
ul {
flex-direction: column;
gap: 0.3rem;
padding: 0.3rem;
}
}
.nav-item {
padding: 1rem;
padding-right: 2rem;
border-radius: 0.5rem;
flex-direction: row;
justify-content: start;
gap: 0.5rem;
font-size: 0.9rem;
opacity: 0.8;
&--active {
opacity: 1;
}
&__indicator {
width: 0.25rem;
height: 50%;
left: 0;
border-radius: 0 1rem 1rem 0;
bottom: auto;
}
} }
.card { .card {
padding: 1.5rem; padding: 1.5rem;
} }
.balance-card { #home {
& > * { grid-template-columns: 26rem 1fr;
padding: 1.5rem; gap: 4vw;
padding-right: 3vw;
} }
#profile_popup {
--width: 32rem;
}
#notifications_panel {
position: absolute;
top: 100%;
left: auto;
bottom: auto;
right: 0;
border-radius: 0.5rem;
z-index: 10;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
border: solid 1px rgba(var(--text-color), 0.1);
background-color: rgba(var(--foreground-color), 1);
width: max(24rem, 100%);
}
#wallet_section {
padding: 0 3vw;
grid-row: 1/3;
} }
#contact { #contact {
& > * { & > * {
@ -1642,10 +1625,16 @@ ol {
} }
} }
#saved_ids_list { #saved_ids_list {
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
} }
.inner-page { .hide-on-desktop {
padding: 1.5rem 14vw; display: none;
}
}
@media screen and (min-width: 68rem) {
#home {
grid-template-columns: 26rem 1fr 1fr;
} }
} }
@media (any-hover: hover) { @media (any-hover: hover) {
@ -1661,19 +1650,21 @@ ol {
background: rgba(var(--text-color), 0.5); background: rgba(var(--text-color), 0.5);
} }
} }
.interact:not([disabled]) { .button--primary:not([disabled]),
.button--danger:not([disabled]) {
transition: filter 0.3s;
&:hover {
filter: brightness(1.3);
}
}
.interact:not([disabled]),
button:not([disabled], .button--primary, .button--danger),
.button:not([disabled], .button--primary, .button--danger) {
transition: background-color 0.3s; transition: background-color 0.3s;
&:hover { &:hover {
background-color: rgba(var(--text-color), 0.06); background-color: rgba(var(--text-color), 0.06);
} }
} }
button:not([disabled]),
.button:not([disabled]) {
transition: background-color 0.3s, filter 0.3s;
&:hover {
filter: contrast(2);
}
}
} }
@supports (overflow: overlay) { @supports (overflow: overlay) {

1567
index.html

File diff suppressed because one or more lines are too long

747
scripts/btcOperator.js Normal file
View File

@ -0,0 +1,747 @@
(function (EXPORTS) { //btcOperator v1.0.14b
/* 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", "multisigBech32"].includes(type))
return type;
else
return false;
}
btcOperator.multiSigAddress = function (pubKeys, minRequired, bech32 = true) {
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";
if (bech32)
return coinjs.pubkeys2MultisigAddressBech32(pubKeys, minRequired);
else
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,
BECH32_MULTISIG_INPUT_SIZE = 35,
SEGWIT_INPUT_SIZE = 59,
MULTISIG_INPUT_SIZE_ES = 351,
BASE_OUTPUT_SIZE = 9,
LEGACY_OUTPUT_SIZE = 25,
BECH32_OUTPUT_SIZE = 23,
BECH32_MULTISIG_OUTPUT_SIZE = 34,
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 "multisigBech32":
return BASE_INPUT_SIZE + BECH32_MULTISIG_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 "multisigBech32":
return BASE_OUTPUT_SIZE + BECH32_MULTISIG_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_address && !validateAddress(parameters.change_address))
throw "Invalid change_address:" + parameters.change_address;
//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_address, fee_from_receiver) {
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_address);
addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver).then(result => {
if (result.change_amount > 0 && result.change_amount > result.fee) //add change amount if any (ignore dust change)
tx.outs[tx.outs.length - 1].value = parseInt(result.change_amount * SATOSHI_IN_BTC); //values are in satoshi
if (fee_from_receiver) { //deduce fee from receivers if fee_from_receiver
let fee_remaining = parseInt(result.fee * SATOSHI_IN_BTC);
for (let i = 0; i < tx.outs.length - 1 && fee_remaining > 0; i++) {
if (fee_remaining < tx.outs[i].value) {
tx.outs[i].value -= fee_remaining;
fee_remaining = 0;
} else {
fee_remaining -= tx.outs[i].value;
tx.outs[i].value = 0;
}
}
if (fee_remaining > 0)
return reject("Send amount is less than fee");
}
tx.outs = tx.outs.filter(o => o.value != 0); //remove all output with value 0
result.output_size = output_size;
result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0);
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, fee_from_receiver) {
return new Promise((resolve, reject) => {
if (fee !== null) {
addUTXOs(tx, senders, redeemScripts, fee_from_receiver ? total_amount : 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);
(fee_from_receiver ?
addUTXOs(tx, senders, redeemScripts, total_amount, false) :
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 addr_type = coinjs.addressDecode(addr).type;
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)) || addr_type === 'multisigBech32') {
//redeemScript for segwit/bech32 and multisig (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 (segwit)
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_address) {
let size = 0;
for (let i in receivers) {
tx.addoutput(receivers[i], amounts[i]);
size += _sizePerOutput(receivers[i]);
}
tx.addoutput(change_address, 0);
size += _sizePerOutput(change_address);
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 = null, options = {}) {
return new Promise((resolve, reject) => {
createSignedTx(senders, privkeys, receivers, amounts, fee, options).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, options = {}) {
return new Promise((resolve, reject) => {
try {
({
senders,
privkeys,
receivers,
amounts
} = validateTxParameters({
senders,
privkeys,
receivers,
amounts,
fee,
change_address: options.change_address
}));
} 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, options.change_address || senders[0], options.fee_from_receiver).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, options = {}) {
return new Promise((resolve, reject) => {
try {
({
senders,
receivers,
amounts
} = validateTxParameters({
senders,
receivers,
amounts,
fee,
change_address: options.change_address
}));
} 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, options.change_address || senders[0], options.fee_from_receiver).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, options = {}) {
return new Promise((resolve, reject) => {
//validate tx parameters
let addr_type = validateAddress(sender);
if (!(["multisig", "multisigBech32"].includes(addr_type)))
return reject("Invalid sender (multisig):" + sender);
else {
let script = coinjs.script();
let decode = (addr_type == "multisig") ?
script.decodeRedeemScript(redeemScript) :
script.decodeRedeemScriptBech32(redeemScript);
if (!decode || decode.address !== sender)
return reject("Invalid redeem-script");
}
try {
({
receivers,
amounts
} = validateTxParameters({
receivers,
amounts,
fee,
change_address: options.change_address
}));
} catch (e) {
return reject(e)
}
//create transaction
createTransaction([sender], [redeemScript], receivers, amounts, fee, options.change_address || sender, options.fee_from_receiver).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' && s['type'] !== 'multisig_bech32')
n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2))
else {
var rs = coinjs.script().decodeRedeemScript(s.script); //will work for bech32 too, as only address is diff
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, multisig-bech32
address = encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp);
break;
case 169: //segwit, 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(`get_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 = {});

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //floBlockchainAPI v2.3.3b (function (EXPORTS) { //floBlockchainAPI v2.3.3d
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict'; 'use strict';
const floBlockchainAPI = EXPORTS; const floBlockchainAPI = EXPORTS;
@ -11,6 +11,7 @@
}, },
sendAmt: 0.001, sendAmt: 0.001,
fee: 0.0005, fee: 0.0005,
minChangeAmt: 0.0005,
receiverID: floGlobals.adminID receiverID: floGlobals.adminID
}; };
@ -171,7 +172,7 @@
else { else {
trx.addoutput(receiverAddr, sendAmt); trx.addoutput(receiverAddr, sendAmt);
var change = utxoAmt - sendAmt - fee; var change = utxoAmt - sendAmt - fee;
if (change > 0) if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change); trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' ')); trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1); var signedTxHash = trx.sign(privKey, 1);
@ -484,8 +485,8 @@
floBlockchainAPI.readData = function (addr, options = {}) { floBlockchainAPI.readData = function (addr, options = {}) {
options.limit = options.limit || 0; options.limit = options.limit || 0;
options.ignoreOld = options.ignoreOld || 0; options.ignoreOld = options.ignoreOld || 0;
if (typeof options.sender === "string") options.sender = [options.sender]; if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receiver === "string") options.receiver = [options.receiver]; if (typeof options.receivers === "string") options.receivers = [options.receivers];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => { promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
var newItems = response.totalItems - options.ignoreOld; var newItems = response.totalItems - options.ignoreOld;
@ -520,10 +521,10 @@
} }
if (!flag) continue; if (!flag) continue;
} }
if (Array.isArray(options.sender)) { if (Array.isArray(options.senders)) {
let flag = false; let flag = false;
for (let vin of response.items[i].vin) for (let vin of response.items[i].vin)
if (options.sender.includes(vin.addr)) { if (options.senders.includes(vin.addr)) {
flag = true; flag = true;
break; break;
} }
@ -538,10 +539,10 @@
} }
if (!flag) continue; if (!flag) continue;
} }
if (Array.isArray(options.receiver)) { if (Array.isArray(options.receivers)) {
let flag = false; let flag = false;
for (let vout of response.items[i].vout) for (let vout of response.items[i].vout)
if (options.receiver.includes(vout.scriptPubKey.addresses[0])) { if (options.receivers.includes(vout.scriptPubKey.addresses[0])) {
flag = true; flag = true;
break; break;
} }
@ -555,6 +556,8 @@
d.txid = response.items[i].txid; d.txid = response.items[i].txid;
d.time = response.items[i].time; d.time = response.items[i].time;
d.blockheight = response.items[i].blockheight; 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; d.data = response.items[i].floData;
filteredData.push(d); filteredData.push(d);
} else } else

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //floCloudAPI v2.4.2d (function (EXPORTS) { //floCloudAPI v2.4.3
/* FLO Cloud operations to send/request application data*/ /* FLO Cloud operations to send/request application data*/
'use strict'; 'use strict';
const floCloudAPI = EXPORTS; const floCloudAPI = EXPORTS;
@ -79,6 +79,9 @@
get: () => generalData, get: () => generalData,
set: data => generalData = data set: data => generalData = data
}, },
generalDataset: {
value: (type, options = {}) => generalData[filterKey(type, options)]
},
lastVC: { lastVC: {
get: () => lastVC, get: () => lastVC,
set: vc => lastVC = vc set: vc => lastVC = vc
@ -375,7 +378,7 @@
return JSON.parse(decodeURIComponent(escape(atob(message)))) return JSON.parse(decodeURIComponent(escape(atob(message))))
} }
const filterKey = util.filterKey = function(type, options) { const filterKey = util.filterKey = function (type, options = {}) {
return type + (options.comment ? ':' + options.comment : '') + return type + (options.comment ? ':' + options.comment : '') +
'|' + (options.group || options.receiverID || DEFAULT.adminID) + '|' + (options.group || options.receiverID || DEFAULT.adminID) +
'|' + (options.application || DEFAULT.application); '|' + (options.application || DEFAULT.application);
@ -983,23 +986,23 @@
}, {}); }, {});
}; };
const mergeRecursive = (obj1, obj2) => { const mergeRecursive = (obj1, obj2, deleteMode = false) => {
for (var p in obj2) { for (var p in obj2) {
try { try {
if (obj2[p].constructor == Object) if (obj2[p].constructor == Object)
obj1[p] = mergeRecursive(obj1[p], obj2[p]); obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
// Property in destination object set; update its value. // Property in destination object set; update its value.
else if (Ext.isArray(obj2[p])) { else if (Array.isArray(obj2[p])) {
// obj1[p] = []; // obj1[p] = [];
if (obj2[p].length < 1) if (obj2[p].length < 1)
obj1[p] = obj2[p]; obj1[p] = obj2[p];
else else
obj1[p] = mergeRecursive(obj1[p], obj2[p]); obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
} else } else
obj1[p] = obj2[p]; obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
} catch (e) { } catch (e) {
// Property in destination object not set; create it and set its value. // Property in destination object not set; create it and set its value.
obj1[p] = obj2[p]; obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
} }
} }
return obj1; return obj1;
@ -1008,20 +1011,13 @@
const cleanse = (obj) => { const cleanse = (obj) => {
Object.keys(obj).forEach(key => { Object.keys(obj).forEach(key => {
var value = obj[key]; var value = obj[key];
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null)
// Recurse... obj[key] = cleanse(value);
cleanse(value); else if (typeof value === 'undefined')
// ...and remove if now "empty" (NOTE: insert your definition of "empty" here) delete obj[key]; // undefined, remove it
//if (!Object.keys(value).length)
// delete obj[key];
} else if (value === null)
delete obj[key]; // null, remove it
}); });
if (obj.constructor.toString().indexOf("Array") != -1) { if (Array.isArray(obj))
obj = obj.filter(function(el) { obj = obj.filter(v => typeof v !== 'undefined');
return el != null;
});
}
return obj; return obj;
} }
@ -1037,7 +1033,7 @@
if (Object.keys(diff.updated).length !== 0) if (Object.keys(diff.updated).length !== 0)
obj = mergeRecursive(obj, diff.updated) obj = mergeRecursive(obj, diff.updated)
if (Object.keys(diff.deleted).length !== 0) { if (Object.keys(diff.deleted).length !== 0) {
obj = mergeRecursive(obj, diff.deleted) obj = mergeRecursive(obj, diff.deleted, true)
obj = cleanse(obj) obj = cleanse(obj)
} }
if (Object.keys(diff.added).length !== 0) if (Object.keys(diff.added).length !== 0)

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //floCrypto v2.3.3d (function (EXPORTS) { //floCrypto v2.3.3e
/* FLO Crypto Operators */ /* FLO Crypto Operators */
'use strict'; 'use strict';
const floCrypto = EXPORTS; const floCrypto = EXPORTS;
@ -276,12 +276,18 @@
} }
//Convert the given address (any blockchain) to equivalent floID //Convert the given address (any blockchain) to equivalent floID
floCrypto.toFloID = function(address) { floCrypto.toFloID = function (address, options = null) {
if (!address) if (!address)
return; return;
let raw = decodeAddress(address); let raw = decodeAddress(address);
if (!raw) if (!raw)
return; 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); raw.bytes.unshift(bitjs.pub);
let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, {
asBytes: true asBytes: true

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //floDapps v2.3.2d (function (EXPORTS) { //floDapps v2.3.3
/* General functions for FLO Dapps*/ /* General functions for FLO Dapps*/
'use strict'; 'use strict';
const floDapps = EXPORTS; const floDapps = EXPORTS;
@ -181,6 +181,7 @@
credentials: {}, credentials: {},
//for Dapps //for Dapps
subAdmins: {}, subAdmins: {},
trustedIDs: {},
settings: {}, settings: {},
appObjects: {}, appObjects: {},
generalData: {}, generalData: {},
@ -244,6 +245,12 @@
compactIDB.removeData("supernodes", sn, DEFAULT.root); compactIDB.removeData("supernodes", sn, DEFAULT.root);
for (let sn in content.newNodes) for (let sn in content.newNodes)
compactIDB.writeData("supernodes", content.newNodes[sn], sn, DEFAULT.root); compactIDB.writeData("supernodes", content.newNodes[sn], sn, DEFAULT.root);
for (let sn in content.updateNodes)
compactIDB.readData("supernodes", sn, DEFAULT.root).then(r => {
r = r || {}
r.uri = content.updateNodes[sn];
compactIDB.writeData("supernodes", r, sn, DEFAULT.root);
});
} }
compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root); compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root);
compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => { compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
@ -274,6 +281,12 @@
if (Array.isArray(content.addSubAdmin)) if (Array.isArray(content.addSubAdmin))
for (var k = 0; k < content.addSubAdmin.length; k++) for (var k = 0; k < content.addSubAdmin.length; k++)
compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]); compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]);
if (Array.isArray(content.removeTrustedID))
for (var j = 0; j < content.removeTrustedID.length; j++)
compactIDB.removeData("trustedIDs", content.removeTrustedID[j]);
if (Array.isArray(content.addTrustedID))
for (var k = 0; k < content.addTrustedID.length; k++)
compactIDB.writeData("trustedIDs", true, content.addTrustedID[k]);
if (content.settings) if (content.settings)
for (let l in content.settings) for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l) compactIDB.writeData("settings", content.settings[l], l)
@ -589,6 +602,28 @@
}) })
} }
floDapps.manageAppTrustedIDs = function (adminPrivKey, addList, rmList) {
return new Promise((resolve, reject) => {
if (!Array.isArray(addList) || !addList.length) addList = undefined;
if (!Array.isArray(rmList) || !rmList.length) rmList = undefined;
if (!addList && !rmList)
return reject("No change in list")
var floData = {
[DEFAULT.application]: {
addTrustedID: addList,
removeTrustedID: rmList
}
}
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 () { const clearCredentials = floDapps.clearCredentials = function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
compactIDB.clearData('credentials', DEFAULT.application).then(result => { compactIDB.clearData('credentials', DEFAULT.application).then(result => {

View File

@ -1,11 +1,11 @@
(function(EXPORTS) { //floTokenAPI v1.0.3b (function (EXPORTS) { //floTokenAPI v1.0.3c
/* Token Operator to send/receive tokens via blockchain using API calls*/ /* Token Operator to send/receive tokens via blockchain using API calls*/
'use strict'; 'use strict';
const tokenAPI = EXPORTS; const tokenAPI = EXPORTS;
const DEFAULT = { const DEFAULT = {
apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/", apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/",
currency: "rupee" currency: floGlobals.currency || "rupee"
} }
Object.defineProperties(tokenAPI, { Object.defineProperties(tokenAPI, {
@ -29,7 +29,7 @@
const fetch_api = tokenAPI.fetch = function (apicall) { const fetch_api = tokenAPI.fetch = function (apicall) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log(DEFAULT.apiURL + apicall); console.debug(DEFAULT.apiURL + apicall);
fetch(DEFAULT.apiURL + apicall).then(response => { fetch(DEFAULT.apiURL + apicall).then(response => {
if (response.ok) if (response.ok)
response.json().then(data => resolve(data)); response.json().then(data => resolve(data));

View File

@ -1,4 +1,4 @@
(function(GLOBAL) { //lib v1.3.1 (function (GLOBAL) { //lib v1.3.2
'use strict'; 'use strict';
/* Utility Libraries required for Standard operations /* Utility Libraries required for Standard operations
* All credits for these codes belong to their respective creators, moderators and owners. * All credits for these codes belong to their respective creators, moderators and owners.
@ -6478,6 +6478,20 @@
}; };
} }
//Return a Bech32 address for the multisig. Format is same as above
coinjs.pubkeys2MultisigAddressBech32 = function (pubkeys, required) {
var r = coinjs.pubkeys2MultisigAddress(pubkeys, required);
var program = Crypto.SHA256(Crypto.util.hexToBytes(r.redeemScript), {
asBytes: true
});
var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true)));
return {
'address': address,
'redeemScript': r.redeemScript,
'size': r.size
};
}
/* new time locked address, provide the pubkey and time necessary to unlock the funds. /* new time locked address, provide the pubkey and time necessary to unlock the funds.
when time is greater than 500000000, it should be a unix timestamp (seconds since epoch), when time is greater than 500000000, it should be a unix timestamp (seconds since epoch),
otherwise it should be the block height required before this transaction can be released. otherwise it should be the block height required before this transaction can be released.
@ -6567,6 +6581,18 @@
}; };
} }
coinjs.multisigBech32Address = function (raw_redeemscript) {
var program = Crypto.SHA256(Crypto.util.hexToBytes(raw_redeemscript), {
asBytes: true
});
var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true)));
return {
'address': address,
'type': 'multisigBech32',
'redeemscript': Crypto.util.bytesToHex(program)
};
}
/* extract the redeemscript from a bech32 address */ /* extract the redeemscript from a bech32 address */
coinjs.bech32redeemscript = function (address) { coinjs.bech32redeemscript = function (address) {
var r = false; var r = false;
@ -6658,6 +6684,9 @@
} else if (o.version == coinjs.multisig) { // multisig address } else if (o.version == coinjs.multisig) { // multisig address
o.type = 'multisig'; o.type = 'multisig';
} else if (o.version == coinjs.multisigBech32) { // multisigBech32 added
o.type = 'multisigBech32';
} else if (o.version == coinjs.priv) { // wifkey } else if (o.version == coinjs.priv) { // wifkey
o.type = 'wifkey'; o.type = 'wifkey';
@ -6698,11 +6727,16 @@
} }
} catch (e) { } catch (e) {
let bech32rs = coinjs.bech32redeemscript(addr); let bech32rs = coinjs.bech32redeemscript(addr);
if (bech32rs) { if (bech32rs && bech32rs.length == 40) {
return { return {
'type': 'bech32', 'type': 'bech32',
'redeemscript': bech32rs 'redeemscript': bech32rs
}; };
} else if (bech32rs && bech32rs.length == 64) {
return {
'type': 'multisigBech32',
'redeemscript': bech32rs
};
} else { } else {
return false; return false;
} }
@ -6711,7 +6745,7 @@
/* retreive the balance from a given address */ /* retreive the balance from a given address */
coinjs.addressBalance = function (address, callback) { coinjs.addressBalance = function (address, callback) {
coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=bal&address=' + address + '&r=' + securedMathRandom(), callback, "GET"); coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=bal&address=' + address + '&r=' + Math.random(), callback, "GET");
} }
/* decompress an compressed public key */ /* decompress an compressed public key */
@ -7328,11 +7362,39 @@
return r; return r;
} }
/* decode the redeemscript of a multisignature transaction for Bech32*/
r.decodeRedeemScriptBech32 = function (script) {
var r = false;
try {
var s = coinjs.script(Crypto.util.hexToBytes(script));
if ((s.chunks.length >= 3) && s.chunks[s.chunks.length - 1] == 174) { //OP_CHECKMULTISIG
r = {};
r.signaturesRequired = s.chunks[0] - 80;
var pubkeys = [];
for (var i = 1; i < s.chunks.length - 2; i++) {
pubkeys.push(Crypto.util.bytesToHex(s.chunks[i]));
}
r.pubkeys = pubkeys;
var multi = coinjs.pubkeys2MultisigAddressBech32(pubkeys, r.signaturesRequired);
r.address = multi['address'];
r.type = 'multisig__'; // using __ for now to differentiat from the other object .type == "multisig"
var rs = Crypto.util.bytesToHex(s.buffer);
r.redeemscript = rs;
}
} catch (e) {
// console.log(e);
r = false;
}
return r;
}
/* create output script to spend */ /* create output script to spend */
r.spendToScript = function (address) { r.spendToScript = function (address) {
var addr = coinjs.addressDecode(address); var addr = coinjs.addressDecode(address);
var s = coinjs.script(); var s = coinjs.script();
if (addr.type == "bech32") { if (addr.type == "bech32" || addr.type == "multisigBech32") {
s.writeOp(0); s.writeOp(0);
s.writeBytes(Crypto.util.hexToBytes(addr.redeemscript)); s.writeBytes(Crypto.util.hexToBytes(addr.redeemscript));
} else if (addr.version == coinjs.multisig) { // multisig address } else if (addr.version == coinjs.multisig) { // multisig address
@ -7490,12 +7552,12 @@
/* list unspent transactions */ /* list unspent transactions */
r.listUnspent = function (address, callback) { r.listUnspent = function (address, callback) {
coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=unspent&address=' + address + '&r=' + securedMathRandom(), callback, "GET"); coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=unspent&address=' + address + '&r=' + Math.random(), callback, "GET");
} }
/* list transaction data */ /* list transaction data */
r.getTransaction = function (txid, callback) { r.getTransaction = function (txid, callback) {
coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=gettransaction&txid=' + txid + '&r=' + securedMathRandom(), callback, "GET"); coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=gettransaction&txid=' + txid + '&r=' + Math.random(), callback, "GET");
} }
/* add unspent to transaction */ /* add unspent to transaction */
@ -7525,7 +7587,7 @@
var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue; var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue;
var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue; var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue;
if (segwit) { if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = raw_redeemscript; for p2wsh-multisig)
/* this is a small hack to include the value with the redeemscript to make the signing procedure smoother. /* this is a small hack to include the value with the redeemscript to make the signing procedure smoother.
It is not standard and removed during the signing procedure. */ It is not standard and removed during the signing procedure. */
@ -7664,7 +7726,7 @@
// start redeem script check // start redeem script check
var extract = this.extractScriptKey(index); var extract = this.extractScriptKey(index);
if (extract['type'] != 'segwit') { if (extract['type'] != 'segwit' && extract['type'] != 'multisig_bech32') {
return { return {
'result': 0, 'result': 0,
'fail': 'redeemscript', 'fail': 'redeemscript',
@ -7693,6 +7755,8 @@
scriptcode = scriptcode.slice(1); scriptcode = scriptcode.slice(1);
scriptcode.unshift(25, 118, 169); scriptcode.unshift(25, 118, 169);
scriptcode.push(136, 172); scriptcode.push(136, 172);
} else if (scriptcode[0] > 80) {
scriptcode.unshift(scriptcode.length)
} }
var value = coinjs.numToBytes(extract['value'], 8); var value = coinjs.numToBytes(extract['value'], 8);
@ -7855,11 +7919,26 @@
'signatures': 0, 'signatures': 0,
'script': Crypto.util.bytesToHex(this.ins[index].script.buffer) 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
}; };
} else if (this.ins[index].script.chunks.length == 3 && this.ins[index].script.chunks[0][0] >= 80 && this.ins[index].script.chunks[0][this.ins[index].script.chunks[0].length - 1] == 174 && this.ins[index].script.chunks[1] == 0) { //OP_CHECKMULTISIG_BECH32
// multisig bech32 script
let last_index = this.ins[index].script.chunks.length - 1;
var value = -1;
if (last_index >= 2 && this.ins[index].script.chunks[last_index].length == 8) {
value = coinjs.bytesToNum(this.ins[index].script.chunks[last_index]); // value found encoded in transaction (THIS IS NON STANDARD)
}
var sigcount = (!this.witness[index]) ? 0 : this.witness[index].length - 2;
return {
'type': 'multisig_bech32',
'signed': 'false',
'signatures': sigcount,
'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[0]),
'value': value
};
} else if (this.ins[index].script.chunks.length == 0) { } else if (this.ins[index].script.chunks.length == 0) {
// empty // empty
//bech32 witness check //bech32 witness check
var signed = ((this.witness[index]) && this.witness[index].length == 2) ? 'true' : 'false'; var signed = ((this.witness[index]) && this.witness[index].length >= 2) ? 'true' : 'false';
var sigs = (signed == 'true') ? 1 : 0; var sigs = (signed == 'true') ? (!this.witness[index][0] ? this.witness[index].length - 2 : 1) : 0;
return { return {
'type': 'empty', 'type': 'empty',
'signed': signed, 'signed': signed,
@ -8038,6 +8117,71 @@
return true; return true;
} }
r.signmultisig_bech32 = function (index, wif, sigHashType) {
function scriptListPubkey(redeemScript) {
var r = {};
for (var i = 1; i < redeemScript.chunks.length - 2; i++) {
r[i] = Crypto.util.hexToBytes(coinjs.pubkeydecompress(Crypto.util.bytesToHex(redeemScript.chunks[i])));
}
return r;
}
function scriptListSigs(sigList) {
let r = {};
var c = 0;
if (Array.isArray(sigList)) {
for (let i = 1; i < sigList.length - 1; i++) {
c++;
r[c] = Crypto.util.hexToBytes(sigList[i]);
}
}
return r;
}
var redeemScript = Crypto.util.bytesToHex(this.ins[index].script.chunks[0]); //redeemScript
if (!coinjs.isArray(this.witness)) {
this.witness = new Array(this.ins.length);
this.witness.fill([]);
}
var pubkeyList = scriptListPubkey(coinjs.script(redeemScript));
var sigsList = scriptListSigs(this.witness[index]);
let decode_rs = coinjs.script().decodeRedeemScriptBech32(redeemScript);
var shType = sigHashType || 1;
var txhash = this.transactionHashSegWitV0(index, shType);
if (txhash.result == 1 && decode_rs.pubkeys.includes(coinjs.wif2pubkey(wif)['pubkey'])) {
var segwitHash = Crypto.util.hexToBytes(txhash.hash);
var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType, segwitHash)); //CHECK THIS
sigsList[coinjs.countObject(sigsList) + 1] = signature;
var w = [];
for (let x in pubkeyList) {
for (let y in sigsList) {
var sighash = this.transactionHashSegWitV0(index, sigsList[y].slice(-1)[0] * 1).hash
sighash = Crypto.util.hexToBytes(sighash);
if (coinjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) {
w.push((Crypto.util.bytesToHex(sigsList[y])))
}
}
}
// when enough signatures collected, remove any non standard data we store, i.e. input value
if (w.length >= decode_rs.signaturesRequired) {
this.ins[index].script = coinjs.script();
}
w.unshift(0);
w.push(redeemScript);
this.witness[index] = w;
}
}
/* sign a multisig input */ /* sign a multisig input */
r.signmultisig = function (index, wif, sigHashType) { r.signmultisig = function (index, wif, sigHashType) {
@ -8187,6 +8331,9 @@
} else if (d['type'] == 'multisig') { } else if (d['type'] == 'multisig') {
this.signmultisig(i, wif, shType); this.signmultisig(i, wif, shType);
} else if (d['type'] == 'multisig_bech32' && d['signed'] == "false") {
this.signmultisig_bech32(i, wif, shType);
} else if (d['type'] == 'segwit') { } else if (d['type'] == 'segwit') {
this.signsegwit(i, wif, shType); this.signsegwit(i, wif, shType);
@ -8240,6 +8387,63 @@
return Crypto.util.bytesToHex(buffer); return Crypto.util.bytesToHex(buffer);
} }
//Utility funtion added to directly compute signatures without transaction index
r.transactionSigNoIndex = function (wif, sigHashType, txhash) {
function serializeSig(r, s) {
var rBa = r.toByteArraySigned();
var sBa = s.toByteArraySigned();
var sequence = [];
sequence.push(0x02); // INTEGER
sequence.push(rBa.length);
sequence = sequence.concat(rBa);
sequence.push(0x02); // INTEGER
sequence.push(sBa.length);
sequence = sequence.concat(sBa);
sequence.unshift(sequence.length);
sequence.unshift(0x30); // SEQUENCE
return sequence;
}
var shType = sigHashType || 1;
var hash = Crypto.util.hexToBytes(txhash);
if (hash) {
var curve = EllipticCurve.getSECCurveByName("secp256k1");
var key = coinjs.wif2privkey(wif);
var priv = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(key['privkey']));
var n = curve.getN();
var e = BigInteger.fromByteArrayUnsigned(hash);
var badrs = 0
do {
var k = this.deterministicK(wif, hash, badrs);
var G = curve.getG();
var Q = G.multiply(k);
var r = Q.getX().toBigInteger().mod(n);
var s = k.modInverse(n).multiply(e.add(priv.multiply(r))).mod(n);
badrs++
} while (r.compareTo(BigInteger.ZERO) <= 0 || s.compareTo(BigInteger.ZERO) <= 0);
// Force lower s values per BIP62
var halfn = n.shiftRight(1);
if (s.compareTo(halfn) > 0) {
s = n.subtract(s);
};
var sig = serializeSig(r, s);
sig.push(parseInt(shType, 10));
return Crypto.util.bytesToHex(sig);
} else {
return false;
}
}
/* deserialize a transaction */ /* deserialize a transaction */
r.deserialize = function (buffer) { r.deserialize = function (buffer) {
if (typeof buffer == "string") { if (typeof buffer == "string") {
@ -8582,12 +8786,116 @@
return count; return count;
} }
//Nine utility functions added for generating transaction hashes and verification of signatures
coinjs.changeEndianness = (string) => {
const result = [];
let len = string.length - 2;
while (len >= 0) {
result.push(string.substr(len, 2));
len -= 2;
}
return result.join('');
}
coinjs.getTransactionHash = function (transaction_in_hex, changeOutputEndianess) {
var x1, x2, x3, x4, x5;
x1 = Crypto.util.hexToBytes(transaction_in_hex);
x2 = Crypto.SHA256(x1);
x3 = Crypto.util.hexToBytes(x2);
x4 = Crypto.SHA256(x3);
x5 = coinjs.changeEndianness(x4);
if (changeOutputEndianess == true) { x5 = x5 } else if ((typeof changeOutputEndianess == 'undefined') || (changeOutputEndianess == false)) { x5 = x4 };
return x5;
}
coinjs.compressedToUncompressed = function (compressed) {
var t1, t2;
var curve = EllipticCurve.getSECCurveByName("secp256k1");
t1 = curve.curve.decodePointHex(compressed);
t2 = curve.curve.encodePointHex(t1);
return t2;
}
coinjs.uncompressedToCompressed = function (uncompressed) {
var t1, t2, t3;
t1 = uncompressed.charAt(uncompressed.length - 1)
t2 = parseInt(t1, 10);
//Check if the last digit is odd
if (t2 % 2 == 1) { t3 = "03"; } else { t3 = "02" };
return t3 + uncompressed.substr(2, 64);
}
coinjs.verifySignatureHex = function (hashHex, sigHex, pubHexCompressed) {
var h1, s1, p1, p2;
h1 = Crypto.util.hexToBytes(hashHex);
s1 = Crypto.util.hexToBytes(sigHex);
p1 = coinjs.compressedToUncompressed(pubHexCompressed);
p2 = Crypto.util.hexToBytes(p1);
return coinjs.verifySignature(h1, s1, p2);
}
coinjs.generateBitcoinSignature = function (private_key, hash, sighash_type_int = 1) {
var wif, tx1;
if (private_key.length < 60) { wif = private_key } else { wif = coinjs.privkey2wif(private_key) };
tx1 = coinjs.transaction();
return tx1.transactionSigNoIndex(wif, sighash_type_int, hash);
}
coinjs.dSHA256 = function (data) {
var t1, t2, t3;
t1 = Crypto.SHA256(Crypto.util.hexToBytes(data));
t2 = Crypto.util.hexToBytes(t1);
t3 = Crypto.SHA256(t2);
return t3;
}
coinjs.fromBitcoinAmountFormat = function (data) {
var x1, x2, x3;
x1 = coinjs.changeEndianness(data);
x2 = parseInt(x1, 16);
x3 = x2 / (10 ** 8);
return x3;
}
coinjs.toBitcoinAmountFormat = function (countBitcoin) {
var t2, t3, t4, t5;
t2 = countBitcoin * 10 ** 8;
t3 = t2.toString(16);
t4 = coinjs.changeEndianness(t3);
t5 = t4.padEnd(16, "0");
return t5;
}
coinjs.scriptcodeCreatorBasic = function (scriptpubkey) {
var t1, t2, t3, t4;
if (scriptpubkey.substr(0, 4) == "0014") {
//Scriptpubkey case
t1 = scriptpubkey.slice(2);
t2 = "1976a9" + t1 + "88ac";
} else {
//Redeemscript case
t3 = (scriptpubkey.length) / 2;
t4 = t3.toString(16);
t2 = t4 + scriptpubkey;
}
return t2;
}
coinjs.ripemd160sha256 = function (data) {
var t1, t2;
t1 = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(data), { asBytes: true }), { asBytes: true });
t2 = Crypto.util.bytesToHex(t1)
return t2;
}
coinjs.random = function (length) { coinjs.random = function (length) {
var r = ""; var r = "";
var l = length || 25; var l = length || 25;
var chars = "!$%^&*()_+{}:@~?><|\./;'#][=-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; var chars = "!$%^&*()_+{}:@~?><|\./;'#][=-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
for (let x = 0; x < l; x++) { for (let x = 0; x < l; x++) {
r += chars.charAt(Math.floor(securedMathRandom() * 62)); r += chars.charAt(Math.floor(Math.random() * 62));
} }
return r; return r;
} }