Feature update

-- added loan process ID for easier grouping and UI utility

-added BTC, FLO, USD balance checking
This commit is contained in:
sairaj mote 2023-09-03 05:19:00 +05:30
parent 160ad50784
commit 86bfffc049
5 changed files with 491 additions and 70 deletions

View File

@ -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;
}

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -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;
}

View File

@ -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 @@
</div>
</div>
<div class="flex gap-0-5 align-center margin-left-auto">
<button class="button button--primary" onclick=${acceptLoanProposal}>Accept loan proposal</button>
<button class="button button--primary" onclick=${acceptLoanProposal}>Start lending</button>
</div>
`
}
},
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`
<li class="loan-process">
<ul>
<li class="done">
<div class="progress">
<div class="circle"></div>
<div class="line"></div>
</div>
<div class="details">
<h4>Initiated loan request</h4>
<time>${getFormattedTime(time)}</time>
</div>
</li>
<li class=${`${hasProvidedCollateral ? 'done' : ''}`}>
<div class="progress">
<div class="circle"></div>
<div class="line"></div>
</div>
<div class="details">
${hasProvidedCollateral ? html`
<h4>Collateral provided</h4>
${hasAgreedToLend ? html`
<time>Collateral time</time>
`: html`
<p>Collateral has been sent by the collateral provider. Waiting for borrower to accept the loan proposal.</p>
`}
`: html`
<h4>Waiting for collateral</h4>
<p>Collateral amount is <b>${formatAmount(collateralAmount)}</b></p>
${floCrypto.isSameAddr(coborrower, floDapps.user.id) ? html`
<button class="button button--primary margin-right-auto" disabled=${sendingCollateral} onclick=${approveCollateralRequest}>
${sendingCollateral ? html`
Sending collateral
<sm-spinner class="margin-left-0-5"></sm-spinner>
`: html`
Send collateral
`}
</button>
`: html`
<p>Waiting for collateral to be sent by <strong class="wrap-around">${coborrower}</strong>.</p>
`}
`}
</div>
</li>
<li class=${`${hasAgreedToLend ? 'done' : ''}`}>
<div class="progress">
<div class="circle"></div>
<div class="line"></div>
</div>
<div class="details">
${hasAgreedToLend ? html`
<h4>Lender accepted loan proposal</h4>
${hasLockedCollateral ? html`
<time>Loan time</time>
`: html`
<p>Waiting for collateral to be locked.</p>
`}
`: html`
<h4>Waiting for a lender</h4>
<p>Waiting for a lender to accept the loan request.</p>
`}
</div>
</li>
<li class=${`${hasIssuedLoan ? 'done' : ''}`}>
<div class="progress">
<div class="circle"></div>
</div>
<div class="details">
${hasLockedCollateral ? html`
<h4>Collateral locked</h4>
${hasIssuedLoan ? html`
<time>Loan time</time>
`: html`
<p>Collateral has been locked. Waiting for lender to issue the loan.</p>
`}
`: html`
<h4>Waiting for collateral to be locked</h4>
<p>Waiting for collateral to be locked.</p>
`}
</div>
</li>
</ul>
</li>
`
})()
},
}
// 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`
<section>
@ -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`
<section class="grid gap-1">
<sm-chips onchange=${(e) => { location.hash = `#/home/${e.target.value}` }}>
<sm-chips onchange=${handleChange}>
<sm-chip value="my-loans" selected=${view === 'my-loans'}>
My Loans
${loans.length ? html`<span class="badge">${loans.length}</span>` : ''}
</sm-chip>
<sm-chip value="collateral-requests" selected=${view === 'collateral-requests'}>
Collateral requests
${collateralRequests.length ? html`<span class="badge">${collateralRequests.length}</span>` : ''}
${myLoanRequests.length ? html`<span class="badge">${myLoanRequests.length}</span>` : ''}
</sm-chip>
<sm-chip value="loan-requests" selected=${view === 'loan-requests'}>
Loan requests
@ -865,9 +973,9 @@
</sm-chips>
<div class="grid gap-1">
${view === 'my-loans' ? html`
${loans.length ? html`
${myLoanRequests.length ? html`
<ul class="grid gap-1">
${myLoanRequests.map(loan => render.loanProcess(loan))}
</ul>
`: html`
<div class="grid gap-2 align-center justify-center" style="padding: 2vw 0;">
@ -879,15 +987,6 @@
</div>
`}
`: ''}
${view === 'collateral-requests' ? html`
${collateralRequests.length ? html`
<ul id="collateral_requests_list">
${collateralRequests.map(requestId => render.collateralRequest(requestId, floGlobals.myInbox[requestId]))}
</ul>
`: html`
<p>No collateral requests</p>
`}
`: ''}
${view === 'loan-requests' ? html`
${loanRequests.length ? html`
<ul id="loan_requests_list">
@ -908,8 +1007,69 @@
</section>
`
})
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`
<div class="grid gap-1-5">
<div class="flex align-center space-between gap-1">
<h4>My balances</h4>
<button class="button button--colored button--small" disabled=${isRefreshing} onclick=${refreshBalance}>
${isRefreshing ? html`
Refreshing <sm-spinner class="margin-left-0-5"></sm-spinner>
`: html`
Refresh
`}
</button>
</div>
<ul id="balance_list">
<li class="balance-card">
<span>BTC</span>
<span>${formatAmount(btcBalance)}</span>
</li>
<li class="balance-card">
<span>FLO</span>
<span>${formatAmount(floBalance, 'FLO')}</span>
</li>
<li class="balance-card">
<span>USD</span>
<span>${formatAmount(usdBalance, 'usd')}</span>
</li>
</ul>
</div>
`
})
renderElem(getRef('sub_page_container'), html`
<div id="main_page" class="grid gap-2">
${balanceCard()}
${inbox()}
</div>
`);
@ -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)

View File

@ -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;