diff --git a/FLO_webWallet_minified.html b/FLO_webWallet_minified.html deleted file mode 100644 index d4c981b..0000000 --- a/FLO_webWallet_minified.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -FLO web wallet - - - - - - -
There seems to be a problem connecting to the internet.
Monitor
Monitor FLO data
Send
Send FLO data
Generate
Generate address
Settings
Settings
Settings
Copied

Add new address to monitoring list

Edit display card

Copy FLO address

Add new address to monitor
Go back to monitoring page

Refresh transactions
Available balance
0
To send FLO data, make sure you have enough balance.
Go back to monitoring page

Send

Transaction successful



Dark mode

Automatic
Dark mode active : 6pm - 6am
Manual
Dark mode

Clear all local data

This will delete all local Web Wallet data like added addresses and locally stored transactions.After clearing local data you may experience slow loading of newly added address, please proceed cautiously!

About

Version 2.7.2
Powered by
- - - diff --git a/css/main.css b/css/main.css index 2339754..a7ebe05 100644 --- a/css/main.css +++ b/css/main.css @@ -776,7 +776,7 @@ h3 { } .transaction { - gap: 1rem; + gap: 1.2rem; padding: 1rem; background-color: rgba(var(--text-color), 0.03); border-radius: 0.3rem; @@ -792,7 +792,29 @@ h3 { } .transaction__receiver { margin-left: 0.5rem; - color: rgba(var(--text-color), 0.8); + color: rgba(var(--text-color), 0.9); + font-weight: 500; +} +.transaction__amount { + font-weight: 700; +} +.transaction.mined .transaction__icon .icon, .transaction.received .transaction__icon .icon, .transaction.self .transaction__icon .icon { + fill: var(--green); +} +.transaction.mined .transaction__amount, .transaction.received .transaction__amount, .transaction.self .transaction__amount { + color: var(--green); +} +.transaction.mined .transaction__amount::before, .transaction.received .transaction__amount::before, .transaction.self .transaction__amount::before { + content: "+ "; +} +.transaction.sent .transaction__icon .icon { + fill: var(--danger-color); +} +.transaction.sent .transaction__amount { + color: var(--danger-color); +} +.transaction.sent .transaction__amount::before { + content: "- "; } .transaction p { font-size: 0.9rem; diff --git a/css/main.min.css b/css/main.min.css index aa28dc4..5d40581 100644 --- a/css/main.min.css +++ b/css/main.min.css @@ -1 +1 @@ -*{padding:0;margin:0;box-sizing:border-box;font-family:"Roboto",sans-serif}:root{font-size:clamp(1rem,1.2vmax,1.2rem)}html,body{height:100%}body{--accent-color: #256eff;--text-color: 20, 20, 20;--background-color: 240, 240, 240;--foreground-color: 250, 250, 250;--danger-color: rgb(255, 75, 75);--green: #1cad59;scrollbar-width:thin;scrollbar-gutter:stable;color:rgba(var(--text-color), 1);background-color:rgba(var(--background-color), 1);transition:background-color .3s;--dark-red: #d40e1e;--red: #f50000;--kinda-pink: #e40273;--purple: #462191;--shady-blue: #324de6;--nice-blue: #256eff;--maybe-cyan: #00b0ff;--teal: #00bcd4;--mint-green: #16c79a;--yellowish-green: #66bb6a;--greenish-yellow: #8bc34a;--dark-teal: #11698e;--tangerine: #ff6f00;--orange: #ff9100;--redish-orange: #ff3d00}body[data-theme=dark]{--accent-color: #86afff;--text-color: 220, 220, 220;--background-color: 10, 10, 10;--foreground-color: 24, 24, 24;--danger-color: rgb(255, 106, 106);--green: #00e676;--dark-red: #ff5e7e;--red: #ff6098;--kinda-pink: #c44ae6;--purple: #9565f7;--shady-blue: #7084f5;--nice-blue: #86afff;--maybe-cyan: #66cfff;--teal: #6aeeff;--mint-green: #4dffd2;--yellowish-green: #9effa2;--greenish-yellow: #c7fc8b;--dark-teal: #51cbff;--tangerine: #ffac6d;--orange: #ffbe68;--redish-orange: #ff8560}body[data-theme=dark] sm-popup::part(popup){background-color:rgba(var(--foreground-color), 1)}body[data-theme=dark] ::-webkit-calendar-picker-indicator{filter:invert(1)}p,strong{font-size:.9rem;max-width:65ch;line-height:1.7;color:rgba(var(--text-color), 0.9)}p:not(:last-of-type),strong:not(:last-of-type){margin-bottom:1.5rem}a{text-decoration:none;color:var(--accent-color)}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color), 1) inset}button,.button{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;display:inline-flex;border:none;background-color:rgba(0,0,0,0);overflow:hidden;color:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;font-size:inherit;font-weight:500;white-space:nowrap;padding:.9rem;border-radius:.3rem;justify-content:center}button:focus-visible,.button:focus-visible{outline:var(--accent-color) solid medium}button:not(:disabled),.button:not(:disabled){cursor:pointer}.button{background-color:rgba(var(--text-color), 0.02);border:solid thin rgba(var(--text-color), 0.06)}.button--primary{color:rgba(var(--background-color), 1);background-color:var(--accent-color)}.button--primary .icon{fill:rgba(var(--background-color), 1)}.button--colored{color:var(--accent-color)}.button--colored .icon{fill:var(--accent-color)}.button--danger{background-color:rgba(255,115,115,.062745098);color:var(--danger-color)}.button--danger .icon{fill:var(--danger-color)}.button--small{padding:.4rem .6rem}.button--outlined{border:solid rgba(var(--text-color), 0.3) .1rem;background-color:rgba(var(--foreground-color), 1)}.button--transparent{background-color:rgba(0,0,0,0)}button:disabled{opacity:.4;cursor:not-allowed;filter:saturate(0)}.cta{text-transform:uppercase;font-size:.9rem;font-weight:700;letter-spacing:.05em;padding:.8rem 1rem}.icon{width:1.2rem;height:1.2rem;fill:rgba(var(--text-color), 0.8);flex-shrink:0}.icon-only{padding:.5rem;border-radius:.3rem}.icon--big{width:3rem;height:3rem}a:-webkit-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:-moz-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}details{padding:1rem 0}details summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;justify-content:space-between;color:var(--accent-color)}details[open] summary{margin-bottom:1rem}details[open]>summary .down-arrow{transform:rotate(180deg)}sm-input,sm-textarea{font-size:.9rem;--border-radius: 0.5rem;--background-color: rgba(var(--foreground-color), 1)}sm-input button .icon,sm-textarea button .icon{fill:var(--accent-color)}sm-textarea{--max-height: auto}sm-spinner{--size: 1rem;--stroke-width: 0.1rem}sm-form{--gap: 1rem}sm-chips{--gap: 0.3rem}sm-chip{position:relative;font-size:.9rem;--border-radius: 0.5rem;--padding: 0.5rem 0.8rem;--background: rgba(var(--text-color), 0.06);-webkit-user-select:none;-moz-user-select:none;user-select:none}sm-chip[selected]{--background: var(--accent-color);color:rgba(var(--background-color), 1)}sm-select::part(options){max-height:40vh}sm-option{flex-shrink:0}sm-option::part(option){grid-template-columns:none}ul{list-style:none}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.breakable{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word}.full-bleed{grid-column:1/-1}.h1{font-size:1.5rem}.h2{font-size:1.2rem}h3{font-size:1.2rem;line-height:1.3}.h4{font-size:.9rem}.h5{font-size:.75rem}.uppercase{text-transform:uppercase}.capitalize::first-letter{text-transform:uppercase}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.flex{display:flex}.flex-1{flex:1}.flex-wrap{flex-wrap:wrap}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-3{gap:.3rem}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-content:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.align-content-center{align-content:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.justify-items-center{justify-items:center}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.h-100{height:100%}.margin-left-0-5{margin-left:.5rem}.margin-left-auto{margin-left:auto}.margin-right-0-5{margin-right:.5rem}.margin-right-auto{margin-right:auto}.ripple{height:8rem;width:8rem;position:absolute;border-radius:50%;transform:scale(0);background:radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%);pointer-events:none}.button--primary .ripple,.button--danger .ripple{background:radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%)}.interact{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0)}.empty-state{display:grid;width:100%;padding:1.5rem 0}.observe-empty-state:empty{display:none !important}.observe-empty-state:not(:empty)+.empty-state{display:none}.bullet-point{display:flex;align-items:center;justify-content:center;margin:0 .8ch}.bullet-point::after{content:"";height:.4ch;width:.4ch;border-radius:.5em;background-color:var(--accent-color)}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{margin-bottom:.5rem}#confirmation_popup .flex,#prompt_popup .flex{margin-top:1rem}.popup__header{position:relative;display:grid;gap:.5rem;width:100%;padding:0 1.5rem;align-items:center}.popup__header>*{grid-row:1}.popup__header h3,.popup__header h4{grid-column:1/-1;justify-self:center;align-self:center}.popup__header__close{grid-column:1;margin-left:-1rem;justify-self:flex-start}#loader{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(var(--foreground-color), 1);z-index:100;display:grid;place-content:center;place-items:center;gap:1rem;text-align:center}#show_character_count{font-size:.8rem;margin-left:auto}#flo_data_status:not(:empty){color:red;padding:.5rem 0}#saved_ids_popup{--height: 80vh}#main_header{padding:.6rem 1rem}.app-brand{display:flex;gap:.3rem;align-items:center}.app-brand .icon{height:1.7rem;width:1.7rem}.app-name__company{font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8)}#main_card{display:flex;flex-direction:column;height:100%;width:100%;background-color:rgba(var(--foreground-color), 1);transition:background-color .3s}#pages_container{flex:1;overflow-y:auto}#main_navbar{display:flex;background:rgba(var(--text-color), 0.03)}#main_navbar.hide-away{position:absolute}#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%;height:100%;flex-direction:column;align-items:center;justify-content:center;padding:.5rem .3rem;color:var(--text-color);font-size:.7rem;border-radius:.3rem;text-align:center}.nav-item .icon{transition:transform .2s}.nav-item__title{transition:opacity .2s,transform .2s}.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:.3rem;background:var(--accent-color);border-radius:1rem 1rem 0 0;z-index:1}#fix_invalid_button{margin-bottom:1rem !important}.password-field label{display:flex}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}.multi-state-button{display:grid;text-align:center;align-items:center;justify-items:center}.multi-state-button>*{grid-area:1/1/2/2}.multi-state-button button{z-index:1;width:100%}.clip{-webkit-clip-path:circle(0);clip-path:circle(0)}#flo_id_warning{padding-bottom:1.5rem;border-bottom:thin solid rgba(var(--text-color), 0.3)}#flo_id_warning .icon{height:4rem;width:4rem;padding:1rem;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8);margin-bottom:1.5rem}.generated-id-card{display:grid;gap:1rem}.generated-id-card h5{margin-bottom:.3rem}#contacts>:first-child{overflow-y:auto;align-content:flex-start;padding-bottom:4rem}#primary_actions_wrapper{display:grid;gap:.5rem}.primary-action{display:flex;padding:.8rem 1rem;gap:.5rem;white-space:normal;font-size:.85rem;border-radius:.5rem;background-color:rgba(0,0,0,0);border:thin solid rgba(var(--text-color), 0.3);text-align:left}.primary-action .icon{fill:var(--accent-color)}#search{position:relative;height:100%}#queried_flo_address h4{font-size:1.1rem}#queried_flo_address>sm-copy{font-size:.8rem}#token_list{display:flex;flex-wrap:wrap;gap:.5rem}.token-item{font-size:.9rem;padding:.5rem 1rem;background-color:rgba(var(--text-color), 0.06);border-radius:.3rem}.transaction{gap:1rem;padding:1rem;background-color:rgba(var(--text-color), 0.03);border-radius:.3rem}.transaction:not(:last-of-type){margin-bottom:1rem}.transaction .icon{fill:var(--accent-color)}.transaction__time,.transaction__link,.transaction__receiver{font-size:.8rem}.transaction__receiver{margin-left:.5rem;color:rgba(var(--text-color), 0.8)}.transaction p{font-size:.9rem;max-width:unset}.transaction__time{justify-self:flex-end;color:rgba(var(--text-color), 0.8)}#search_wrapper{width:min(100%,36rem)}#search_query_input{justify-self:center}#saved_ids_list{position:relative;align-content:flex-start;padding-bottom:1.5rem;margin-top:1rem;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.saved-id{grid-template-columns:auto 1fr;gap:0 .8rem;border-radius:.3rem;padding:.5rem;background-color:rgba(var(--text-color), 0.03);-webkit-user-select:none;-moz-user-select:none;user-select:none}.saved-id.highlight{box-shadow:0 0 .1rem .1rem var(--accent-color) inset}.saved-id .edit-saved{grid-area:1/1/3/2;padding:.3rem;position:relative}.saved-id .edit-saved .icon{position:absolute;height:1.2rem;width:1.2rem;right:0;bottom:0;border-radius:.5rem;padding:.2rem;background-color:rgba(var(--background-color), 1)}.saved-id__initial{display:flex;align-items:center;justify-content:center;height:2.4rem;width:2.4rem;font-size:1.2rem;text-transform:uppercase;color:rgba(var(--background-color), 1);font-weight:700;line-height:1;background-color:var(--accent-color);justify-self:flex-start;border-radius:2rem}.saved-id__label{align-self:flex-end}.saved-id__flo-id{font-size:.8rem}.page{position:relative;display:flex;flex-direction:column;overflow-y:auto;align-content:flex-start;padding:0 1rem;padding-bottom:3rem}.fab{position:absolute;right:0;bottom:0;margin:1.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);z-index:2}#add_address_button{border-radius:.5rem;color:rgba(var(--background-color), 1);background-color:var(--accent-color)}#add_address_button .icon{fill:rgba(var(--background-color), 1)}#balance_card{display:flex;flex-direction:column;gap:2rem;padding:max(1rem,2vw);background-color:rgba(var(--text-color), 0.06);aspect-ratio:4/2;justify-content:flex-end;border-radius:.5rem}#balance_card form{margin-top:1rem}#balance_card fieldset{border:none}.token-balance{display:flex;align-items:center;cursor:pointer;gap:.5rem;background-color:rgba(var(--text-color), 0.06);padding:.8rem;border-radius:.3rem;font-size:.9rem}.token-balance span:first-of-type::first-letter{text-transform:capitalize}.token-balance input{height:1rem;width:1rem;accent-color:var(--accent-color)}.token-receiver-combo{border:solid thin rgba(var(--text-color), 0.2);padding:.5rem;border-radius:.8rem}.token-receiver-combo--removable{grid-template-columns:1fr auto;grid-template-areas:"receiver receiver" "amount remove"}.token-receiver-combo--removable .token-receiver{grid-area:receiver}.token-receiver-combo--removable .token-amount{grid-area:amount}.token-receiver-combo--removable .remove-token-receiver{grid-area:remove}#transaction_result{display:grid;gap:.5rem;height:max(40vh,24rem);align-items:center;justify-content:center;text-align:center;align-content:center}#transaction_result:empty{display:none}#transaction_result h3{text-align:center;width:100%}#transaction_result .icon{justify-self:center;height:4rem;width:4rem;border-radius:5rem;margin-bottom:1rem;-webkit-animation:popup 1s;animation:popup 1s}#transaction_result .icon--success{fill:rgba(var(--background-color), 1);padding:1rem;background-color:#0bbe56}#transaction_result .icon--failed{background-color:rgba(var(--text-color), 0.03);fill:var(--danger-color)}#transaction_result sm-copy{font-size:.8rem}@-webkit-keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}@keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}#queried_address_transactions{display:flex;flex-direction:column;padding-bottom:4rem}#pagination_wrapper{position:fixed;bottom:0;right:0;margin:0 auto;padding:.5rem;background-color:rgba(var(--foreground-color), 1);z-index:5;border-radius:.7rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);margin:.5rem}.pagination__item{display:flex;padding:.2rem .5rem;border-radius:.3rem}.pagination__item--active{background-color:var(--accent-color);color:rgba(var(--background-color), 1)}legend,.label{font-size:.8rem;color:rgba(var(--text-color), 0.8)}#smartcontracts{display:grid;min-width:0;height:100%}#smartcontracts>*{grid-area:1/1}#smartcontracts fieldset{padding:.5rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}#smartcontracts fieldset legend{padding:0 .5rem}#smartcontracts label{padding:.3rem .5rem}#smartcontracts label:has(input:not(:disabled):not(:checked)){cursor:pointer}#smartcontracts input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}#smartcontracts input[type=datetime-local]{width:100%;padding:.8rem .6rem;border:none;border-radius:.5rem;font-weight:500;font-family:inherit;font-size:inherit;color:inherit;background-color:rgba(var(--text-color), 0.06)}#smartcontracts input[type=datetime-local]:focus{outline:none;box-shadow:0 0 0 .1rem var(--accent-color)}#smartcontracts sm-input:not([placeholder]){--min-height: 3rem}.smart-contract-action{flex-direction:column;white-space:normal;gap:.5rem;color:rgba(var(--text-color), 0.8);width:5rem;font-size:.9rem;aspect-ratio:1/1;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3);text-align:center}.smart-contract-action .icon{fill:var(--accent-color)}#smart_contract_creation_templates{display:grid;gap:.5rem}#smart_contract_creation_templates li{display:flex;flex:1}.smart-contract-template{display:grid;grid-template-areas:"heading arrow" "description arrow";grid-template-columns:1fr auto;padding:max(1rem,2vw);border:solid 1px rgba(var(--text-color), 0.3);border-radius:.5rem;gap:.5rem;text-align:start;white-space:normal;width:100%;justify-content:flex-start}.smart-contract-template h4{grid-area:heading}.smart-contract-template p{grid-area:description;font-weight:400;line-height:1.3;font-size:.9rem;color:rgba(var(--text-color), 0.8);text-transform:none}.smart-contract-template .icon{margin-top:auto;grid-area:arrow;border:solid 1px rgba(var(--text-color), 0.3);border-radius:3rem;padding:.4rem;height:2rem;width:2rem;fill:var(--accent-color)}.payee-address-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address" "share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address address" "share button"}.payee-address-wrapper .payee-address{grid-area:address}.payee-address-wrapper .payee-share{grid-area:share}.payee-address-wrapper .icon-only{grid-area:button}.payee-address-wrapper .icon-only{height:3.18rem}.choice-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.choice-wrapper .icon-only{aspect-ratio:1/1;height:3.18rem}@media screen and (max-width: 40rem){#main_navbar.hide-away{bottom:0;left:0;right:0}#primary_actions_wrapper{grid-template-columns:1fr 1fr}.nav-item__title{margin-top:.3rem}.nav-item--active .icon{transform:translateY(50%)}.nav-item--active .nav-item__title{transform:translateY(100%);opacity:0}#pagination_wrapper{margin:0;margin-bottom:4.8rem;left:50%;right:auto;transform:translateX(-50%);width:calc(100vw - 2rem);flex-wrap:wrap}}@media screen and (min-width: 40rem){sm-popup{--width: 24rem}.popup__header{padding:1rem 1.5rem 0 1.5rem}body{display:flex;align-items:center;justify-content:center}#main_card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:"header header" "nav main";position:relative}#main_header{grid-area:header}#pages_container{grid-area:main}.page{padding:0 1.5rem}#main_navbar{grid-area:nav;border-top:none;flex-direction:column;height:100%}#main_navbar ul{flex-direction:column}.nav-item{flex-direction:row;text-align:left;justify-content:flex-start;gap:.5rem;padding:1rem;font-weight:500;font-size:.8rem;border-radius:0;min-width:10rem}.nav-item__indicator{width:.25rem;height:50%;left:0;border-radius:0 1rem 1rem 0;bottom:auto}#primary_actions_wrapper{display:flex;flex-wrap:wrap}#create_flo_id_popup,#retrieve_flo_id_popup{--width: 26rem}#send{padding:0 6vw}#send sm-form{width:min(56rem,100%);margin:auto}#smart_contract_creation_templates{grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}#smart_contract_deposit_form,#smart_contract_participate_form,#smart_contract_update_form,#smart_contract_trigger_form{width:min(36rem,100%);margin:auto;--gap: 1.5rem}#smart_contract_creation_form::part(form){gap:1.5rem;margin:auto;width:min(36rem,100%)}#smart_contract_creation_form::part(form).split-layout{grid-template-columns:1fr 1.5fr;align-items:flex-start;width:min(50rem,100%)}.payee-address-wrapper{display:grid;grid-template-columns:1fr 8rem 3rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address share share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address share button"}}@media screen and (min-width: 56rem){#send sm-form::part(form){align-items:flex-start;grid-template-columns:1fr 1.5fr}}@media screen and (min-width: 64rem){#address_details_wrapper{grid-template-columns:auto 1fr;align-items:flex-start}#transactions_hero_section{position:-webkit-sticky;position:sticky;top:0}}@media(any-hover: hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color), 0.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color), 0.5)}.interact:not([disabled]){transition:background-color .3s}.interact:not([disabled]):hover{background-color:rgba(var(--text-color), 0.06)}.button:not([disabled]){transition:background-color .3s,filter .3s}.button:not([disabled]):hover{filter:contrast(2)}}@supports(overflow: overlay){body{overflow:overlay}}.hidden{display:none !important} \ No newline at end of file +*{padding:0;margin:0;box-sizing:border-box;font-family:"Roboto",sans-serif}:root{font-size:clamp(1rem,1.2vmax,1.2rem)}html,body{height:100%}body{--accent-color: #256eff;--text-color: 20, 20, 20;--background-color: 240, 240, 240;--foreground-color: 250, 250, 250;--danger-color: rgb(255, 75, 75);--green: #1cad59;scrollbar-width:thin;scrollbar-gutter:stable;color:rgba(var(--text-color), 1);background-color:rgba(var(--background-color), 1);transition:background-color .3s;--dark-red: #d40e1e;--red: #f50000;--kinda-pink: #e40273;--purple: #462191;--shady-blue: #324de6;--nice-blue: #256eff;--maybe-cyan: #00b0ff;--teal: #00bcd4;--mint-green: #16c79a;--yellowish-green: #66bb6a;--greenish-yellow: #8bc34a;--dark-teal: #11698e;--tangerine: #ff6f00;--orange: #ff9100;--redish-orange: #ff3d00}body[data-theme=dark]{--accent-color: #86afff;--text-color: 220, 220, 220;--background-color: 10, 10, 10;--foreground-color: 24, 24, 24;--danger-color: rgb(255, 106, 106);--green: #00e676;--dark-red: #ff5e7e;--red: #ff6098;--kinda-pink: #c44ae6;--purple: #9565f7;--shady-blue: #7084f5;--nice-blue: #86afff;--maybe-cyan: #66cfff;--teal: #6aeeff;--mint-green: #4dffd2;--yellowish-green: #9effa2;--greenish-yellow: #c7fc8b;--dark-teal: #51cbff;--tangerine: #ffac6d;--orange: #ffbe68;--redish-orange: #ff8560}body[data-theme=dark] sm-popup::part(popup){background-color:rgba(var(--foreground-color), 1)}body[data-theme=dark] ::-webkit-calendar-picker-indicator{filter:invert(1)}p,strong{font-size:.9rem;max-width:65ch;line-height:1.7;color:rgba(var(--text-color), 0.9)}p:not(:last-of-type),strong:not(:last-of-type){margin-bottom:1.5rem}a{text-decoration:none;color:var(--accent-color)}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color), 1) inset}button,.button{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;display:inline-flex;border:none;background-color:rgba(0,0,0,0);overflow:hidden;color:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;font-size:inherit;font-weight:500;white-space:nowrap;padding:.9rem;border-radius:.3rem;justify-content:center}button:focus-visible,.button:focus-visible{outline:var(--accent-color) solid medium}button:not(:disabled),.button:not(:disabled){cursor:pointer}.button{background-color:rgba(var(--text-color), 0.02);border:solid thin rgba(var(--text-color), 0.06)}.button--primary{color:rgba(var(--background-color), 1);background-color:var(--accent-color)}.button--primary .icon{fill:rgba(var(--background-color), 1)}.button--colored{color:var(--accent-color)}.button--colored .icon{fill:var(--accent-color)}.button--danger{background-color:rgba(255,115,115,.062745098);color:var(--danger-color)}.button--danger .icon{fill:var(--danger-color)}.button--small{padding:.4rem .6rem}.button--outlined{border:solid rgba(var(--text-color), 0.3) .1rem;background-color:rgba(var(--foreground-color), 1)}.button--transparent{background-color:rgba(0,0,0,0)}button:disabled{opacity:.4;cursor:not-allowed;filter:saturate(0)}.cta{text-transform:uppercase;font-size:.9rem;font-weight:700;letter-spacing:.05em;padding:.8rem 1rem}.icon{width:1.2rem;height:1.2rem;fill:rgba(var(--text-color), 0.8);flex-shrink:0}.icon-only{padding:.5rem;border-radius:.3rem}.icon--big{width:3rem;height:3rem}a:-webkit-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:-moz-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}details{padding:1rem 0}details summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;justify-content:space-between;color:var(--accent-color)}details[open] summary{margin-bottom:1rem}details[open]>summary .down-arrow{transform:rotate(180deg)}sm-input,sm-textarea{font-size:.9rem;--border-radius: 0.5rem;--background-color: rgba(var(--foreground-color), 1)}sm-input button .icon,sm-textarea button .icon{fill:var(--accent-color)}sm-textarea{--max-height: auto}sm-spinner{--size: 1rem;--stroke-width: 0.1rem}sm-form{--gap: 1rem}sm-chips{--gap: 0.3rem}sm-chip{position:relative;font-size:.9rem;--border-radius: 0.5rem;--padding: 0.5rem 0.8rem;--background: rgba(var(--text-color), 0.06);-webkit-user-select:none;-moz-user-select:none;user-select:none}sm-chip[selected]{--background: var(--accent-color);color:rgba(var(--background-color), 1)}sm-select::part(options){max-height:40vh}sm-option{flex-shrink:0}sm-option::part(option){grid-template-columns:none}ul{list-style:none}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.breakable{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word}.full-bleed{grid-column:1/-1}.h1{font-size:1.5rem}.h2{font-size:1.2rem}h3{font-size:1.2rem;line-height:1.3}.h4{font-size:.9rem}.h5{font-size:.75rem}.uppercase{text-transform:uppercase}.capitalize::first-letter{text-transform:uppercase}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.flex{display:flex}.flex-1{flex:1}.flex-wrap{flex-wrap:wrap}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-3{gap:.3rem}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-content:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.align-content-center{align-content:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.justify-items-center{justify-items:center}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.h-100{height:100%}.margin-left-0-5{margin-left:.5rem}.margin-left-auto{margin-left:auto}.margin-right-0-5{margin-right:.5rem}.margin-right-auto{margin-right:auto}.ripple{height:8rem;width:8rem;position:absolute;border-radius:50%;transform:scale(0);background:radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%);pointer-events:none}.button--primary .ripple,.button--danger .ripple{background:radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%)}.interact{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0)}.empty-state{display:grid;width:100%;padding:1.5rem 0}.observe-empty-state:empty{display:none !important}.observe-empty-state:not(:empty)+.empty-state{display:none}.bullet-point{display:flex;align-items:center;justify-content:center;margin:0 .8ch}.bullet-point::after{content:"";height:.4ch;width:.4ch;border-radius:.5em;background-color:var(--accent-color)}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{margin-bottom:.5rem}#confirmation_popup .flex,#prompt_popup .flex{margin-top:1rem}.popup__header{position:relative;display:grid;gap:.5rem;width:100%;padding:0 1.5rem;align-items:center}.popup__header>*{grid-row:1}.popup__header h3,.popup__header h4{grid-column:1/-1;justify-self:center;align-self:center}.popup__header__close{grid-column:1;margin-left:-1rem;justify-self:flex-start}#loader{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(var(--foreground-color), 1);z-index:100;display:grid;place-content:center;place-items:center;gap:1rem;text-align:center}#show_character_count{font-size:.8rem;margin-left:auto}#flo_data_status:not(:empty){color:red;padding:.5rem 0}#saved_ids_popup{--height: 80vh}#main_header{padding:.6rem 1rem}.app-brand{display:flex;gap:.3rem;align-items:center}.app-brand .icon{height:1.7rem;width:1.7rem}.app-name__company{font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8)}#main_card{display:flex;flex-direction:column;height:100%;width:100%;background-color:rgba(var(--foreground-color), 1);transition:background-color .3s}#pages_container{flex:1;overflow-y:auto}#main_navbar{display:flex;background:rgba(var(--text-color), 0.03)}#main_navbar.hide-away{position:absolute}#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%;height:100%;flex-direction:column;align-items:center;justify-content:center;padding:.5rem .3rem;color:var(--text-color);font-size:.7rem;border-radius:.3rem;text-align:center}.nav-item .icon{transition:transform .2s}.nav-item__title{transition:opacity .2s,transform .2s}.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:.3rem;background:var(--accent-color);border-radius:1rem 1rem 0 0;z-index:1}#fix_invalid_button{margin-bottom:1rem !important}.password-field label{display:flex}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}.multi-state-button{display:grid;text-align:center;align-items:center;justify-items:center}.multi-state-button>*{grid-area:1/1/2/2}.multi-state-button button{z-index:1;width:100%}.clip{-webkit-clip-path:circle(0);clip-path:circle(0)}#flo_id_warning{padding-bottom:1.5rem;border-bottom:thin solid rgba(var(--text-color), 0.3)}#flo_id_warning .icon{height:4rem;width:4rem;padding:1rem;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8);margin-bottom:1.5rem}.generated-id-card{display:grid;gap:1rem}.generated-id-card h5{margin-bottom:.3rem}#contacts>:first-child{overflow-y:auto;align-content:flex-start;padding-bottom:4rem}#primary_actions_wrapper{display:grid;gap:.5rem}.primary-action{display:flex;padding:.8rem 1rem;gap:.5rem;white-space:normal;font-size:.85rem;border-radius:.5rem;background-color:rgba(0,0,0,0);border:thin solid rgba(var(--text-color), 0.3);text-align:left}.primary-action .icon{fill:var(--accent-color)}#search{position:relative;height:100%}#queried_flo_address h4{font-size:1.1rem}#queried_flo_address>sm-copy{font-size:.8rem}#token_list{display:flex;flex-wrap:wrap;gap:.5rem}.token-item{font-size:.9rem;padding:.5rem 1rem;background-color:rgba(var(--text-color), 0.06);border-radius:.3rem}.transaction{gap:1.2rem;padding:1rem;background-color:rgba(var(--text-color), 0.03);border-radius:.3rem}.transaction:not(:last-of-type){margin-bottom:1rem}.transaction .icon{fill:var(--accent-color)}.transaction__time,.transaction__link,.transaction__receiver{font-size:.8rem}.transaction__receiver{margin-left:.5rem;color:rgba(var(--text-color), 0.9);font-weight:500}.transaction__amount{font-weight:700}.transaction.mined .transaction__icon .icon,.transaction.received .transaction__icon .icon,.transaction.self .transaction__icon .icon{fill:var(--green)}.transaction.mined .transaction__amount,.transaction.received .transaction__amount,.transaction.self .transaction__amount{color:var(--green)}.transaction.mined .transaction__amount::before,.transaction.received .transaction__amount::before,.transaction.self .transaction__amount::before{content:"+ "}.transaction.sent .transaction__icon .icon{fill:var(--danger-color)}.transaction.sent .transaction__amount{color:var(--danger-color)}.transaction.sent .transaction__amount::before{content:"- "}.transaction p{font-size:.9rem;max-width:unset}.transaction__time{justify-self:flex-end;color:rgba(var(--text-color), 0.8)}#search_wrapper{width:min(100%,36rem)}#search_query_input{justify-self:center}#saved_ids_list{position:relative;align-content:flex-start;padding-bottom:1.5rem;margin-top:1rem;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.saved-id{grid-template-columns:auto 1fr;gap:0 .8rem;border-radius:.3rem;padding:.5rem;background-color:rgba(var(--text-color), 0.03);-webkit-user-select:none;-moz-user-select:none;user-select:none}.saved-id.highlight{box-shadow:0 0 .1rem .1rem var(--accent-color) inset}.saved-id .edit-saved{grid-area:1/1/3/2;padding:.3rem;position:relative}.saved-id .edit-saved .icon{position:absolute;height:1.2rem;width:1.2rem;right:0;bottom:0;border-radius:.5rem;padding:.2rem;background-color:rgba(var(--background-color), 1)}.saved-id__initial{display:flex;align-items:center;justify-content:center;height:2.4rem;width:2.4rem;font-size:1.2rem;text-transform:uppercase;color:rgba(var(--background-color), 1);font-weight:700;line-height:1;background-color:var(--accent-color);justify-self:flex-start;border-radius:2rem}.saved-id__label{align-self:flex-end}.saved-id__flo-id{font-size:.8rem}.page{position:relative;display:flex;flex-direction:column;overflow-y:auto;align-content:flex-start;padding:0 1rem;padding-bottom:3rem}.fab{position:absolute;right:0;bottom:0;margin:1.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);z-index:2}#add_address_button{border-radius:.5rem;color:rgba(var(--background-color), 1);background-color:var(--accent-color)}#add_address_button .icon{fill:rgba(var(--background-color), 1)}#balance_card{display:flex;flex-direction:column;gap:2rem;padding:max(1rem,2vw);background-color:rgba(var(--text-color), 0.06);aspect-ratio:4/2;justify-content:flex-end;border-radius:.5rem}#balance_card form{margin-top:1rem}#balance_card fieldset{border:none}.token-balance{display:flex;align-items:center;cursor:pointer;gap:.5rem;background-color:rgba(var(--text-color), 0.06);padding:.8rem;border-radius:.3rem;font-size:.9rem}.token-balance span:first-of-type::first-letter{text-transform:capitalize}.token-balance input{height:1rem;width:1rem;accent-color:var(--accent-color)}.token-receiver-combo{border:solid thin rgba(var(--text-color), 0.2);padding:.5rem;border-radius:.8rem}.token-receiver-combo--removable{grid-template-columns:1fr auto;grid-template-areas:"receiver receiver" "amount remove"}.token-receiver-combo--removable .token-receiver{grid-area:receiver}.token-receiver-combo--removable .token-amount{grid-area:amount}.token-receiver-combo--removable .remove-token-receiver{grid-area:remove}#transaction_result{display:grid;gap:.5rem;height:max(40vh,24rem);align-items:center;justify-content:center;text-align:center;align-content:center}#transaction_result:empty{display:none}#transaction_result h3{text-align:center;width:100%}#transaction_result .icon{justify-self:center;height:4rem;width:4rem;border-radius:5rem;margin-bottom:1rem;-webkit-animation:popup 1s;animation:popup 1s}#transaction_result .icon--success{fill:rgba(var(--background-color), 1);padding:1rem;background-color:#0bbe56}#transaction_result .icon--failed{background-color:rgba(var(--text-color), 0.03);fill:var(--danger-color)}#transaction_result sm-copy{font-size:.8rem}@-webkit-keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}@keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}#queried_address_transactions{display:flex;flex-direction:column;padding-bottom:4rem}#pagination_wrapper{position:fixed;bottom:0;right:0;margin:0 auto;padding:.5rem;background-color:rgba(var(--foreground-color), 1);z-index:5;border-radius:.7rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);margin:.5rem}.pagination__item{display:flex;padding:.2rem .5rem;border-radius:.3rem}.pagination__item--active{background-color:var(--accent-color);color:rgba(var(--background-color), 1)}legend,.label{font-size:.8rem;color:rgba(var(--text-color), 0.8)}#smartcontracts{display:grid;min-width:0;height:100%}#smartcontracts>*{grid-area:1/1}#smartcontracts fieldset{padding:.5rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}#smartcontracts fieldset legend{padding:0 .5rem}#smartcontracts label{padding:.3rem .5rem}#smartcontracts label:has(input:not(:disabled):not(:checked)){cursor:pointer}#smartcontracts input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}#smartcontracts input[type=datetime-local]{width:100%;padding:.8rem .6rem;border:none;border-radius:.5rem;font-weight:500;font-family:inherit;font-size:inherit;color:inherit;background-color:rgba(var(--text-color), 0.06)}#smartcontracts input[type=datetime-local]:focus{outline:none;box-shadow:0 0 0 .1rem var(--accent-color)}#smartcontracts sm-input:not([placeholder]){--min-height: 3rem}.smart-contract-action{flex-direction:column;white-space:normal;gap:.5rem;color:rgba(var(--text-color), 0.8);width:5rem;font-size:.9rem;aspect-ratio:1/1;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3);text-align:center}.smart-contract-action .icon{fill:var(--accent-color)}#smart_contract_creation_templates{display:grid;gap:.5rem}#smart_contract_creation_templates li{display:flex;flex:1}.smart-contract-template{display:grid;grid-template-areas:"heading arrow" "description arrow";grid-template-columns:1fr auto;padding:max(1rem,2vw);border:solid 1px rgba(var(--text-color), 0.3);border-radius:.5rem;gap:.5rem;text-align:start;white-space:normal;width:100%;justify-content:flex-start}.smart-contract-template h4{grid-area:heading}.smart-contract-template p{grid-area:description;font-weight:400;line-height:1.3;font-size:.9rem;color:rgba(var(--text-color), 0.8);text-transform:none}.smart-contract-template .icon{margin-top:auto;grid-area:arrow;border:solid 1px rgba(var(--text-color), 0.3);border-radius:3rem;padding:.4rem;height:2rem;width:2rem;fill:var(--accent-color)}.payee-address-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address" "share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address address" "share button"}.payee-address-wrapper .payee-address{grid-area:address}.payee-address-wrapper .payee-share{grid-area:share}.payee-address-wrapper .icon-only{grid-area:button}.payee-address-wrapper .icon-only{height:3.18rem}.choice-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.choice-wrapper .icon-only{aspect-ratio:1/1;height:3.18rem}@media screen and (max-width: 40rem){#main_navbar.hide-away{bottom:0;left:0;right:0}#primary_actions_wrapper{grid-template-columns:1fr 1fr}.nav-item__title{margin-top:.3rem}.nav-item--active .icon{transform:translateY(50%)}.nav-item--active .nav-item__title{transform:translateY(100%);opacity:0}#pagination_wrapper{margin:0;margin-bottom:4.8rem;left:50%;right:auto;transform:translateX(-50%);width:calc(100vw - 2rem);flex-wrap:wrap}}@media screen and (min-width: 40rem){sm-popup{--width: 24rem}.popup__header{padding:1rem 1.5rem 0 1.5rem}body{display:flex;align-items:center;justify-content:center}#main_card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:"header header" "nav main";position:relative}#main_header{grid-area:header}#pages_container{grid-area:main}.page{padding:0 1.5rem}#main_navbar{grid-area:nav;border-top:none;flex-direction:column;height:100%}#main_navbar ul{flex-direction:column}.nav-item{flex-direction:row;text-align:left;justify-content:flex-start;gap:.5rem;padding:1rem;font-weight:500;font-size:.8rem;border-radius:0;min-width:10rem}.nav-item__indicator{width:.25rem;height:50%;left:0;border-radius:0 1rem 1rem 0;bottom:auto}#primary_actions_wrapper{display:flex;flex-wrap:wrap}#create_flo_id_popup,#retrieve_flo_id_popup{--width: 26rem}#send{padding:0 6vw}#send sm-form{width:min(56rem,100%);margin:auto}#smart_contract_creation_templates{grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}#smart_contract_deposit_form,#smart_contract_participate_form,#smart_contract_update_form,#smart_contract_trigger_form{width:min(36rem,100%);margin:auto;--gap: 1.5rem}#smart_contract_creation_form::part(form){gap:1.5rem;margin:auto;width:min(36rem,100%)}#smart_contract_creation_form::part(form).split-layout{grid-template-columns:1fr 1.5fr;align-items:flex-start;width:min(50rem,100%)}.payee-address-wrapper{display:grid;grid-template-columns:1fr 8rem 3rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address share share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address share button"}}@media screen and (min-width: 56rem){#send sm-form::part(form){align-items:flex-start;grid-template-columns:1fr 1.5fr}}@media screen and (min-width: 64rem){#address_details_wrapper{grid-template-columns:auto 1fr;align-items:flex-start}#transactions_hero_section{position:-webkit-sticky;position:sticky;top:0}}@media(any-hover: hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color), 0.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color), 0.5)}.interact:not([disabled]){transition:background-color .3s}.interact:not([disabled]):hover{background-color:rgba(var(--text-color), 0.06)}.button:not([disabled]){transition:background-color .3s,filter .3s}.button:not([disabled]):hover{filter:contrast(2)}}@supports(overflow: overlay){body{overflow:overlay}}.hidden{display:none !important} \ No newline at end of file diff --git a/css/main.scss b/css/main.scss index cf75982..fa866b4 100644 --- a/css/main.scss +++ b/css/main.scss @@ -558,17 +558,17 @@ h3 { #main_header { padding: 0.6rem 1rem; } -.app-brand{ +.app-brand { display: flex; gap: 0.3rem; align-items: center; - .icon{ + .icon { height: 1.7rem; width: 1.7rem; } } -.app-name{ - &__company{ +.app-name { + &__company { font-size: 0.8rem; font-weight: 500; color: rgba(var(--text-color), 0.8); @@ -741,7 +741,7 @@ h3 { border-radius: 0.3rem; } .transaction { - gap: 1rem; + gap: 1.2rem; padding: 1rem; background-color: rgba(var(--text-color), 0.03); border-radius: 0.3rem; @@ -758,7 +758,39 @@ h3 { } &__receiver { margin-left: 0.5rem; - color: rgba(var(--text-color), 0.8); + color: rgba(var(--text-color), 0.9); + font-weight: 500; + } + &__amount { + font-weight: 700; + } + &.mined, + &.received, + &.self { + .transaction__icon { + .icon { + fill: var(--green); + } + } + .transaction__amount { + color: var(--green); + &::before { + content: "+ "; + } + } + } + &.sent { + .transaction__icon { + .icon { + fill: var(--danger-color); + } + } + .transaction__amount { + color: var(--danger-color); + &::before { + content: "- "; + } + } } p { font-size: 0.9rem; diff --git a/index.html b/index.html index 7a48fa2..5280c2a 100644 --- a/index.html +++ b/index.html @@ -187,13 +187,14 @@
-
+

Transactions

@@ -830,7 +831,14 @@
-

+
+
Amount
+
+
+
+
FLO Data
+

+
{ - filterFetchedTransactions() - render.paginatedTransactions(page) - }) - } else if (page % Math.ceil(1000 / txsPerPage) === 0 && floGlobals.query.transactions.length <= page * txsPerPage) { + }/* else if (floGlobals.query.totalPages <= page * txsPerPage) { loadMoreTransactions() } else { render.paginatedTransactions(page) - } + }*/ + fetchTransactions(query, page).then(() => { + filterFetchedTransactions() + render.paginatedTransactions(page) + }) } catch (err) { notify(err, 'error') } @@ -1877,45 +1885,55 @@ }) }, transactionCard(details) { - const { sender, receiver, floData, time, txid } = details + const { sender, receiver, floData, time, txid, netValue, mine } = details const { query: queriedFloId } = pagesData.params const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild; - if (sender === receiver) { + if (mine) { + clone.classList.add('mined') + clone.querySelector('.transaction__icon').innerHTML = ` `; + clone.querySelector('.transaction__receiver').textContent = 'Coinbase' + } else if (sender === receiver) { + clone.classList.add('self') clone.querySelector('.transaction__icon').innerHTML = `sender and receiver is same`; + clone.querySelector('.transaction__receiver').textContent = receiver } else if (queriedFloId === sender) { - clone.querySelector('.transaction__icon').innerHTML = ``; + clone.classList.add('sent') + clone.querySelector('.transaction__icon').innerHTML = ``; + clone.querySelector('.transaction__receiver').textContent = receiver } else { - clone.querySelector('.transaction__icon').innerHTML = ``; + clone.classList.add('received') + clone.querySelector('.transaction__icon').innerHTML = ``; + clone.querySelector('.transaction__receiver').textContent = sender + } + if (netValue) { + clone.querySelector('.transaction__amount').textContent = `${netValue} FLO` + } else { + clone.querySelector('.transaction__amount').parentNode.remove() + } + if (floData) { + clone.querySelector('.transaction__flo-data').textContent = floData + } else { + clone.querySelector('.transaction__flo-data').parentNode.remove() } - clone.querySelector('.transaction__receiver').textContent = queriedFloId === sender ? receiver : sender - clone.querySelector('.transaction__flo-data').textContent = floData clone.querySelector('.transaction__link').href = `${floBlockchainAPI.current_server}tx/${txid}` clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000) return clone }, paginatedTransactions(page = parseInt(pagesData.params.page) || 1) { - const { transactions, string: address, filteredTransactions } = floGlobals.query - let startingIndex = ((page - 1) * txsPerPage) - if ((filteredTransactions?.length || transactions.length) < startingIndex) { - startingIndex = 0; - window.history.replaceState({}, '', `#/search?type=address&query=${address}&page=1`) - pagesData.params.page = page = 1; - } - const endingIndex = startingIndex + txsPerPage + const { transactions, string: address, filteredTransactions, totalPages } = floGlobals.query const renderedTransactions = (filteredTransactions || transactions) - .slice(startingIndex, endingIndex) .map(transaction => render.transactionCard(transaction)) renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`) getRef('transactions_hero_section').scrollIntoView({ behavior: 'smooth', block: 'start' }) - if (floGlobals.query.transactions.length) { + if (transactions.length) { getRef('filter_selector').classList.remove('hidden') } else { getRef('filter_selector').classList.add('hidden') } - const paginationSegments = (filteredTransactions || transactions) ? Math.ceil((filteredTransactions || transactions).length / txsPerPage) : 0; + const paginationSegments = totalPages; let pagination = [] let startingPage = page - 2; let showTill = page + 2; @@ -1956,11 +1974,11 @@ } else { getRef('pagination_wrapper').classList.add('hidden') } - if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) { + /* if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) { document.getElementById('load_more_transactions').classList.remove('hidden') } else { document.getElementById('load_more_transactions').classList.add('hidden') - } + } */ }, availableAssetOptions() { return (floGlobals.tokens || []).map(token => html` ${token} `) @@ -2496,12 +2514,12 @@ getRef('address_details_wrapper').classList.remove('hidden') floWebWallet.getLabels().then(allLabels => { if (allLabels[queriedFloId]) { - renderElem(getRef('queried_flo_address'), html`

${allLabels[queriedFloId]}

`) + getRef('queried_flo_address').innerHTML = `

${allLabels[queriedFloId]}

`; } else { - renderElem(getRef('queried_flo_address'), html` + getRef('queried_flo_address').innerHTML = `

FLO Address

- `) + `; } }) const queriedFloId = address || getRef('search_query_input').value.trim() @@ -2524,7 +2542,7 @@ getRef('token_list_wrapper').classList.remove('hidden') } // retrieve FLO balance - getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(3))} FLO`; + getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(8))} FLO`; } catch (e) { console.error(e) } @@ -2551,8 +2569,8 @@ string: '', filteredTransactions: null } - const txsPerPage = 25; - async function fetchTransactions(address, loadOlder = false) { + const txsPerPage = 100; + async function fetchTransactions(address, page = 1) { try { document.getElementById('load_more_transactions').classList.add('hidden') renderElem(getRef('pagination_wrapper'), html``) @@ -2562,23 +2580,12 @@ Loading transactions...
`) - if (loadOlder) { - const { items, initItem } = await floWebWallet.listTransactions.syncOld(address, floGlobals.query.initItem) - floGlobals.query = { - transactions: [...items, ...floGlobals.query.transactions], - string: address, - initItem, - filteredTransactions: null - } - } else { - const { items, lastItem, initItem } = await floWebWallet.listTransactions(address) - floGlobals.query = { - transactions: items, - string: address, - lastItem, - initItem, - filteredTransactions: null - } + const { items, totalPages } = await floWebWallet.listTransactions(address, { pageSize: txsPerPage, page }) + floGlobals.query = { + transactions: items, + string: address, + filteredTransactions: null, + totalPages } } catch (err) { renderElem(getRef('queried_address_transactions'), html` Failed to load transactions `) @@ -2588,7 +2595,19 @@ function filterFetchedTransactions() { const filter = getRef('filter_selector').value; if (filter !== 'all') { - floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => filter === 'sent' ? t.sender === floGlobals.query.string : t.receiver === floGlobals.query.string) + floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => { + switch (filter) { + case 'sent': + return t.sender === floGlobals.query.string + break + case 'received': + return t.receiver === floGlobals.query.string + break + case 'mined': + return t.mined + break + } + }) } else { floGlobals.query.filteredTransactions = null } diff --git a/old.html b/old.html deleted file mode 100644 index 9e35a94..0000000 --- a/old.html +++ /dev/null @@ -1,6766 +0,0 @@ - - - - - FLO web wallet - - - - - - - -
-
- There seems to be a problem connecting to the internet. -
-
- -
-
- -
- - - - - - - -
Generate
- Generate address -
-
- - - - - - -
Send
- Send FLO data -
-
- - - -
Monitor
- Monitor FLO data -
-
- - Settings - - - - - -
Settings
- Settings -
-
-
-
-
- -
- - - - - - - - - \ No newline at end of file diff --git a/scripts/components.js b/scripts/components.js index 75d3898..1f1e510 100644 --- a/scripts/components.js +++ b/scripts/components.js @@ -1,7 +1,7 @@ /*jshint esversion: 6 */ // Components downloaded: chips,copy,form,input,notifications,popup,select,spinner,textarea,theme-toggle const smChips = document.createElement("template"); smChips.innerHTML = '
', customElements.define("sm-chips", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChips.content.cloneNode(!0)), this.chipsWrapper = this.shadowRoot.querySelector(".sm-chips"), this.coverLeft = this.shadowRoot.querySelector(".cover--left"), this.coverRight = this.shadowRoot.querySelector(".cover--right"), this.navButtonLeft = this.shadowRoot.querySelector(".nav-button--left"), this.navButtonRight = this.shadowRoot.querySelector(".nav-button--right"), this.slottedOptions = void 0, this._value = void 0, this.scrollDistance = 0, this.assignedElements = [], this.scrollLeft = this.scrollLeft.bind(this), this.scrollRight = this.scrollRight.bind(this), this.fireEvent = this.fireEvent.bind(this), this.setSelectedOption = this.setSelectedOption.bind(this) } get value() { return this._value } set value(t) { this.setSelectedOption(t) } scrollLeft() { this.chipsWrapper.scrollBy({ left: -this.scrollDistance, behavior: "smooth" }) } scrollRight() { this.chipsWrapper.scrollBy({ left: this.scrollDistance, behavior: "smooth" }) } setSelectedOption(t) { this._value !== t && (this._value = t, this.assignedElements.forEach(e => { e.value == t ? (e.setAttribute("selected", ""), e.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })) : e.removeAttribute("selected") })) } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this._value } })) } connectedCallback() { this.setAttribute("role", "listbox"); const t = this.shadowRoot.querySelector("slot"); t.addEventListener("slotchange", e => { n.disconnect(), i.disconnect(), this.observeSelf.disconnect(), clearTimeout(this.slotChangeTimeout), this.slotChangeTimeout = setTimeout(() => { this.assignedElements = t.assignedElements(), this.assignedElements.forEach(t => { t.hasAttribute("selected") && (this._value = t.value) }), this.observeSelf.observe(this) }, 0) }); const e = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { const e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.scrollDistance = .6 * e.inlineSize } else this.scrollDistance = .6 * t.contentRect.width }) }); e.observe(this), this.observeSelf = new IntersectionObserver((t, e) => { t.forEach(t => { t.isIntersecting && !this.hasAttribute("multiline") && this.assignedElements.length > 0 && (n.observe(this.assignedElements[0]), i.observe(this.assignedElements[this.assignedElements.length - 1]), e.unobserve(this)) }) }, { threshold: 1 }), this.chipsWrapper.addEventListener("option-clicked", t => { this._value !== t.target.value && (this.setSelectedOption(t.target.value), this.fireEvent()) }); const n = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonLeft.classList.add("hide"), this.coverLeft.classList.add("hide")) : (this.navButtonLeft.classList.remove("hide"), this.coverLeft.classList.remove("hide")) }) }, { threshold: 1, root: this }), i = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonRight.classList.add("hide"), this.coverRight.classList.add("hide")) : (this.navButtonRight.classList.remove("hide"), this.coverRight.classList.remove("hide")) }) }, { threshold: 1, root: this }); this.navButtonLeft.addEventListener("click", this.scrollLeft), this.navButtonRight.addEventListener("click", this.scrollRight) } disconnectedCallback() { this.navButtonLeft.removeEventListener("click", this.scrollLeft), this.navButtonRight.removeEventListener("click", this.scrollRight) } }); const smChip = document.createElement("template"); smChip.innerHTML = ' ', customElements.define("sm-chip", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChip.content.cloneNode(!0)), this._value = void 0, this.radioButton = this.shadowRoot.querySelector("input"), this.fireEvent = this.fireEvent.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this) } get value() { return this._value } fireEvent() { this.dispatchEvent(new CustomEvent("option-clicked", { bubbles: !0, composed: !0, detail: { value: this._value } })) } handleKeyDown(t) { "Enter" !== t.key && "Space" !== t.key || this.fireEvent() } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this._value = this.getAttribute("value"), this.addEventListener("click", this.fireEvent), this.addEventListener("keydown", this.handleKeyDown) } disconnectedCallback() { this.removeEventListener("click", this.fireEvent), this.removeEventListener("keydown", this.handleKeyDown) } }); -const smCopy = document.createElement("template"); smCopy.innerHTML = '

', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { "value" === t && this.copyContent.querySelector("slot").assignedNodes() && 0 === this.copyContent.querySelector("slot").assignedNodes().length && (this.copyContent.textContent = o) } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); +const smCopy = document.createElement("template"); smCopy.innerHTML = '

', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { if ("value" === t) { const t = this.copyContent.querySelector("slot"); if (!t) return; const n = t.assignedNodes(); n && n.length || (this.copyContent.textContent = o) } } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); const smForm = document.createElement("template"); smForm.innerHTML = '
', customElements.define("sm-form", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smForm.content.cloneNode(!0)), this.form = this.shadowRoot.querySelector("form"), this.invalidFieldsCount, this.skipSubmit = !1, this.isFormValid = void 0, this.supportedElements = "input, sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio", this.formElements = [], this._requiredElements = [], this.debounce = this.debounce.bind(this), this._checkValidity = this._checkValidity.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.reset = this.reset.bind(this), this.elementsChanged = this.elementsChanged.bind(this) } static get observedAttributes() { return ["skip-submit"] } get validity() { return this.isFormValid } debounce(e, t) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout((() => { e.apply(null, s) }), t) } } _checkValidity() { this.submitButton && 0 !== this._requiredElements.length && (this.invalidFieldsCount = 0, this._requiredElements.forEach((([e, t]) => { (!e.disabled && t && !e.isValid || !t && !e.checkValidity()) && this.invalidFieldsCount++ })), this.isFormValid !== (0 === this.invalidFieldsCount) && (this.isFormValid = 0 === this.invalidFieldsCount, this.dispatchEvent(new CustomEvent(this.isFormValid ? "valid" : "invalid", { bubbles: !0, composed: !0 })), this.skipSubmit || (this.submitButton.disabled = !this.isFormValid))) } handleKeydown(e) { if ("Enter" === e.key && e.target.tagName.includes("INPUT")) if (0 === this.invalidFieldsCount) this.submitButton && this.submitButton.click(), this.dispatchEvent(new CustomEvent("submit", { bubbles: !0, composed: !0 })); else for (const [e, t] of this._requiredElements) { if (t ? !e.isValid : !e.checkValidity()) { (e?.shadowRoot?.lastElementChild || e).animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" }], { duration: 300, easing: "ease" }), t ? e.focusIn() : e.focus(); break } } } reset() { this.formElements.forEach((([e, t]) => { if (t) e.reset(); else switch (e.type) { case "checkbox": case "radio": e.checked = !1; break; default: e.value = "" } })), this._checkValidity() } elementsChanged() { this.formElements = [...this.querySelectorAll(this.supportedElements)].map((e => [e, e.tagName.includes("-")])), this._requiredElements = this.formElements.filter((([e]) => e.hasAttribute("required"))), this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'), this.resetButton = this.querySelector('[type="reset"]'), this.resetButton && this.resetButton.addEventListener("click", this.reset), this._checkValidity() } connectedCallback() { const e = this.debounce(this.elementsChanged, 100); this.addEventListener("input", this.debounce(this._checkValidity, 100)), this.addEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.shadowRoot.querySelector("slot").addEventListener("slotchange", e), this.mutationObserver = new MutationObserver((t => { t.forEach((t => { ("childList" === t.type && [...t.addedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements))) || [...t.removedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements)))) && e() })) })), this.mutationObserver.observe(this, { childList: !0, subtree: !0 }) } attributeChangedCallback(e, t, i) { "skip-submit" === e && (this.skipSubmit = null !== i) } disconnectedCallback() { this.removeEventListener("input", this.debounce(this._checkValidity, 100)), this.removeEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.mutationObserver.disconnect() } }); const smInput = document.createElement("template"); smInput.innerHTML = '
', customElements.define("sm-input", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smInput.content.cloneNode(!0)), this.inputParent = this.shadowRoot.querySelector(".input"), this.input = this.shadowRoot.querySelector("input"), this.clearBtn = this.shadowRoot.querySelector(".clear"), this.label = this.shadowRoot.querySelector(".label"), this.feedbackText = this.shadowRoot.querySelector(".feedback-text"), this.outerContainer = this.shadowRoot.querySelector(".outer-container"), this.optionList = this.shadowRoot.querySelector(".datalist"), this._helperText = "", this._errorText = "", this.isRequired = !1, this.datalist = [], this.validationFunction = void 0, this.reflectedAttributes = ["value", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "list", "autocomplete"], this.reset = this.reset.bind(this), this.clear = this.clear.bind(this), this.focusIn = this.focusIn.bind(this), this.focusOut = this.focusOut.bind(this), this.fireEvent = this.fireEvent.bind(this), this.checkInput = this.checkInput.bind(this), this.showError = this.showError.bind(this), this.allowOnlyNum = this.allowOnlyNum.bind(this), this.handleOptionClick = this.handleOptionClick.bind(this), this.handleInputNavigation = this.handleInputNavigation.bind(this), this.handleDatalistNavigation = this.handleDatalistNavigation.bind(this), this.handleFocus = this.handleFocus.bind(this), this.handleBlur = this.handleBlur.bind(this) } static get observedAttributes() { return ["value", "placeholder", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "helper-text", "error-text", "list"] } get value() { return this.input.value } set value(t) { t !== this.input.value && (this.input.value = t, this.checkInput()) } get placeholder() { return this.getAttribute("placeholder") } set placeholder(t) { this.setAttribute("placeholder", t) } get type() { return this.getAttribute("type") } set type(t) { this.setAttribute("type", t) } get validity() { return this.input.validity } get disabled() { return this.hasAttribute("disabled") } set disabled(t) { t ? (this.inputParent.classList.add("disabled"), this.setAttribute("disabled", "")) : (this.inputParent.classList.remove("disabled"), this.removeAttribute("disabled")) } get readOnly() { return this.hasAttribute("readonly") } set readOnly(t) { t ? this.setAttribute("readonly", "") : this.removeAttribute("readonly") } set customValidation(t) { this.validationFunction = t } set errorText(t) { this._errorText = t } showError() { this.feedbackText.className = "feedback-text error", this.feedbackText.innerHTML = ` ${this._errorText}` } set helperText(t) { this._helperText = t } get isValid() { if ("" !== this.input.value) { const t = this.input.checkValidity(); let e = !0; return this.validationFunction && (e = Boolean(this.validationFunction(this.input.value))), t && e ? (this.feedbackText.className = "feedback-text success", this.feedbackText.textContent = "") : this._errorText && this.showError(), t && e } } reset() { this.value = "" } clear() { this.value = "", this.input.focus(), this.fireEvent() } focusIn() { this.input.focus() } focusOut() { this.input.blur() } fireEvent() { let t = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(t) } searchDatalist(t) { const e = this.datalist.filter(e => e.toLowerCase().includes(t.toLowerCase())); if (e.sort((e, n) => { const i = e.toLowerCase().indexOf(t.toLowerCase()), s = n.toLowerCase().indexOf(t.toLowerCase()); return i - s }), e.length) { if (this.optionList.children.length > e.length) { const t = this.optionList.children.length - e.length; for (let e = 0; e < t; e++)this.optionList.removeChild(this.optionList.lastChild) } e.forEach((t, e) => { if (this.optionList.children[e]) this.optionList.children[e].textContent = t; else { const e = document.createElement("li"); e.textContent = t, e.classList.add("datalist-item"), e.setAttribute("tabindex", "0"), this.optionList.appendChild(e) } }), this.optionList.classList.remove("hidden") } else this.optionList.classList.add("hidden") } checkInput(t) { this.hasAttribute("readonly") || ("" !== this.input.value ? this.clearBtn.classList.remove("hidden") : this.clearBtn.classList.add("hidden")), this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder").trim() && ("" !== this.input.value ? (this.animate ? this.inputParent.classList.add("animate-placeholder") : this.label.classList.add("hidden"), this.datalist.length && (this.searchTimeout && clearTimeout(this.searchTimeout), this.searchTimeout = setTimeout(() => { this.searchDatalist(this.input.value.trim()) }, 100))) : (this.animate ? this.inputParent.classList.remove("animate-placeholder") : this.label.classList.remove("hidden"), this.feedbackText.textContent = "", this.datalist.length && (this.optionList.innerHTML = "", this.optionList.classList.add("hidden")))) } allowOnlyNum(t) { 1 === t.key.length && (("." !== t.key || !t.target.value.includes(".") && 0 !== t.target.value.length) && ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(t.key) || t.preventDefault()) } handleOptionClick(t) { this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus() } handleInputNavigation(t) { "ArrowDown" === t.key ? (t.preventDefault(), this.optionList.children.length && this.optionList.children[0].focus()) : "ArrowUp" === t.key && (t.preventDefault(), this.optionList.children.length && this.optionList.children[this.optionList.children.length - 1].focus()) } handleDatalistNavigation(t) { "ArrowUp" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.previousElementSibling ? this.shadowRoot.activeElement.previousElementSibling.focus() : this.input.focus()) : "ArrowDown" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.nextElementSibling ? this.shadowRoot.activeElement.nextElementSibling.focus() : this.input.focus()) : "Enter" !== t.key && " " !== t.key || (t.preventDefault(), this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus()) } handleFocus(t) { this.datalist.length && this.searchDatalist(this.input.value.trim()) } handleBlur(t) { this.datalist.length && this.optionList.classList.add("hidden") } connectedCallback() { this.animate = this.hasAttribute("animate"), this.setAttribute("role", "textbox"), this.input.addEventListener("input", this.checkInput), this.clearBtn.addEventListener("click", this.clear), this.datalist.length && (this.optionList.addEventListener("click", this.handleOptionClick), this.input.addEventListener("keydown", this.handleInputNavigation), this.optionList.addEventListener("keydown", this.handleDatalistNavigation)), this.input.addEventListener("focusin", this.handleFocus), this.addEventListener("focusout", this.handleBlur) } attributeChangedCallback(t, e, n) { if (e !== n) switch (this.reflectedAttributes.includes(t) && (this.hasAttribute(t) ? this.input.setAttribute(t, this.getAttribute(t) ? this.getAttribute(t) : "") : this.input.removeAttribute(t)), t) { case "placeholder": this.label.textContent = n, this.setAttribute("aria-label", n); break; case "value": this.checkInput(); break; case "type": this.hasAttribute("type") && "number" === this.getAttribute("type") ? (this.input.setAttribute("inputmode", "decimal"), this.input.addEventListener("keydown", this.allowOnlyNum)) : this.input.removeEventListener("keydown", this.allowOnlyNum); break; case "helper-text": this._helperText = n; break; case "error-text": this._errorText = n; break; case "required": this.isRequired = this.hasAttribute("required"), this.isRequired ? this.setAttribute("aria-required", "true") : this.setAttribute("aria-required", "false"); break; case "readonly": this.hasAttribute("readonly") ? this.inputParent.classList.add("readonly") : this.inputParent.classList.remove("readonly"); break; case "disabled": this.hasAttribute("disabled") ? this.inputParent.classList.add("disabled") : this.inputParent.classList.remove("disabled"); break; case "list": this.hasAttribute("list") && "" !== this.getAttribute("list").trim() && (this.datalist = this.getAttribute("list").split(",")) } } disconnectedCallback() { this.input.removeEventListener("input", this.checkInput), this.clearBtn.removeEventListener("click", this.clear), this.input.removeEventListener("keydown", this.allowOnlyNum), this.optionList.removeEventListener("click", this.handleOptionClick), this.input.removeEventListener("keydown", this.handleInputNavigation), this.optionList.removeEventListener("keydown", this.handleDatalistNavigation), this.input.removeEventListener("focusin", this.handleFocus), this.removeEventListener("focusout", this.handleBlur) } }); const smNotifications = document.createElement("template"); smNotifications.innerHTML = '
', customElements.define("sm-notifications", class extends HTMLElement { constructor() { super(), this.shadow = this.attachShadow({ mode: "open" }).append(smNotifications.content.cloneNode(!0)), this.notificationPanel = this.shadowRoot.querySelector(".notification-panel"), this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" }, this.push = this.push.bind(this), this.createNotification = this.createNotification.bind(this), this.removeNotification = this.removeNotification.bind(this), this.clearAll = this.clearAll.bind(this), this.remove = this.remove.bind(this), this.handlePointerMove = this.handlePointerMove.bind(this), this.startX = 0, this.currentX = 0, this.endX = 0, this.swipeDistance = 0, this.swipeDirection = "", this.swipeThreshold = 0, this.startTime = 0, this.swipeTime = 0, this.swipeTimeThreshold = 200, this.currentTarget = null, this.mediaQuery = window.matchMedia("(min-width: 640px)"), this.handleOrientationChange = this.handleOrientationChange.bind(this), this.isLandscape = !1 } randString(n) { let t = ""; const e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; for (let i = 0; i < n; i++)t += e.charAt(Math.floor(Math.random() * e.length)); return t } createNotification(n, t = {}) { const { pinned: e = !1, icon: i = "", action: o } = t, r = document.createElement("div"); r.id = this.randString(8), r.className = `notification ${e ? "pinned" : ""}`; const a = document.createElement("div"); a.className = "icon-container", a.innerHTML = i; const s = document.createElement("output"); if (s.textContent = n, r.append(a, s), o) { const n = document.createElement("button"); n.className = "action", n.innerText = o.label, n.addEventListener("click", o.callback) } if (e) { const n = document.createElement("button"); n.className = "close", n.innerHTML = ' ', n.addEventListener("click", () => { this.remove(r.id) }), r.append(n) } return r } push(n, t = {}) { const e = this.createNotification(n, t); return this.isLandscape ? this.notificationPanel.append(e) : this.notificationPanel.prepend(e), this.notificationPanel.animate([{ transform: `translateY(${this.isLandscape ? "" : "-"}${e.clientHeight}px)` }, { transform: "none" }], this.animationOptions), e.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), e.querySelector(".action") && e.querySelector(".action").addEventListener("click", t.action.callback), e.id } removeNotification(n, t = "left") { if (!n) return; const e = "left" === t ? "-" : "+"; n.animate([{ transform: this.currentX ? `translateX(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateX(calc(${e}${Math.abs(this.currentX)}px ${e} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = (() => { n.remove() }) } remove(n) { const t = this.notificationPanel.querySelector(`#${n}`); t && this.removeNotification(t) } clearAll() { Array.from(this.notificationPanel.children).forEach(n => { this.removeNotification(n) }) } handlePointerMove(n) { this.currentX = n.clientX - this.startX, this.currentTarget.style.transform = `translateX(${this.currentX}px)` } handleOrientationChange(n) { this.isLandscape = n.matches, n.matches } connectedCallback() { this.handleOrientationChange(this.mediaQuery), this.mediaQuery.addEventListener("change", this.handleOrientationChange), this.notificationPanel.addEventListener("pointerdown", n => { n.target.closest(".close") ? this.removeNotification(n.target.closest(".notification")) : n.target.closest(".notification") && (this.swipeThreshold = n.target.closest(".notification").getBoundingClientRect().width / 2, this.currentTarget = n.target.closest(".notification"), this.currentTarget.setPointerCapture(n.pointerId), this.startTime = Date.now(), this.startX = n.clientX, this.startY = n.clientY, this.notificationPanel.addEventListener("pointermove", this.handlePointerMove)) }), this.notificationPanel.addEventListener("pointerup", n => { this.endX = n.clientX, this.endY = n.clientY, this.swipeDistance = Math.abs(this.endX - this.startX), this.swipeTime = Date.now() - this.startTime, this.endX > this.startX ? this.swipeDirection = "right" : this.swipeDirection = "left", this.swipeTime < this.swipeTimeThreshold ? this.swipeDistance > 50 && this.removeNotification(this.currentTarget, this.swipeDirection) : this.swipeDistance > this.swipeThreshold ? this.removeNotification(this.currentTarget, this.swipeDirection) : this.currentTarget.animate([{ transform: `translateX(${this.currentX}px)` }, { transform: "none" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), this.notificationPanel.removeEventListener("pointermove", this.handlePointerMove), this.notificationPanel.releasePointerCapture(n.pointerId), this.currentX = 0 }); const n = new MutationObserver(n => { n.forEach(n => { "childList" === n.type && n.addedNodes.length && !n.addedNodes[0].classList.contains("pinned") && setTimeout(() => { this.removeNotification(n.addedNodes[0]) }, 5e3) }) }); n.observe(this.notificationPanel, { childList: !0 }) } disconnectedCallback() { mediaQueryList.removeEventListener("change", handleOrientationChange) } }); diff --git a/scripts/flo-webwallet.js b/scripts/flo-webwallet.js index 5d6538a..6ecf0b8 100644 --- a/scripts/flo-webwallet.js +++ b/scripts/flo-webwallet.js @@ -48,46 +48,76 @@ }) } - function listTransactions_raw(address, options = {}) { + function formatTx(address, tx) { + let result = { + time: tx.time, + block: tx.blockheight, + blockhash: tx.blockhash, + txid: tx.txid, + floData: tx.floData, + confirmations: tx.confirmations + } + + //format receivers + let receivers = {}; + for (let vout of tx.vout) { + if (vout.scriptPubKey.isAddress) { + let id = vout.scriptPubKey.addresses[0]; + if (id in receivers) + receivers[id] += vout.value; + else receivers[id] = vout.value; + } + } + result.receivers = receivers; + //format senders (or mined) + if (!tx.vin[0].isAddress) { //mined (ie, coinbase) + let coinbase = tx.vin[0].coinbase; + result.mine = coinbase; + result.mined = { [coinbase]: tx.valueOut } + } else { + result.sender = tx.vin[0].addresses[0]; + result.receiver = tx.vout[0].scriptPubKey.addresses[0]; + result.fees = tx.fees; + let senders = {}; + for (let vin of tx.vin) { + if (vin.isAddress) { + let id = vin.addresses[0]; + if (id in senders) + senders[id] += vin.value; + else senders[id] = vin.value; + } + } + result.senders = senders; + + //remove change amounts + for (let id in senders) { + if (id in receivers) { + if (senders[id] > receivers[id]) { + senders[id] -= receivers[id]; + delete receivers[id]; + } else if (senders[id] < receivers[id]) { //&& id != address + receivers[id] -= senders[id]; + delete senders[id]; + } + } + } + } + + return result; + } + + floWebWallet.listTransactions = function (address, page_options = {}) { return new Promise((resolve, reject) => { - options.latest = true; + let options = {}; + if (Number.isInteger(page_options.page)) + options.page = page_options.page; + if (Number.isInteger(page_options.pageSize)) + options.pageSize = page_options.pageSize; floBlockchainAPI.readTxs(address, options).then(response => { const result = {} - result.items = response.items.map(({ time, txid, floData, isCoinBase, vin, vout }) => ({ - time, txid, floData, isCoinBase, - sender: isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr, - receiver: isCoinBase ? address : vout[0].scriptPubKey.addresses[0] - })); - result.lastItem = response.lastItem; - result.initItem = response.initItem; - resolve(result); - }).catch(error => reject(error)) - }) - } - - - floWebWallet.listTransactions = function (address) { - return new Promise((resolve, reject) => { - listTransactions_raw(address) - .then(result => resolve(result)) - .catch(error => reject(error)) - }) - } - - - floWebWallet.listTransactions.syncNew = function (address, lastItem) { - return new Promise((resolve, reject) => { - listTransactions_raw(address, { after: lastItem }).then(result => { - delete result.initItem; - resolve(result); - }).catch(error => reject(error)) - }) - } - - floWebWallet.listTransactions.syncOld = function (address, initItem) { - return new Promise((resolve, reject) => { - listTransactions_raw(address, { before: initItem }).then(result => { - delete result.lastItem; + result.items = response.txs.map(tx => formatTx(address, tx)); + result.page = response.page; + result.totalPages = response.totalPages; resolve(result); }).catch(error => reject(error)) }) diff --git a/scripts/floBlockchainAPI.js b/scripts/floBlockchainAPI.js index 58bbc5b..9dedb90 100644 --- a/scripts/floBlockchainAPI.js +++ b/scripts/floBlockchainAPI.js @@ -1,13 +1,13 @@ -(function (EXPORTS) { //floBlockchainAPI v2.5.6a - /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ +(function (EXPORTS) { //floBlockchainAPI v3.0.1b + /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; const DEFAULT = { blockchain: floGlobals.blockchain, apiURL: { - FLO: ['https://flosight.ranchimall.net/'], - FLO_TEST: ['https://flosight-testnet.ranchimall.net/'] + FLO: ['https://blockbook.ranchimall.net/'], + FLO_TEST: [] }, sendAmt: 0.0003, fee: 0.0002, @@ -61,9 +61,9 @@ var serverList = Array.from(allServerList); var curPos = floCrypto.randInt(0, serverList.length - 1); - function fetch_retry(apicall, rm_flosight) { + function fetch_retry(apicall, rm_node) { return new Promise((resolve, reject) => { - let i = serverList.indexOf(rm_flosight) + let i = serverList.indexOf(rm_node) if (i != -1) serverList.splice(i, 1); curPos = floCrypto.randInt(0, serverList.length - 1); fetch_api(apicall, false) @@ -82,19 +82,19 @@ .then(result => resolve(result)) .catch(error => reject(error)); } else - reject("No floSight server working"); + reject("No FLO blockbook server working"); } else { - let flosight = serverList[curPos]; - fetch(flosight + apicall).then(response => { + let serverURL = serverList[curPos]; + fetch(serverURL + apicall).then(response => { if (response.ok) response.json().then(data => resolve(data)); else { - fetch_retry(apicall, flosight) + fetch_retry(apicall, serverURL) .then(result => resolve(result)) .catch(error => reject(error)); } }).catch(error => { - fetch_retry(apicall, flosight) + fetch_retry(apicall, serverURL) .then(result => resolve(result)) .catch(error => reject(error)); }) @@ -124,43 +124,27 @@ } //Get balance for the given Address - const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) { + const getBalance = floBlockchainAPI.getBalance = function (addr) { return new Promise((resolve, reject) => { - let api = `api/addr/${addr}/balance`, query_params = {}; - if (after) { - if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after)) - query_params.after = after; - else return reject("Invalid 'after' parameter"); - } - promisedAPI(api, query_params).then(result => { - if (typeof result === 'object' && result.lastItem) { - getBalance(addr, result.lastItem) - .then(r => resolve(util.toFixed(r + result.data))) - .catch(error => reject(error)) - } else resolve(result); - }).catch(error => reject(error)) + let api = `api/address/${addr}`; + promisedAPI(api, { details: "basic" }) + .then(result => resolve(result["balance"])) + .catch(error => reject(error)) }); } - const getUTXOs = address => new Promise((resolve, reject) => { - promisedAPI(`api/addr/${address}/utxo`) - .then(utxo => resolve(utxo)) - .catch(error => reject(error)) - }) + function getScriptPubKey(address) { + var tx = bitjs.transaction(); + tx.addoutput(address, 0); + let outputBuffer = tx.outputs.pop().script; + return Crypto.util.bytesToHex(outputBuffer) + } - const getUnconfirmedSpent = address => new Promise((resolve, reject) => { - readTxs(address, { mempool: "only" }).then(result => { - let unconfirmedSpent = {}; - for (let tx of result.items) - if (tx.confirmations == 0) - for (let vin of tx.vin) - if (vin.addr === address) { - if (Array.isArray(unconfirmedSpent[vin.txid])) - unconfirmedSpent[vin.txid].push(vin.vout); - else - unconfirmedSpent[vin.txid] = [vin.vout]; - } - resolve(unconfirmedSpent); + const getUTXOs = address => new Promise((resolve, reject) => { + promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => { + let scriptPubKey = getScriptPubKey(address); + utxos.forEach(u => u.scriptPubKey = scriptPubKey); + resolve(utxos); }).catch(error => reject(error)) }) @@ -180,32 +164,28 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { - getUTXOs(senderAddr).then(utxos => { - //form/construct the transaction data - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (var i = utxos.length - 1; - (i >= 0) && (utxoAmt < sendAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < sendAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - trx.addoutput(receiverAddr, sendAmt); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + trx.addoutput(receiverAddr, sendAmt); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -293,34 +273,30 @@ if (balance < totalAmt + fee) return reject("Insufficient FLO balance!"); //get unconfirmed tx list - getUnconfirmedSpent(floID).then(unconfirmedSpent => { - getUTXOs(floID).then(utxos => { - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < totalAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i = 0; i < count; i++) - trx.addoutput(floID, splitAmt); - var change = utxoAmt - totalAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(floID, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - var signedTxHash = trx.sign(privKey, 1); - broadcastTx(signedTxHash) - .then(txid => resolve(txid)) - .catch(error => reject(error)) - } - }).catch(error => reject(error)) + getUTXOs(floID).then(utxos => { + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < totalAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i = 0; i < count; i++) + trx.addoutput(floID, splitAmt); + var change = utxoAmt - totalAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(floID, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + } }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -551,33 +527,29 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { - getUTXOs(senderAddr).then(utxos => { - //form/construct the transaction data - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (var i = utxos.length - 1; - (i >= 0) && (utxoAmt < sendAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < sendAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i in receivers) - trx.addoutput(receivers[i], amounts[i]); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i in receivers) + trx.addoutput(receivers[i], amounts[i]); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } }).catch(error => reject(error)) }).catch(error => reject(error)) }); @@ -773,20 +745,11 @@ const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { return new Promise((resolve, reject) => { if (signedTxHash.length < 1) - return reject("Empty Signature"); - var url = serverList[curPos] + 'api/tx/send'; - fetch(url, { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: `{"rawtx":"${signedTxHash}"}` - }).then(response => { - if (response.ok) - response.json().then(data => resolve(data.txid.result)); - else - response.text().then(data => resolve(data)); - }).catch(error => reject(error)); + return reject("Empty Transaction Data"); + + promisedAPI('/api/sendtx/' + signedTxHash) + .then(response => resolve(response["result"])) + .catch(error => reject(error)) }) } @@ -825,61 +788,96 @@ }) } - //Read Txs of Address between from and to + //Read Txs of Address const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { - let api = `api/addrs/${addr}/txs`; //API options - let query_params = {}; - if (!isUndefined(options.after) || !isUndefined(options.before)) { - if (!isUndefined(options.after)) - query_params.after = options.after; - if (!isUndefined(options.before)) - query_params.before = options.before; - } else { - if (!isUndefined(options.from)) - query_params.from = options.from; - if (!isUndefined(options.to)) - query_params.to = options.to; - } - if (!isUndefined(options.latest)) - query_params.latest = options.latest; - if (!isUndefined(options.mempool)) - query_params.mempool = options.mempool; - promisedAPI(api, query_params) - .then(response => resolve(response)) - .catch(error => reject(error)) + let query_params = { details: 'txs' }; + //page options + if (!isUndefined(options.page) && Number.isInteger(options.page)) + query_params.page = options.page; + if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize)) + query_params.pageSize = options.pageSize; + //only confirmed tx + if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value + query_params.confirmed = true; + + promisedAPI(`api/address/${addr}`, query_params).then(response => { + if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx + response.txs = []; + resolve(response) + }).catch(error => reject(error)) }); } + //backward support (floBlockchainAPI < v2.5.6) + function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + cacheTotal += response.txs.length; + let n_remaining = response.txApperances - cacheTotal + if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call + let n_remove = ignoreOld - n_remaining; + resolve(response.txs.slice(0, -n_remove)); + } else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + function readAllTxs_new(addr, options, lastItem) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + let i = response.txs.findIndex(t => t.txid === lastItem); + if (i != -1) //found lastItem + resolve(response.txs.slice(0, i)) + else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_new(addr, options, lastItem) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + //Read All Txs of Address (newest first) const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { - readTxs(addr, options).then(response => { - if (response.incomplete) { - let next_options = Object.assign({}, options); - if (options.latest) - next_options.before = response.initItem; //update before for chain query (latest 1st) - else - next_options.after = response.lastItem; //update after for chain query (oldest 1st) - readAllTxs(addr, next_options).then(r => { - r.items = r.items.concat(response.items); //latest tx are 1st in array - resolve(r); - }).catch(error => reject(error)) - } else + if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6 + readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld; resolve({ - lastItem: response.lastItem || options.after, - items: response.items - }); - }) - }); + lastItem: new_lastItem, + items: txs + }) + + }).catch(error => reject(error)) + else //New format for floBlockchainAPI >= v2.5.6 + readAllTxs_new(addr, options, options.after).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.after; + resolve({ + lastItem: new_lastItem, + items: txs + }) + }).catch(error => reject(error)) + }) } /*Read flo Data from txs of given Address options can be used to filter data after : query after the given txid - before : query before the given txid - mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx) + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after') sentOnly : filters only sent data receivedOnly: filters only received data @@ -894,18 +892,14 @@ //fetch options let query_options = {}; - query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx - if (!isUndefined(options.after) || !isUndefined(options.before)) { - if (!isUndefined(options.ignoreOld)) //Backward support - return reject("Invalid options: cannot use after/before and ignoreOld in same query"); - //use passed after and/or before options (options remain undefined if not passed) - query_options.after = options.after; - query_options.before = options.before; - } - readAllTxs(addr, query_options).then(response => { + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx - if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after or options.before - response.items.splice(-options.ignoreOld); //negative to count from end of the array + if (!isUndefined(options.after)) + query_options.after = options.after; + else if (!isUndefined(options.ignoreOld)) + query_options.ignoreOld = options.ignoreOld; + + readAllTxs(addr, query_options).then(response => { if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.receivers === "string") options.receivers = [options.receivers]; @@ -916,9 +910,9 @@ if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -944,7 +938,7 @@ txid: tx.txid, time: tx.time, blockheight: tx.blockheight, - senders: new Set(tx.vin.map(v => v.addr)), + senders: new Set(tx.vin.map(v => v.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), data: tx.floData } : tx.floData); @@ -964,8 +958,7 @@ caseFn: (function) flodata => return bool value options can be used to filter data after : query after the given txid - before : query before the given txid - mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx) + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) sentOnly : filters only sent data receivedOnly: filters only received data tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) @@ -975,23 +968,37 @@ const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) { return new Promise((resolve, reject) => { //fetch options - let query_options = { latest: true }; - query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx - if (!isUndefined(options.after)) query_options.after = options.after; - if (!isUndefined(options.before)) query_options.before = options.before; + let query_options = {}; + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only + if (!isUndefined(options.page)) + query_options.page = options.page; + //if (!isUndefined(options.after)) query_options.after = options.after; + let new_lastItem; readTxs(addr, query_options).then(response => { + //lastItem confirmed tx checked + if (!new_lastItem) { + let last_tx = response.items.find(t => t.confirmations > 0); + if (last_tx) + new_lastItem = last_tx.txid; + } + if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.receivers === "string") options.receivers = [options.receivers]; + //check if `after` txid is in the response + let i_after = response.txs.findIndex(t => t.txid === options.after); + if (i_after != -1) //found lastItem, hence remove it and all txs before that + response.items.splice(i_after); + var item = response.items.find(tx => { if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -1004,32 +1011,31 @@ //if item found, then resolve the result if (!isUndefined(item)) { - const result = { lastItem: response.lastItem }; + const result = { lastItem: new_lastItem || item.txid }; if (options.tx) { result.item = { - txid: tx.txid, - time: tx.time, - blockheight: tx.blockheight, - senders: new Set(tx.vin.map(v => v.addr)), - receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), - data: tx.floData + txid: item.txid, + time: item.time, + blockheight: item.blockheight, + senders: new Set(item.vin.map(v => v.addresses[0])), + receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])), + data: item.floData } } else - result.data = tx.floData; + result.data = item.floData; return resolve(result); } + + if (response.page == response.totalPages || i_after != -1) //reached last page to check + resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem + //else if address needs chain query - else if (response.incomplete) { - let next_options = Object.assign({}, options); - options.before = response.initItem; //this fn uses latest option, so using before to chain query - getLatestData(addr, caseFn, next_options).then(r => { - r.lastItem = response.lastItem; //update last key as it should be the newest tx - resolve(r); - }).catch(error => reject(error)) + else { + options.page = response.page + 1; + getLatestData(addr, caseFn, options) + .then(result => resolve(result)) + .catch(error => reject(error)) } - //no data match the caseFn, resolve just the lastItem - else - resolve({ lastItem: response.lastItem }); }).catch(error => reject(error)) }) diff --git a/testnet.html b/testnet.html deleted file mode 100644 index c3eeff7..0000000 --- a/testnet.html +++ /dev/null @@ -1,11088 +0,0 @@ - - - - - FLO web wallet - - - - - - - - -
-
- There seems to be a problem connecting to the internet. -
-
- -
-
- -
- - - - - - - -
Generate
- Generate address -
-
- - - - - - -
Send
- Send FLO data -
-
- - - -
Monitor
- Monitor FLO data -
-
- - Settings - - - - - -
Settings
- Settings -
-
-
-
- Copied -
-
-
-
-

