bobsfund/index.html
2022-11-02 03:18:54 +05:30

1332 lines
73 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bob's Fund on Blockchain</title>
<link rel="stylesheet" href="css/main.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Overpass:wght@700&family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
rel="stylesheet">
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = {
blockchain: "FLO",
adminID: "FT9qkvuWXWBDRhHd42tDr5nMYFSx7bEhV7",
application: "BobsFund"
}
</script>
<script src="scripts/lib.js"></script>
<script src="scripts/floCrypto.js"></script>
<script src="scripts/floBlockchainAPI.js"></script>
<script src="scripts/floExchangeAPI.js"></script>
<script src="scripts/compactIDB.js"></script>
<script src="scripts/bobs-fund.js"></script>
<script src="scripts/components.min.js"></script>
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
<script>
function onLoadStartUp() {
floExchangeAPI.init().then(result => {
compactIDB.initDB(floGlobals.application, {
funds: {},
appendix: {}
}).then(result => refresh())
.catch(error => console.error(error))
}).catch(error => reject(error))
}
</script>
</head>
<body onload="onLoadStartUp()" data-theme="light">
<sm-notifications id="notification_drawer"></sm-notifications>
<article id="loading_page" class="page">
<h3 class="h3 overpass">Bob's Fund</h3>
<svg id="loader" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<polygon points="17.76 23.78 17.76 40.22 32 48.44 46.24 40.22 46.24 23.78 32 15.56 17.76 23.78" />
<polyline points="17.76 23.78 32 32 46.24 23.78" />
<line x1="32" y1="48.44" x2="32" y2="32" />
<polyline points="32 2 6.02 17 6.02 47" />
<polyline points="57.98 47 57.98 17 32 2" />
<polyline points="6.02 47 32 62 57.98 47" />
</svg>
<h4>Loading data from blockchain</h4>
<footer class="page__footer flex align-items-center">
<svg id="rm_logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z" />
</svg>
<h4 class="h4 color-0-8 weight-500">RanchiMall</h4>
</footer>
</article>
<article id="error_page" class="page hidden">
<h3 class="h3 overpass">Bob's Fund</h3>
<svg class="sad-face" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<g class="face">
<g class="eyes">
<circle cx="187.52" cy="249.17" r="13.42" />
<circle cx="312.48" cy="249.17" r="13.42" />
</g>
<path
d="M288,349.35a6.07,6.07,0,0,1-3.81-1.34c-.09-.07-12.31-9.4-34.14-9.4S216,347.94,215.83,348a6.1,6.1,0,0,1-7.59-9.55c.61-.5,15.4-12.08,41.76-12.08s41.15,11.58,41.76,12.08A6.1,6.1,0,0,1,288,349.35Z" />
<path
d="M344.21,237.41a52,52,0,0,1-23.45-6c-15.91-8.09-21.28-19.61-21.5-20.1a4.07,4.07,0,0,1,7.41-3.36c0,.08,4.56,9.49,17.77,16.21S347.93,229,348,229a4.07,4.07,0,0,1,1.63,8A29.26,29.26,0,0,1,344.21,237.41Z" />
<path
d="M155.79,237.41a29.26,29.26,0,0,1-5.45-.43,4.07,4.07,0,0,1,1.63-8c.2,0,10.43,1.87,23.59-4.81s17.73-16.13,17.78-16.23a4.07,4.07,0,0,1,7.4,3.38c-.22.49-5.59,12-21.5,20.1A52,52,0,0,1,155.79,237.41Z" />
</g>
<path
d="M450.16,220.16A200.16,200.16,0,0,0,108.46,78.63,200.69,200.69,0,0,0,57.61,275.68V407.4a10.17,10.17,0,1,0,20.33,0V322.6a201.91,201.91,0,0,0,99.41,84.19v52.27l-11.45.62a10.17,10.17,0,0,0,.54,20.32H167l30.69-1.66V413.45a202.3,202.3,0,0,0,104.62,0v64.88L333,480h.56a10.17,10.17,0,0,0,.54-20.32l-11.45-.62V406.79a201.91,201.91,0,0,0,99.41-84.19v84.8a10.17,10.17,0,1,0,20.33,0V275.68A200.81,200.81,0,0,0,450.16,220.16ZM250,400c-99.16,0-179.83-80.67-179.83-179.83S150.84,40.34,250,40.34,429.83,121,429.83,220.16,349.16,400,250,400Z" />
</svg>
<h4 class="margin-bottom-0-5r">Oops, Couldn't get data from blockchain</h4>
<footer class="page__footer flex align-items-center">
<svg id="rm_logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z" />
</svg>
<h4 class="h4 color-0-8 weight-500">RanchiMall</h4>
</footer>
</article>
<header id="main_header" class="hidden">
<div class="flex align-items-center">
<svg id="main_header__logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z" />
</svg>
<div class="grid">
<h5 class="header__company-name">RanchiMall</h5>
</div>
</div>
<div id="current_price" class="grid gap-1 flow-column align-items-center">
<span id="btc-usd-rate" class="weight-700"></span>
<span id="usd-rate" class="weight-700"></span>
</div>
<div class="dropdown">
<button id="profile_button" onclick="changeDropdownState('profile_dropdown', 'toggle', this)">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<g>
<path d="M0,0h24v24H0V0z" fill="none" />
<path
d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
</g>
</svg>
</button>
<ul id="profile_dropdown" class="dropdown__panel hidden">
<li class="grid gap-0-5">
<h4 class="weight-700 margin-bottom-0-5r">Preferred currency</h4>
<p>This will convert all amounts to preferred currency.</p>
<sm-select id="currency_selector" class="justify-self-start">
<sm-option value="inr" selected>INR</sm-option>
<sm-option value="usd">USD</sm-option>
</sm-select>
</li>
<li class="flex align-items-center space-between">
<div class="flex weight-700">Dark theme</div>
<theme-toggle></theme-toggle>
</li>
<li class="interact weight-700" onclick="showPage('admin_page')">
Admin panel
</li>
</ul>
</div>
</header>
<main id="home_page" class="page hidden">
<section id="homepage__hero-section">
<h3 class="h3 overpass margin-bottom-1r">Bob's Fund<br>on FLO Blockchain</h3>
<p>
Bob's Fund is a 20 year long term Bitcoin price linked product. Investors are entitled to 100%
of Bitcoin price gains, but they must hold for 20 years. Over a very long time period, investor returns
on an asset like Bitcoin should outstrip returns on conventional assets like real estate and stocks. The
management fees on this product is zero. RanchiMall earns by having invested an equal amount as
every investor, thus the interests of fund manager, and fund investors are totally aligned.
</p>
</section>
<header class="fund-list__header">
<h4 class="h4">Funds</h4>
<button id="refresh_button" class="button justify-right">Refresh</button>
</header>
<sm-input id="search_investor" type="search" placeholder="Search investor with FLO address">
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</sm-input>
<!-- <div class="warning-container grid gap-1">
<strong style="color: #fb3640">Do not enter private key in similar looking pages.</strong>
</div> -->
<section>
<ul id="fund_list"></ul>
<div id="fund_list__empty-state" class="grid hidden">
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-4-6h8v2H8v-2zm0-3a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm8 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" />
</svg>
<h4>No Funds found</h4>
</div>
</main>
<article id="admin_page" class="page page-layout hidden">
<header class="flex margin-top-1-5 align-items-center margin-bottom-1-5r">
<button onclick="showPage('home_page')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z" />
</svg>
</button>
<h3>Admin Panel</h3>
</header>
<form id="create_fund_form" class="grid gap-1-5">
<sm-switch id="fund_creation_toggle">
<div class="flex" slot="left">
Add investors to existing fund
</div>
</sm-switch>
<section id="fund_details_form" class="grid gap-1-5 margin-bottom-0-5r">
<label class="grid gap-0-5">
Fund start date
<input type="date" name="start_date" required>
</label>
<label class="grid gap-0-5">
Base BTC value ($)
<input type="number" name="btc_base" step="0.01" pattern="[\d,]+(.\d+)?" required>
</label>
<label class="grid gap-0-5">
Base USD rate (₹)
<input type="number" name="usd_rate" min=0 step="0.01" required>
</label>
<div class="grid gap-0-5">
Maximum Duration
<div class="flex">
<input type="number" name="max_pv" inputmode="numeric" required>
<sm-select id="max_pt" align-select="right">
<sm-option value="year(s)" selected>year(s)</sm-option>
<sm-option value="month(s)">month(s)</sm-option>
<sm-option value="week(s)">week(s)</sm-option>
<sm-option value="day(s)">day(s)</sm-option>
</sm-select>
</div>
</div>
<sm-switch id="tapout_toggle">
<div class="flex" slot="left">
Tapout
</div>
</sm-switch>
<section id="tapout_container" class="grid gap-1-5 hidden">
<div class="grid gap-0-5">
Tapout window
<div class="flex">
<input type="number" name="tap_wv" inputmode="numeric" disabled required>
<sm-select id="tap_wt" align-select="right">
<sm-option value="year(s)">year(s)</sm-option>
<sm-option value="month(s)" selected>month(s)</sm-option>
<sm-option value="week(s)">week(s)</sm-option>
<sm-option value="day(s)">day(s)</sm-option>
</sm-select>
</div>
</div>
<div class="grid gap-0-5">
Tapout interval(Use comma separated values. e.g. 10, 15...)
<div class="flex">
<input type="text" name="tap_iv" disabled required>
<sm-select id="tap_it" align-select="right">
<sm-option value="year(s)" selected>year(s)</sm-option>
<sm-option value="month(s)">month(s)</sm-option>
</sm-select>
</div>
</div>
</section>
<label class="grid gap-0-5">
Fee
<input type="number" name="fee" min=0 max=100 step="0.01" value="0" inputmode="numeric" required>
</label>
</section>
<div id="fund_selector_container" class="grid margin-bottom-0-5r hidden">
<span class="margin-bottom-0-5r">Select Fund</span>
<sm-select id="fund_selector"></sm-select>
</div>
<h4>Add Investors</h4>
<ul id="investors_input_list" class="grid gap-1">
<li class="investor-input grid">
<sm-input placeholder="FLO address" class="outlined" animate></sm-input>
<sm-input placeholder="Amount(₹)" type="number" min=0 class="outlined" animate></sm-input>
<button class="remove-investor" title="Remove this investor">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>
</button>
</li>
</ul>
<button type="button" class="margin-bottom-1r justify-self-start" style="margin-top: -1rem;"
onclick="renderInvestor()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" />
</svg>
Add new
</button>
<button id="add_investors_button" type="submit" class="button--primary justify-self-start hidden">Add
Investors</button>
<button id="create_fund_button" type="submit" class="button--primary justify-self-start">Create
Fund</button>
</form>
</article>
<article id="confirm_term_page" class="page page-layout hidden">
<header class="flex margin-top-1-5 align-items-center margin-bottom-1-5r">
<button class="back-button" onclick="showPage('admin_page')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z" />
</svg>
</button>
<h3>Confirm Details</h3>
</header>
<form class="grid gap-1-5" onsubmit="return false">
<section id="term_details" class="breakable"></section>
<div class="grid">
<h4 class="weight-400 margin-bottom-1r" id="term_admin_id"></h4>
<sm-input id="get_term_private_key" type="password" placeholder="Private key"></sm-input>
</div>
<button id="create_term_button" type="submit" class="button--primary justify-self-start">Create
term</button>
</form>
</article>
<article id="confirm_fund_page" class="page page-layout hidden">
<header class="flex margin-top-1-5 align-items-center margin-bottom-1-5r">
<button class="back-button" onclick="showPage('admin_page')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z" />
</svg>
</button>
<h3>Confirm Details</h3>
</header>
<form class="grid gap-1-5" onsubmit="return false">
<section id="fund_details" class="breakable"></section>
<div class="grid">
<h4 class="weight-400 margin-bottom-1r" id="fund_admin_id"></h4>
<sm-input id="get_fund_private_key" type="password" placeholder="Private key"></sm-input>
</div>
<button id="confirm_fund_button" type="submit" class="button--primary justify-self-start">Create
Fund</button>
</form>
</article>
<sm-popup id="redeem_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>
</button>
<h3>Redeem fund</h3>
</header>
<div id="redeem_process">
<div id="redeem_precess__get_priv_key">
<sm-form class="grid gap-1-5">
<div id="redeem__id"></div>
<sm-input id="redeem_private_key" class="password-field" type="password"
placeholder="FLO private key" error-text="Private key is invalid" autofocus data-private-key
required>
<label slot="right" class="interact">
<input type="checkbox" class="hidden" autocomplete="off" readonly
onchange="togglePrivateKeyVisibility(this)">
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<title>Hide password</title>
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<title>Show password</title>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
</label>
</sm-input>
<div class="multi-state-button">
<button id="redeem_private_key_button" type="submit"
class="button button--primary cta">Redeem</button>
</div>
</sm-form>
</div>
<div class="grid gap-0-5 hidden justify-center text-center">
<svg class="icon user-action-result__icon success" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>
<h4>Redeem request sent</h4>
<p>Balance may take upto 30mins to reflect in your FLO address</p>
</div>
<div class="grid gap-0-5 hidden justify-center text-center">
<svg class="icon user-action-result__icon failed" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
<h3>Failed</h3>
<p id="redeem_failed_message"></p>
</div>
</div>
</sm-popup>
<script id="ui">
const { html, render: renderElem } = uhtml;
const domRefs = {}
//Checks for internet connection status
if (!navigator.onLine)
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', '', true)
window.addEventListener('offline', () => {
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', true, true)
})
window.addEventListener('online', () => {
getRef('notification_drawer').clearAll()
notify('We are back online.', 'success')
})
function getRef(elementId) {
if (!domRefs.hasOwnProperty(elementId)) {
domRefs[elementId] = {
count: 1,
ref: null,
};
return document.getElementById(elementId);
} else {
if (domRefs[elementId].count < 3) {
domRefs[elementId].count = domRefs[elementId].count + 1;
return document.getElementById(elementId);
} else {
if (!domRefs[elementId].ref)
domRefs[elementId].ref = document.getElementById(elementId);
return domRefs[elementId].ref;
}
}
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, options = {}) {
let icon
switch (mode) {
case 'success':
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
break;
case 'error':
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
options.pinned = true
break;
}
if (mode === 'error') {
console.error(message)
}
return getRef("notification_drawer").push(message, { icon, ...options });
}
window.addEventListener("load", () => {
document.querySelectorAll('sm-input[data-flo-id]').forEach(input => input.customValidation = floCrypto.validateAddr)
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
document.addEventListener('keyup', (e) => {
if (e.code === 'Escape') {
closePopup()
}
})
document.addEventListener('keydown', e => {
if (e.key === '/') {
e.preventDefault();
getRef('search_investor').focusIn()
}
})
document.addEventListener('copy', () => {
notify('copied', 'success')
})
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button:not([disabled]), .interact")) {
createRipple(e, e.target.closest("button, .interact"));
}
else if (isDropdownOpen && !e.target.closest('.dropdown')) {
changeDropdownState('profile_dropdown', 'hide')
}
});
if (localStorage.getItem('preferred-currency')) {
preferredCurrency = localStorage.getItem('preferred-currency')
setTimeout(() => {
getRef('currency_selector').value = preferredCurrency
}, 1000);
} else {
preferredCurrency = 'inr'
localStorage.setItem('preferred-currency', 'inr')
}
});
function createRipple(event, target) {
const circle = document.createElement("span");
const diameter = Math.max(target.clientWidth, target.clientHeight);
const radius = diameter / 2;
const targetDimensions = target.getBoundingClientRect();
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
circle.classList.add("ripple");
const rippleAnimation = circle.animate(
[
{
transform: "scale(4)",
opacity: 0,
},
],
{
duration: floGlobals.prefersReducedMotion ? 0 : 600,
fill: "forwards",
easing: "ease-out",
}
);
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove();
};
}
// Use when a function needs to be executed after user finishes changes
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
getRef(popupId).show({ pinned })
return getRef(popupId);
}
// hides the popup or modal
function closePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
function buttonLoader(id, show) {
const button = typeof id === 'string' ? getRef(id) : id;
button.disabled = show;
const animOptions = {
duration: floGlobals.prefersReducedMotion ? 0 : 200,
fill: 'forwards',
easing: 'ease'
}
if (show) {
button.animate([
{
clipPath: 'circle(100%)',
},
{
clipPath: 'circle(0)',
},
], animOptions)
button.parentNode.append(document.createElement('sm-spinner'))
} else {
button.getAnimations().forEach(anim => anim.cancel())
const potentialTarget = button.parentNode.querySelector('sm-spinner')
if (potentialTarget) potentialTarget.remove();
}
}
document.addEventListener('popupopened', e => {
getRef('home_page').setAttribute('inert', '')
})
document.addEventListener('popupclosed', e => {
switch (e.detail.popup.id) {
case 'redeem_popup':
showChildElement(getRef('redeem_process'), 0);
buttonLoader('redeem_private_key_button', false)
getRef('redeem_private_key_button').disabled = true;
break;
}
if (popupStack.items.length === 0) {
getRef('home_page').removeAttribute('inert')
}
})
const slideInLeft = [
{
opacity: 0,
transform: 'translateX(1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutLeft = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(-1rem)'
},
]
const slideInRight = [
{
opacity: 0,
transform: 'translateX(-1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutRight = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(1rem)'
},
]
const slideInDown = [
{
opacity: 0,
transform: 'translateY(-1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutDown = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(1rem)'
},
]
const slideInUp = [
{
opacity: 0,
transform: 'translateY(1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1rem)'
},
]
function showChildElement(id, index, options = {}) {
return new Promise((resolve) => {
const { mobileView = false, entry, exit } = options
const animOptions = {
duration: floGlobals.prefersReducedMotion ? 0 : 150,
easing: 'ease',
fill: 'forwards'
}
const parent = typeof id === 'string' ? document.getElementById(id) : id;
const visibleElement = [...parent.children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden'));
if (visibleElement === parent.children[index]) return;
visibleElement.getAnimations().forEach(anim => anim.cancel())
parent.children[index].getAnimations().forEach(anim => anim.cancel())
if (visibleElement) {
if (exit) {
parent.style.overflow = 'hidden'
visibleElement.animate(exit, animOptions).onfinish = () => {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
parent.style.overflow = ''
}
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
if (entry)
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
} else {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
resolve()
}
} else {
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
}
})
}
function togglePrivateKeyVisibility(input) {
const target = input.closest('sm-input')
target.type = target.type === 'password' ? 'text' : 'password';
target.focusIn()
}
function formatAmount(amount = 0, currency = 'inr') {
amount = parseFloat(amount);
if (!amount)
return '₹0';
return amount.toLocaleString(currency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency })
}
const render = {
investorInput() {
const investorInput = document.createElement('li')
investorInput.classList.add('investor-input', 'grid')
investorInput.innerHTML = `
<sm-input placeholder="FLO address" class="outlined" animate></sm-input>
<sm-input placeholder="Amount(₹)" type="number" min=0 class="outlined" animate></sm-input>
<button class="remove-investor" title="Remove this investor">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
</button>
`
return investorInput
},
fundPlaceholder() {
const fund_ph = document.createElement('li')
fund_ph.classList.add('fund-placeholder', 'grid')
fund_ph.innerHTML = `
<div class="flex-grid justify-start">
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
</div>
<div class="grid flow-column gap-1">
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
</div>
<div class="placeholder__block justify-self-end"></div>
`
return fund_ph
},
investmentCard(details) {
const {
floId,
amountInvested,
netValue,
fundId,
hasMatured = false,
allowsEarlyWithdrawal = false,
} = details
const isRedeemable = hasMatured || allowsEarlyWithdrawal
return html`
<li class="fund-investor" id=${`${fundId}_${floId}`}>
<div class="grid">
<span class="label">FLO address</span>
<span class="value flo-id">${floId}</span>
</div>
<div class="flex gap-1-5">
<div class="transaction-column">
<span class="label">Invested</span>
<span class="value amount-invested">${formatAmount(amountInvested[preferredCurrency], preferredCurrency)}</span>
</div>
<div class="transaction-column">
<span class="label">Present value</span>
<span class="value net-value" style="color: var(--green)">${formatAmount(netValue[preferredCurrency], preferredCurrency)}</span>
</div>
</div>
<button class="button fund-investor__redeem" ?disabled=${!isRedeemable}>Redeem</button>
</li>
`
},
closedInvestmentCard(details) {
const {
floId,
amountInvested,
fundId,
BTC_net,
endDate,
finalAmount,
USD_net,
payment_refRef
} = details
return html`
<li class="fund-investor" id=${`${fundId}_${floId}`}>
<div class="flex align-items-center space-between w-100">
<span class="tag">Closed: ${bobsFund.dateFormat(endDate)}</span>
</div>
<div class="grid">
<span class="label">FLO address</span>
<span class="value flo-id">${floId}</span>
</div>
<div class="flex gap-1-5">
<div class="transaction-column">
<span class="label">Invested</span>
<span class="value amount-invested">${formatAmount(amountInvested[preferredCurrency], preferredCurrency)}</span>
</div>
<div class="transaction-column">
<span class="label">Withdrawn amount</span>
<span class="value net-value" style="color: var(--green)">${formatAmount(finalAmount[preferredCurrency], preferredCurrency)}</span>
</div>
</div>
<div class="flex gap-1-5 align-center space-between w-100">
<div class="transaction-column">
<a href=${`${floBlockchainAPI.current_server}tx/${payment_refRef}`} target="_blank">See withdrawal transaction</a>
</div>
</div>
</li>
`
},
investmentCard(details) {
const {
floId,
amountInvested,
netValue,
fundId,
hasMatured = false,
allowsEarlyWithdrawal = false,
} = details
const isRedeemable = hasMatured || allowsEarlyWithdrawal
return html`
<li class="fund-investor" id=${`${fundId}_${floId}`}>
<div class="grid">
<span class="label">FLO address</span>
<span class="value flo-id">${floId}</span>
</div>
<div class="flex gap-1-5">
<div class="transaction-column">
<span class="label">Invested</span>
<span class="value amount-invested">${formatAmount(amountInvested[preferredCurrency], preferredCurrency)}</span>
</div>
<div class="transaction-column">
<span class="label">Present value</span>
<span class="value net-value" style="color: var(--green)">${formatAmount(netValue[preferredCurrency], preferredCurrency)}</span>
</div>
</div>
<button class="button fund-investor__redeem" ?disabled=${!isRedeemable}>Redeem</button>
</li>
`
},
fundBlock(details) {
let {
fundTxs,
startDate,
endDate,
baseUsd,
baseBtc,
tapouts,
totalInvestment,
totalNet,
investorsFrag
} = details
const tapoutsPoints = tapouts.map((tapout, index) => {
const duration = `${bobsFund.dateFormat(tapout.start)} to ${bobsFund.dateFormat(tapout.end)}`
return html`
<li class="tapout-point grid">
<span class="label">Tapout ${index + 1}</span>
<span class="value">${duration}</span>
</li>`;
})
const renderedFundTxs = fundTxs.map((tx, index) => {
return html`<a href=${`${floBlockchainAPI.current_server}tx/${tx.txid}`} target="_blank">Transaction ${index + 1}</a>`
})
return html.node`
<li class="fund-block">
<h4 class="start-date">${startDate} Fund</h4>
<div class="fund-block__details margin-bottom-3r">
<div class="grid">
<span class="label">End date</span>
<span class="value end-date">${bobsFund.dateFormat(endDate)}</span>
</div>
<div class="flex flex-wrap gap-1">
<div class="grid">
<span class="label">Initial BTC value</span>
<span class="value base-btc">${formatAmount(baseBtc[preferredCurrency], preferredCurrency)}</span>
</div>
<div class="grid">
<span class="label">Base USD rate</span>
<span class="value base-usd">${baseUsd.toLocaleString(`en-US`, { style: 'currency', currency: 'INR' })}</span>
</div>
</div>
<div class="flex flex-wrap gap-1">
<div class="grid">
<span class="label">Total investment</span>
<span class="value total-investment">${formatAmount(totalInvestment[preferredCurrency], preferredCurrency)}</span>
</div>
<div class="grid">
<span class="label">Total present value</span>
<span class="value net-value" style="color: var(--green)">${formatAmount(totalNet[preferredCurrency], preferredCurrency)}</span>
</div>
</div>
${tapoutsPoints.length ? html`
<div class="grid">
<span class="value margin-bottom-0-5r">Tapouts</span>
<ul class="tapout-list">${tapoutsPoints}</ul>
</div>` : ''}
<div class="grid">
<span class="label">Fund transactions</span>
<div class="flex gap-0-5 flex-wrap">${renderedFundTxs}</div>
</div>
</div>
<div class="grid">
<h4 class="margin-bottom-0-5r">Investors</h4>
<ul class="investors-list grid">${investorsFrag}</ul>
</div>
</li>
`;
}
}
let preferredCurrency
getRef('currency_selector').addEventListener('change', e => {
preferredCurrency = e.target.value
localStorage.setItem('preferred-currency', preferredCurrency)
document.querySelectorAll('.fund-block').forEach(fundBlock => {
const { baseBtc, totalInvestment, totalNet } = floGlobals.investments[fundBlock.id]
fundBlock.querySelector('.base-btc').textContent = formatAmount(baseBtc[preferredCurrency], preferredCurrency)
fundBlock.querySelector('.total-investment').textContent = formatAmount(totalInvestment[preferredCurrency], preferredCurrency)
fundBlock.querySelector('.net-value').textContent = formatAmount(totalNet[preferredCurrency], preferredCurrency)
})
document.querySelectorAll('.fund-investor').forEach(investor => {
const { amountInvested, netValue } = floGlobals.investments[investor.id]
investor.querySelector('.amount-invested').textContent = formatAmount(amountInvested[preferredCurrency], preferredCurrency)
investor.querySelector('.net-value').textContent = formatAmount(netValue[preferredCurrency], preferredCurrency)
})
})
let isDropdownOpen = false
function changeDropdownState(target, mode, trigger) {
const options = {
duration: 300,
easing: 'ease',
fill: 'both',
}
if (mode === 'show') {
showDropdown(target, options, trigger)
}
else if (mode === 'toggle') {
if (isDropdownOpen) {
hideDropdown(target, options)
}
else {
showDropdown(target, options, trigger)
}
}
else if (mode === 'hide') {
hideDropdown(target, options)
}
}
function showDropdown(target, options, trigger) {
if (isDropdownOpen) return
if (trigger) {
const triggerDimensions = trigger.getBoundingClientRect()
getRef(target).setAttribute('style', `top: ${triggerDimensions.top + triggerDimensions.height + document.documentElement.scrollTop}px; right: calc(${window.innerWidth - triggerDimensions.right}px - 1.5rem)`)
}
getRef(target).classList.remove('hidden')
getRef(target).animate([
{ transform: 'translateY(-1rem)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 },
], options)
.onfinish = () => {
isDropdownOpen = true
}
}
function hideDropdown(target, options) {
if (!isDropdownOpen) return
getRef(target).animate([
{ transform: 'translateY(0)', opacity: 1 },
{ transform: 'translateY(-1rem)', opacity: 0 },
], options)
.onfinish = () => {
isDropdownOpen = false
getRef(target).classList.add('hidden')
}
}
function showPage(target) {
document.querySelector('.page:not(.hidden)')?.classList.add('hidden')
getRef(target).classList.remove('hidden')
if (target === 'home_page') {
clearAddedInvestors()
getRef("create_fund_form").reset()
if (window.location.hash !== '') {
getRef(`${window.location.hash.split('#').pop()}`).scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
if (target !== 'confirm_term_page') {
getRef('get_term_private_key').value = ''
getRef('get_fund_private_key').value = ''
}
if (target === 'loading_page' || target === 'error_page') {
getRef('main_header').classList.add('hidden')
}
else {
getRef('main_header').classList.remove('hidden')
}
}
getRef('search_investor').addEventListener('input', debounce(() => {
document.querySelectorAll('.fund-block').forEach(block => {
block.querySelectorAll('.fund-investor').forEach(child => {
if (child.id.toLowerCase().includes(getRef('search_investor').value.trim().toLowerCase())) {
child.classList.remove('hidden')
}
else {
child.classList.add('hidden')
}
})
if (Array.from(block.querySelectorAll('.fund-investor')).every(elem => elem.classList.contains('hidden'))) {
block.classList.add('hidden')
} else {
block.classList.remove('hidden')
}
if (Array.from(getRef('fund_list').children).every(elem => elem.classList.contains('hidden'))) {
document.getElementById('fund_list__empty-state')?.classList.remove('hidden')
} else {
document.getElementById('fund_list__empty-state')?.classList.add('hidden')
}
})
}, 100))
getRef('investors_input_list').addEventListener('click', e => {
if (e.target.closest('.remove-investor')) {
e.target.closest('.investor-input').remove()
}
})
function renderInvestor() {
getRef('investors_input_list').append(render.investorInput())
getRef('investors_input_list').lastElementChild.scrollIntoView()
getRef('investors_input_list').lastElementChild.children[0].focusIn()
}
function clearAddedInvestors() {
getRef('investors_input_list').innerHTML = ``
getRef('investors_input_list').append(render.investorInput())
}
function getAddedInvestors() {
const allInvestorsInput = getRef('investors_input_list').querySelectorAll('.investor-input')
const addedInvestors = []
allInvestorsInput.forEach(investor => {
const floId = investor.children[0].value.trim()
const amount = investor.children[1].value.trim()
addedInvestors.push([floId, amount])
})
return addedInvestors
}
getRef('fund_creation_toggle').addEventListener('change', e => {
if (e.target.checked) {
getRef('fund_details_form').classList.add('hidden')
getRef('fund_details_form').querySelectorAll('input').forEach(input => input.disabled = true)
getRef('create_fund_button').classList.add('hidden')
getRef('add_investors_button').classList.remove('hidden')
getRef('fund_selector_container').classList.remove('hidden')
}
else {
getRef('fund_details_form').classList.remove('hidden')
getRef('create_fund_button').classList.remove('hidden')
getRef('fund_details_form').querySelectorAll('input').forEach(input => input.disabled = false)
getRef('add_investors_button').classList.add('hidden')
getRef('fund_selector_container').classList.add('hidden')
}
})
getRef('tapout_toggle').addEventListener('change', e => {
if (!e.target.checked) {
getRef('tapout_container').classList.add('hidden')
getRef('tapout_container').querySelectorAll('input').forEach(input => input.disabled = true)
}
else {
getRef('tapout_container').classList.remove('hidden')
getRef('tapout_container').querySelectorAll('input').forEach(input => input.disabled = false)
}
})
/* function renderfundPlaceholder(){
getRef('fund_list').innerHTML = ``
const frag = document.createDocumentFragment()
for (let index = 0; index < 4; index++) {
frag.append(render.fundPlaceholder())
}
getRef('fund_list').append(frag)
} */
/* getRef('term_selector').addEventListener('change', e => {
const floID = e.detail.value
getRef('fund_selector').querySelectorAll('.fund-option').forEach(option => option.classList.add('hidden'))
getRef('fund_selector').querySelector(`.fund-option[data-flo-id="${floID}"]`)?.classList.remove('hidden')
}) */
getRef('refresh_button').addEventListener("click", refresh);
var USD_current, BTC_current;
function refresh(showLoader = true) {
if (showLoader)
showPage('loading_page')
getCurrentRates().then(async (rates) => {
USD_current = rates.USD_INR;
BTC_current = rates.BTC_USD;
console.log(`USD rate: ${USD_current} INR\nBTC rate: ${BTC_current} USD`);
getRef("usd-rate").textContent = `USD: ₹${rates.USD_INR.toFixed(2)}`;
getRef("btc-usd-rate").textContent = `BTC: ${parseFloat(rates.BTC_USD.toFixed(2)).toLocaleString(`en-US`, { style: 'currency', currency: 'USD' })}`;
getRef('fund_list').innerHTML = '';
getRef('fund_selector').innerHTML = ''
//getRef('term_selector').innerHTML = ''
refreshBlockchainData().then(funds => {
renderFunds(funds)
showPage('home_page')
}).catch(error => {
console.error(error)
showPage('error_page')
})
}).catch(error => console.error(error))
}
floGlobals.investments = {}
function renderFunds(funds) {
if (!Object.keys(funds).length)
return;
const removeElementIfExist = id => {
let existing = document.getElementById(id);
if (existing)
existing.remove();
}
const selectableFunds = []
let sortArray = [];
for (let k in funds) {
let f = bobsFund.parse(funds[k].map(a => a.data));
// console.info(f);
let startDate = new Date(f.start_date).getTime()
let tapouts = []
if (f.tapoutInterval)
tapouts = f.tapoutInterval.map((interval, index) => {
const start = bobsFund.dateAdder(startDate, interval)
const end = bobsFund.dateAdder(start, f.topoutWindow)
return { start, end }
})
const fundObj = {
fundTxs: funds[k],
startDate: bobsFund.dateFormat(f.start_date),
endDate: bobsFund.dateAdder(startDate, f.duration),
baseUsd: f.USD_base,
baseBtc: {
usd: f.BTC_base,
inr: f.BTC_base * f.USD_base
},
tapouts,
}
const hasMatured = new Date(fundObj.endDate) < new Date()
const allowsEarlyWithdrawal = tapouts.some(tapout => new Date(tapout.start) < new Date() && new Date(tapout.end) > new Date())
// Creating fund selection options
selectableFunds.push(html`
<sm-option value="${funds[k][0].txid}">
<div class="grid gap-0-5">
<span>Start date: ${fundObj.startDate}</span>
<span>Base BTC: ${parseFloat(fundObj.baseBtc.usd).toLocaleString(`en-US`, { style: 'currency', currency: 'USD' })} | Base USD: ₹${fundObj.baseUsd}</span>
</div>
</sm-option>
`)
const investorsFrag = []
let total_invested = total_net = 0;
for (let investor in f.investments) {
const { amount, closed } = f.investments[investor]
let netVal = bobsFund.calcNetValue(f.BTC_base, BTC_current, f.USD_base, USD_current, amount, f.fee);
const obj = {
fundId: k,
floId: investor,
amountInvested: {
inr: amount.toFixed(2),
usd: (amount / f.USD_base).toFixed(2),
},
netValue: {
inr: netVal.toFixed(2),
usd: (netVal / USD_current).toFixed(2),
},
hasMatured,
allowsEarlyWithdrawal,
}
total_invested += amount;
total_net += netVal;
if (closed) {
console.log(closed)
/* TODO: UI: render closing data
if closed, netVal should not be displayed
f.investments[investor].closed -> Object {
BTC_net: BTC value at closing date
endDate: closing date
amountFinal: final amount (withdrawn)
payment_refRef: if amount is withdrawn via token system, txid of the token transfer
USD_net: USD value at closing date
refSign: signature of the investor for closing (hidden)
i: index of the txid (of closing tx) ie, funds[k][i].txid
}
*/
obj.finalAmount = {
inr: closed.amountFinal.toFixed(2),
usd: (closed.amountFinal / closed.USD_net).toFixed(2),
}
investorsFrag.push(render.closedInvestmentCard({ ...obj, ...closed }))
floGlobals.investments[`${k}_${investor}`] = { amountInvested: obj.amountInvested, netValue: obj.finalAmount }
} else {
investorsFrag.push(render.investmentCard(obj))
floGlobals.investments[`${k}_${investor}`] = { amountInvested: obj.amountInvested, netValue: obj.netValue }
}
}
fundObj.totalInvestment = {
inr: total_invested.toFixed(2),
usd: (total_invested / f.USD_base).toFixed(2)
}
fundObj.totalNet = {
inr: total_net.toFixed(2),
usd: (total_net / USD_current).toFixed(2)
}
floGlobals.investments[k] = { totalInvestment: fundObj.totalInvestment, totalNet: fundObj.totalNet, baseBtc: fundObj.baseBtc }
fundObj.investorsFrag = investorsFrag
const fundBlock = render.fundBlock(fundObj)
removeElementIfExist(k);
fundBlock.id = k;
let i = sortArray.length - 1;
while (i >= 0 && sortArray[i] < startDate) { //[9, 7, 5, 3]
sortArray[i + 1] = sortArray[i]
i--;
}
sortArray[i + 1] = startDate;
getRef('fund_list').insertBefore(fundBlock, getRef('fund_list').childNodes[i + 1]);
}
//const fundGroup = document.createElement('section')
//fundGroup.dataset.floId = term.floID
//fundGroup.classList.add('fund-option')
//fundGroup.append(fundsFrag)
renderElem(getRef('fund_selector'), html`${selectableFunds}`)
}
/* getRef("create_term_form").addEventListener("submit", evt => {
evt.preventDefault();
let f = evt.target;
let tap_it = getRef("tap_it").value
let tapoutInterval = f["tap_iv"].value.split(",").map(v => v.trim() + " " + tap_it);
let termStr = createTermString(f["floid"].value, f["max_pv"].value + " " + getRef("max_pt").value, f["tap_wv"].value + " " + getRef("tap_wt").value, tapoutInterval)
console.log(termStr)
getRef('term_details').innerHTML = termStr.replace(/\|/g, "<br>")
getRef('term_admin_id').innerHTML = `Enter Private key of adminID <h5 class="weight-400">${floGlobals.adminID}</h5>`
showPage('confirm_term_page')
getRef('create_term_button').onclick = () => {
const privKey = getRef('get_term_private_key').value
if (!floCrypto.verifyPrivKey(privKey, floGlobals.adminID)) {
notify("Access Denied! incorrect private key", 'error');
return
}
floBlockchainAPI.writeData(floGlobals.adminID, termStr, privKey, f["floid"].value).then(result => {
console.log(result);
showPage('admin_page')
notify("Term added in blockchain", 'success');
getRef("create_term_form").reset()
refresh(false)
}).catch(error => console.error(error))
}
}) */
getRef("create_fund_form").addEventListener("submit", evt => {
evt.preventDefault(); //tapout_toggle
let f, fStr, duration, tapoutWindow, tapoutInterval, investments;
f = evt.target
duration = f["max_pv"].value + " " + getRef("max_pt").value
if (!getRef("tapout_toggle").checked)
tapoutWindow = tapoutInterval = null
else {
let tap_it = getRef("tap_it").value
tapoutInterval = f["tap_iv"].value.split(",").map(v => v.trim() + " " + tap_it)
tapoutWindow = f["tap_wv"].value + " " + getRef("tap_wt").value
}
investments = getAddedInvestors()
let createMod = !getRef("fund_creation_toggle").checked;
console.info(investments)
if (createMod) //create new fund
fStr = bobsFund.stringify.main(f["btc_base"].value, f["usd_rate"].value, f["start_date"].value, duration, investments, f["fee"].value, tapoutWindow, tapoutInterval);
else //add investments to existing fund
fStr = bobsFund.stringify.continue(getRef("fund_selector").value, investments);
console.log(fStr);
if (fStr.length >= 1040) {
console.error("flo data length is too long, Please reduce it and try again");
notify("floData is too large! Please reduce it and try again.", 'error');
return
}
getRef('fund_details').innerHTML = fStr.replace(/\|/g, "<br>")
getRef('fund_admin_id').innerHTML = `Enter Private key of admin ID <h5 class="weight-400">${floGlobals.adminID}</h5>`
showPage('confirm_fund_page')
getRef('confirm_fund_button').onclick = () => {
const privKey = getRef('get_fund_private_key').value
console.log(privKey)
if (!floCrypto.verifyPrivKey(privKey, floGlobals.adminID)) {
notify("Access Denied! incorrect private key", 'error');
return
}
floBlockchainAPI.writeData(floGlobals.adminID, fStr, privKey, floGlobals.adminID).then(result => {
console.log(result);
showPage('admin_page')
notify(createMod ? "New Fund created" : "Invesments added to fund", 'success');
getRef("create_fund_form").reset()
clearAddedInvestors()
refresh(false)
}).catch(error => console.error(error))
}
})
getRef('fund_list').addEventListener('click', e => {
if (e.target.closest('.fund-investor__redeem')) {
const button = e.target.closest('.fund-investor__redeem')
floGlobals.redeemId = button.closest('.fund-investor').id.split('_')
const [fundId, investorId] = floGlobals.redeemId
renderElem(getRef('redeem__id'), html`<div class='label'>Investor address</div> <strong class="value">${investorId}</strong>`)
openPopup('redeem_popup')
}
})
getRef('redeem_private_key_button').addEventListener('click', e => {
buttonLoader('redeem_private_key_button', true)
const privKey = getRef('redeem_private_key').value
const [fundId, investorId] = floGlobals.redeemId
floExchangeAPI.closeBobsFundInvestment(fundId, investorId, privKey).then(result => {
console.log(result)
showChildElement(getRef('redeem_process'), 1, { entry: slideInLeft, exit: slideOutLeft });
}).catch(error => {
getRef('redeem_failed_message').textContent = error.message
showChildElement(getRef('redeem_process'), 2, { entry: slideInLeft, exit: slideOutLeft });
console.error(error)
}).finally(() => {
buttonLoader('redeem_private_key_button', false)
})
})
</script>
</body>
</html>