diff --git a/css/main.css b/css/main.css index 78f0ce0..835c7cf 100644 --- a/css/main.css +++ b/css/main.css @@ -128,9 +128,9 @@ button:not(:disabled), } button:disabled { - opacity: 0.4; cursor: not-allowed; - filter: saturate(0); + background-color: rgba(var(--text-color), 0.03); + color: rgba(var(--text-color), 0.5); } .cta { @@ -699,9 +699,29 @@ h3 { #main_page { display: grid; min-width: 0; - width: min(64rem, 100%); + width: min(72rem, 100%); margin: auto; grid-template-columns: minmax(0, 1fr); + align-items: flex-start; +} + +#balance_list { + display: flex; + flex-direction: column; + justify-items: flex-start; + gap: 0.5rem; +} + +.balance-card { + display: flex; + justify-content: space-between; + padding: 0.7rem; + border-radius: 0.5rem; + background-color: rgba(var(--foreground-color), 1); +} +.balance-card span:last-of-type { + font-weight: 700; + font-size: 1rem; } #apply_loan { @@ -749,6 +769,61 @@ h3 { width: 1.2em; } +.loan-process { + background-color: rgba(var(--foreground-color), 1); + padding: max(1rem, 1.5vw); + border-radius: 0.5rem; +} +.loan-process ul { + display: grid; +} +.loan-process ul li { + display: grid; + gap: 1rem; + grid-template-columns: auto 1fr; +} +.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 { + background-color: var(--green); + border: solid 0.1rem var(--green); +} +.loan-process li.done .line { + background-color: var(--green); +} +.loan-process .circle { + width: 1rem; + height: 1rem; + border-radius: 50%; + background-color: rgba(var(--foreground-color), 1); + z-index: 1; + border: solid 0.1rem rgba(var(--text-color), 0.5); +} +.loan-process .line { + width: 0.1rem; + height: 100%; + background-color: rgba(var(--text-color), 0.5); +} +.loan-process .details { + display: grid; + gap: 0.3rem; + padding-bottom: 2rem; +} +.loan-process .details button { + margin-top: 0.5rem; +} +.loan-process time { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); +} + #collateral_requests_list { display: grid; gap: 1rem; @@ -805,6 +880,15 @@ h3 { .popup__header { padding: 1.5rem 1.5rem 0 0.75rem; } + #main_page { + grid-template-columns: 18rem 1fr; + } + #main_page > :first-child { + position: -webkit-sticky; + position: sticky; + top: 1rem; + overflow-y: auto; + } #request_loan_popup { --width: 32rem; } diff --git a/css/main.min.css b/css/main.min.css index fe483f5..2a07d90 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{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=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(64rem,100%);margin:auto;grid-template-columns:minmax(0, 1fr)}#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}#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;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.loan-request{display:grid;gap:1.5rem;background-color:rgba(var(--foreground-color), 1);padding:1rem}.loan-request b{font-weight:500;color:rgba(var(--text-color), 0.9)}@media screen and (max-width: 40rem){theme-toggle{order:2}#user_popup_button{flex:1;order:1}.hide-on-small{display:none !important}}@media screen and (min-width: 64rem){h1{font-size:3vw}sm-popup{--width: 24rem}.popup__header{padding:1.5rem 1.5rem 0 .75rem}#request_loan_popup{--width: 32rem}}@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)}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{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=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;flex-direction:column;justify-items:flex-start;gap:.5rem}.balance-card{display:flex;justify-content:space-between;padding:.7rem;border-radius:.5rem;background-color:rgba(var(--foreground-color), 1)}.balance-card span:last-of-type{font-weight:700;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-process{background-color:rgba(var(--foreground-color), 1);padding:max(1rem,1.5vw);border-radius:.5rem}.loan-process ul{display:grid}.loan-process ul li{display:grid;gap:1rem;grid-template-columns:auto 1fr}.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{background-color:var(--green);border:solid .1rem var(--green)}.loan-process li.done .line{background-color:var(--green)}.loan-process .circle{width:1rem;height:1rem;border-radius:50%;background-color:rgba(var(--foreground-color), 1);z-index:1;border:solid .1rem rgba(var(--text-color), 0.5)}.loan-process .line{width:.1rem;height:100%;background-color:rgba(var(--text-color), 0.5)}.loan-process .details{display:grid;gap:.3rem;padding-bottom:2rem}.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;grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}.loan-request{display:grid;gap:1.5rem;background-color:rgba(var(--foreground-color), 1);padding:1rem}.loan-request b{font-weight:500;color:rgba(var(--text-color), 0.9)}@media screen and (max-width: 40rem){theme-toggle{order:2}#user_popup_button{flex:1;order:1}.hide-on-small{display:none !important}}@media screen and (min-width: 64rem){h1{font-size:3vw}sm-popup{--width: 24rem}.popup__header{padding:1.5rem 1.5rem 0 .75rem}#main_page{grid-template-columns:18rem 1fr}#main_page>:first-child{position:-webkit-sticky;position:sticky;top:1rem;overflow-y:auto}#request_loan_popup{--width: 32rem}}@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 56534b9..96994b8 100644 --- a/css/main.scss +++ b/css/main.scss @@ -124,9 +124,9 @@ button, } } button:disabled { - opacity: 0.4; cursor: not-allowed; - filter: saturate(0); + background-color: rgba(var(--text-color), 0.03); + color: rgba(var(--text-color), 0.5); } .cta { @@ -658,9 +658,27 @@ h3 { #main_page { display: grid; min-width: 0; - width: min(64rem, 100%); + width: min(72rem, 100%); margin: auto; grid-template-columns: minmax(0, 1fr); + align-items: flex-start; +} +#balance_list { + display: flex; + flex-direction: column; + justify-items: flex-start; + gap: 0.5rem; +} +.balance-card { + display: flex; + justify-content: space-between; + padding: 0.7rem; + border-radius: 0.5rem; + background-color: rgba(var(--foreground-color), 1); + span:last-of-type { + font-weight: 700; + font-size: 1rem; + } } #apply_loan { width: min(64rem, 100%); @@ -707,6 +725,62 @@ h3 { } } } +.loan-process { + background-color: rgba(var(--foreground-color), 1); + padding: max(1rem, 1.5vw); + border-radius: 0.5rem; + ul { + display: grid; + li { + display: grid; + gap: 1rem; + grid-template-columns: auto 1fr; + } + } + .progress { + display: grid; + justify-content: center; + justify-items: center; + isolation: isolate; + & > * { + grid-area: 1/1/2/2; + } + } + li.done { + .circle { + background-color: var(--green); + border: solid 0.1rem var(--green); + } + .line { + background-color: var(--green); + } + } + .circle { + width: 1rem; + height: 1rem; + border-radius: 50%; + background-color: rgba(var(--foreground-color), 1); + z-index: 1; + border: solid 0.1rem rgba(var(--text-color), 0.5); + } + .line { + width: 0.1rem; + height: 100%; + background-color: rgba(var(--text-color), 0.5); + } + .details { + display: grid; + gap: 0.3rem; + padding-bottom: 2rem; + button { + margin-top: 0.5rem; + } + } + time { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); + } +} #collateral_requests_list { display: grid; gap: 1rem; @@ -760,6 +834,14 @@ h3 { .popup__header { padding: 1.5rem 1.5rem 0 0.75rem; } + #main_page { + grid-template-columns: 18rem 1fr; + & > :first-child { + position: sticky; + top: 1rem; + overflow-y: auto; + } + } #request_loan_popup { --width: 32rem; } diff --git a/index.html b/index.html index 15527e4..77e2ca9 100644 --- a/index.html +++ b/index.html @@ -404,7 +404,7 @@ } if (!amount) return '0'; - return amount.toLocaleString(currency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency, maximumFractionDigits: currency === 'btc' ? 8 : 2 }) + return amount.toLocaleString(currency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency, maximumFractionDigits: currency === 'usd' ? 2 : 8 }) } const render = { policy(id, details = {}) { @@ -538,7 +538,7 @@ const { message: { borrower, coborrower, loan_amount, policy_id }, vectorClock } = details; const collateralAmount = btcMortgage.util.toFixedDecimal(btcMortgage.policies[policy_id].loan_collateral_ratio * (loan_amount / floGlobals.btcRate)) async function approveCollateralRequest(e) { - const confirmation = await getConfirmation('Send collateral?', { message: `You are about to send ${formatAmount(collateralAmount)} collateral to the borrower. Continue?`, confirmText: 'Send', cancelText: 'Cancel', danger: true }) + const confirmation = await getConfirmation('Send collateral?', { message: `You are about to send ${formatAmount(collateralAmount)} collateral to the borrower. Continue?`, confirmText: 'Send', cancelText: 'Cancel' }) if (!confirmation) return; e.target.disabled = true; @@ -557,7 +557,7 @@ } } async function rejectCollateralRequest(e) { - const confirmation = await getConfirmation('Reject collateral request?', { message: `You are about to reject the collateral request. Continue?`, confirmText: 'Reject', cancelText: 'Cancel', danger: true }) + const confirmation = await getConfirmation('Reject collateral request?', { message: `You are about to reject the collateral request. Continue?`, confirmText: 'Reject', cancelText: 'Cancel' }) if (!confirmation) return; e.target.disabled = true; @@ -606,7 +606,7 @@ const { message: { borrower, coborrower, collateral: { btc_id, quantity, rate }, loan_amount, loan_collateral_req_id, policy_id }, vectorClock } = details; const { duration, interest } = btcMortgage.policies[policy_id]; async function acceptLoanProposal() { - const confirmation = await getConfirmation('Accept loan proposal?', { message: `You are about to accept the loan proposal. Continue?`, confirmText: 'Accept', cancelText: 'Cancel', danger: true }) + const confirmation = await getConfirmation('Accept loan proposal?', { message: `You are about to accept the loan proposal. Continue?`, confirmText: 'Accept', cancelText: 'Cancel' }) if (!confirmation) return; btcMortgage.respondLoan(loan_collateral_req_id, borrower, coborrower).then(() => { @@ -654,10 +654,122 @@
- +
` - } + }, + loanProcess(details = {}) { + const { + time, + hasProvidedCollateral, hasAgreedToLend, hasLockedCollateral, hasIssuedLoan, + collateralRequestID, loanRequestID, loanResponseID, collateralLockAckID + } = details + const { message: { borrower, coborrower, loan_amount, policy_id } } = floGlobals.myInbox[collateralRequestID] + return Component(() => { + const [sendingCollateral, setSendingCollateral] = useState(false) + const collateralAmount = btcMortgage.util.toFixedDecimal(btcMortgage.policies[policy_id].loan_collateral_ratio * (loan_amount / floGlobals.btcRate)) + async function approveCollateralRequest(e) { + const confirmation = await getConfirmation('Send collateral?', { message: `You are about to send ${formatAmount(collateralAmount)} as collateral. Continue?`, confirmText: 'Send', cancelText: 'Cancel' }) + if (!confirmation) + return; + setSendingCollateral(true) + try { + await btcMortgage.requestLoan(collateralRequestID, borrower) + await floCloudAPI.noteApplicationData(collateralRequestID, 'approved') + floGlobals.myInbox[collateralRequestID].note = 'approved'; + notify('Collateral sent successfully', 'success') + } catch (err) { + notify(err, 'error') + setSendingCollateral(false) + } + } + + return html` +
  • + +
  • + ` + })() + }, } // routing logic const router = new Router({ @@ -819,7 +931,7 @@ } let userAddressTimeInterval; function renderHome(appState) { - const { lastPage, page, params: { filter = 'pending', search } = {}, wildcards: [view = 'my-loans'] = [] } = appState || {}; + const { lastPage, page, params: { filter = 'pending', search } = {}, wildcards: [section = 'my-loans'] = [] } = appState || {}; if (floGlobals.isSubAdmin) { renderElem(getRef('sub_page_container'), html`
    @@ -835,28 +947,24 @@ } else { // user homepage const inbox = Component(() => { + const myLoanRequests = groupLoanProcess() + const [view, setView] = useState(section) let loanRequests = []; - let collateralRequests = []; - const loans = Object.keys(btcMortgage.loans) - for (const key in floGlobals.myInbox) { - if (!floGlobals.myInbox[key].note) - collateralRequests.push(key) - } for (const key in floGlobals.loanRequests) { const { message: { borrower, coborrower } } = floGlobals.loanRequests[key] if (!floCrypto.isSameAddr(borrower, floGlobals.myFloID) && !floCrypto.isSameAddr(coborrower, floGlobals.myFloID)) loanRequests.push(key) } + function handleChange(e) { + history.pushState(null, null, `#/home/${e.target.value}`) + setView(e.target.value) + } return html`
    - { location.hash = `#/home/${e.target.value}` }}> + My Loans - ${loans.length ? html`${loans.length}` : ''} - - - Collateral requests - ${collateralRequests.length ? html`${collateralRequests.length}` : ''} + ${myLoanRequests.length ? html`${myLoanRequests.length}` : ''} Loan requests @@ -865,9 +973,9 @@
    ${view === 'my-loans' ? html` - ${loans.length ? html` + ${myLoanRequests.length ? html`
      - + ${myLoanRequests.map(loan => render.loanProcess(loan))}
    `: html`
    @@ -879,15 +987,6 @@
    `} `: ''} - ${view === 'collateral-requests' ? html` - ${collateralRequests.length ? html` -
      - ${collateralRequests.map(requestId => render.collateralRequest(requestId, floGlobals.myInbox[requestId]))} -
    - `: html` -

    No collateral requests

    - `} - `: ''} ${view === 'loan-requests' ? html` ${loanRequests.length ? html`
      @@ -908,8 +1007,69 @@
    ` }) + const balanceCard = Component(() => { + const [btcBalance, setBtcBalance] = useState(0) + const [floBalance, setFloBalance] = useState(0) + const [usdBalance, setUsdBalance] = useState(0) + const [isRefreshing, setIsRefreshing] = useState(false) + function refreshBalance() { + setIsRefreshing(true) + fetchBalance().then(() => { + setIsRefreshing(false) + }).catch(error => { + setIsRefreshing(false) + console.error(error) + }) + } + function fetchBalance() { + return Promise.allSettled([ + btcOperator.getBalance(floGlobals.myBtcID), + floBlockchainAPI.getBalance(floGlobals.myFloID), + floTokenAPI.getBalance(floGlobals.myFloID, 'usd') + ]).then(result => { + const [btcBalance, floBalance, usdBalance] = result.map(({ value }) => value) + setBtcBalance(btcBalance) + setFloBalance(floBalance) + setUsdBalance(usdBalance) + }).catch(error => { + console.error(error) + }) + } + useEffect(() => { + fetchBalance() + }, []) + return html` +
    +
    +

    My balances

    + +
    + +
    + ` + }) renderElem(getRef('sub_page_container'), html`
    + ${balanceCard()} ${inbox()}
    `); @@ -918,6 +1078,58 @@ router.addRoute('', renderHome) router.addRoute('home', renderHome) + function groupLoanProcess() { + 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 + } + const loansInProcess = {} + for (const key in allMessages) { + const { message: { loan_amount, policy_id, loan_opening_process_id } = {}, type, time } = allMessages[key] + if (loan_opening_process_id) { + if (!loansInProcess[loan_opening_process_id]) { + loansInProcess[loan_opening_process_id] = { + loanOpeningProcessID: loan_opening_process_id, + hasProvidedCollateral: false, + hasAgreedToLend: false, + hasLockedCollateral: false, + } + } + switch (type) { + case 'type_loan_collateral_request': + loansInProcess[loan_opening_process_id].time = time + loansInProcess[loan_opening_process_id].collateralRequestID = key + break; + case 'type_loan_request': + loansInProcess.hasProvidedCollateral = true + loansInProcess[loan_opening_process_id].loanRequestID = key + break; + case 'type_loan_response': + loansInProcess.hasAgreedToLend = true + loansInProcess[loan_opening_process_id].loanResponseID = key + break; + case 'type_collateral_lock_ack': + loansInProcess.hasLockedCollateral = true + loansInProcess[loan_opening_process_id].collateralLockAckID = key + break; + } + } + } + // sort by time + const sortedLoansInProcess = Object.values(loansInProcess).sort((a, b) => { + return b.time - a.time + }) + return sortedLoansInProcess + } + router.addRoute('apply-loan', async () => { const loanApplication = Component(() => { return html` @@ -990,7 +1202,9 @@ floGlobals.isSubAdmin = floGlobals.subAdmins.includes(floGlobals.myFloID) floGlobals.isAdmin = floGlobals.myFloID === floGlobals.adminID floGlobals.myInbox = {} + floGlobals.myOutbox = {} floGlobals.loanRequests = {} + floGlobals.requestTypes = {} floGlobals.btcRate = 1 function fetchBtcRate() { btcMortgage.getRate['BTC']().then(rate => { @@ -1011,6 +1225,7 @@ ...d } console.log('INBOX', d) + router.routeTo(location.hash) resolve() }).catch(error => { reject(error) @@ -1023,10 +1238,28 @@ ...d } console.log('LOAN REQUESTS', d) + router.routeTo(location.hash) resolve() }).catch(error => { reject(error) }) + }), + new Promise((resolve, reject) => { + btcMortgage.viewMyOutbox(d => { + floGlobals.myOutbox = { + ...floGlobals.myOutbox, + ...d + } + console.log('OUTBOX', d) + router.routeTo(location.hash) + resolve() + }).then(d => { + floGlobals.myOutbox = d; + console.log('OUTBOX', d) + }) + .catch(error => { + reject(error) + }) }) ]).then(result => { console.log(result) diff --git a/scripts/btcMortgage.js b/scripts/btcMortgage.js index 2b53dd4..91d6a39 100644 --- a/scripts/btcMortgage.js +++ b/scripts/btcMortgage.js @@ -415,7 +415,7 @@ function stringifyLoanOpenData( borrower, loan_amount, policy_id, btc_start_rate, coborrower, collateral_value, collateral_lock_id, - lender, loan_transfer_id, lender_sign + lender, loan_transfer_id, lender_sign, loan_opening_process_id ) { return [ LOAN_DETAILS_IDENTIFIER, @@ -428,7 +428,8 @@ "BTC rate:" + btc_start_rate + "USD", "Lender:" + floCrypto.toFloID(lender), "TokenTransfer:" + loan_transfer_id, - "Signature-L:" + lender_sign + "Signature-L:" + lender_sign, + "LoanOpeningProcessID:" + loan_opening_process_id ].join('|'); /*MAYDO: Maybe make it a worded sentence? BTC Mortgage: @@ -457,6 +458,7 @@ case "Lender": details.lender = d[1]; break; case "TokenTransfer": details.loan_transfer_id = d[1]; break; case "Signature-L": details.lender_sign = d[1]; break; + case "LoanOpeningProcessID": details.loan_opening_process_id = d[1]; break; } }); return details; @@ -900,7 +902,6 @@ .catch(error => reject(error)) }) } - //view responses btcMortgage.viewMyInbox = function (callback = undefined) { return new Promise((resolve, reject) => { @@ -913,6 +914,27 @@ }) } + btcMortgage.viewMyOutbox = (callback = undefined) => { //view all inbox + return new Promise((resolve, reject) => { + let options = { senderID: floDapps.user.id } + if (callback instanceof Function) + options.callback = callback; + floCloudAPI.requestApplicationData(null, options) + .then(_ => { + compactIDB.readAllData("outbox") + .then(result => { + for (const key in result) { + result[key].message = floCloudAPI.util.decodeMessage(result[key].message); + } + resolve(result); + }) + .catch(error => reject(error)) + }) + .catch(error => { + console.log(error); + }) + }) + } /*Loan Opening*/ @@ -931,7 +953,8 @@ //request collateral from coborrower floCloudAPI.sendApplicationData({ borrower, coborrower, - loan_amount, policy_id + loan_amount, policy_id, + loan_opening_process_id: floCrypto.randString(12, true) }, TYPE_LOAN_COLLATERAL_REQUEST, { receiverID: coborrower }) .then(result => { compactIDB.addData("outbox", result, result.vectorClock); @@ -943,20 +966,18 @@ function validate_loanCollateral_request(loan_collateral_req_id, borrower, coborrower) { return new Promise((resolve, reject) => { floCloudAPI.requestApplicationData(TYPE_LOAN_COLLATERAL_REQUEST, { atVectorClock: loan_collateral_req_id, receiverID: coborrower }).then(loan_collateral_req => { - loan_collateral_req = loan_collateral_req[loan_collateral_req_id]; - if (!loan_collateral_req) + const { senderID, receiverID, message: { loan_amount, policy_id, loan_opening_process_id }, pubkey } = loan_collateral_req[loan_collateral_req_id]; + if (!loan_collateral_req[loan_collateral_req_id]) return reject(RequestValidationError(TYPE_LOAN_REQUEST, "request not found")); - if (!floCrypto.isSameAddr(loan_collateral_req.senderID, borrower)) + if (!floCrypto.isSameAddr(senderID, borrower)) return reject(RequestValidationError(TYPE_LOAN_COLLATERAL_REQUEST, "sender is not borrower")); - if (!floCrypto.isSameAddr(loan_collateral_req.receiverID, coborrower)) + if (!floCrypto.isSameAddr(receiverID, coborrower)) return reject(RequestValidationError(TYPE_LOAN_COLLATERAL_REQUEST, "receiver is not coborrower")); - let { loan_amount, policy_id } = loan_collateral_req.message; if (typeof loan_amount !== 'number' || loan_amount <= 0 || !VALUE_REGEX.test(loan_amount)) return reject(RequestValidationError(TYPE_LOAN_COLLATERAL_REQUEST, "Invalid loan amount")); if (!(policy_id in POLICIES)) return reject(RequestValidationError(TYPE_LOAN_COLLATERAL_REQUEST, "Invalid policy")); - let result = { loan_amount, policy_id, borrower, coborrower }; - result.borrower_pubKey = loan_collateral_req.pubKey; + let result = { loan_amount, policy_id, borrower, coborrower, loan_opening_process_id, borrower_pubKey: pubkey }; resolve(result); }).catch(error => reject(error)) }) @@ -967,7 +988,7 @@ return new Promise((resolve, reject) => { const coborrower = floDapps.user.id; //validate request - validate_loanCollateral_request(loan_collateral_req_id, borrower, coborrower).then(({ loan_amount, policy_id }) => { + validate_loanCollateral_request(loan_collateral_req_id, borrower, coborrower).then(({ loan_amount, policy_id, loan_opening_process_id }) => { //calculate required collateral getRate["BTC"]().then(rate => { let policy = POLICIES[policy_id]; @@ -981,7 +1002,7 @@ //post request floCloudAPI.sendApplicationData({ borrower, coborrower, - loan_amount, policy_id, loan_collateral_req_id, + loan_amount, policy_id, loan_collateral_req_id, loan_opening_process_id, collateral: { rate, btc_id: coborrower_btcID, @@ -1001,12 +1022,11 @@ function validate_loan_request(loan_req_id, borrower, coborrower) { return new Promise((resolve, reject) => { floCloudAPI.requestApplicationData(TYPE_LOAN_REQUEST, { atVectorClock: loan_req_id }).then(loan_req => { - loan_req = loan_req[loan_req_id]; - if (!loan_req) + const { senderID, message: { loan_collateral_req_id, loan_amount, policy_id, collateral }, pubKey } = loan_req[loan_req_id]; + if (!loan_req[loan_req_id]) return reject(RequestValidationError(TYPE_LOAN_REQUEST, "request not found")); - if (!floCrypto.isSameAddr(coborrower, loan_req.senderID)) + if (!floCrypto.isSameAddr(coborrower, senderID)) return reject(RequestValidationError(TYPE_LOAN_REQUEST, "request not posted by coborrower")) - let { loan_collateral_req_id, loan_amount, policy_id, collateral } = loan_req.message; if (!floCrypto.isSameAddr(collateral.btc_id, coborrower)) return reject(RequestValidationError(TYPE_LOAN_REQUEST, "collateral btc id is not coborrower")); validate_loanCollateral_request(loan_collateral_req_id, borrower, coborrower).then(result => { @@ -1022,7 +1042,7 @@ if (required_collateral > collateral.quantity) return reject(RequestValidationError(TYPE_LOAN_REQUEST, "Insufficient collateral value")); result.collateral = collateral; - result.coborrower_pubKey = loan_req.pubKey; + result.coborrower_pubKey = pubKey; resolve(result) }).catch(error => reject(error)) }).catch(error => reject(error)) @@ -1035,7 +1055,7 @@ btcMortgage.respondLoan = function (loan_req_id, borrower, coborrower) { return new Promise((resolve, reject) => { const lender = floDapps.user.id; - validate_loan_request(loan_req_id, borrower, coborrower).then(({ loan_amount, borrower, collateral }) => { + validate_loan_request(loan_req_id, borrower, coborrower).then(({ loan_amount, borrower, collateral, loan_opening_process_id }) => { //check if collateral is available btcOperator.getBalance(collateral.btc_id).then(coborrower_balance => { if (coborrower_balance < collateral.quantity) @@ -1047,7 +1067,8 @@ return reject("Insufficient tokens to lend"); floCloudAPI.sendApplicationData({ lender, borrower, coborrower, - loan_req_id + loan_req_id, + loan_opening_process_id }, TYPE_LENDER_RESPONSE, { receiverID: borrower }) .then(result => { compactIDB.addData("outbox", result, result.vectorClock); @@ -1062,12 +1083,11 @@ function validate_lender_response(lender_res_id, borrower, coborrower, lender) { return new Promise((resolve, reject) => { floCloudAPI.requestApplicationData(TYPE_LENDER_RESPONSE, { atVectorClock: lender_res_id, receiverID: borrower }).then(lender_res => { - lender_res = lender_res[lender_res_id]; - if (!lender_res) + const { senderID, message: { loan_req_id }, pubKey } = lender_res[lender_res_id]; + if (!lender_res[lender_res_id]) return reject(RequestValidationError(TYPE_LENDER_RESPONSE, "request not found")); - if (!floCrypto.isSameAddr(lender, lender_res.senderID)) + if (!floCrypto.isSameAddr(lender, senderID)) return reject(RequestValidationError(TYPE_LENDER_RESPONSE, "request not sent by lender")) - let { loan_req_id } = lender_res.message; validate_loan_request(loan_req_id, borrower, coborrower).then(result => { let { loan_amount } = result; //check if loan amount (token) is available to lend @@ -1076,7 +1096,7 @@ if (lender_tokenBalance < loan_amount) return reject(RequestValidationError(TYPE_LENDER_RESPONSE, "lender doesnot have sufficient funds to lend")); result.lender = lender; - result.lender_pubKey = lender_res.pubKey; + result.lender_pubKey = pubKey; resolve(result); }).catch(error => reject(error)) }).catch(error => reject(error)) @@ -1088,12 +1108,13 @@ btcMortgage.requestCollateralLock = function (lender_res_id, coborrower, lender, privKey) { return new Promise((resolve, reject) => { const borrower = floDapps.user.id; - validate_lender_response(lender_res_id, borrower, coborrower, lender).then(({ loan_amount, policy_id }) => { + validate_lender_response(lender_res_id, borrower, coborrower, lender).then(({ loan_amount, policy_id, loan_opening_process_id }) => { //send request to coborrower for locking the collateral asset let borrower_sign = sign_borrower(privKey, loan_amount, policy_id, coborrower, lender); floCloudAPI.sendApplicationData({ lender, borrower, coborrower, - lender_res_id, borrower_sign + lender_res_id, borrower_sign, + loan_opening_process_id }, TYPE_COLLATERAL_LOCK_REQUEST, { receiverID: coborrower }) .then(result => { compactIDB.addData("outbox", result, result.vectorClock); @@ -1129,7 +1150,7 @@ btcMortgage.lockCollateral = function (collateral_lock_req_id, borrower, lender, privKey) { return new Promise((resolve, reject) => { const coborrower = floDapps.user.id; - validate_collateralLock_request(collateral_lock_req_id, borrower, coborrower, lender).then(({ borrower_sign, collateral, lender_pubKey }) => { + validate_collateralLock_request(collateral_lock_req_id, borrower, coborrower, lender).then(({ borrower_sign, collateral, lender_pubKey, loan_opening_process_id }) => { //lock collateral lockCollateralInBlockchain(privKey, lender_pubKey, collateral.quantity).then(collateral_txid => { //sign and request lender to finalize @@ -1137,7 +1158,8 @@ floCloudAPI.sendApplicationData({ borrower, coborrower, lender, collateral_lock_id: collateral_txid, - coborrower_sign, collateral_lock_req_id + coborrower_sign, collateral_lock_req_id, + loan_opening_process_id }, TYPE_COLLATERAL_LOCK_ACK, { receiverID: lender }) .then(result => { compactIDB.addData("outbox", result, result.vectorClock); @@ -1193,7 +1215,7 @@ return new Promise((resolve, reject) => { const lender = floDapps.user.id; validate_collateralLock_ack(collateral_lock_ack_id, borrower, coborrower, lender).then(result => { - let { loan_amount, policy_id, collateral, collateral_lock_id, borrower_sign, coborrower_sign } = result; + let { loan_amount, policy_id, collateral, collateral_lock_id, borrower_sign, coborrower_sign, loan_opening_process_id } = result; //check if collateral is already used on a different loan for (let l in LOANS) if (LOANS[l].collateral_lock_id === collateral_lock_id) @@ -1212,14 +1234,14 @@ let loan_blockchain_data = stringifyLoanOpenData( borrower, loan_amount, policy_id, collateral.rate, coborrower, collateral.quantity, collateral_lock_id, - lender, token_txid, lender_sign + lender, token_txid, lender_sign, loan_opening_process_id ); let receivers = [borrower, coborrower].map(addr => floCrypto.toFloID(addr)); //write loan details in blockchain floBlockchainAPI.writeDataMultiple([privKey], loan_blockchain_data, receivers) .then(loan_txid => resolve(loan_txid)) .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)) @@ -1382,7 +1404,7 @@ btcMortgage.banker = {}; btcMortgage.requestBanker = {}; - // C: request T (banker) for collateral refund when lender hasnt dispersed the loan for 24 hrs + // C: request T (banker) for collateral refund when lender hasn't dispersed the loan for 24 hrs btcMortgage.requestBanker.refundCollateral = function (collateral_lock_ack_id, borrower, lender, privKey) { return new Promise((resolve, reject) => { const coborrower = floDapps.user.id;