Add new address to monitoring list

-
- - -
-
- - -
- - -
-
-
-
-

Edit display card

- - Copy FLO address - - -
- - - - - - - -

-
-
- - -
- - - -
-
-
-
- - Add new address to monitor - - -
-
-
- - Go back to monitoring page - - - -

-
-
- - Refresh transactions - - -
- - - - -
- -
-
-
- - - - - - -
Available balance
- 0 - FLO(s) -
To send FLO data, make sure you have enough balance.
-
-
- - - -
-
-
-
- - -
-
- - -
-
- - -
1040/1040
-
-

- - -
-
-
-
- - Go back to monitoring page - - - -

Send

-
-
-
-
- - -
- -
-
-
- - - - - - -

Transaction successful

-

- -
-
- -

-
-
-
-
-
-

Dark mode

-
- - Automatic
- Dark mode active : 6pm - 6am -
- -
-
- Manual
- Dark mode -
- -
-
-
-

Clear all local data

-
This will delete all local Web Wallet data like added addresses and locally stored - transactions.After clearing local data you may experience slow loading of newly added address, - please proceed cautiously!
- -
-
-
- -

Developer options

- - - - -
-
-
-

Allow unconfirmed UTXOs

- - This is a developer option only and not recommended for normal use. - -
- -
-
-
-
-

About

-
Version 2.7.4
- -
Powered by
- - - - - - - - - - - - - -
-
-
-
-
-
- -
- - - - - - - - - \ No newline at end of file