From c6affb69137c63e1edd4eb062bc43787095b210f Mon Sep 17 00:00:00 2001 From: sairaj mote Date: Mon, 11 Sep 2023 02:30:32 +0530 Subject: [PATCH] Feature update and code optimizations -- added option for lender to liquidate or pre-liquidate loan if conditions are met -- Reduced repeated processing of requests -- added request caching --- css/main.css | 4 - css/main.min.css | 2 +- css/main.scss | 4 - index.html | 490 ++++++++++++++++++++++------------------- scripts/btcMortgage.js | 45 ++-- 5 files changed, 285 insertions(+), 260 deletions(-) diff --git a/css/main.css b/css/main.css index 1eb22e4..8778237 100644 --- a/css/main.css +++ b/css/main.css @@ -50,10 +50,6 @@ strong { 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; diff --git a/css/main.min.css b/css/main.min.css index 5aca132..7b9da9c 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.5rem)}html,body{height:100%}body{--accent-color: #365eff;--text-color: 30, 30, 30;--background-color: 248, 248, 248;--foreground-color: 255, 255, 255;--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}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}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:.6rem .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{cursor:not-allowed;background-color:rgba(var(--text-color), 0.03);color:rgba(var(--text-color), 0.5)}.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 summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;gap:.5rem;color:var(--accent-color);font-weight:500}details summary .down-arrow{fill: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=true] .badge{background-color:rgba(var(--foreground-color), 1);color:var(--accent-color)}sm-chip .badge{position:relative;display:flex;align-items:center;justify-content:center;height:1.2rem;border-radius:2rem;margin-left:.5rem;font-weight:700;aspect-ratio:1/1;background-color: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}fieldset{display:grid;gap:.5rem;padding:1rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}fieldset legend{font-size:.9rem;font-weight:500}input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.wrap-around{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-content-center{justify-content:center}.justify-items-center{justify-items: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}.flex-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:not([disabled=true]){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)}.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%}.password-field label{display:flex;justify-content:center}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}#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 0 .5rem;align-items:center;grid-template-columns:auto 1fr}.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}#app_body{display:block;height:100%;width:100%}#main_header{grid-area:header;background-color:rgba(var(--foreground-color), 1)}#sub_page_container{grid-area:main;padding:max(1rem,1.5vw)}#loading{position:fixed;display:grid;top:0;left:0;bottom:0;right:0;place-content:center;justify-items:center;background:rgba(var(--foreground-color), 1);z-index:10}#loading h4{margin-top:1.5rem;font-weight:500}#loading sm-spinner{--size: 1.5rem}#sign_in,#sign_up{display:grid;width:100%;height:100%;justify-items:center;align-content:center;padding:1.5rem}#sign_in section,#sign_up section{width:min(26rem,100%)}#sign_in sm-form,#sign_up sm-form{margin:2rem 0}.generated-keys-wrapper{padding:1rem;background-color:rgba(var(--foreground-color), 1);border-radius:.5rem}#flo_id_warning{padding-bottom:1.5rem}#flo_id_warning .icon{height:3rem;width:3rem;padding:.8rem;overflow:visible;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8)}#main_header{display:flex;gap:1rem;padding:1rem max(1rem,1.5vw);width:100%;align-items:center}.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)}#user_popup_button{background-color:rgba(var(--text-color), 0.06);border-radius:2rem;font-size:.9rem;text-overflow:ellipsis;overflow:hidden}.card{padding:1.5rem;border-radius:.5rem;box-shadow:0 0 0 1px rgba(var(--text-color), 0.1);background-color:rgba(var(--foreground-color), 1);transition:box-shadow .2s,background-color .2s}#main_page{display:grid;min-width:0;width:min(72rem,100%);margin:auto;grid-template-columns:minmax(0, 1fr);align-items:flex-start}#balance_list{display:flex;justify-items:flex-start;gap:.5rem}.balance-card{display:flex;flex-direction:column;gap:.5rem;width:100%;padding:.7rem;border-radius:.5rem;background-color:rgba(var(--foreground-color), 1)}.balance-card>span:last-of-type{font-weight:500;font-size:1rem}#apply_loan{width:min(64rem,100%);margin:auto}#policy_list{display:grid;width:100%;gap:.5rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.policy{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:1rem;border-radius:.5rem;background-color:rgba(var(--foreground-color), 1);box-shadow:0 0 0 1px rgba(var(--text-color), 0.1);flex:1 1 12rem}.policy>div h5{font-weight:400;color:rgba(var(--text-color), 0.8)}.policy>div p{font-size:1.1rem;font-weight:500}.policy button{grid-column:1/-1;margin-left:auto}#request_loan_form label{display:flex;align-items:center;gap:.5rem}#request_loan_form label input{height:1.2em;width:1.2em}.loan{display:grid;gap:1.5rem;background-color:rgba(var(--foreground-color), 1);padding:max(1rem,1.5vw);border-radius:.5rem}.loan .status{display:flex;align-items:center;gap:.5rem;font-weight:500;font-size:.9rem;box-shadow:0 .3rem .5rem rgba(0,0,0,.1);padding:.8rem;border-radius:.5rem;margin-right:auto;border:solid thin rgba(var(--text-color), 0.2)}.loan .status .icon{fill:var(--accent-color)}.loan-process{display:grid;gap:1rem;background-color:rgba(var(--foreground-color), 1);padding:max(1rem,1.5vw);border-radius:.5rem;--progress-line-thickness: 0.15rem}.loan-process__type{position:relative;padding-bottom:.5rem;color:rgba(var(--text-color), 0.8);margin-right:auto}.loan-process__type::before{content:"";position:absolute;height:.2rem;width:40%;left:0;bottom:0;transform:translateY(-50%);border-radius:0 .5rem .5rem 0;background-color:var(--accent-color)}.loan-process ul{display:grid}.loan-process ul li{display:grid;gap:1rem;grid-template-columns:auto 1fr}.loan-process ul li:not(:last-of-type) .details{padding-bottom:2rem}.loan-process .progress{display:grid;justify-content:center;justify-items:center;isolation:isolate}.loan-process .progress>*{grid-area:1/1/2/2}.loan-process li.done .circle{border-width:.3rem;border-color:var(--green)}.loan-process li.done .line{background-color:var(--green)}.loan-process .circle{margin-top:.2rem;width:.8rem;height:.8rem;border-radius:50%;background-color:rgba(var(--foreground-color), 1);z-index:1;border:solid var(--progress-line-thickness) rgba(var(--text-color), 0.5)}.loan-process .line{margin-top:.2rem;width:var(--progress-line-thickness);height:100%;background-color:rgba(var(--text-color), 0.5)}.loan-process .details{display:grid;gap:.3rem}.loan-process .details button{margin-top:.5rem}.loan-process time{font-size:.9rem;color:rgba(var(--text-color), 0.8)}#collateral_requests_list{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.collateral-request{display:flex;gap:1.5rem;flex-direction:column;background-color:rgba(var(--foreground-color), 1);padding:1rem}.collateral-request b{color:rgba(var(--text-color), 0.9)}#loan_requests_list{display:grid;gap:1rem;align-items:flex-start;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.loan-request{display:flex;flex-direction:column;justify-items:flex-start;gap:1rem;background-color:rgba(var(--foreground-color), 1);padding:1rem;border-radius:.5rem}.loan-request b{font-weight:500;color:rgba(var(--text-color), 0.9)}.loan-request .button{margin-top:auto}@media screen and (max-width: 40rem){theme-toggle{order:2}#user_popup_button{flex:1;order:1}#balance_list{overflow-x:auto;margin:0 -1rem;padding:0 1rem;padding-bottom:1.5rem;margin-bottom:-1.5rem;scrollbar-width:0}#balance_list::-webkit-scrollbar{display:none}.balance-card{flex:1 1 12rem;min-width:42vw;box-shadow:0 .5rem 1rem rgba(0,0,0,.1)}.balance-card .button{border:none;background-color:rgba(0,0,0,0);padding:.2rem 0}.hide-on-small{display:none !important}}@media screen and (min-width: 40rem){h1{font-size:3vw}sm-popup{--width: 24rem}.popup__header{padding:1.5rem 1.5rem 0 .75rem}#balance_list{flex-wrap:wrap}#request_loan_popup{--width: 32rem}}@media screen and (min-width: 46rem){#main_page{grid-template-columns:18rem 1fr}#main_page>:first-child{position:-webkit-sticky;position:sticky;top:1rem;overflow-y:auto}}@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.5rem)}html,body{height:100%}body{--accent-color: #365eff;--text-color: 30, 30, 30;--background-color: 248, 248, 248;--foreground-color: 255, 255, 255;--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}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}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)}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:.6rem .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{cursor:not-allowed;background-color:rgba(var(--text-color), 0.03);color:rgba(var(--text-color), 0.5)}.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 summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;gap:.5rem;color:var(--accent-color);font-weight:500}details summary .down-arrow{fill: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=true] .badge{background-color:rgba(var(--foreground-color), 1);color:var(--accent-color)}sm-chip .badge{position:relative;display:flex;align-items:center;justify-content:center;height:1.2rem;border-radius:2rem;margin-left:.5rem;font-weight:700;aspect-ratio:1/1;background-color: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}fieldset{display:grid;gap:.5rem;padding:1rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}fieldset legend{font-size:.9rem;font-weight:500}input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.wrap-around{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-content-center{justify-content:center}.justify-items-center{justify-items: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}.flex-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:not([disabled=true]){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)}.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%}.password-field label{display:flex;justify-content:center}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}#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 0 .5rem;align-items:center;grid-template-columns:auto 1fr}.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}#app_body{display:block;height:100%;width:100%}#main_header{grid-area:header;background-color:rgba(var(--foreground-color), 1)}#sub_page_container{grid-area:main;padding:max(1rem,1.5vw)}#loading{position:fixed;display:grid;top:0;left:0;bottom:0;right:0;place-content:center;justify-items:center;background:rgba(var(--foreground-color), 1);z-index:10}#loading h4{margin-top:1.5rem;font-weight:500}#loading sm-spinner{--size: 1.5rem}#sign_in,#sign_up{display:grid;width:100%;height:100%;justify-items:center;align-content:center;padding:1.5rem}#sign_in section,#sign_up section{width:min(26rem,100%)}#sign_in sm-form,#sign_up sm-form{margin:2rem 0}.generated-keys-wrapper{padding:1rem;background-color:rgba(var(--foreground-color), 1);border-radius:.5rem}#flo_id_warning{padding-bottom:1.5rem}#flo_id_warning .icon{height:3rem;width:3rem;padding:.8rem;overflow:visible;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8)}#main_header{display:flex;gap:1rem;padding:1rem max(1rem,1.5vw);width:100%;align-items:center}.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)}#user_popup_button{background-color:rgba(var(--text-color), 0.06);border-radius:2rem;font-size:.9rem;text-overflow:ellipsis;overflow:hidden}.card{padding:1.5rem;border-radius:.5rem;box-shadow:0 0 0 1px rgba(var(--text-color), 0.1);background-color:rgba(var(--foreground-color), 1);transition:box-shadow .2s,background-color .2s}#main_page{display:grid;min-width:0;width:min(72rem,100%);margin:auto;grid-template-columns:minmax(0, 1fr);align-items:flex-start}#balance_list{display:flex;justify-items:flex-start;gap:.5rem}.balance-card{display:flex;flex-direction:column;gap:.5rem;width:100%;padding:.7rem;border-radius:.5rem;background-color:rgba(var(--foreground-color), 1)}.balance-card>span:last-of-type{font-weight:500;font-size:1rem}#apply_loan{width:min(64rem,100%);margin:auto}#policy_list{display:grid;width:100%;gap:.5rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.policy{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:1rem;border-radius:.5rem;background-color:rgba(var(--foreground-color), 1);box-shadow:0 0 0 1px rgba(var(--text-color), 0.1);flex:1 1 12rem}.policy>div h5{font-weight:400;color:rgba(var(--text-color), 0.8)}.policy>div p{font-size:1.1rem;font-weight:500}.policy button{grid-column:1/-1;margin-left:auto}#request_loan_form label{display:flex;align-items:center;gap:.5rem}#request_loan_form label input{height:1.2em;width:1.2em}.loan{display:grid;gap:1.5rem;background-color:rgba(var(--foreground-color), 1);padding:max(1rem,1.5vw);border-radius:.5rem}.loan .status{display:flex;align-items:center;gap:.5rem;font-weight:500;font-size:.9rem;box-shadow:0 .3rem .5rem rgba(0,0,0,.1);padding:.8rem;border-radius:.5rem;margin-right:auto;border:solid thin rgba(var(--text-color), 0.2)}.loan .status .icon{fill:var(--accent-color)}.loan-process{display:grid;gap:1rem;background-color:rgba(var(--foreground-color), 1);padding:max(1rem,1.5vw);border-radius:.5rem;--progress-line-thickness: 0.15rem}.loan-process__type{position:relative;padding-bottom:.5rem;color:rgba(var(--text-color), 0.8);margin-right:auto}.loan-process__type::before{content:"";position:absolute;height:.2rem;width:40%;left:0;bottom:0;transform:translateY(-50%);border-radius:0 .5rem .5rem 0;background-color:var(--accent-color)}.loan-process ul{display:grid}.loan-process ul li{display:grid;gap:1rem;grid-template-columns:auto 1fr}.loan-process ul li:not(:last-of-type) .details{padding-bottom:2rem}.loan-process .progress{display:grid;justify-content:center;justify-items:center;isolation:isolate}.loan-process .progress>*{grid-area:1/1/2/2}.loan-process li.done .circle{border-width:.3rem;border-color:var(--green)}.loan-process li.done .line{background-color:var(--green)}.loan-process .circle{margin-top:.2rem;width:.8rem;height:.8rem;border-radius:50%;background-color:rgba(var(--foreground-color), 1);z-index:1;border:solid var(--progress-line-thickness) rgba(var(--text-color), 0.5)}.loan-process .line{margin-top:.2rem;width:var(--progress-line-thickness);height:100%;background-color:rgba(var(--text-color), 0.5)}.loan-process .details{display:grid;gap:.3rem}.loan-process .details button{margin-top:.5rem}.loan-process time{font-size:.9rem;color:rgba(var(--text-color), 0.8)}#collateral_requests_list{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.collateral-request{display:flex;gap:1.5rem;flex-direction:column;background-color:rgba(var(--foreground-color), 1);padding:1rem}.collateral-request b{color:rgba(var(--text-color), 0.9)}#loan_requests_list{display:grid;gap:1rem;align-items:flex-start;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.loan-request{display:flex;flex-direction:column;justify-items:flex-start;gap:1rem;background-color:rgba(var(--foreground-color), 1);padding:1rem;border-radius:.5rem}.loan-request b{font-weight:500;color:rgba(var(--text-color), 0.9)}.loan-request .button{margin-top:auto}@media screen and (max-width: 40rem){theme-toggle{order:2}#user_popup_button{flex:1;order:1}#balance_list{overflow-x:auto;margin:0 -1rem;padding:0 1rem;padding-bottom:1.5rem;margin-bottom:-1.5rem;scrollbar-width:0}#balance_list::-webkit-scrollbar{display:none}.balance-card{flex:1 1 12rem;min-width:42vw;box-shadow:0 .5rem 1rem rgba(0,0,0,.1)}.balance-card .button{border:none;background-color:rgba(0,0,0,0);padding:.2rem 0}.hide-on-small{display:none !important}}@media screen and (min-width: 40rem){h1{font-size:3vw}sm-popup{--width: 24rem}.popup__header{padding:1.5rem 1.5rem 0 .75rem}#balance_list{flex-wrap:wrap}#request_loan_popup{--width: 32rem}}@media screen and (min-width: 46rem){#main_page{grid-template-columns:18rem 1fr}#main_page>:first-child{position:-webkit-sticky;position:sticky;top:1rem;overflow-y:auto}}@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 e616112..5d6ea76 100644 --- a/css/main.scss +++ b/css/main.scss @@ -48,10 +48,6 @@ strong { max-width: 65ch; line-height: 1.7; color: rgba(var(--text-color), 0.9); - - &:not(:last-of-type) { - margin-bottom: 1.5rem; - } } a { text-decoration: none; diff --git a/index.html b/index.html index 95ae149..ccedb69 100644 --- a/index.html +++ b/index.html @@ -643,7 +643,7 @@ `: html``} ` }, - loanProcess(details = {}) { + loanProcess(processId) { const { type, loanOpeningProcessID, @@ -653,7 +653,7 @@ collateralRequestID, loanResponseID, collateralLockRequestID, collateralLockAckID, loanID, closingTxID, unlockCollateralRequestTime, hasRequestedCollateralUnlock, unlockCollateralAckTime, hasRefundedCollateral, unlockTxHex - } = details + } = uiGlobals.inProcessRequests[processId] if (type === 'loanOpening') { return Component(() => { const [verifyingCollateral, setVerifyCollateral] = useState(false) @@ -1086,6 +1086,8 @@ lender, lender_sign, loan_amount, loan_id, loan_opening_process_id, loan_transfer_id, open_time, close_time, policy_id } = btcMortgage.loans[loanID]; + const { hasRepaidLoan, hasRefundedCollateral, collateralUnlockAckTime, hasRequestedCollateralLiquidation, hasRequestedCollateralPreLiquidation } = uiGlobals.activeLoanQueries[loanID] || {} + const amountDue = btcMortgage.util.calcDueAmount(loan_amount, policy_id, open_time) const [isRepayingLoan, setIsRepayingLoan] = useState(false) async function initLoanRepayment() { @@ -1109,10 +1111,9 @@ const isBorrower = floCrypto.isSameAddr(borrower, floDapps.user.id) const isCoborrower = floCrypto.isSameAddr(coborrower, floDapps.user.id) const isLender = floCrypto.isSameAddr(lender, floDapps.user.id) - let hasStartedRepayment = false let loanStatus = ''; let loanStatusBadge = ''; - if (close_time) { + if (close_time || hasRefundedCollateral) { loanStatus = 'Closed' loanStatusBadge = html`
@@ -1121,12 +1122,7 @@
` } else { - hasStartedRepayment = Object.values({ - ...floGlobals.myInbox, - ...floGlobals.myOutbox - }) - .find(request => request.message.loan_id === loanID) - if (hasStartedRepayment) { + if (hasRepaidLoan) { loanStatus = 'Repaying' loanStatusBadge = html`
@@ -1160,6 +1156,22 @@ setIsClaimingCollateral(false) } } + const hasGoneBelowThreshold = btcMortgage.util.calcRateRatio(floGlobals.btcRate, btcMortgage.policies[policy_id]?.pre_liquidation_threshold) || false; + const [isRequestingCollateralPreLiquidation, setIsRequestingCollateralPreLiquidation] = useState(false) + async function requestCollateralPreLiquidation() { + const confirmation = await getConfirmation('Request collateral pre-liquidation?', { message: `You are about to request for collateral pre-liquidation. Continue?`, confirmText: 'Request', cancelText: 'Cancel' }) + if (!confirmation) + return; + setIsRequestingCollateralPreLiquidation(true) + try { + await btcMortgage.requestBanker.preLiquidateCollateral(loan_id, await floDapps.user.private) + notify('Collateral pre-liquidation request sent successfully', 'success') + } catch (err) { + notify(err, 'error') + } finally { + setIsRequestingCollateralPreLiquidation(false) + } + } return html`
  • ${loanStatusBadge} @@ -1167,40 +1179,44 @@

    Loan ID: ${loan_id}

    -
    -

    Loan amount

    - ${formatAmount(loan_amount, 'usd')} -
    -
    -

    Collateral amount

    - ${formatAmount(collateral_value)} -
    -
    -

    Duration

    - ${btcMortgage.policies[policy_id]?.duration} -
    -
    -

    Interest

    - ${btcMortgage.policies[policy_id]?.interest * 100}% p.a -
    -
    -

    Opening date

    - ${getFormattedTime(open_time)} -
    - ${close_time ? html` +
    -

    Closing date

    - ${getFormattedTime(close_time)} -
    - `: html` -
    -

    Loan deadline

    - ${getFormattedTime(open_time + decodeDateStringToMilliseconds(btcMortgage.policies[policy_id]?.duration))} +

    Loan amount

    + ${formatAmount(loan_amount, 'usd')}
    - `} +
    +

    Collateral amount

    + ${formatAmount(collateral_value)} +
    +
    +

    Duration

    + ${btcMortgage.policies[policy_id]?.duration} +
    +
    +

    Interest

    + ${btcMortgage.policies[policy_id]?.interest * 100}% p.a +
    +
    +
    +
    +

    Opening date

    + ${getFormattedTime(open_time)} +
    + ${loanStatus === 'Closed' ? html` +
    +

    Closing date

    + ${getFormattedTime(close_time || collateralUnlockAckTime)} +
    + `: html` +
    +

    Loan deadline

    + ${getFormattedTime(open_time + decodeDateStringToMilliseconds(btcMortgage.policies[policy_id]?.duration))} +
    + `} +
    -

    Interest ${isLender ? 'earned' : close_time ? 'paid' : 'accrued'}

    +

    Interest ${isLender ? 'earned' : loanStatus === 'Closed' ? 'paid' : 'accrued'}

    ${formatAmount(amountDue - loan_amount, 'usd')}
    @@ -1241,20 +1257,27 @@
  • `: ''} - ${isLender && isLoanOverdue ? html` + ${loanStatus === 'closed' && isLender && isLoanOverdue ? html`
    -
    -

    Loan repayment overdue

    -

    Borrower has not repaid the loan even after the deadline. You can now claim the collateral.

    -
    - + ${hasRequestedCollateralLiquidation ? html` +
    +

    Collateral liquidation requested

    +

    Our banker will resolve your request within 48hrs.

    +
    + `: html` +
    +

    Loan repayment overdue

    +

    Borrower has not repaid the loan even after the deadline. You can now claim the collateral.

    +
    + + `}
    `: ''} @@ -1421,7 +1444,7 @@ } let userAddressTimeInterval; function renderHome(appState) { - const { lastPage, page, params: { } = {}, wildcards: [section = 'my-loans'] = [] } = appState || {}; + const { lastPage, page, params: { } = {}, wildcards = [] } = appState || {}; const balanceCard = Component(() => { const [btcBalance, setBtcBalance] = useState(floGlobals.memoBalance['BTC']) @@ -1505,7 +1528,7 @@ case 'type_liquate_collateral_request': break; - case 'type_preliquate_collateral_request': + case 'type_pre_liquidate_collateral_request': break; default: @@ -1536,18 +1559,14 @@ } else { // user homepage const inbox = Component(() => { - const loanRequestsInProcess = Object.values(floGlobals.inProcessRequests).sort((a, b) => { - const aTime = a.initiationTime || a.loanResponseTime || a.loanClosedAckTime || a.unlockCollateralRequestTime; - const bTime = b.initiationTime || b.loanResponseTime || b.loanClosedAckTime || b.unlockCollateralRequestTime; - return bTime - aTime - }) const { borrowed, coBorrowed, lent } = groupLoans() - const [view, setView] = useState(section) + const defaultView = wildcards.length ? wildcards[0] : uiGlobals.sortedProcesses.length ? 'in-process' : 'my-loans' + const [view, setView] = useState(defaultView) let loanRequests = []; for (const key in floGlobals.loanRequests) { const { message: { borrower, coborrower, loan_opening_process_id } } = floGlobals.loanRequests[key] if (!loan_opening_process_id) continue // TODO: remove this check after all requests are updated - if (floGlobals.loansOpeningInProcess.has(loan_opening_process_id)) continue // if loan request is in process, don't show loan request + if (floGlobals.hasAgreedToLend.has(loan_opening_process_id)) continue // if user has already agreed to lend, don't show the request loanRequests.push(key) } function handleChange(e) { @@ -1566,10 +1585,10 @@ return html`
    - ${loanRequestsInProcess.length ? html` + ${uiGlobals.sortedProcesses.length ? html` In process - ${loanRequestsInProcess.length} + ${uiGlobals.sortedProcesses.length} ` : ''} @@ -1612,9 +1631,9 @@ `} `: ''} - ${view === 'in-process' && loanRequestsInProcess.length ? html` + ${view === 'in-process' && uiGlobals.sortedProcesses.length ? html`
      - ${loanRequestsInProcess.map(loan => render.loanProcess(loan))} + ${uiGlobals.sortedProcesses.map(processId => render.loanProcess(processId))}
    `: ''} ${view === 'coBorrowed' && coBorrowed.length ? html` @@ -1732,10 +1751,8 @@ floGlobals.isSubAdmin = floGlobals.subAdmins.includes(floGlobals.myFloID) floGlobals.isAdmin = floGlobals.myFloID === floGlobals.adminID floGlobals.loaded = false - floGlobals.myInbox = {} - floGlobals.myOutbox = {} floGlobals.loanRequests = {} - floGlobals.requestTypes = {} + uiGlobals.activeLoanQueries = {} floGlobals.btcRate = 1 floGlobals.memoBalance = { 'FLO': 0, @@ -1759,24 +1776,23 @@ callback: (d, e) => { if (e) return for (const key in d) { - if ( - d[key].type === 'type_unlock_collateral_request' || - d[key].type === 'type_unlock_collateral_ack' || - d[key].type === 'type_refund_collateral_request' || - d[key].type === 'type_refund_collateral_ack' || - d[key].type === 'type_liquate_collateral_request' || - d[key].type === 'type_liquate_collateral_ack' || - d[key].type === 'type_preliquate_collateral_request' || - d[key].type === 'type_preliquate_collateral_ack' - ) { - floGlobals.bankerRequests = { - ...floGlobals.bankerRequests || {}, - [key]: d[key] - } + switch (d[key].type) { + case 'type_unlock_collateral_request': + case 'type_unlock_collateral_ack': + case 'type_refund_collateral_request': + case 'type_refund_collateral_ack': + case 'type_liquate_collateral_request': + case 'type_liquate_collateral_ack': + case 'type_pre_liquidate_collateral_request': + case 'type_preLiquidate_collateral_ack': + floGlobals.bankerRequests = { + ...(floGlobals.bankerRequests || {}), + [key]: d[key] + }; + break; } } console.log('LOAN REQUESTS', d) - processInProcessRequests() if (floGlobals.loaded) { router.routeTo(window.location.hash) } @@ -1785,14 +1801,10 @@ } else { const result = await Promise.allSettled([ new Promise((resolve, reject) => { - btcMortgage.viewMyInbox((d, e) => { + btcMortgage.viewMyInbox((requests, e) => { if (e) return - floGlobals.myInbox = { - ...floGlobals.myInbox, - ...d - } - console.log('MY INBOX', d) - processInProcessRequests() + console.log('MY INBOX', requests) + processInProcessRequests(requests, { isInbox: true }) if (floGlobals.loaded) { router.routeTo(window.location.hash) } @@ -1802,14 +1814,15 @@ }) }), new Promise((resolve, reject) => { - btcMortgage.listLoanRequests((d, e) => { + btcMortgage.listLoanRequests((requests, e) => { if (e) return + console.log('LOAN REQUESTS', requests) floGlobals.loanRequests = { ...floGlobals.loanRequests, - ...d + ...requests } - console.log('LOAN REQUESTS', d) - processInProcessRequests() + // if loan requests are from me, process them + processInProcessRequests(requests, { areLoanRequests: true }) if (floGlobals.loaded) { router.routeTo(window.location.hash) } @@ -1820,11 +1833,10 @@ }), new Promise((resolve, reject) => { floCloudAPI.requestApplicationData('in_process_loan_request', { - callback: (d, e) => { + callback: (requests, e) => { if (e) return - parseInProcessRequests(d) - console.log('IN PROCESS LOAN REQUEST', d) - processInProcessRequests() + parseInProcessRequests(requests) + console.log('IN PROCESS LOAN REQUEST', requests) if (floGlobals.loaded) { router.routeTo(window.location.hash) } @@ -1835,19 +1847,15 @@ ]) } - await Promise.all([...getAllInvolvedAddresses()].map(address => { + await Promise.all([...uiGlobals.relatedAddresses].map(address => { return new Promise((resolve, reject) => { floCloudAPI.requestApplicationData(null, { senderID: floDapps.user.id, receiverID: address, - callback: (d, e) => { + callback: (requests, e) => { if (e) return - floGlobals.myOutbox = { - ...floGlobals.myOutbox || {}, - ...d - } - console.log('MY OUTBOX', d) - processInProcessRequests() + console.log('MY OUTBOX', requests) + processInProcessRequests(requests) if (floGlobals.loaded) { router.routeTo(window.location.hash) } @@ -1857,7 +1865,6 @@ }) })) console.log(result) - processInProcessRequests() if (['#/landing', '#/sign_in', '#/sign_up'].includes(window.location.hash)) { history.replaceState(null, null, '#/home') router.routeTo('home') @@ -1880,31 +1887,20 @@ }) }).catch(error => console.error(error)) } + /** + * @param {Object} requests + */ function parseInProcessRequests(requests) { - if (!floGlobals.loansOpeningInProcess) - floGlobals.loansOpeningInProcess = new Set() + if (!floGlobals.hasAgreedToLend) + floGlobals.hasAgreedToLend = new Set() for (const key in requests) { const { message: { loan_opening_process_id, closing_txid } } = requests[key] - floGlobals.loansOpeningInProcess.add(loan_opening_process_id) + floGlobals.hasAgreedToLend.add(loan_opening_process_id) } } - function getAllInvolvedAddresses() { // get all addresses involved in loan process (except my address) - const addresses = new Set() - for (const key in floGlobals.myInbox) { - const { message: { borrower, coborrower, lender } } = floGlobals.myInbox[key] - if (borrower) - addresses.add(borrower) - if (coborrower) - addresses.add(coborrower) - if (lender) - addresses.add(lender) - } - if (addresses.has(floGlobals.myFloID)) - addresses.delete(floGlobals.myFloID) - if (addresses.has(floGlobals.myBtcID)) - addresses.delete(floGlobals.myBtcID) - return addresses - } + /** + * @param {string} inputString + */ function decodeDateStringToMilliseconds(inputString) { // Define a mapping for units to milliseconds (Y: years, M: months, D: days) const unitToMilliseconds = { @@ -1931,22 +1927,18 @@ return null; } - const processedRequests = new Set() - floGlobals.inProcessRequests = {} - function processInProcessRequests() { - let myLoanRequests = {} - for (const key in floGlobals.loanRequests) { - const { message: { borrower } } = floGlobals.loanRequests[key] - if (floCrypto.isSameAddr(borrower, floGlobals.myFloID)) { - myLoanRequests[key] = floGlobals.loanRequests[key] - } - } - const allMessages = { - ...floGlobals.myInbox, - ...myLoanRequests, - ...floGlobals.myOutbox - } - for (const key in allMessages) { + const completedLoanProcess = new Set() + uiGlobals.inProcessRequests = {} + uiGlobals.sortedProcesses = [] + uiGlobals.relatedAddresses = new Set() + /** + * @param {Object} newRequests + * @param {Object} options + * @param {Boolean} options.isInbox + */ + function processInProcessRequests(newRequests = {}, options = {}) { + const { isInbox = false, areLoanRequests = false } = options + for (const key in newRequests) { const { message: { borrower, coborrower, lender, @@ -1954,16 +1946,17 @@ loan_id, closing_txid, unlock_tx_hex, unlock_collateral_id, type_unlock_collateral_ack } = {}, type, time - } = allMessages[key]; - if (processedRequests.has(loan_opening_process_id) || Object.values(btcMortgage.loans).find(loan => loan.loan_opening_process_id === loan_opening_process_id)) { - processedRequests.add(loan_opening_process_id) - continue // Request has been processed - } + } = newRequests[key]; + if (borrower && areLoanRequests && !floCrypto.isSameAddr(borrower, floDapps.user.id)) continue // if loan request is not from me, ignore if (loan_id && btcMortgage.loans[loan_id] && btcMortgage.loans[loan_id].close_time) continue // Loan closed if (loan_opening_process_id) { - if (!floGlobals.inProcessRequests[loan_opening_process_id]) { - floGlobals.inProcessRequests[loan_opening_process_id] = { + if (completedLoanProcess.has(loan_opening_process_id) || Object.values(btcMortgage.loans).find(loan => loan.loan_opening_process_id === loan_opening_process_id)) { + completedLoanProcess.add(loan_opening_process_id) + continue // Request has been processed + } + if (!uiGlobals.inProcessRequests[loan_opening_process_id]) { + uiGlobals.inProcessRequests[loan_opening_process_id] = { loanOpeningProcessID: loan_opening_process_id, hasProvidedCollateral: false, hasAgreedToLend: false, @@ -1972,113 +1965,154 @@ } } if (borrower) { - floGlobals.inProcessRequests[loan_opening_process_id].borrower = borrower - floGlobals.inProcessRequests[loan_opening_process_id].isBorrower = floCrypto.isSameAddr(borrower, floDapps.user.id) + uiGlobals.inProcessRequests[loan_opening_process_id].borrower = borrower + uiGlobals.inProcessRequests[loan_opening_process_id].isBorrower = floCrypto.isSameAddr(borrower, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(borrower) } if (coborrower) { - floGlobals.inProcessRequests[loan_opening_process_id].coborrower = coborrower - floGlobals.inProcessRequests[loan_opening_process_id].isCoborrower = floCrypto.isSameAddr(coborrower, floDapps.user.id) - } - if (loan_amount) { - floGlobals.inProcessRequests[loan_opening_process_id].loanAmount = loan_amount - } - if (policy_id) { - floGlobals.inProcessRequests[loan_opening_process_id].policyID = policy_id + uiGlobals.inProcessRequests[loan_opening_process_id].coborrower = coborrower + uiGlobals.inProcessRequests[loan_opening_process_id].isCoborrower = floCrypto.isSameAddr(coborrower, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(coborrower) } if (lender) { - floGlobals.inProcessRequests[loan_opening_process_id].lender = lender - floGlobals.inProcessRequests[loan_opening_process_id].isLender = floCrypto.isSameAddr(lender, floDapps.user.id) + uiGlobals.inProcessRequests[loan_opening_process_id].lender = lender + uiGlobals.inProcessRequests[loan_opening_process_id].isLender = floCrypto.isSameAddr(lender, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(lender) + } + if (loan_amount) { + uiGlobals.inProcessRequests[loan_opening_process_id].loanAmount = loan_amount + } + if (policy_id) { + uiGlobals.inProcessRequests[loan_opening_process_id].policyID = policy_id } if (loan_request_id) { - floGlobals.inProcessRequests[loan_opening_process_id].loanRequestID = loan_request_id - floGlobals.inProcessRequests[loan_opening_process_id].hasProvidedCollateral = true + uiGlobals.inProcessRequests[loan_opening_process_id].loanRequestID = loan_request_id + uiGlobals.inProcessRequests[loan_opening_process_id].hasProvidedCollateral = true } if (collateral_lock_id) { - floGlobals.inProcessRequests[loan_opening_process_id].collateralLockID = collateral_lock_id + uiGlobals.inProcessRequests[loan_opening_process_id].collateralLockID = collateral_lock_id } switch (type) { case 'type_loan_collateral_request': - floGlobals.inProcessRequests[loan_opening_process_id].initiationTime = time - floGlobals.inProcessRequests[loan_opening_process_id].collateralRequestID = key + uiGlobals.inProcessRequests[loan_opening_process_id].initiationTime = time + uiGlobals.inProcessRequests[loan_opening_process_id].collateralRequestID = key break; case 'type_loan_request': - floGlobals.inProcessRequests[loan_opening_process_id].hasProvidedCollateral = true - floGlobals.inProcessRequests[loan_opening_process_id].loanRequestID = key + uiGlobals.inProcessRequests[loan_opening_process_id].hasProvidedCollateral = true + uiGlobals.inProcessRequests[loan_opening_process_id].loanRequestID = key break; case 'type_loan_response': - floGlobals.inProcessRequests[loan_opening_process_id].loanResponseTime = time - floGlobals.inProcessRequests[loan_opening_process_id].hasAgreedToLend = true - floGlobals.inProcessRequests[loan_opening_process_id].loanResponseID = key + uiGlobals.inProcessRequests[loan_opening_process_id].loanResponseTime = time + uiGlobals.inProcessRequests[loan_opening_process_id].hasAgreedToLend = true + uiGlobals.inProcessRequests[loan_opening_process_id].loanResponseID = key break; case "type_collateral_lock_request": - floGlobals.inProcessRequests[loan_opening_process_id].collateralLockRequestID = key + uiGlobals.inProcessRequests[loan_opening_process_id].collateralLockRequestID = key break; case 'type_collateral_lock_ack': - floGlobals.inProcessRequests[loan_opening_process_id].collateralLockAckTime = time - floGlobals.inProcessRequests[loan_opening_process_id].hasLockedCollateral = true - floGlobals.inProcessRequests[loan_opening_process_id].collateralLockAckID = key + uiGlobals.inProcessRequests[loan_opening_process_id].collateralLockAckTime = time + uiGlobals.inProcessRequests[loan_opening_process_id].hasLockedCollateral = true + uiGlobals.inProcessRequests[loan_opening_process_id].collateralLockAckID = key break; case 'type_refund_collateral_request': - floGlobals.inProcessRequests[loan_opening_process_id].hasRequestedCollateralRefund = true + uiGlobals.inProcessRequests[loan_opening_process_id].hasRequestedCollateralRefund = true break; } - } else if (closing_txid) { - if (!floGlobals.inProcessRequests[closing_txid]) - floGlobals.inProcessRequests[closing_txid] = { - loanID: loan_id, - closingTxID: closing_txid, - hasRequestedCollateralRefund: false, - hasRefundedCollateral: false, - hasPaidLoan: false, - type: 'loanClosing' + } else { + if (loan_id) { + if (!uiGlobals.activeLoanQueries[loan_id]) + uiGlobals.activeLoanQueries[loan_id] = { + loanID: loan_id, + } + switch (type) { + case 'type_loan_closed_ack': + uiGlobals.activeLoanQueries[loan_id].hasRepaidLoan = true + break; + case 'type_unlock_collateral_request': + uiGlobals.activeLoanQueries[loan_id].hasRequestedCollateralUnlock = true + break; + case 'type_unlock_collateral_ack': + uiGlobals.activeLoanQueries[loan_id].hasRefundedCollateral = true + uiGlobals.activeLoanQueries[loan_id].collateralUnlockAckTime = time + delete uiGlobals.inProcessRequests[closing_txid] + completedLoanProcess.add(closing_txid) + break; + case 'type_liquate_collateral_request': + uiGlobals.activeLoanQueries[loan_id].hasRequestedCollateralLiquidation = true + break; + case 'type_liquate_collateral_ack': + uiGlobals.activeLoanQueries[loan_id].hasLiquidatedCollateral = true + break; + case 'type_pre_liquidate_collateral_request': + uiGlobals.activeLoanQueries[loan_id].hasRequestedCollateralPreLiquidation = true + break; } - const { borrower, coborrower, lender } = btcMortgage.loans[loan_id] - if (borrower) { - floGlobals.inProcessRequests[closing_txid].borrower = borrower - floGlobals.inProcessRequests[closing_txid].isBorrower = floCrypto.isSameAddr(borrower, floDapps.user.id) } - if (coborrower) { - floGlobals.inProcessRequests[closing_txid].coborrower = coborrower - floGlobals.inProcessRequests[closing_txid].isCoborrower = floCrypto.isSameAddr(coborrower, floDapps.user.id) - } - if (lender) { - floGlobals.inProcessRequests[closing_txid].lender = lender - floGlobals.inProcessRequests[closing_txid].isLender = floCrypto.isSameAddr(lender, floDapps.user.id) - } - if (unlock_tx_hex) { - floGlobals.inProcessRequests[closing_txid].unlockTxHex = unlock_tx_hex - } - switch (type) { - case 'type_loan_closed_ack': - floGlobals.inProcessRequests[closing_txid].loanClosedAckTime = time - floGlobals.inProcessRequests[closing_txid].hasPaidLoan = true - break; - case 'type_unlock_collateral_request': - floGlobals.inProcessRequests[closing_txid].unlockCollateralRequestTime = time - floGlobals.inProcessRequests[closing_txid].hasRequestedCollateralUnlock = true - break; - case 'type_unlock_collateral_ack': - floGlobals.inProcessRequests[closing_txid].unlockCollateralAckTime = time - floGlobals.inProcessRequests[closing_txid].hasRefundedCollateral = true - break; + if (closing_txid && !completedLoanProcess.has(closing_txid)) { + if (!uiGlobals.inProcessRequests[closing_txid]) + uiGlobals.inProcessRequests[closing_txid] = { + loanID: loan_id, + closingTxID: closing_txid, + hasRequestedCollateralRefund: false, + hasRefundedCollateral: false, + hasPaidLoan: false, + type: 'loanClosing' + } + const { borrower, coborrower, lender } = btcMortgage.loans[loan_id] + if (borrower) { + uiGlobals.inProcessRequests[closing_txid].borrower = borrower + uiGlobals.inProcessRequests[closing_txid].isBorrower = floCrypto.isSameAddr(borrower, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(borrower) + } + if (coborrower) { + uiGlobals.inProcessRequests[closing_txid].coborrower = coborrower + uiGlobals.inProcessRequests[closing_txid].isCoborrower = floCrypto.isSameAddr(coborrower, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(coborrower) + } + if (lender) { + uiGlobals.inProcessRequests[closing_txid].lender = lender + uiGlobals.inProcessRequests[closing_txid].isLender = floCrypto.isSameAddr(lender, floDapps.user.id) + if (isInbox) + uiGlobals.relatedAddresses.add(lender) + } + if (unlock_tx_hex) { + uiGlobals.inProcessRequests[closing_txid].unlockTxHex = unlock_tx_hex + } + switch (type) { + case 'type_loan_closed_ack': + uiGlobals.inProcessRequests[closing_txid].loanClosedAckTime = time + uiGlobals.inProcessRequests[closing_txid].hasPaidLoan = true + break; + case 'type_unlock_collateral_request': + uiGlobals.inProcessRequests[closing_txid].unlockCollateralRequestTime = time + uiGlobals.inProcessRequests[closing_txid].hasRequestedCollateralUnlock = true + break; + case 'type_unlock_collateral_ack': + uiGlobals.inProcessRequests[closing_txid].unlockCollateralAckTime = time + uiGlobals.inProcessRequests[closing_txid].hasRefundedCollateral = true + completedLoanProcess.add(closing_txid) + break; + } } } } - // .reduce((acc, loan) => { // group by borrower, coborrower and lender - // if (loan.isBorrower) - // acc.borrowing.push(loan) - // else if (loan.isCoborrower) - // acc.coBorrowing.push(loan) - // else if (loan.isLender) - // acc.lending.push(loan) - // return acc - // }, { - // borrowing: [], - // coBorrowing: [], - // lending: [] - // }) + if (uiGlobals.relatedAddresses.has(floGlobals.myBtcID)) + uiGlobals.relatedAddresses.delete(floGlobals.myBtcID) + if (uiGlobals.relatedAddresses.has(floGlobals.myFloID)) + uiGlobals.relatedAddresses.delete(floGlobals.myFloID) + uiGlobals.sortedProcesses = Object.keys(uiGlobals.inProcessRequests).sort((a, b) => { + const aTime = uiGlobals.inProcessRequests[a].initiationTime || uiGlobals.inProcessRequests[a].loanResponseTime || uiGlobals.inProcessRequests[a].loanClosedAckTime || uiGlobals.inProcessRequests[a].unlockCollateralRequestTime; + const bTime = uiGlobals.inProcessRequests[b].initiationTime || uiGlobals.inProcessRequests[b].loanResponseTime || uiGlobals.inProcessRequests[b].loanClosedAckTime || uiGlobals.inProcessRequests[b].unlockCollateralRequestTime; + return bTime - aTime + }) } + function groupLoans() { return Object.values(btcMortgage.loans) .sort((a, b) => b.open_time - a.open_time) diff --git a/scripts/btcMortgage.js b/scripts/btcMortgage.js index 5d842be..ed0274b 100644 --- a/scripts/btcMortgage.js +++ b/scripts/btcMortgage.js @@ -30,8 +30,7 @@ TYPE_REFUND_COLLATERAL_ACK = "type_refund_collateral_ack", TYPE_LIQUATE_COLLATERAL_REQUEST = "type_liquate_collateral_request", TYPE_LIQUATE_COLLATERAL_ACK = "type_liquate_collateral_ack", - TYPE_PRELIQUATE_COLLATERAL_REQUEST = "type_preliquate_collateral_request", - TYPE_PRELIQUATE_COLLATERAL_ACK = "type_preliquate_collateral_ack"; + TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST = "type_pre_liquidate_collateral_request" const POLICIES = {}, LOANS = {}; const owned_collateral_locks = {}; @@ -124,7 +123,7 @@ const util = btcMortgage.util = { toFixedDecimal, encodePeriod, decodePeriod, - calcAllowedLoan, calcRequiredCollateral, calcDueAmount, + calcAllowedLoan, calcRequiredCollateral, calcDueAmount, calcRateRatio, findLocker, extractPubKeyFromSign } @@ -1495,7 +1494,7 @@ resolve(result); }).catch(error => reject(error)) }).catch(error => { - compactIDB.writeData("fail_safe", loan_blockchain_data, token_txid); //fail-safe mech if token is transfered but details not added to blockchain. this helps to retry fail-safe + compactIDB.writeData("fail_safe", loan_blockchain_data, token_txid); //fail-safe mech if token is transferred but details not added to blockchain. this helps to retry fail-safe reject({ error, fail_safe: token_txid }) }) }).catch(error => reject(error)) @@ -1516,7 +1515,7 @@ if (!floCrypto.isSameAddr(loan_details.lender, collateral_liquate_req.senderID)) return reject(RequestValidationError(TYPE_LIQUATE_COLLATERAL_REQUEST, "request not sent by lender")); if (!verify_liquidationSign(liquidation_sign, loan_details.lender, loan_id, loan_details.lender_sign, btc_liquid_rate)) - return reject("Invalid liquiadtion signature"); + return reject("Invalid liquidation signature"); checkIfLoanClosedFailed(loan, loan_details.borrower, loan_details.lender).then(result => { if (result) //close/fail loan data found return reject(RequestValidationError(TYPE_LIQUATE_COLLATERAL_REQUEST, "Loan already closed")); @@ -1527,8 +1526,8 @@ }) } - // L: request T (banker) to preliquidate collateral when collateral value has dropped to risk threshold - btcMortgage.requestBanker.preliquateCollateral = function (loan_id, privKey) { + // L: request T (banker) to pre-liquidate collateral when collateral value has dropped to risk threshold + btcMortgage.requestBanker.preLiquidateCollateral = function (loan_id, privKey) { return new Promise((resolve, reject) => { let lender_pubKey = floDapps.user.public; getLoanDetails(loan_id).then(loan_details => { @@ -1552,7 +1551,7 @@ floCloudAPI.sendApplicationData({ loan_id, liquidation_sign, liquidate_tx_hex: txHex - }, TYPE_PRELIQUATE_COLLATERAL_REQUEST) + }, TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST) .then(result => { compactIDB.addData("outbox", result, result.vectorClock); resolve(result); @@ -1564,9 +1563,9 @@ }) } - btcMortgage.banker.preliquateCollateral = function (collateral_preliquate_req_id, privKey) { + btcMortgage.banker.preLiquidateCollateral = function (collateral_pre_liquidate_req_id, privKey) { return new Promise((resolve, reject) => { - validate_preliquateCollateral_request(collateral_preliquate_req_id).then(result => { + validate_pre_liquidateCollateral_request(collateral_pre_liquidate_req_id).then(result => { let { loan_details, liquidate_tx_hex, btc_liquid_rate, liquidation_sign } = result; //calculate due amount let due_amount = calcDueAmount(loan_details.loan_amount, loan_details.policy_id, loan_details.open_time) @@ -1591,7 +1590,7 @@ resolve(result); }).catch(error => reject(error)) }).catch(error => { - compactIDB.writeData("fail_safe", loan_blockchain_data, token_txid); //fail-safe mech if token is transfered but details not added to blockchain. this helps to retry fail-safe + compactIDB.writeData("fail_safe", loan_blockchain_data, token_txid); //fail-safe mech if token is transferred but details not added to blockchain. this helps to retry fail-safe reject({ error, fail_safe: token_txid }) }) }).catch(error => reject(error)) @@ -1601,30 +1600,30 @@ }) } - function validate_preliquateCollateral_request(collateral_preliquate_req_id) { + function validate_pre_liquidateCollateral_request(collateral_pre_liquidate_req_id) { return new Promise((resolve, reject) => { - floCloudAPI.requestApplicationData(TYPE_PRELIQUATE_COLLATERAL_REQUEST, { atVectorClock: collateral_preliquate_req_id }).then(collateral_preliquate_req => { - collateral_preliquate_req = collateral_preliquate_req[collateral_preliquate_req_id]; - if (!collateral_preliquate_req) - return reject(RequestValidationError(TYPE_PRELIQUATE_COLLATERAL_REQUEST, "request not found")); - let { loan_id, liquidation_sign, btc_liquid_rate, liquidate_tx_hex } = collateral_preliquate_req.message; + floCloudAPI.requestApplicationData(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, { atVectorClock: collateral_pre_liquidate_req_id }).then(collateral_pre_liquidate_req => { + collateral_pre_liquidate_req = collateral_pre_liquidate_req[collateral_pre_liquidate_req_id]; + if (!collateral_pre_liquidate_req) + return reject(RequestValidationError(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, "request not found")); + let { loan_id, liquidation_sign, btc_liquid_rate, liquidate_tx_hex } = collateral_pre_liquidate_req.message; getLoanDetails(loan_id).then(loan_details => { - if (!floCrypto.isSameAddr(loan_details.lender, collateral_preliquate_req.senderID)) - return reject(RequestValidationError(TYPE_PRELIQUATE_COLLATERAL_REQUEST, "request not sent by lender")); + if (!floCrypto.isSameAddr(loan_details.lender, collateral_pre_liquidate_req.senderID)) + return reject(RequestValidationError(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, "request not sent by lender")); if (!verify_liquidationSign(liquidation_sign, loan_details.lender, loan_id, loan_details.lender_sign, btc_liquid_rate)) - return reject("Invalid liquiadtion signature"); + return reject("Invalid liquidation signature"); checkIfLoanClosedFailed(loan, loan_details.borrower, loan_details.lender).then(result => { if (result) //close/fail loan data found - return reject(RequestValidationError(TYPE_PRELIQUATE_COLLATERAL_REQUEST, "Loan already closed")); + return reject(RequestValidationError(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, "Loan already closed")); let policy = POLICIES[loan_details.policy_id]; if (isNaN(policy.pre_liquidation_threshold)) return reject("This loan policy doesn't allow pre-liquidation"); getRate["USD"].then(cur_btc_rate => { if (cur_btc_rate >= loan_details.btc_start_rate) - return reject(RequestValidationError(TYPE_PRELIQUATE_COLLATERAL_REQUEST, "BTC rate hasn't reduced from the start rate")); + return reject(RequestValidationError(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, "BTC rate hasn't reduced from the start rate")); let current_rate_ratio = calcRateRatio(cur_btc_rate, loan_details.btc_start_rate) if (current_rate_ratio > policy.pre_liquidation_threshold) - return reject(RequestValidationError(TYPE_PRELIQUATE_COLLATERAL_REQUEST, "BTC rate hasn't dropped beyond threshold")); + return reject(RequestValidationError(TYPE_PRE_LIQUIDATE_COLLATERAL_REQUEST, "BTC rate hasn't dropped beyond threshold")); resolve({ loan_details, liquidate_tx_hex, btc_liquid_rate, liquidation_sign }); }).catch(error => reject(error)) }).catch(error => reject(error))