3273 lines
198 KiB
HTML
3273 lines
198 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<title>FLO Wallet</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
<meta charset="UTF-8">
|
|
<meta name="description"
|
|
content="This webapp allows monitoring FLO addresses and performing transactions based on FLO blockchain.">
|
|
<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=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
rel="stylesheet">
|
|
<link rel="stylesheet" href="css/main.min.css">
|
|
<script>
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
|
|
const floGlobals = {
|
|
blockchain: "FLO",
|
|
tokenApiUrl: 'https://ranchimallflo.duckdns.org',
|
|
tokenURL: 'https://ranchimallflo.duckdns.org/',
|
|
expirationDays: 60,
|
|
}
|
|
</script>
|
|
<script src="scripts/components.js" defer></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6" defer></script>
|
|
<script src="scripts/lib.js"></script>
|
|
<script src="scripts/floCrypto.js"></script>
|
|
<script src="scripts/floBlockchainAPI.js"></script>
|
|
<script src="scripts/floTokenAPI.js"></script>
|
|
<script src="scripts/compactIDB.js"></script>
|
|
<script src="scripts/flo-webwallet.js"></script>
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script>
|
|
function onLoadStartUp() {
|
|
var IDBObjects = {
|
|
lastSync: {},
|
|
labels: {},
|
|
pinnedIds: {},
|
|
transactions: {}
|
|
}
|
|
loader()
|
|
compactIDB.initDB("FLOwebWallet", IDBObjects).then(async result => {
|
|
render.savedIds();
|
|
if (!floGlobals.tokens || !floGlobals.smartContracts) {
|
|
fetchJSON(`${floGlobals.tokenApiUrl}/api/v2/tokenSmartContractList`).then(({ tokens, smartContracts }) => {
|
|
floGlobals.scMap = new Map()
|
|
floGlobals.tokens = tokens.sort((a, b) => a.localeCompare(b))
|
|
floGlobals.smartContracts = smartContracts
|
|
.sort((a, b) => a.contractName.localeCompare(b.contractName))
|
|
floGlobals.smartContracts.forEach((sc, index) => {
|
|
floGlobals.scMap.set(`${sc.contractName}_${sc.contractAddress}`, index)
|
|
})
|
|
routeTo(window.location.hash, { firstLoad: true })
|
|
}).catch(e => {
|
|
console.error(e)
|
|
if (window.location.hash.includes('smartcontracts'))
|
|
routeTo('', { firstLoad: true })
|
|
else
|
|
routeTo(window.location.hash, { firstLoad: true })
|
|
getRef('smart_contract_link').classList.add('hidden')
|
|
}).finally(() => {
|
|
loader(false)
|
|
})
|
|
}
|
|
console.log(result)
|
|
}).catch(error => console.error(error))
|
|
}
|
|
</script>
|
|
</head>
|
|
|
|
<body onload="onLoadStartUp()" class="hidden">
|
|
<sm-notifications id="notification_drawer"></sm-notifications>
|
|
<sm-popup id="confirmation_popup">
|
|
<h4 id="confirm_title"></h4>
|
|
<p id="confirm_message" class="breakable"></p>
|
|
<div class="flex align-center gap-0-5 margin-left-auto">
|
|
<button class="button cancel-button">Cancel</button>
|
|
<button class="button button--primary confirm-button">OK</button>
|
|
</div>
|
|
</sm-popup>
|
|
<div id="loader" class="hidden">
|
|
<sm-spinner></sm-spinner>
|
|
<p>
|
|
Loading FLO Wallet
|
|
</p>
|
|
</div>
|
|
<div id="main_card">
|
|
<header id="main_header" class="flex align-center space-between">
|
|
<div class="app-brand">
|
|
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="app-name">
|
|
<div class="app-name__company">RanchiMall</div>
|
|
<h4 class="app-name__title">
|
|
FLO Wallet
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
<theme-toggle></theme-toggle>
|
|
</header>
|
|
<div id="pages_container">
|
|
<div id="search" class="page flex h-100 gap-1-5">
|
|
<section id="primary_actions_wrapper">
|
|
<button id="gen_new_addr_btn" class="button primary-action interact">
|
|
<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>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<g>
|
|
<path
|
|
d="M18.32,4.26C16.84,3.05,15.01,2.25,13,2.05v2.02c1.46,0.18,2.79,0.76,3.9,1.62L18.32,4.26z M19.93,11h2.02 c-0.2-2.01-1-3.84-2.21-5.32L18.31,7.1C19.17,8.21,19.75,9.54,19.93,11z M18.31,16.9l1.43,1.43c1.21-1.48,2.01-3.32,2.21-5.32 h-2.02C19.75,14.46,19.17,15.79,18.31,16.9z M13,19.93v2.02c2.01-0.2,3.84-1,5.32-2.21l-1.43-1.43 C15.79,19.17,14.46,19.75,13,19.93z M13,12V7h-2v5H7l5,5l5-5H13z M11,19.93v2.02c-5.05-0.5-9-4.76-9-9.95s3.95-9.45,9-9.95v2.02 C7.05,4.56,4,7.92,4,12S7.05,19.44,11,19.93z" />
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
Generate FLO address
|
|
</button>
|
|
<button id="retrieve_addr_btn" class="button primary-action interact"
|
|
onclick="openPopup('retrieve_flo_id_popup')">
|
|
<svg class="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="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" />
|
|
</svg>
|
|
Retrieve FLO address
|
|
</button>
|
|
</section>
|
|
<sm-form id="search_wrapper" class="flex margin-bottom-2" style="--gap: 0.5rem;">
|
|
<sm-input type="search" id="search_query_input" class="flex-1" placeholder="Search FLO address"
|
|
required>
|
|
<svg slot="icon" class="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>
|
|
<button id="process_query" class="button button--primary cta" style="height: 3.2rem;"
|
|
onclick="processQuery()" type="submit" disabled>Search</button>
|
|
</sm-form>
|
|
<div id="address_details_wrapper" class="grid gap-1-5 hidden">
|
|
<div id="transactions_hero_section" class="grid gap-2"
|
|
style="background-color: rgba(var(--text-color), 0.03); padding: 1.5rem 1rem; border-radius: 0.5rem;">
|
|
<div id="queried_flo_address"></div>
|
|
<div class="flex align-center">
|
|
<svg class="icon margin-right-0-5" viewBox="0 0 48 48"
|
|
style="enable-background:new 0 0 107.65 47.07;" xml:space="preserve">
|
|
<path class="flo-logo"
|
|
d="M34.2,32.4c0,0,3.75-0.18,7.41-3.86c2.96-2.98,3.65-6.66,3.99-8.52c-11.04-0.63-12.36,0.99-13.71,1.68
|
|
c-1.19,0.61-5.33,4.55-5.33,4.55s3.06-3.13,3.2-9.94c0.09-4.54-1.02-7.39-2.72-10.64C25.29,2.33,22.79,0,22.79,0l0.01,4.97
|
|
c0,0,4.35,2.84,4.35,11.84c0,6.52-4.35,11.02-4.35,11.02s-4.35-4.5-4.35-11.02c0-9.01,4.35-11.84,4.35-11.84L22.79,0
|
|
c0,0-2.48,2.33-4.23,5.67c-1.7,3.25-2.81,6.1-2.72,10.64c0.13,6.81,3.2,9.94,3.2,9.94s-4.14-3.95-5.33-4.55
|
|
c-1.35-0.69-2.67-2.31-13.71-1.68c0.34,1.86,1.03,5.54,3.99,8.52c3.66,3.68,7.41,3.86,7.41,3.86s-5.05-2.03-7.15-9.45
|
|
c0,0,5.76-0.7,9.63,1.87c2.52,1.67,4.86,4.26,6.79,6.01c0,0-2.58-0.04-6.81,1.88c-2.54,1.15-3.92,2.84-4.44,4.38
|
|
c-0.36,1.06-0.2,2.27-0.2,2.27s3.31,0.31,5.94,0c1.99-0.23,3.42-2.16,3.42-2.16s-2,0.78-3.95,0.78c-2.06,0-2.67-0.66-2.67-0.66
|
|
c0.98-3.64,8.68-5.19,8.68-5.19s-1.34,2.6-1.42,6.5c-0.1,4.79,3.57,8.52,3.57,8.45c0,0.07,3.67-3.66,3.57-8.45
|
|
c-0.08-3.9-1.42-6.5-1.42-6.5s7.71,1.55,8.68,5.19c0,0-0.61,0.66-2.67,0.66c-1.95,0-3.95-0.78-3.95-0.78s1.43,1.93,3.42,2.16
|
|
c2.63,0.31,5.94,0,5.94,0s0.16-1.21-0.2-2.27c-0.52-1.54-1.9-3.23-4.44-4.38c-4.23-1.92-6.81-1.88-6.81-1.88
|
|
c1.93-1.76,4.27-4.34,6.79-6.01c3.87-2.57,9.63-1.87,9.63-1.87C39.26,30.38,34.2,32.4,34.2,32.4z M22.8,43.06
|
|
c-0.95-1.37-1.47-2.13-1.47-4.26c0-2.4,1.12-4.61,1.47-5.14c0.35,0.52,1.47,2.74,1.47,5.14C24.27,40.92,23.75,41.69,22.8,43.06z">
|
|
</path>
|
|
</svg>
|
|
<div class="grid gap-0-5">
|
|
<h5>FLO balance</h5>
|
|
<span id="flo_balance"></span>
|
|
</div>
|
|
</div>
|
|
<div id="token_list_wrapper" class="grid gap-0-5 hidden">
|
|
<div class="flex align-center">
|
|
<svg class="icon margin-right-0-5" 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 4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" />
|
|
<path
|
|
d="M3 12c0-2.61 1.67-4.83 4-5.65V4.26C3.55 5.15 1 8.27 1 12s2.55 6.85 6 7.74v-2.09c-2.33-.82-4-3.04-4-5.65z" />
|
|
</svg>
|
|
<h5>Tokens owned</h5>
|
|
</div>
|
|
<ul id="token_list"></ul>
|
|
</div>
|
|
</div>
|
|
<section class="grid gap-1">
|
|
<div class="flex align-center space-between sticky top-0 flex-wrap gap-1"
|
|
style="background-color: rgba(var(--foreground-color), 1);transition: background-color .3s; padding-bottom: 0.5rem; z-index: 2">
|
|
<h4>Transactions</h4>
|
|
<sm-chips id="filter_selector" class="hidden">
|
|
<sm-chip value="all" selected>All</sm-chip>
|
|
<sm-chip value="sent">Sent</sm-chip>
|
|
<sm-chip value="received">Received</sm-chip>
|
|
<sm-chip value="mined">Mined</sm-chip>
|
|
</sm-chips>
|
|
</div>
|
|
<ul id="queried_address_transactions" class="observe-empty-state"></ul>
|
|
<div class="empty-state">
|
|
<h4>
|
|
Looks a bit empty here, no transactions yet.
|
|
</h4>
|
|
</div>
|
|
<button id="load_more_transactions" onclick="loadMoreTransactions()"
|
|
style="margin-top: -4rem; height: fit-content;"
|
|
class="button justify-self-center hidden">Load more</button>
|
|
</section>
|
|
</div>
|
|
<div id="pagination_wrapper" class="flex gap-0-3 align-center"> </div>
|
|
</div>
|
|
<div id="contacts" class="page flex h-100">
|
|
<div class="flex direction-column gap-2 h-100">
|
|
<section class="flex direction-column h-100">
|
|
<div class="grid align-center gap-0-5">
|
|
<div class="flex align-center">
|
|
<svg class="icon margin-right-0-5" 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="M21 5v14h2V5h-2zm-4 14h2V5h-2v14zM14 5H2c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1zM8 7.75c1.24 0 2.25 1.01 2.25 2.25S9.24 12.25 8 12.25 5.75 11.24 5.75 10 6.76 7.75 8 7.75zM12.5 17h-9v-.75c0-1.5 3-2.25 4.5-2.25s4.5.75 4.5 2.25V17z" />
|
|
</svg>
|
|
<h4>Saved FLO addresses</h4>
|
|
</div>
|
|
<p class="flex align-center">
|
|
<svg class="icon margin-right-0-5" style="fill: #ffc107;"
|
|
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="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7z" />
|
|
</svg>
|
|
<span id="saved_ids_tip">Click 'Add FLO address' to add a new FLO address.</span>
|
|
</p>
|
|
</div>
|
|
<ul id="saved_ids_list" class="observe-empty-state grid"></ul>
|
|
<div class="empty-state justify-center text-center align-center h-100"
|
|
style="align-content: center;">
|
|
<svg class="justify-self-center" style="height: 12rem;"
|
|
id="bb7dac0d-c86d-4eae-9345-05ead570be6d" data-name="Layer 1"
|
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
|
<defs>
|
|
<style>
|
|
.e4b4c873-5e79-4c66-a530-269f7775150b {
|
|
fill: rgba(var(--text-color), 0.03);
|
|
}
|
|
|
|
.f8c35eef-c260-42fc-be6f-7c8afb0beeeb {
|
|
fill: rgba(var(--text-color), 0.2);
|
|
}
|
|
|
|
.ee8c2e6d-b8f3-4b81-80ab-31d470d121b9 {
|
|
fill: rgba(var(--text-color), 0.1);
|
|
}
|
|
</style>
|
|
</defs>
|
|
<rect class="e4b4c873-5e79-4c66-a530-269f7775150b" x="34.76" y="40.75" width="177.53"
|
|
height="41.42" rx="4" />
|
|
<circle class="f8c35eef-c260-42fc-be6f-7c8afb0beeeb" cx="57.21" cy="61.46" r="9.29" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="75.4" y="51.37" width="40.44"
|
|
height="9.03" rx="1.92" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="75.4" y="62.79" width="80.09"
|
|
height="9.03" rx="1.92" />
|
|
<rect class="e4b4c873-5e79-4c66-a530-269f7775150b" x="10" y="99.29" width="177.53"
|
|
height="41.42" rx="4" />
|
|
<circle class="f8c35eef-c260-42fc-be6f-7c8afb0beeeb" cx="32.45" cy="120" r="9.29" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="50.64" y="109.91" width="40.44"
|
|
height="9.03" rx="1.92" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="50.64" y="121.33" width="80.09"
|
|
height="9.03" rx="1.92" />
|
|
<rect class="e4b4c873-5e79-4c66-a530-269f7775150b" x="52.47" y="157.83" width="177.53"
|
|
height="41.42" rx="4" />
|
|
<circle class="f8c35eef-c260-42fc-be6f-7c8afb0beeeb" cx="74.93" cy="178.54" r="9.29" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="93.12" y="168.46" width="40.44"
|
|
height="9.03" rx="1.92" />
|
|
<rect class="ee8c2e6d-b8f3-4b81-80ab-31d470d121b9" x="93.12" y="179.87" width="80.09"
|
|
height="9.03" rx="1.92" />
|
|
</svg>
|
|
<h4>No Saved FLO address</h4>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<button id="add_address_button" class="button interact fab" onclick="openPopup('add_address_popup')">
|
|
<svg class="icon margin-right-0-5" 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="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" />
|
|
</svg>
|
|
Add FLO address
|
|
</button>
|
|
</div>
|
|
<div id="send" class="page hidden gap-1">
|
|
<div class="grid full-bleed">
|
|
<h3>Send</h3>
|
|
<p>Perform FLO blockchain transactions</p>
|
|
</div>
|
|
<sm-form id="send_form">
|
|
<div class="grid gap-1">
|
|
<div id="balance_card">
|
|
<div class="flex align-center space-between">
|
|
<h5>Balance</h5>
|
|
</div>
|
|
<p style="line-height: 1.2; opacity: 0.7;">
|
|
Sender balance will be shown once you enter it's private key
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="grid gap-1">
|
|
<sm-input id="get_private_key_field" placeholder="Sender's private key" class="password-field"
|
|
type="password" error-text="Invalid private key" data-private-key required autofocus>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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="grid gap-1">
|
|
<div id="tx_receiver_wrapper" class="grid gap-1">
|
|
<sm-input id="tx_receiver" class="w-100" placeholder="Receiver's FLO address"
|
|
error-text="Invalid FLO address" data-flo-address="" animate required>
|
|
<button slot="right" class="icon-only" onclick="openPopup('saved_ids_popup')"
|
|
title="Select from saved IDs">
|
|
<svg class="icon" 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="M21 5v14h2V5h-2zm-4 14h2V5h-2v14zM14 5H2c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1zM8 7.75c1.24 0 2.25 1.01 2.25 2.25S9.24 12.25 8 12.25 5.75 11.24 5.75 10 6.76 7.75 8 7.75zM12.5 17h-9v-.75c0-1.5 3-2.25 4.5-2.25s4.5.75 4.5 2.25V17z" />
|
|
</svg>
|
|
</button>
|
|
</sm-input>
|
|
</div>
|
|
<button id="add_token_receiver"
|
|
class="button button--small gap-0-5 justify-self-start hidden"
|
|
onclick="addTokenReceiver()">
|
|
<svg class="icon" 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="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
|
</svg>
|
|
<span>Add token receiver</span>
|
|
</button>
|
|
</div>
|
|
<sm-input id="tx_flo_amount" type="number" placeholder="FLO amount" step="0.00000001"
|
|
min="0.00000001" error-text="Invalid amount" animate required>
|
|
</sm-input>
|
|
<div id="flo_data_wrapper" class="grid gap-0-5">
|
|
<sm-textarea id="flo_data_textarea" placeholder="FLO data" rows="8" maxlength="1040"
|
|
animate>
|
|
</sm-textarea>
|
|
<div id="show_character_count">1040/1040</div>
|
|
<p id="flo_data_status"></p>
|
|
</div>
|
|
<button class="button button--primary hidden" id="fix_invalid_button"
|
|
onclick="removeInvalid()">Fix</button>
|
|
<div class="multi-state-button">
|
|
<button id="send_button" class="button button--primary cta" type="submit"
|
|
onclick="initTransaction()">Send</button>
|
|
</div>
|
|
</div>
|
|
</sm-form>
|
|
</div>
|
|
<div id="smartcontracts" class="page hidden">
|
|
<div class="grid gap-2">
|
|
<div class="grid full-bleed">
|
|
<h3>Smart Contracts</h3>
|
|
<p>
|
|
Create, participate and manage smart contracts on FLO blockchain.
|
|
</p>
|
|
</div>
|
|
<div class="grid gap-1">
|
|
<h4>Actions</h4>
|
|
<ul id="smart_contract_actions" class="flex align-center flex-wrap gap-0-5">
|
|
<li>
|
|
<a class="button smart-contract-action" href="#/smartcontracts/deposit">
|
|
<svg class="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="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" />
|
|
</svg>
|
|
<span>Deposit</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="button smart-contract-action" href="#/smartcontracts/participate">
|
|
<svg class="icon" 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 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
|
|
</svg>
|
|
<span>Participate</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="button smart-contract-action" href="#/smartcontracts/updateprice">
|
|
<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">
|
|
<rect fill="none" height="24" width="24" />
|
|
<path
|
|
d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M12.06,19v-2.01c-0.02,0-0.04,0-0.06,0 c-1.28,0-2.56-0.49-3.54-1.46c-1.71-1.71-1.92-4.35-0.64-6.29l1.1,1.1c-0.71,1.33-0.53,3.01,0.59,4.13c0.7,0.7,1.62,1.03,2.54,1.01 v-2.14l2.83,2.83L12.06,19z M16.17,14.76l-1.1-1.1c0.71-1.33,0.53-3.01-0.59-4.13C13.79,8.84,12.9,8.5,12,8.5c-0.02,0-0.04,0-0.06,0 v2.15L9.11,7.83L11.94,5v2.02c1.3-0.02,2.61,0.45,3.6,1.45C17.24,10.17,17.45,12.82,16.17,14.76z" />
|
|
</svg>
|
|
<span>Update price</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="button smart-contract-action" href="#/smartcontracts/trigger">
|
|
<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">
|
|
<rect fill="none" height="24" width="24" />
|
|
<path
|
|
d="M14.59,7.41L18.17,11H6v2h12.17l-3.59,3.59L16,18l6-6l-6-6L14.59,7.41z M2,6v12h2V6H2z" />
|
|
</svg>
|
|
<span>Trigger</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="grid gap-1">
|
|
<h4>Creation templates</h4>
|
|
<ul id="smart_contract_creation_templates">
|
|
<li>
|
|
<a class="button smart-contract-template"
|
|
href="#/smartcontracts/create?type=one-time-event&subtype=time-trigger">
|
|
<h4>
|
|
Time trigger (One time event)
|
|
</h4>
|
|
<p>
|
|
Suitable for time-specific events like crowdfunding.
|
|
</p>
|
|
<svg class="icon margin-left-0-5" 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 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
|
|
</svg>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="button smart-contract-template"
|
|
href="#/smartcontracts/create?type=one-time-event&subtype=external-trigger">
|
|
<h4>
|
|
External trigger (One time event)
|
|
</h4>
|
|
<p>
|
|
Suitable for externally triggered events like voting.
|
|
</p>
|
|
<svg class="icon margin-left-0-5" 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 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
|
|
</svg>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="button smart-contract-template"
|
|
href="#/smartcontracts/create?type=continuous-event&subtype=tokenswap">
|
|
<h4>
|
|
Continuous event
|
|
</h4>
|
|
<p>
|
|
Suitable for ongoing processes involving multiple participants, such as
|
|
token swaps.
|
|
</p>
|
|
<svg class="icon margin-left-0-5" 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 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
|
|
</svg>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="grid hidden gap-1">
|
|
<div class="flex gap-0-5 align-center">
|
|
<a class="button icon-only" href="#/smartcontracts">
|
|
<svg class="icon" 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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4>Create smart contract</h4>
|
|
</div>
|
|
<sm-form id="smart_contract_creation_form"></sm-form>
|
|
</div>
|
|
<div class="grid hidden gap-1">
|
|
<div class="flex gap-0-5 align-center">
|
|
<a class="button icon-only" href="#/smartcontracts">
|
|
<svg class="icon" 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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4>Deposit</h4>
|
|
</div>
|
|
<sm-form id="smart_contract_deposit_form"></sm-form>
|
|
</div>
|
|
<div class="grid hidden gap-1">
|
|
<div class="flex gap-0-5 align-center">
|
|
<a class="button icon-only" href="#/smartcontracts">
|
|
<svg class="icon" 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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4>Participate</h4>
|
|
</div>
|
|
<sm-form id="smart_contract_participate_form"></sm-form>
|
|
</div>
|
|
<div class="grid hidden gap-1">
|
|
<div class="flex gap-0-5 align-center">
|
|
<a class="button icon-only" href="#/smartcontracts">
|
|
<svg class="icon" 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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4>Update price</h4>
|
|
</div>
|
|
<sm-form id="smart_contract_update_form"></sm-form>
|
|
</div>
|
|
<div class="grid hidden gap-1">
|
|
<div class="flex gap-0-5 align-center">
|
|
<a class="button icon-only" href="#/smartcontracts">
|
|
<svg class="icon" 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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</a>
|
|
<h4>Trigger</h4>
|
|
</div>
|
|
<sm-form id="smart_contract_trigger_form"></sm-form>
|
|
</div>
|
|
</div>
|
|
<div id="settings" class="page hidden gap-2">
|
|
<h3>Settings</h3>
|
|
<section class="grid gap-1">
|
|
<h4>Accent color</h4>
|
|
<color-grid id="accent_color_selector"></color-grid>
|
|
</section>
|
|
<section id="deleteDB" class="grid gap-0-5">
|
|
<h4>Clear all local data</h4>
|
|
<p>This will delete all local Web Wallet data like added addresses and locally stored
|
|
transactions.After clearing local data you may experience slow loading of newly added address,
|
|
please proceed cautiously! </p>
|
|
<button class="button button--danger cta justify-self-start" onclick="deleteDB()">Clear
|
|
data</button>
|
|
</section>
|
|
<div class="flex">
|
|
<b>
|
|
Powered by
|
|
<a class="border-card" href="https://flo.cash/" target="_blank" rel="noopener noreferrer">FLO
|
|
blockchain</a>
|
|
</b>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<nav id="main_navbar">
|
|
<ul>
|
|
<li>
|
|
<a href="#/search" class="nav-item interact">
|
|
<svg class="icon" 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="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>
|
|
<span class="nav-item__title">Search</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#/contacts" class="nav-item interact">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M20 0H4v2h16V0zM4 24h16v-2H4v2zM20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 2.75c1.24 0 2.25 1.01 2.25 2.25s-1.01 2.25-2.25 2.25S9.75 10.24 9.75 9 10.76 6.75 12 6.75zM17 17H7v-1.5c0-1.67 3.33-2.5 5-2.5s5 .83 5 2.5V17z" />
|
|
</svg>
|
|
<span class="nav-item__title">Contacts</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#/send" class="nav-item interact" title='Send FLO data'>
|
|
<svg class="icon" 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="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
|
|
</svg>
|
|
<span class="nav-item__title">Send</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a id="smart_contract_link" href="#/smartcontracts" class="nav-item interact" title='Send FLO data'>
|
|
<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">
|
|
<path d="M0,0h24v24H0V0z" fill="none" />
|
|
<g>
|
|
<path
|
|
d="M19.5,3.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2L7.5,3.5L6,2v14H3v3c0,1.66,1.34,3,3,3h12c1.66,0,3-1.34,3-3V2 L19.5,3.5z M19,19c0,0.55-0.45,1-1,1s-1-0.45-1-1v-3H8V5h11V19z" />
|
|
<rect height="2" width="6" x="9" y="7" />
|
|
<rect height="2" width="2" x="16" y="7" />
|
|
<rect height="2" width="6" x="9" y="10" />
|
|
<rect height="2" width="2" x="16" y="10" />
|
|
</g>
|
|
</svg>
|
|
<span class="nav-item__title">Smart Contracts</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#/settings" class="nav-item interact">
|
|
<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>
|
|
<span class="nav-item__title">Settings</span>
|
|
</button>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<sm-popup id="add_address_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Add address</h3>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="flo_addr_to_save" placeholder="FLO address" error-text="Invalid FLO address" autofocus
|
|
data-flo-address animate required>
|
|
</sm-input>
|
|
<sm-input id="label_to_save" placeholder="Name" animate></sm-input>
|
|
<button class="button button--primary cta" type="submit" onclick="saveFloId()" disabled>Add</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="edit_saved_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Edit</h3>
|
|
</header>
|
|
<section class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h5>FLO address</h5>
|
|
<sm-copy id="edit_saved_id"></sm-copy>
|
|
</div>
|
|
<sm-form>
|
|
<sm-input id="new_addr_label" placeholder="Name" autofocus animate required></sm-input>
|
|
<div class="flex align-center space-between">
|
|
<button class="button icon-only" title="Delete this FLO address?" onclick="deleteSaved()">
|
|
<svg class="icon" 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="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
|
</svg>
|
|
</button>
|
|
<button class="button button--primary cta" type="submit" onclick="saveChanges()">Save</button>
|
|
</div>
|
|
</sm-form>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="create_flo_id_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<div class="grid gap-2">
|
|
<div id="flo_id_warning" class="grid justify-center gap-0-5">
|
|
<svg class="icon" 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="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
|
|
</svg>
|
|
<h3>Keep your keys safe!</h3>
|
|
<strong>Don't share with anyone. Once lost private key can't be
|
|
recovered.</strong>
|
|
</div>
|
|
<div id="created_flo_id"></div>
|
|
</div>
|
|
</sm-popup>
|
|
<sm-popup id="retrieve_flo_id_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<section class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h4>Forgot FLO address?</h4>
|
|
<p>In case of FLO address being lost and you have the private key associated with it. You can recover
|
|
it.</p>
|
|
</div>
|
|
<sm-form>
|
|
<div id="recovered_flo_id_wrapper" class="hidden">
|
|
<h5>Recovered FLO address</h5>
|
|
<sm-copy id="recovered_flo_id"></sm-copy>
|
|
</div>
|
|
<sm-input id="retrieve_flo_id_field" type="password" error-text="Invalid private key"
|
|
placeholder="Private key" class="password-field" data-private-key required autofocus>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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>
|
|
<button class="button button--primary cta" type="submit" onclick="retrieveFloId()">Recover</button>
|
|
</sm-form>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="saved_ids_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Select an address</h3>
|
|
</header>
|
|
<section class="grid gap-1">
|
|
<sm-input id="search_saved_ids_picker" placeholder="Search"></sm-input>
|
|
<ul id="saved_ids_picker_list" class="observe-empty-state grid gap-0-5 h-100" style="overflow-y: auto;">
|
|
</ul>
|
|
<div class="empty-state">
|
|
<h4>No saved FLO addresses</h4>
|
|
</div>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="transaction_result_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start" onclick="closePopup()">
|
|
<svg class="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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<div id="transaction_result"></div>
|
|
</sm-popup>
|
|
<template id="saved_id_template">
|
|
<li class="saved-id grid interact" tabindex="0">
|
|
<button class="interact edit-saved icon-only" title="Edit name">
|
|
<div class="saved-id__initial"></div>
|
|
<svg class="icon" 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="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
|
</svg>
|
|
</button>
|
|
<h4 class="saved-id__label"></h4>
|
|
<div class="grid align-center" style="grid-template-columns: 1fr auto;">
|
|
<div class="saved-id__flo-id overflow-ellipsis"></div>
|
|
<button class="copy-saved-id icon-only margin-left-0-5" title="Copy FLO address">
|
|
<svg class="icon" 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="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</li>
|
|
</template>
|
|
<template id="transaction_template">
|
|
<li class="transaction grid">
|
|
<div class="flex align-center">
|
|
<div class="transaction__icon flex"></div>
|
|
<a href="" class="transaction__receiver breakable"></a>
|
|
</div>
|
|
<div class="grid gap-0-3">
|
|
<div class="label">Amount</div>
|
|
<div class="transaction__amount"></div>
|
|
</div>
|
|
<div class="grid gap-0-3">
|
|
<div class="label">FLO Data</div>
|
|
<p class="transaction__flo-data breakable"></p>
|
|
</div>
|
|
<div class="flex align-center space-between">
|
|
<a href="" class="transaction__link flex align-center" target="_blank" rel="noopener noreferrer">
|
|
<svg class="icon margin-right-0-5" 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="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
|
|
</svg>
|
|
See transaction
|
|
</a>
|
|
<time class="transaction__time"></time>
|
|
</div>
|
|
</li>
|
|
</template>
|
|
<script>
|
|
// Global variables
|
|
const { html, render: renderElem } = uhtml;
|
|
const domRefs = {};
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
//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"
|
|
);
|
|
window.addEventListener("offline", () => {
|
|
notify(
|
|
"There seems to be a problem connecting to the internet, Please check you internet connection.",
|
|
"error",
|
|
{ pinned: true }
|
|
);
|
|
});
|
|
window.addEventListener("online", () => {
|
|
getRef("notification_drawer").clearAll();
|
|
notify("We are back online.", "success");
|
|
});
|
|
|
|
// Use instead of document.getElementById
|
|
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 $(selector) {
|
|
return document.querySelector(selector);
|
|
}
|
|
|
|
// returns dom with specified element
|
|
function createElement(tagName, options = {}) {
|
|
const { className, textContent, innerHTML, attributes = {} } = options
|
|
const elem = document.createElement(tagName)
|
|
for (let attribute in attributes) {
|
|
elem.setAttribute(attribute, attributes[attribute])
|
|
}
|
|
if (className)
|
|
elem.className = className
|
|
if (textContent)
|
|
elem.textContent = textContent
|
|
if (innerHTML)
|
|
elem.innerHTML = innerHTML
|
|
return elem
|
|
}
|
|
|
|
// 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 = 10
|
|
// 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()
|
|
}
|
|
|
|
document.addEventListener('popupopened', async e => {
|
|
switch (e.target.id) {
|
|
case 'saved_ids_popup':
|
|
const frag = document.createDocumentFragment()
|
|
const allSavedIds = await getArrayOfSavedIds()
|
|
allSavedIds.forEach(({ floID, name }) => {
|
|
frag.append(render.savedIdPickerCard(floID, name))
|
|
})
|
|
getRef('saved_ids_picker_list').innerHTML = ''
|
|
getRef('saved_ids_picker_list').append(frag)
|
|
getRef('search_saved_ids_picker').focusIn()
|
|
break;
|
|
}
|
|
})
|
|
document.addEventListener('popupclosed', e => {
|
|
zIndex--
|
|
switch (e.target.id) {
|
|
case 'saved_ids_popup':
|
|
getRef('saved_ids_picker_list').innerHTML = ''
|
|
getRef('search_saved_ids_picker').value = ''
|
|
break;
|
|
case 'transaction_result_popup':
|
|
renderElem(getRef('transaction_result'), html``)
|
|
break;
|
|
case 'retrieve_flo_id_popup':
|
|
getRef('recovered_flo_id_wrapper').classList.add('hidden')
|
|
break;
|
|
}
|
|
})
|
|
|
|
// displays a popup for asking permission. Use this instead of JS confirm
|
|
const getConfirmation = (title, options = {}) => {
|
|
return new Promise(resolve => {
|
|
const { message = '', cancelText = 'Cancel', confirmText = 'OK', danger = false } = options
|
|
openPopup('confirmation_popup', true)
|
|
getRef('confirm_title').innerText = title;
|
|
getRef('confirm_message').innerText = message;
|
|
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
|
|
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
|
|
confirmButton.textContent = confirmText
|
|
cancelButton.textContent = cancelText
|
|
if (danger)
|
|
confirmButton.classList.add('button--danger')
|
|
else
|
|
confirmButton.classList.remove('button--danger')
|
|
confirmButton.onclick = () => {
|
|
closePopup()
|
|
resolve(true);
|
|
}
|
|
cancelButton.onclick = () => {
|
|
closePopup()
|
|
resolve(false);
|
|
}
|
|
})
|
|
}
|
|
|
|
//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 });
|
|
}
|
|
|
|
function getFormattedTime(time, format) {
|
|
try {
|
|
if (String(time).indexOf('_'))
|
|
time = String(time).split('_')[0]
|
|
const intTime = parseInt(time)
|
|
if (String(intTime).length < 13)
|
|
time *= 1000
|
|
let [day, month, date, year] = new Date(intTime).toString().split(' '),
|
|
minutes = new Date(intTime).getMinutes(),
|
|
hours = new Date(intTime).getHours(),
|
|
currentTime = new Date().toString().split(' ')
|
|
|
|
minutes = minutes < 10 ? `0${minutes}` : minutes
|
|
let finalHours = ``;
|
|
if (hours > 12)
|
|
finalHours = `${hours - 12}:${minutes}`
|
|
else if (hours === 0)
|
|
finalHours = `12:${minutes}`
|
|
else
|
|
finalHours = `${hours}:${minutes}`
|
|
|
|
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
|
|
switch (format) {
|
|
case 'date-only':
|
|
return `${month} ${date}, ${year}`;
|
|
break;
|
|
default:
|
|
return `${month} ${date} ${year}, ${finalHours}`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
return time;
|
|
}
|
|
}
|
|
// implement event delegation
|
|
function delegate(el, event, selector, fn) {
|
|
el.addEventListener(event, function (e) {
|
|
const potentialTarget = e.target.closest(selector)
|
|
if (potentialTarget) {
|
|
e.delegateTarget = potentialTarget
|
|
fn.call(this, e)
|
|
}
|
|
})
|
|
}
|
|
|
|
function buttonLoader(id, show) {
|
|
const button = typeof id === 'string' ? document.getElementById(id) : id;
|
|
button.disabled = show;
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}
|
|
if (show) {
|
|
button.parentNode.append(createElement('sm-spinner'))
|
|
button.animate([
|
|
{
|
|
clipPath: 'circle(100%)',
|
|
},
|
|
{
|
|
clipPath: 'circle(0)',
|
|
},
|
|
], animOptions)
|
|
} else {
|
|
button.getAnimations().forEach(anim => anim.cancel())
|
|
const potentialTarget = button.parentNode.querySelector('sm-spinner')
|
|
if (potentialTarget) potentialTarget.remove();
|
|
}
|
|
}
|
|
// detect browser version
|
|
function detectBrowser() {
|
|
let ua = navigator.userAgent,
|
|
tem,
|
|
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
|
if (/trident/i.test(M[1])) {
|
|
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
|
return 'IE ' + (tem[1] || '');
|
|
}
|
|
if (M[1] === 'Chrome') {
|
|
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
|
|
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
|
|
}
|
|
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
|
|
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
|
|
return M.join(' ');
|
|
}
|
|
window.addEventListener('hashchange', e => routeTo(window.location.hash))
|
|
window.addEventListener("load", () => {
|
|
const [browserName, browserVersion] = detectBrowser().split(' ');
|
|
const supportedVersions = {
|
|
Chrome: 85,
|
|
Firefox: 75,
|
|
Safari: 13,
|
|
}
|
|
if (browserName in supportedVersions) {
|
|
if (parseInt(browserVersion) < supportedVersions[browserName]) {
|
|
notify(`${browserName} ${browserVersion} is not fully supported, some features may not work properly. Please update to ${supportedVersions[browserName]} or higher.`, 'error')
|
|
}
|
|
} else {
|
|
notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error')
|
|
}
|
|
document.body.classList.remove('hidden')
|
|
document.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closePopup()
|
|
}
|
|
})
|
|
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"));
|
|
}
|
|
});
|
|
getRef('accent_color_selector').colors = selectedColors
|
|
if (localStorage.getItem(`accent-color`)) {
|
|
const color = localStorage.getItem(`accent-color`)
|
|
getRef('accent_color_selector').selectedColor = color
|
|
document.body.style.setProperty('--accent-color', `var(${color})`);
|
|
}
|
|
else {
|
|
getRef('accent_color_selector').selectedColor = '--nice-blue'
|
|
}
|
|
document.addEventListener('colorselected', e => {
|
|
const color = e.detail.value
|
|
localStorage.setItem(`accent-color`, color);
|
|
document.body.style.setProperty('--accent-color', `var(${color})`);
|
|
})
|
|
|
|
});
|
|
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: 600,
|
|
fill: "forwards",
|
|
easing: "ease-out",
|
|
}
|
|
);
|
|
target.append(circle);
|
|
rippleAnimation.onfinish = () => {
|
|
circle.remove();
|
|
};
|
|
}
|
|
|
|
const pagesData = {
|
|
params: {}
|
|
}
|
|
|
|
let tempData
|
|
async function routeTo(targetPage, options = {}) {
|
|
const { firstLoad, hashChange } = options
|
|
let pageId
|
|
let params = {}
|
|
let searchParams
|
|
let wildcards
|
|
if (targetPage === '') {
|
|
pageId = 'search'
|
|
} else {
|
|
if (targetPage.includes('/')) {
|
|
let path;
|
|
[path, searchParams] = targetPage.split('?');
|
|
[, pageId, ...wildcards] = path.split('/')
|
|
} else {
|
|
pageId = targetPage
|
|
}
|
|
}
|
|
if (searchParams) {
|
|
const urlSearchParams = new URLSearchParams('?' + searchParams);
|
|
params = Object.fromEntries(urlSearchParams.entries());
|
|
}
|
|
if (pagesData.lastPage !== pageId) {
|
|
pagesData.wildcards = wildcards
|
|
}
|
|
if (params)
|
|
pagesData.params = params
|
|
switch (pageId) {
|
|
case 'search': {
|
|
let { type, query, page = 1 } = params
|
|
switch (type) {
|
|
case 'address':
|
|
try {
|
|
page = parseInt(page)
|
|
if (floGlobals.query.string !== query) {
|
|
checkBalance(query)
|
|
}/* else if (floGlobals.query.totalPages <= page * txsPerPage) {
|
|
loadMoreTransactions()
|
|
} else {
|
|
render.paginatedTransactions(page)
|
|
}*/
|
|
fetchTransactions(query, page).then(() => {
|
|
filterFetchedTransactions()
|
|
render.paginatedTransactions(page)
|
|
})
|
|
} catch (err) {
|
|
notify(err, 'error')
|
|
}
|
|
break;
|
|
case 'txid':
|
|
// location.href = `#/tx/${query}`
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'smartcontracts':
|
|
const [subpage] = wildcards
|
|
const { type, subtype, scName, scAddress } = params
|
|
if (subpage) {
|
|
switch (subpage) {
|
|
case 'create':
|
|
if (!type && !subtype)
|
|
getRef('smart_contract_creation_form').classList.add('split-layout')
|
|
else
|
|
getRef('smart_contract_creation_form').classList.remove('split-layout')
|
|
renderElem(getRef('smart_contract_creation_form'), render.contractCreationForm(type, subtype))
|
|
getRef('smart_contract_creation_form').querySelectorAll('[data-flo-address]').forEach(input => {
|
|
input.customValidation = floCrypto.validateFloID
|
|
})
|
|
switch (type) {
|
|
case 'one-time-event':
|
|
switch (subtype) {
|
|
case 'time-trigger':
|
|
const payeeAddressObserver = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.addedNodes.length) {
|
|
|
|
}
|
|
if (mutation.removedNodes.length) {
|
|
|
|
}
|
|
}
|
|
});
|
|
});
|
|
payeeAddressObserver.observe(document.getElementById('payee_container'), { childList: true });
|
|
break;
|
|
case 'external-trigger':
|
|
break;
|
|
}
|
|
break;
|
|
case 'continuous-event':
|
|
|
|
break;
|
|
}
|
|
showChildElement('smartcontracts', 1, { entry: slideInLeft, exit: slideOutLeft })
|
|
break;
|
|
case 'deposit': {
|
|
const filteredSmartContracts = filterSmartContracts({ type: 'continuos-event' })
|
|
if (filteredSmartContracts.length) {
|
|
let selectedSmartContract = filteredSmartContracts[0] || {}
|
|
if (scName && scAddress)
|
|
selectedSmartContract = getScDetails(scName, scAddress)
|
|
const { price, contractName, contractAddress, acceptingToken, sellingToken, tokenIdentification, contractSubType } = selectedSmartContract
|
|
history.replaceState(null, null, `#/smartcontracts/deposit?scName=${contractName}&scAddress=${contractAddress}`)
|
|
const defaultExpiration = new Date(new Date().getTime() + (floGlobals.expirationDays * 24 * 60 * 60 * 1000))
|
|
.toISOString().slice(0, -8);
|
|
renderElem(getRef('smart_contract_deposit_form'), html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Select smart contract</span>
|
|
<sm-select id="selected_smart_contract" onchange="handleSmartContractSelection('deposit')">
|
|
${render.availableSmartContractOptions(filteredSmartContracts)}
|
|
</sm-select>
|
|
</div>
|
|
<div class="grid gap-0-3">
|
|
<h3>Deposit ${sellingToken} to get ${acceptingToken}</h3>
|
|
<p>Exchange rate: 1 ${sellingToken} = ${price} ${acceptingToken}</p>
|
|
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Amount (${sellingToken})</span>
|
|
<sm-input id="deposit_amount" type="number" type="number" step="0.00000001" min="0.00000001"></sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Expiration (Time after which unspent amount will be returned)</span>
|
|
<input id="deposit_expiration" value=${defaultExpiration} type="datetime-local" required>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">FLO private key</span>
|
|
<sm-input id="depositor_private_key" class="password-field" type="password" error-text="Invalid private key" data-private-key required>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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> <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"></path> </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> <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"></path> </svg>
|
|
</label>
|
|
</sm-input>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button id="deposit_button" class="button button--primary" onclick=${deposit} type="submit" disabled>Deposit</button>
|
|
</div>
|
|
`)
|
|
} else {
|
|
renderElem(getRef('smart_contract_deposit_form'), html`
|
|
<div class="grid justify-items-center gap-0-5">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><circle cx="15.5" cy="9.5" r="1.5"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3.5c.73 0 1.39.19 1.97.53.12-.14.86-.98 1.01-1.14-.85-.56-1.87-.89-2.98-.89-1.11 0-2.13.33-2.99.88.97 1.09.01.02 1.01 1.14.59-.33 1.25-.52 1.98-.52z"/></svg>
|
|
<strong class="justify-self-center">No smart contracts found</strong>
|
|
</div>
|
|
`)
|
|
}
|
|
showChildElement('smartcontracts', 2, { entry: slideInLeft, exit: slideOutLeft })
|
|
break;
|
|
}
|
|
case 'participate': {
|
|
const filteredSmartContracts = filterSmartContracts()
|
|
if (filteredSmartContracts.length) {
|
|
let selectedSmartContract = filteredSmartContracts[0] || {}
|
|
if (scName && scAddress)
|
|
selectedSmartContract = getScDetails(scName, scAddress)
|
|
const { price, contractName, contractAddress, acceptingToken, sellingToken, tokenIdentification, userChoices, contractSubType } = selectedSmartContract
|
|
history.replaceState(null, null, `#/smartcontracts/participate?scName=${contractName}&scAddress=${contractAddress}`)
|
|
renderElem(getRef('smart_contract_participate_form'), html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Select smart contract</span>
|
|
<sm-select id="selected_smart_contract" onchange="handleSmartContractSelection('participate')">
|
|
${render.availableSmartContractOptions(filteredSmartContracts, `${contractName}_${contractAddress}`)}
|
|
</sm-select>
|
|
</div>
|
|
${userChoices ? html`
|
|
<fieldset>
|
|
<legend>Choices</legend>
|
|
<div class="grid gap-0-5">
|
|
${userChoices.map(choice => html`
|
|
<label class="flex align-center">
|
|
<input type="radio" name="choice" value="${choice}" required>
|
|
<span class="capitalize">${choice}</span>
|
|
</label>
|
|
`)}
|
|
</div>
|
|
`: ''}
|
|
${contractSubType === 'tokenswap' ? html`
|
|
<div class="grid gap-0-3">
|
|
<h3>Swap ${acceptingToken} with ${sellingToken}</h3>
|
|
<p>Exchange rate: 1 ${sellingToken} = ${price} ${acceptingToken}</p>
|
|
</div>
|
|
`: ''}
|
|
<div class="grid gap-0-5">
|
|
<span id="participation_amount_label" class="label">Participation amount (${acceptingToken || tokenIdentification})</span>
|
|
<sm-input id="participation_amount" type="number" step="0.00000001" min="0.00000001" required></sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">FLO private key</span>
|
|
<sm-input id="participant_private_key" class="password-field" type="password" error-text="Invalid private key" data-private-key required>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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> <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"></path> </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> <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"></path> </svg>
|
|
</label>
|
|
</sm-input>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button id="participate_button" class="button button--primary" onclick=${participate} type="submit" disabled>Participate</button>
|
|
</div>
|
|
`)
|
|
} else {
|
|
renderElem(getRef('smart_contract_participate_form'), html`
|
|
<div class="grid justify-items-center gap-0-5">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><circle cx="15.5" cy="9.5" r="1.5"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3.5c.73 0 1.39.19 1.97.53.12-.14.86-.98 1.01-1.14-.85-.56-1.87-.89-2.98-.89-1.11 0-2.13.33-2.99.88.97 1.09.01.02 1.01 1.14.59-.33 1.25-.52 1.98-.52z"/></svg>
|
|
<strong class="justify-self-center">No smart contracts found</strong>
|
|
</div>
|
|
`)
|
|
}
|
|
showChildElement('smartcontracts', 3, { entry: slideInLeft, exit: slideOutLeft })
|
|
break;
|
|
}
|
|
case 'updateprice': {
|
|
const filteredSmartContracts = filterSmartContracts({ type: 'continuos-event', dynamic: true })
|
|
if (filteredSmartContracts.length) {
|
|
let selectedSmartContract = filteredSmartContracts[0] || {}
|
|
if (scName && scAddress)
|
|
selectedSmartContract = getScDetails(scName, scAddress)
|
|
const { contractName, contractAddress, oracle_address, price, acceptingToken } = selectedSmartContract
|
|
history.replaceState(null, null, `#/smartcontracts/participate?scName=${contractName}&scAddress=${contractAddress}`)
|
|
renderElem(getRef('smart_contract_update_form'), html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Oracle FLO Address</span>
|
|
<sm-copy id="oracle_address" value=${oracle_address}></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Oracle FLO private key</span>
|
|
<sm-input id="oracle_private_key" class="password-field" type="password" error-text="Invalid private key" data-private-key="" required="">
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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> <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"></path> </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> <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"></path> </svg>
|
|
</label>
|
|
</sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Select smart contract</span>
|
|
<sm-select id="selected_smart_contract" onchange="handleSmartContractSelection('updateprice')">
|
|
${render.availableSmartContractOptions(filteredSmartContracts, `${contractName}_${contractAddress}`)}
|
|
</sm-select>
|
|
</div>
|
|
<p>
|
|
<span class="label">Current price: </span>
|
|
<span id="current_price" class="label">${price} ${acceptingToken}</span>
|
|
</p>
|
|
<div class="grid gap-0-5">
|
|
<span id="updated_price_label" class="label">Updated price (${acceptingToken})</span>
|
|
<sm-input id="updated_price" type="number" step="0.00000001" min="0.00000001" error-text="Minimum 0.00000001 required" required></sm-input>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button id="update_price_button" class="button button--primary" type="submit" onclick=${updatePrice} disabled>Update</button>
|
|
</div>
|
|
`)
|
|
} else {
|
|
renderElem(getRef('smart_contract_update_form'), html`
|
|
<div class="grid justify-items-center gap-0-5">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><circle cx="15.5" cy="9.5" r="1.5"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3.5c.73 0 1.39.19 1.97.53.12-.14.86-.98 1.01-1.14-.85-.56-1.87-.89-2.98-.89-1.11 0-2.13.33-2.99.88.97 1.09.01.02 1.01 1.14.59-.33 1.25-.52 1.98-.52z"/></svg>
|
|
<strong class="justify-self-center">No smart contracts found</strong>
|
|
</div>
|
|
`)
|
|
}
|
|
showChildElement('smartcontracts', 4, !firstLoad ? { entry: slideInLeft, exit: slideOutLeft } : {})
|
|
break;
|
|
}
|
|
case 'trigger': {
|
|
const filteredSmartContracts = filterSmartContracts({ type: 'one-time-event', subType: 'external-trigger' })
|
|
if (filteredSmartContracts.length) {
|
|
let selectedSmartContract = filteredSmartContracts[0] || {}
|
|
if (scName && scAddress)
|
|
selectedSmartContract = getScDetails(scName, scAddress)
|
|
const { contractName, contractAddress, price, tokenIdentification, userChoices } = selectedSmartContract
|
|
history.replaceState(null, null, `#/smartcontracts/trigger?scName=${contractName}&scAddress=${contractAddress}`)
|
|
renderElem(getRef('smart_contract_trigger_form'), html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Select smart contract</span>
|
|
<sm-select id="selected_smart_contract" onchange="handleSmartContractSelection('trigger')">
|
|
${render.availableSmartContractOptions(filteredSmartContracts, `${contractName}_${contractAddress}`)}
|
|
</sm-select>
|
|
</div>
|
|
<fieldset>
|
|
<legend>Select outcome</legend>
|
|
<div class="grid gap-0-5">
|
|
${userChoices.map(choice => html`
|
|
<label class="flex align-center">
|
|
<input type="radio" name="outcome" value=${choice} required>
|
|
<span class="capitalize">${choice}</span>
|
|
</label>
|
|
`)}
|
|
</div>
|
|
</fieldset>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Contract address FLO private key</span>
|
|
<sm-input id="trigger_private_key" class="password-field" type="password" error-text="Invalid private key" data-private-key="" required="">
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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> <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"></path> </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> <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"></path> </svg>
|
|
</label>
|
|
</sm-input>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button id="trigger_contract_button" class="button button--primary" onclick=${triggerContract} type="submit" disabled>Trigger</button>
|
|
</div>
|
|
`)
|
|
} else {
|
|
renderElem(getRef('smart_contract_trigger_form'), html`
|
|
<div class="grid justify-items-center gap-0-5">
|
|
<svg class="icon icon--big" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><circle cx="15.5" cy="9.5" r="1.5"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3.5c.73 0 1.39.19 1.97.53.12-.14.86-.98 1.01-1.14-.85-.56-1.87-.89-2.98-.89-1.11 0-2.13.33-2.99.88.97 1.09.01.02 1.01 1.14.59-.33 1.25-.52 1.98-.52z"/></svg>
|
|
<strong class="justify-self-center">No smart contracts with external triggers found</strong>
|
|
</div>
|
|
`)
|
|
}
|
|
showChildElement('smartcontracts', 5, { entry: slideInLeft, exit: slideOutLeft })
|
|
break;
|
|
}
|
|
default:
|
|
notify('Invalid page', 'error')
|
|
}
|
|
} else {
|
|
showChildElement('smartcontracts', 0, { entry: slideInRight, exit: slideOutRight })
|
|
renderElem(getRef('smart_contract_creation_form'), html``)
|
|
}
|
|
break
|
|
}
|
|
const animOptions = {
|
|
duration: 100,
|
|
fill: 'forwards',
|
|
}
|
|
let previousActiveElement = getRef('main_navbar').querySelector('.nav-item--active')
|
|
const currentActiveElement = document.querySelector(`.nav-item[href="#/${pageId}"]`)
|
|
if (currentActiveElement) {
|
|
if (getRef('main_navbar').classList.contains('hidden')) {
|
|
getRef('main_navbar').classList.remove('hide-away')
|
|
getRef('main_navbar').classList.remove('hidden')
|
|
getRef('main_navbar').animate([
|
|
{
|
|
transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
|
opacity: 0,
|
|
},
|
|
{
|
|
transform: `none`,
|
|
opacity: 1,
|
|
},
|
|
], {
|
|
duration: 100,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
})
|
|
}
|
|
getRef('main_header').classList.remove('hidden')
|
|
const previousActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(previousActiveElement)
|
|
const currentActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(currentActiveElement)
|
|
const isOnTop = previousActiveElementIndex < currentActiveElementIndex
|
|
const currentIndicator = createElement('div', { className: 'nav-item__indicator' });
|
|
let previousIndicator = getRef('main_navbar').querySelector('.nav-item__indicator')
|
|
if (!previousIndicator) {
|
|
previousIndicator = currentIndicator.cloneNode(true)
|
|
previousActiveElement = currentActiveElement
|
|
previousActiveElement.append(previousIndicator)
|
|
} else if (currentActiveElementIndex !== previousActiveElementIndex) {
|
|
const indicatorDimensions = previousIndicator.getBoundingClientRect()
|
|
const currentActiveElementDimensions = currentActiveElement.getBoundingClientRect()
|
|
let moveBy
|
|
if (isMobileView) {
|
|
moveBy = ((currentActiveElementDimensions.width - indicatorDimensions.width) / 2) + indicatorDimensions.width
|
|
} else {
|
|
moveBy = ((currentActiveElementDimensions.height - indicatorDimensions.height) / 2) + indicatorDimensions.height
|
|
}
|
|
indicatorObserver.observe(previousIndicator)
|
|
previousIndicator.animate([
|
|
{
|
|
transform: 'none',
|
|
opacity: 1,
|
|
},
|
|
{
|
|
transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `${moveBy}px` : `-${moveBy}px`})`,
|
|
opacity: 0,
|
|
},
|
|
], { ...animOptions, easing: 'ease-in' }).onfinish = () => {
|
|
previousIndicator.remove()
|
|
}
|
|
tempData = {
|
|
currentActiveElement,
|
|
currentIndicator,
|
|
isOnTop,
|
|
animOptions,
|
|
moveBy
|
|
}
|
|
}
|
|
previousActiveElement.classList.remove('nav-item--active');
|
|
currentActiveElement.classList.add('nav-item--active')
|
|
} else {
|
|
if (!getRef('main_navbar').classList.contains('hidden')) {
|
|
getRef('main_navbar').classList.add('hide-away')
|
|
getRef('main_navbar').animate([
|
|
{
|
|
transform: `none`,
|
|
opacity: 1,
|
|
},
|
|
{
|
|
transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
|
opacity: 0,
|
|
},
|
|
], {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}).onfinish = () => {
|
|
getRef('main_navbar').classList.add('hidden')
|
|
}
|
|
getRef('main_header').classList.add('hidden')
|
|
}
|
|
}
|
|
if (pagesData.lastPage !== pageId) {
|
|
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
|
|
getRef(pageId).classList.remove('hidden')
|
|
getRef(pageId).animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, fill: 'forwards', easing: 'ease' })
|
|
pagesData.lastPage = pageId
|
|
}
|
|
document.querySelectorAll('sm-input[data-flo-address]').forEach(input => input.customValidation = floCrypto.validateFloID)
|
|
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
|
|
}
|
|
|
|
const indicatorObserver = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (!entry.isIntersecting) {
|
|
const { currentActiveElement, currentIndicator, isOnTop, animOptions, moveBy } = tempData
|
|
currentActiveElement.append(currentIndicator)
|
|
currentIndicator.animate([
|
|
{
|
|
transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `-${moveBy}px` : `${moveBy}px`})`,
|
|
opacity: 0,
|
|
},
|
|
{
|
|
transform: 'none',
|
|
opacity: 1
|
|
},
|
|
], { ...animOptions, easing: 'ease-out' })
|
|
}
|
|
})
|
|
}, {
|
|
threshold: 1
|
|
})
|
|
|
|
// class based lazy loading
|
|
class LazyLoader {
|
|
constructor(container, elementsToRender, renderFn, options = {}) {
|
|
const { batchSize = 10, freshRender } = options
|
|
|
|
this.elementsToRender = elementsToRender
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
this.renderFn = renderFn
|
|
this.intersectionObserver
|
|
|
|
this.batchSize = batchSize
|
|
this.freshRender = freshRender
|
|
|
|
this.lazyContainer = document.querySelector(container)
|
|
|
|
this.update = this.update.bind(this)
|
|
this.render = this.render.bind(this)
|
|
this.init = this.init.bind(this)
|
|
this.clear = this.clear.bind(this)
|
|
}
|
|
init() {
|
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
observer.disconnect()
|
|
this.render({ lazyLoad: true })
|
|
}
|
|
})
|
|
}, {
|
|
threshold: 0.3
|
|
})
|
|
this.mutationObserver = new MutationObserver(mutationList => {
|
|
mutationList.forEach(mutation => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.addedNodes.length) {
|
|
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
this.mutationObserver.observe(this.lazyContainer, {
|
|
childList: true,
|
|
})
|
|
this.render()
|
|
}
|
|
update(elementsToRender) {
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
this.render()
|
|
}
|
|
render(options = {}) {
|
|
let { lazyLoad = false } = options
|
|
const frag = document.createDocumentFragment();
|
|
if (lazyLoad) {
|
|
this.updateStartIndex = this.updateEndIndex
|
|
this.updateEndIndex = this.arrayOfElements.length > this.updateEndIndex + this.batchSize ? this.updateEndIndex + this.batchSize : this.arrayOfElements.length
|
|
} else {
|
|
this.intersectionObserver.disconnect()
|
|
this.lazyContainer.innerHTML = ``;
|
|
this.updateStartIndex = 0
|
|
this.updateEndIndex = this.arrayOfElements.length > this.batchSize ? this.batchSize : this.arrayOfElements.length
|
|
}
|
|
for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
|
|
frag.append(this.renderFn(this.arrayOfElements[index]))
|
|
}
|
|
this.lazyContainer.append(frag)
|
|
// Callback to be called if elements are updated or rendered for first time
|
|
if (!lazyLoad && this.freshRender)
|
|
this.freshRender()
|
|
}
|
|
clear() {
|
|
this.intersectionObserver.disconnect()
|
|
this.mutationObserver.disconnect()
|
|
this.lazyContainer.innerHTML = ``;
|
|
}
|
|
reset() {
|
|
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
|
|
this.render()
|
|
}
|
|
}
|
|
function animateTo(element, keyframes, options) {
|
|
const anime = element.animate(keyframes, { ...options, fill: 'both' })
|
|
anime.finished.then(() => {
|
|
anime.commitStyles()
|
|
anime.cancel()
|
|
})
|
|
return anime
|
|
}
|
|
let isMobileView = false
|
|
const mobileQuery = window.matchMedia('(max-width: 40rem)')
|
|
function handleMobileChange(e) {
|
|
isMobileView = e.matches
|
|
}
|
|
mobileQuery.addEventListener('change', handleMobileChange)
|
|
|
|
handleMobileChange(mobileQuery)
|
|
|
|
// fetch data and return json
|
|
async function fetchJSON(url, options = {}) {
|
|
const response = await fetch(url, options)
|
|
const json = await response.json()
|
|
if (response.ok) {
|
|
return json
|
|
} else {
|
|
throw new Error(json.description)
|
|
}
|
|
}
|
|
|
|
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: 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) {
|
|
visibleElement.animate(exit, animOptions).onfinish = () => {
|
|
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
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()
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
<script>
|
|
//UI for webWallet
|
|
const render = {
|
|
savedIdCard(floID, name) {
|
|
const clone = getRef('saved_id_template').content.cloneNode(true).firstElementChild;
|
|
clone.dataset.floAddress = floID;
|
|
clone.dataset.name = name;
|
|
clone.querySelector('.saved-id__initial').textContent = name[0];
|
|
clone.querySelector('.saved-id__label').textContent = name;
|
|
clone.querySelector('.saved-id__flo-id').textContent = floID;
|
|
return clone
|
|
},
|
|
async savedIds() {
|
|
const frag = document.createDocumentFragment()
|
|
try {
|
|
const allSavedIds = await getArrayOfSavedIds();
|
|
allSavedIds.forEach(({ floID, name }) => {
|
|
frag.append(render.savedIdCard(floID, name))
|
|
})
|
|
getRef('saved_ids_list').innerHTML = '';
|
|
getRef('saved_ids_list').append(frag)
|
|
} catch (err) {
|
|
notify(err, 'error')
|
|
}
|
|
},
|
|
savedIdPickerCard(floID, name) {
|
|
return createElement('li', {
|
|
className: 'saved-id grid interact',
|
|
attributes: { 'tabindex': '0', 'data-flo-address': floID },
|
|
innerHTML: `
|
|
<div class="saved-id__initial">${name[0]}</div>
|
|
<div class="grid gap-0-5">
|
|
<h4 class="saved-id__label">${name}</h4>
|
|
<div class="saved-id__flo-id overflow-ellipsis">${floID}</div>
|
|
</div>
|
|
`
|
|
})
|
|
},
|
|
transactionCard(details) {
|
|
const { sender, receiver, floData, time, txid, netValue, mine } = details
|
|
const { query: queriedFloId } = pagesData.params
|
|
let displayAddress
|
|
const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild;
|
|
if (mine) {
|
|
clone.classList.add('mined')
|
|
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M22.0861 14.6534C22.3924 14.3472 22.7898 14.1486 23.2186 14.0876L44.6946 11.0319C45.2676 10.9504 45.8455 11.1432 46.2547 11.5524C47.2381 12.5358 46.8168 14.2128 45.4853 14.6146L28.6869 19.6843C28.3711 19.7796 28.0838 19.9515 27.8505 20.1848L27.0092 21.0261L28.3236 22.3404C29.0306 23.0475 29.0976 24.1522 28.5245 24.9346L52.2562 48.6662C53.0372 49.4473 53.0372 50.7136 52.2562 51.4946L51.4947 52.2561C50.7137 53.0372 49.4473 53.0372 48.6663 52.2561L24.9346 28.5245C24.1522 29.0976 23.0475 29.0306 22.3404 28.3236L21.0261 27.0092L20.1848 27.8505C19.9515 28.0838 19.7796 28.3711 19.6843 28.6869L14.6146 45.4853C14.2128 46.8168 12.5358 47.2381 11.5524 46.2547C11.1432 45.8455 10.9504 45.2675 11.0319 44.6946L14.0876 23.2186C14.1486 22.7898 14.3472 22.3924 14.6534 22.0861L15.3949 21.3447C14.8777 20.6386 14.8818 19.67 15.4072 18.9681C14.8209 18.1848 14.8837 17.0693 15.5958 16.3572L16.3573 15.5958C17.0694 14.8837 18.1848 14.8208 18.9681 15.4072C19.6701 14.8818 20.6386 14.8777 21.3447 15.3949L22.0861 14.6534Z"/> </svg>`;
|
|
displayAddress = 'Coinbase'
|
|
} else if (sender === receiver) {
|
|
clone.classList.add('self')
|
|
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><title>sender and receiver is same</title><path d="M.01 0h24v24h-24V0z" fill="none"/><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>`;
|
|
displayAddress = receiver
|
|
} else if (queriedFloId === sender) {
|
|
clone.classList.add('sent')
|
|
clone.querySelector('.transaction__icon').innerHTML = `<svg class="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="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
|
|
displayAddress = receiver
|
|
} else {
|
|
clone.classList.add('received')
|
|
clone.querySelector('.transaction__icon').innerHTML = `<svg class="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="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
|
|
displayAddress = sender
|
|
}
|
|
clone.querySelector('.transaction__receiver').textContent = displayAddress
|
|
clone.querySelector('.transaction__receiver').href = `https://ranchimall.github.io/flowallet/#/search?type=address&query=${displayAddress}&page=1`
|
|
if (netValue) {
|
|
clone.querySelector('.transaction__amount').textContent = `${netValue} FLO`
|
|
} else {
|
|
clone.querySelector('.transaction__amount').parentNode.remove()
|
|
}
|
|
if (floData) {
|
|
clone.querySelector('.transaction__flo-data').textContent = floData
|
|
} else {
|
|
clone.querySelector('.transaction__flo-data').parentNode.remove()
|
|
}
|
|
clone.querySelector('.transaction__link').href = `${floBlockchainAPI.current_server}tx/${txid}`
|
|
clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000)
|
|
return clone
|
|
},
|
|
paginatedTransactions(page = parseInt(pagesData.params.page) || 1) {
|
|
const { transactions, string: address, filteredTransactions, totalPages } = floGlobals.query
|
|
const renderedTransactions = (filteredTransactions || transactions)
|
|
.map(transaction => render.transactionCard(transaction))
|
|
renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`)
|
|
getRef('transactions_hero_section').scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
})
|
|
if (transactions.length) {
|
|
getRef('filter_selector').classList.remove('hidden')
|
|
} else {
|
|
getRef('filter_selector').classList.add('hidden')
|
|
}
|
|
const paginationSegments = totalPages;
|
|
let pagination = []
|
|
let startingPage = page - 2;
|
|
let showTill = page + 2;
|
|
if (startingPage < 1) {
|
|
showTill += Math.abs(startingPage) + 1;
|
|
startingPage = 1;
|
|
}
|
|
|
|
if (showTill > paginationSegments) {
|
|
startingPage -= showTill - paginationSegments;
|
|
showTill = paginationSegments;
|
|
}
|
|
if (paginationSegments > 1) {
|
|
for (let i = 1; i <= paginationSegments; i++) {
|
|
if (i === 1) {
|
|
pagination.push(html`
|
|
<a href=${`#/search?type=address&query=${address}&page=${i}`} class=${`pagination__item ${i === page ? 'pagination__item--active' : ''}`}>
|
|
${i}
|
|
</a>
|
|
`)
|
|
} else if (startingPage <= i && i <= showTill) {
|
|
pagination.push(html`
|
|
<a href=${`#/search?type=address&query=${address}&page=${i}`} class=${`pagination__item ${i === page ? 'pagination__item--active' : ''}`}>
|
|
${i}
|
|
</a>
|
|
`)
|
|
} else if (i === showTill + 1 && i < paginationSegments) {
|
|
pagination.push(html` <div class="pagination__item">...</div> `)
|
|
} else if (i == paginationSegments) {
|
|
pagination.push(html` <a href=${`#/search?type=address&query=${address}&page=${i}`} class="pagination__item">${i}</a> `)
|
|
}
|
|
}
|
|
if (startingPage > 2) {
|
|
pagination.splice(1, 0, html` <div class="pagination__item">...</div> `);
|
|
}
|
|
renderElem(getRef('pagination_wrapper'), html`${pagination}`)
|
|
getRef('pagination_wrapper').classList.remove('hidden')
|
|
} else {
|
|
getRef('pagination_wrapper').classList.add('hidden')
|
|
}
|
|
/* if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) {
|
|
document.getElementById('load_more_transactions').classList.remove('hidden')
|
|
} else {
|
|
document.getElementById('load_more_transactions').classList.add('hidden')
|
|
} */
|
|
},
|
|
availableAssetOptions() {
|
|
return (floGlobals.tokens || []).map(token => html` <sm-option value=${token}>${token}</sm-option> `)
|
|
},
|
|
availableSmartContractOptions(smartContracts = [], selected) {
|
|
return smartContracts
|
|
.map(({ contractName, contractAddress }) => html`
|
|
<sm-option class="breakable" value=${`${contractName}_${contractAddress}`} ?selected=${`${contractName}_${contractAddress}` === `${selected}`}>${`${replaceDash(contractName)} (${contractAddress})`}</sm-option>
|
|
`)
|
|
},
|
|
contractCreationForm(type, subtype) {
|
|
return html`
|
|
${!type && !subtype ? html`
|
|
<div class="grid gap-1-5">
|
|
<fieldset class="grid gap-0-5">
|
|
<legend>Type</legend>
|
|
<label>
|
|
<input type="radio" name="contract-type" value="one-time-event" ?checked=${type === 'one-time-event'} ?disabled=${type !== 'one-time-event'}>
|
|
<span>One time event</span>
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="contract-type" value="continuous-event" ?checked=${type === 'continuous-event'} ?disabled=${type !== 'continuous-event'}>
|
|
<span>Continuous event</span>
|
|
</label>
|
|
</fieldset>
|
|
<fieldset class="grid gap-0-5">
|
|
<legend>Subtype</legend>
|
|
<label>
|
|
<input type="radio" name="contract-subtype" value="time-trigger" ?checked=${subtype === 'time-trigger'} ?disabled=${subtype !== 'time-trigger'}>
|
|
<span>Time trigger</span>
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="contract-subtype" value="external-trigger" ?checked=${subtype === 'external-trigger'} ?disabled=${subtype !== 'external-trigger'}>
|
|
<span>External trigger</span>
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="contract-subtype" value="tokenswap" ?checked=${subtype === 'tokenswap'} ?disabled=${subtype !== 'tokenswap'}>
|
|
<span>Token Swap</span>
|
|
</label>
|
|
</fieldset>
|
|
</div>
|
|
` : ''}
|
|
<div class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Contract name</span>
|
|
<sm-input id="contract_name" pattern="^[a-zA-Z0-9 ]+$" error-text="Only alphabet and numbers are allowed" required> </sm-input>
|
|
</div>
|
|
${type === 'one-time-event' ? html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Asset</span>
|
|
<sm-select id="contract_asset">
|
|
${render.availableAssetOptions()}
|
|
</sm-select>
|
|
</div>
|
|
${subtype === 'time-trigger' ? html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Payee FLO addresses</span>
|
|
<ul id="payee_container" class="grid gap-1">
|
|
<li class="payee-address-wrapper">
|
|
<sm-input class="flex w-100 payee-address" placeholder="FLO address" error-text="Invalid FLO address" animate data-flo-address required> </sm-input>
|
|
<sm-input class="payee-share" placeholder="Share (%)" value="100" type="number" min="0" max="100" step="0.01" error-text="Share should be between 0-100" animate required> </sm-input>
|
|
</li>
|
|
</ul>
|
|
<button onclick=${addPayeeAddress} class="margin-right-auto gap-0-5">
|
|
<svg class="icon" 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="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
Add Address
|
|
</button>
|
|
</div>
|
|
` : html`
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Participant choices</span>
|
|
<div id="choices_container" class="grid gap-0-3">
|
|
<sm-input class="user-choice" pattern="^[a-zA-Z0-9 ]+$" placeholder="Choice 1" error-text="Only alphabet and numbers are allowed" required> </sm-input>
|
|
<sm-input class="user-choice" pattern="^[a-zA-Z0-9 ]+$" placeholder="Choice 2" error-text="Only alphabet and numbers are allowed" required> </sm-input>
|
|
</div>
|
|
<button onclick=${addChoice} class="margin-right-auto gap-0-5">
|
|
<svg class="icon" 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="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
Add choice
|
|
</button>
|
|
</div>
|
|
`}
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Expiration</span>
|
|
<input id="contract_expiration" type="datetime-local" min=${new Date(new Date().getTime() + (5 * 60 * 1000)).toISOString().slice(0, -8)} required>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Participation amount (optional)</span>
|
|
<sm-input id="contract_participation_amount" type="number" step="0.00000001" min="0.00000001"> </sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Min. subscription amount (optional)</span>
|
|
<sm-input id="contract_min_sub_amount" type="number" step="0.00000001" min="0.00000001"> </sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Max. subscription amount (optional)</span>
|
|
<sm-input id="contract_max_sub_amount" type="number" step="0.00000001" min="0.00000001"> </sm-input>
|
|
</div>
|
|
` : html`
|
|
<fieldset class="grid gap-0-5" onchange=${handlePriceTypeChange}>
|
|
<legend>Price type</legend>
|
|
<label class="flex align-center">
|
|
<input type="radio" name="price-type" value="predetermined" checked>
|
|
<span>Static</span>
|
|
</label>
|
|
<label class="flex align-center">
|
|
<input type="radio" name="price-type" value="dynamic">
|
|
<span>Dynamic</span>
|
|
</label>
|
|
</fieldset>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Deposit token</span>
|
|
<sm-select id="contract_output_token">
|
|
${render.availableAssetOptions()}
|
|
</sm-select>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Participation token</span>
|
|
<sm-select id="contract_input_token">
|
|
${render.availableAssetOptions()}
|
|
</sm-select>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<span class="label">Price (1 deposit token = ? participation token)</span>
|
|
<sm-input id="contract_initial_price" type="number" required> </sm-input>
|
|
</div>
|
|
`}
|
|
<div class="grid gap-0-5">
|
|
<span class="label">FLO private key</span>
|
|
<sm-input id="contract_creator_private_key" class="password-field"
|
|
type="password" error-text="Invalid private key" data-private-key required>
|
|
<label slot="right" class="interact">
|
|
<input type="checkbox" class="hidden" 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>
|
|
<div class="multi-state-button">
|
|
<button id="create_contract_button" class="button button--primary" onclick=${createSmartContract} type="submit" disabled>Create</button>
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
}
|
|
let transactionsLazyLoader
|
|
delegate(getRef('saved_ids_list'), 'click', '.saved-id', e => {
|
|
if (e.target.closest('.edit-saved')) {
|
|
const target = e.target.closest('.saved-id');
|
|
getRef('edit_saved_id').setAttribute('value', target.dataset.floAddress)
|
|
getRef('new_addr_label').value = target.dataset.name
|
|
openPopup('edit_saved_popup')
|
|
} else if (e.target.closest('.copy-saved-id')) {
|
|
const target = e.target.closest('.saved-id');
|
|
navigator.clipboard.writeText(target.dataset.floAddress)
|
|
target.dispatchEvent(
|
|
new CustomEvent('copy', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
})
|
|
);
|
|
} else {
|
|
const target = e.target.closest('.saved-id');
|
|
window.location.hash = `#/search?type=address&query=${target.dataset.floAddress}&page=1`
|
|
}
|
|
})
|
|
delegate(getRef('saved_ids_picker_list'), 'click', '.saved-id', e => {
|
|
const target = e.target.closest('.saved-id');
|
|
document.getElementById('tx_receiver').value = target.dataset.floAddress
|
|
document.getElementById('tx_receiver').focusIn()
|
|
closePopup()
|
|
getRef('send_form')._checkValidity()
|
|
})
|
|
|
|
getRef('filter_selector').addEventListener('change', e => {
|
|
filterFetchedTransactions()
|
|
render.paginatedTransactions()
|
|
})
|
|
const scrollToTopObserver = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
getRef('scroll_to_top').animate([
|
|
{
|
|
transform: 'scale(1)',
|
|
},
|
|
{
|
|
transform: 'scale(0)',
|
|
},
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease',
|
|
}).onfinish = () => {
|
|
getRef('scroll_to_top').classList.add('hidden')
|
|
}
|
|
} else {
|
|
getRef('scroll_to_top').classList.remove('hidden')
|
|
getRef('scroll_to_top').animate([
|
|
{
|
|
transform: 'scale(0)',
|
|
},
|
|
{
|
|
transform: 'scale(1)',
|
|
},
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease'
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
getRef('gen_new_addr_btn').addEventListener('click', () => {
|
|
const { floID, privKey } = floCrypto.generateNewID(),
|
|
card = createElement('div', {
|
|
className: 'generated-id-card',
|
|
innerHTML: `
|
|
<div>
|
|
<h5>FLO Address</h5>
|
|
<sm-copy value="${floID}"></sm-copy>
|
|
</div>
|
|
<div>
|
|
<h5>Private Key</h5>
|
|
<sm-copy value="${privKey}"></sm-copy>
|
|
</div>
|
|
`
|
|
});
|
|
getRef('created_flo_id').innerHTML = '';
|
|
getRef('created_flo_id').append(card);
|
|
openPopup('create_flo_id_popup');
|
|
});
|
|
|
|
// Create array of objects from object of objects
|
|
async function getArrayOfSavedIds() {
|
|
const savedIds = await floWebWallet.getLabels()
|
|
const arr = []
|
|
for (const key in savedIds) {
|
|
arr.push({
|
|
floID: key,
|
|
name: savedIds[key]
|
|
})
|
|
}
|
|
return arr.sort((a, b) => a.name.localeCompare(b.name))
|
|
}
|
|
|
|
getRef('search_saved_ids_picker').addEventListener('input', debounce(async e => {
|
|
const frag = document.createDocumentFragment()
|
|
const searchKey = e.target.value.trim();
|
|
let allSavedIds = await getArrayOfSavedIds();
|
|
if (searchKey !== '') {
|
|
const fuse = new Fuse(allSavedIds, { keys: ['floID', 'name'] })
|
|
allSavedIds = fuse.search(searchKey).map(v => v.item)
|
|
}
|
|
allSavedIds.forEach(({ floID, name }) => {
|
|
frag.append(render.savedIdPickerCard(floID, name))
|
|
})
|
|
getRef('saved_ids_picker_list').innerHTML = '';
|
|
getRef('saved_ids_picker_list').append(frag);
|
|
if (searchKey !== '') {
|
|
const potentialTarget = getRef('saved_ids_picker_list').firstElementChild
|
|
if (potentialTarget) {
|
|
potentialTarget.classList.add('highlight')
|
|
}
|
|
}
|
|
}, 100))
|
|
getRef('search_saved_ids_picker').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') {
|
|
const potentialTarget = getRef('saved_ids_picker_list').firstElementChild
|
|
if (potentialTarget) {
|
|
potentialTarget.click()
|
|
}
|
|
}
|
|
})
|
|
|
|
const savedIdsObserver = new MutationObserver((mutationList) => {
|
|
mutationList.forEach(mutation => {
|
|
getRef('saved_ids_tip').textContent = mutation.target.children.length === 0 ? `Click 'Add FLO address' to add a new FLO address.` : `Tap on saved IDs to see transactions and balance.`
|
|
})
|
|
})
|
|
|
|
savedIdsObserver.observe(getRef('saved_ids_list'), {
|
|
childList: true,
|
|
})
|
|
|
|
|
|
function saveFloId() {
|
|
const floID = getRef('flo_addr_to_save').value.trim()
|
|
let name = getRef('label_to_save').value.trim();
|
|
if (name == '')
|
|
name = 'Unknown'
|
|
compactIDB.addData('labels', name, floID).then((resolve) => {
|
|
insertElementAlphabetically(name, render.savedIdCard(floID, name))
|
|
closePopup()
|
|
}).catch((error) => {
|
|
notify('FLO address already saved', 'error');
|
|
})
|
|
}
|
|
|
|
function insertElementAlphabetically(name, elementToInsert) {
|
|
const elementInserted = [...getRef('saved_ids_list').children].some(child => {
|
|
if (child.dataset.name.localeCompare(name) > 0) {
|
|
child.before(elementToInsert)
|
|
return true
|
|
}
|
|
})
|
|
if (!elementInserted) {
|
|
getRef('saved_ids_list').append(elementToInsert)
|
|
}
|
|
}
|
|
|
|
getRef('get_private_key_field').addEventListener('input', debounce(e => {
|
|
if (e.target.tagName === 'SM-INPUT') {
|
|
if (e.target.isValid) {
|
|
checkSenderBalance()
|
|
} else {
|
|
resetBalance()
|
|
}
|
|
}
|
|
}, 100))
|
|
|
|
function checkSenderBalance() {
|
|
renderBalance(0, true)
|
|
const senderPrivateKey = getRef('get_private_key_field').value.trim()
|
|
const senderFloAddr = floCrypto.getFloID(senderPrivateKey)
|
|
Promise.all([
|
|
floWebWallet.getBalance(senderFloAddr),
|
|
fetchJSON(`${floGlobals.tokenApiUrl}/api/v2/floAddressBalance/${senderFloAddr}`)
|
|
]).then(([retrievedBal, { floAddressBalances }]) => {
|
|
renderBalance(parseFloat(retrievedBal))
|
|
let ownedTokens = []
|
|
for (const token in floAddressBalances) {
|
|
ownedTokens.push(html`
|
|
<label class="token-balance">
|
|
<input type="radio" name="sender-token" data-balance=${(floAddressBalances[token].balance || 0).toFixed(8)} value=${token}/>
|
|
<span>${token} </span><b class="margin-left-auto">${parseFloat((floAddressBalances[token].balance || 0).toFixed(8))}</b>
|
|
</label>
|
|
`)
|
|
}
|
|
if (ownedTokens.length) {
|
|
ownedTokens.push(html`
|
|
<label class="token-balance">
|
|
<input type="radio" name="sender-token" value="none" checked/>
|
|
<span>Don't send a token</span>
|
|
</label>
|
|
`)
|
|
renderElem(document.getElementById('sender_tokens_wrapper'), html.for(document.getElementById('sender_tokens_wrapper'), senderFloAddr)`
|
|
<div class="grid">
|
|
<h5>Tokens</h5>
|
|
<p>Select a token, if you want to send a token.</p>
|
|
</div>
|
|
<fieldset onchange=${handleTokenSelection} class="grid gap-1">
|
|
<div class="grid gap-0-5">
|
|
${ownedTokens}
|
|
</div>
|
|
</fieldset>
|
|
`)
|
|
document.getElementById('sender_tokens_wrapper').classList.remove('hidden')
|
|
handleTokenSelection()
|
|
} else {
|
|
document.getElementById('sender_tokens_wrapper').classList.add('hidden')
|
|
clearSelection()
|
|
}
|
|
}).catch((error) => {
|
|
notify(error, 'error');
|
|
resetBalance()
|
|
})
|
|
}
|
|
floGlobals.sendType = 'flo'
|
|
function clearSelection() {
|
|
getRef('flo_data_wrapper').classList.remove('hidden')
|
|
getRef('flo_data_textarea').value = ''
|
|
getRef('tx_flo_amount').value = ''
|
|
getRef('tx_flo_amount').classList.remove('hidden')
|
|
renderElem(getRef('tx_receiver_wrapper'), html`
|
|
<sm-input id="tx_receiver" class="w-100" placeholder="Receiver's FLO address" error-text="Invalid FLO address" data-flo-address animate required>
|
|
<button slot="right" class="icon-only" onclick="openPopup('saved_ids_popup')" title="Select from saved IDs">
|
|
<svg class="icon" 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="M21 5v14h2V5h-2zm-4 14h2V5h-2v14zM14 5H2c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1zM8 7.75c1.24 0 2.25 1.01 2.25 2.25S9.24 12.25 8 12.25 5.75 11.24 5.75 10 6.76 7.75 8 7.75zM12.5 17h-9v-.75c0-1.5 3-2.25 4.5-2.25s4.5.75 4.5 2.25V17z" /> </svg>
|
|
</button>
|
|
</sm-input>
|
|
`)
|
|
getRef('add_token_receiver').classList.add('hidden')
|
|
floGlobals.sendType = 'flo'
|
|
getRef('send_form').elementsChanged()
|
|
}
|
|
function handleTokenSelection() {
|
|
const selectedToken = document.getElementById('sender_tokens_wrapper').querySelector('input[type="radio"]:checked')
|
|
if (selectedToken.value === 'none')
|
|
return clearSelection(e)
|
|
const tokenName = selectedToken.value
|
|
const tokenBalance = parseFloat(selectedToken.dataset.balance)
|
|
getRef('flo_data_wrapper').classList.add('hidden')
|
|
getRef('tx_flo_amount').value = '0.001'
|
|
getRef('tx_flo_amount').classList.add('hidden')
|
|
renderElem(getRef('tx_receiver_wrapper'), html``)
|
|
addTokenReceiver()
|
|
getRef('add_token_receiver').classList.remove('hidden')
|
|
floGlobals.sendType = 'token'
|
|
getRef('send_form').elementsChanged()
|
|
}
|
|
function addTokenReceiver() {
|
|
const selectedToken = document.getElementById('sender_tokens_wrapper').querySelector('input[type="radio"]:checked')
|
|
const tokenName = selectedToken.value;
|
|
const isFirst = getRef('tx_receiver_wrapper').children.length === 0
|
|
getRef('tx_receiver_wrapper').append(html.node`
|
|
<div class=${`token-receiver-combo ${isFirst ? '' : 'token-receiver-combo--removable'} grid gap-0-5`}>
|
|
<sm-input class="token-receiver" class="w-100" placeholder="Receiver's FLO address" error-text="Invalid FLO address" data-flo-address animate required>
|
|
<button slot="right" class="icon-only" onclick="openPopup('saved_ids_popup')" title="Select from saved IDs">
|
|
<svg class="icon" 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="M21 5v14h2V5h-2zm-4 14h2V5h-2v14zM14 5H2c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1zM8 7.75c1.24 0 2.25 1.01 2.25 2.25S9.24 12.25 8 12.25 5.75 11.24 5.75 10 6.76 7.75 8 7.75zM12.5 17h-9v-.75c0-1.5 3-2.25 4.5-2.25s4.5.75 4.5 2.25V17z" /> </svg>
|
|
</button>
|
|
</sm-input>
|
|
<sm-input class="token-amount" type="number" placeholder=${`${tokenName.charAt(0).toUpperCase() + tokenName.slice(1)} amount`}
|
|
step="0.00000001" min="0.00000001" error-text="Min. amount is 0.00000001" animate required>
|
|
</sm-input>
|
|
${!isFirst ? html.node`
|
|
<button class="button icon-only remove-token-receiver" onclick="removeTokenReceiver(this)" title="Remove">
|
|
<svg class="icon" 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="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
`)
|
|
getRef('send_form').elementsChanged()
|
|
}
|
|
function removeTokenReceiver(elem) {
|
|
elem.closest('.grid').remove()
|
|
}
|
|
getRef('tx_flo_amount').addEventListener('input', e => {
|
|
const floAmount = parseFloat(e.target.value.trim())
|
|
const { rangeOverflow, rangeUnderflow } = e.target.validity;
|
|
if (rangeUnderflow)
|
|
e.target.setAttribute('error-text', `Minimum 0.00000001 FLO allowed`)
|
|
if (rangeOverflow)
|
|
e.target.setAttribute('error-text', `You can send FLO upto ${parseFloat(document.getElementById('sender_balance').textContent) - floGlobals.fee} only`)
|
|
})
|
|
|
|
function renderBalance(balance = 0, loading = false) {
|
|
getRef('tx_flo_amount').setAttribute('max', balance)
|
|
renderElem(getRef('balance_card'), html`
|
|
<div class="grid">
|
|
<div class="flex align-center space-between">
|
|
<h5>Balance</h5>
|
|
<button id="refresh_balance_button" class="button button--small button--colored hidden"
|
|
onclick="checkSenderBalance()">Refresh</button>
|
|
</div>
|
|
<div class="flex align-end gap-0-3">
|
|
<b id="sender_balance" style="font-size: 2.5rem;line-height: 1;">${loading ? html`<sm-spinner></sm-spinner>` : balance}</b>
|
|
<span>FLO</span>
|
|
</div>
|
|
${!loading && balance == 0 ? html`
|
|
<p style="margin-top: 1.5rem; color: var(--danger-color)">
|
|
You don't have any FLO in your wallet. Please add some FLO to your wallet to send FLO.
|
|
</p>
|
|
` : ''}
|
|
</div>
|
|
<div id="sender_tokens_wrapper" class="grid gap-1 hidden"></div>
|
|
`)
|
|
}
|
|
|
|
function resetBalance() {
|
|
clearSelection()
|
|
renderElem(getRef('balance_card'), html`
|
|
<div class="flex align-center space-between">
|
|
<h5>Balance</h5>
|
|
</div>
|
|
<p style="line-height: 1.2; opacity: 0.7;">
|
|
Sender balance will be shown once you enter it's private key
|
|
</p>
|
|
`)
|
|
}
|
|
|
|
|
|
function saveChanges() {
|
|
let name = getRef('new_addr_label').value.trim();
|
|
if (name == '')
|
|
name = 'Unknown';
|
|
compactIDB.writeData('labels', name, getRef('edit_saved_id').value).then((resolve) => {
|
|
const potentialTarget = getRef('saved_ids_list').querySelector(`.saved-id[data-flo-address="${getRef('edit_saved_id').value}"]`)
|
|
if (potentialTarget) {
|
|
potentialTarget.dataset.name = name;
|
|
potentialTarget.querySelector('.saved-id__label').textContent = name;
|
|
potentialTarget.querySelector('.saved-id__initial').textContent = name.charAt(0).toUpperCase();
|
|
// place the renamed card in alphabetically correct position
|
|
const clone = potentialTarget.cloneNode(true);
|
|
potentialTarget.remove();
|
|
insertElementAlphabetically(name, clone)
|
|
}
|
|
closePopup()
|
|
}).catch((error) => {
|
|
console.error(error);
|
|
})
|
|
}
|
|
|
|
function deleteSaved() {
|
|
getConfirmation('Do you want delete this FLO address?', {
|
|
confirmText: 'Delete',
|
|
}).then(res => {
|
|
if (res) {
|
|
const toDelete = getRef('saved_ids_list').querySelector(`.saved-id[data-flo-address="${getRef('edit_saved_id').value}"]`)
|
|
if (toDelete)
|
|
toDelete.remove();
|
|
compactIDB.removeData('labels', getRef('edit_saved_id').value);
|
|
closePopup()
|
|
}
|
|
})
|
|
}
|
|
|
|
function togglePrivateKeyVisibility(input) {
|
|
const target = input.closest('sm-input')
|
|
target.type = target.type === 'password' ? 'text' : 'password';
|
|
target.focusIn()
|
|
}
|
|
|
|
function retrieveFloId() {
|
|
const privKey = getRef('retrieve_flo_id_field').value.trim()
|
|
floWebWallet.recoverAddr(privKey).then(({ floID }) => {
|
|
getRef('recovered_flo_id_wrapper').classList.remove('hidden')
|
|
getRef('recovered_flo_id').value = floID
|
|
})
|
|
}
|
|
|
|
async function checkBalance(address) {
|
|
try {
|
|
getRef('address_details_wrapper').classList.remove('hidden')
|
|
floWebWallet.getLabels().then(allLabels => {
|
|
if (allLabels[queriedFloId]) {
|
|
getRef('queried_flo_address').innerHTML = `<h4>${allLabels[queriedFloId]}</h4> <sm-copy clip-text value=${queriedFloId}></sm-copy>`;
|
|
} else {
|
|
getRef('queried_flo_address').innerHTML = `
|
|
<p class="label">FLO Address </p>
|
|
<h4><sm-copy clip-text value=${queriedFloId}></sm-copy></h4>
|
|
`;
|
|
}
|
|
})
|
|
const queriedFloId = address || getRef('search_query_input').value.trim()
|
|
getRef('token_list_wrapper').classList.add('hidden')
|
|
getRef('flo_balance').innerHTML = `<sm-spinner></sm-spinner>`;
|
|
const [floBalance, tokenBalances] = await Promise.all([
|
|
floWebWallet.getBalance(queriedFloId),
|
|
fetchJSON(`${floGlobals.tokenApiUrl}/api/v2/floAddressBalance/${queriedFloId}`).then(({ floAddressBalances }) => floAddressBalances)
|
|
])
|
|
let ownedTokens = []
|
|
for (const token in tokenBalances) {
|
|
ownedTokens.push(html`
|
|
<li class="token-item">
|
|
<span>${token}: </span><span>${parseFloat((tokenBalances[token].balance || 0).toFixed(8))}</span>
|
|
</li>
|
|
`)
|
|
}
|
|
if (ownedTokens.length) {
|
|
renderElem(getRef('token_list'), html`${ownedTokens}`)
|
|
getRef('token_list_wrapper').classList.remove('hidden')
|
|
}
|
|
// retrieve FLO balance
|
|
getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(8))} FLO`;
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
function categorizeText(text) {
|
|
return new Promise((resolve, reject) => {
|
|
if (text.length == 34 && floCrypto.validateFloID(text)) {
|
|
//console.log('data entered is a FLO address');
|
|
resolve('address')
|
|
} else if (text.length == 64 && returnHexNumber(text)) {
|
|
fetchJson(`${floGlobals.tokenApiUrl}/api/v2/categoriseString/` + text)
|
|
.then(function (myJson) {
|
|
resolve(myJson['type'])
|
|
}).catch(err => {
|
|
reject(err)
|
|
})
|
|
} else {
|
|
resolve('unknown')
|
|
}
|
|
})
|
|
}
|
|
floGlobals.query = {
|
|
transactions: [],
|
|
string: '',
|
|
filteredTransactions: null
|
|
}
|
|
const txsPerPage = 100;
|
|
async function fetchTransactions(address, page = 1) {
|
|
try {
|
|
document.getElementById('load_more_transactions').classList.add('hidden')
|
|
renderElem(getRef('pagination_wrapper'), html``)
|
|
renderElem(getRef('queried_address_transactions'), html`
|
|
<div class="grid gap-1 justify-items-center text-center" style="margin: 3rem 0">
|
|
<sm-spinner></sm-spinner>
|
|
<span>Loading transactions...</span>
|
|
</div>
|
|
`)
|
|
const { items, totalPages } = await floWebWallet.listTransactions(address, { pageSize: txsPerPage, page })
|
|
floGlobals.query = {
|
|
transactions: items,
|
|
string: address,
|
|
filteredTransactions: null,
|
|
totalPages
|
|
}
|
|
} catch (err) {
|
|
renderElem(getRef('queried_address_transactions'), html` <span>Failed to load transactions</span> `)
|
|
throw err;
|
|
}
|
|
}
|
|
function filterFetchedTransactions() {
|
|
const filter = getRef('filter_selector').value;
|
|
if (filter !== 'all') {
|
|
floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => {
|
|
switch (filter) {
|
|
case 'sent':
|
|
return t.sender === floGlobals.query.string
|
|
break
|
|
case 'received':
|
|
return t.receiver === floGlobals.query.string
|
|
break
|
|
case 'mined':
|
|
return t.mined
|
|
break
|
|
}
|
|
})
|
|
} else {
|
|
floGlobals.query.filteredTransactions = null
|
|
}
|
|
}
|
|
async function processQuery() {
|
|
const query = getRef('search_query_input').value.trim()
|
|
categorizeText(query).then(async type => {
|
|
window.location.hash = `#/search?type=${type}&query=${query}&page=1`
|
|
}).catch(err => {
|
|
notify(err, 'error')
|
|
})
|
|
}
|
|
function loadMoreTransactions() {
|
|
fetchTransactions(floGlobals.query.string, true).then(() => {
|
|
filterFetchedTransactions()
|
|
render.paginatedTransactions()
|
|
})
|
|
}
|
|
|
|
async function initTransaction() {
|
|
try {
|
|
const privKey = getRef('get_private_key_field').value.trim();
|
|
const sender = floCrypto.getFloID(privKey)
|
|
const floAmount = parseFloat(getRef('tx_flo_amount').value.trim());
|
|
const floData = getRef('flo_data_textarea').value.trim();
|
|
const selectedToken = document.getElementById('sender_tokens_wrapper').querySelector('input[type="radio"]:checked')
|
|
let transactionId
|
|
if (floGlobals.sendType === 'flo') {
|
|
const receiver = document.getElementById('tx_receiver').value.trim();
|
|
const consent = await getConfirmation(`Confirm transaction`, {
|
|
message: ` Sending ${floAmount} FLO to ${receiver} `,
|
|
confirmText: 'Send',
|
|
})
|
|
if (!consent) return;
|
|
buttonLoader('send_button', true)
|
|
transactionId = await floWebWallet.sendTransaction(sender, receiver, floAmount, floData, privKey)
|
|
showTransactionResult(true, transactionId);
|
|
} else {
|
|
const bulkTokenReceivers = {}
|
|
getRef('tx_receiver_wrapper').querySelectorAll('.token-receiver-combo').forEach(elem => {
|
|
const receiverFloAddress = elem.querySelector('.token-receiver').value.trim()
|
|
const receiverTokenAmount = parseFloat(elem.querySelector('.token-amount').value.trim())
|
|
if (receiverFloAddress !== '' && receiverTokenAmount) {
|
|
if (!bulkTokenReceivers[receiverFloAddress])
|
|
bulkTokenReceivers[receiverFloAddress] = 0
|
|
bulkTokenReceivers[receiverFloAddress] += receiverTokenAmount
|
|
}
|
|
})
|
|
const tokenReceivers = Object.keys(bulkTokenReceivers)
|
|
if (tokenReceivers.length) {
|
|
const consent = await getConfirmation(`Confirm transaction`, {
|
|
message: `
|
|
Sending ${selectedToken.value} tokens to \n\n ${tokenReceivers.map(r => `${r}: ${bulkTokenReceivers[r]} ${selectedToken.value}`).join('\n')}
|
|
`,
|
|
confirmText: 'Send',
|
|
})
|
|
if (!consent) return;
|
|
buttonLoader('send_button', true)
|
|
if (tokenReceivers.length > 1) {
|
|
// transfer tokens to multiple addresses
|
|
transactionIds = await floWebWallet.bulkTransferTokens(sender, privKey, selectedToken.value, bulkTokenReceivers)
|
|
showTransactionResult(true, null, {
|
|
title: `Multiple transactions have been initiated`,
|
|
description: html`
|
|
<ul class="grid gap-0-5">
|
|
${tokenReceivers.map((receiver, index) => html`
|
|
<li>
|
|
<a href=${`${floBlockchainAPI.current_server}tx/${transactionIds[receiver]}`} target="_blank">
|
|
Check ${receiver} (${bulkTokenReceivers[receiver]} ${selectedToken.value}) transaction
|
|
</a>
|
|
</li>
|
|
`)}
|
|
</ul>
|
|
`
|
|
});
|
|
} else {
|
|
const floData = `send ${bulkTokenReceivers[tokenReceivers[0]]} ${selectedToken.value}#`
|
|
transactionId = await floBlockchainAPI.writeData(sender, floData, privKey, tokenReceivers[0])
|
|
showTransactionResult(true, transactionId);
|
|
}
|
|
} else {
|
|
notify('Please enter at least one receiver', 'error')
|
|
buttonLoader('send_button', false)
|
|
return
|
|
}
|
|
}
|
|
getRef('send_form').reset();
|
|
buttonLoader('send_button', false)
|
|
getRef('send_button').disabled = true;
|
|
resetBalance()
|
|
} catch (e) {
|
|
console.error(e)
|
|
showTransactionResult(false, e);
|
|
buttonLoader('send_button', false)
|
|
}
|
|
}
|
|
|
|
function showTransactionResult(success, result, options = {}) {
|
|
let { title, description } = options
|
|
if (!title)
|
|
title = success ? 'Transaction request sent' : 'Transaction failed'
|
|
if (!description)
|
|
description = success ? 'This might take upto 30 mins to complete and reflect on blockchain.' : result
|
|
|
|
renderElem(getRef('transaction_result'), html`
|
|
${success ? html`
|
|
<svg class="icon icon--success" 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="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" /> </svg>
|
|
` : ''}
|
|
${!success ? html`
|
|
<svg class="icon 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 id="transaction_result__title">${title}</h3>
|
|
<p id="transaction_result__description"> ${description} </p>
|
|
${success && result ? html`
|
|
<div class="grid gap-1">
|
|
<a id="transaction_link" href=${`${floBlockchainAPI.current_server}tx/${result}`} style="margin-top: 1.5rem;" target="_blank">See transaction on blockchain</a>
|
|
<div class="grid">
|
|
<span class="label">Transaction ID</span>
|
|
<sm-copy class="justify-self-center" value=${result}></sm-copy>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
`)
|
|
openPopup('transaction_result_popup')
|
|
}
|
|
|
|
|
|
function deleteDB() {
|
|
getConfirmation('Clear all local data?', {
|
|
confirmText: 'Clear',
|
|
}, () => {
|
|
compactIDB.deleteDB().then((message) => {
|
|
notify(message, 'success');
|
|
onLoadStartUp()
|
|
}).catch((error) => {
|
|
notify(error, 'error');
|
|
})
|
|
})
|
|
}
|
|
|
|
getRef('flo_data_textarea').addEventListener('keydown', e => {
|
|
if (e.target.value.length > 1040)
|
|
e.preventDefault()
|
|
})
|
|
|
|
getRef('flo_data_textarea').addEventListener('input', function (e) {
|
|
if (this.value.trim() !== '') {
|
|
getRef('send_button').classList.add('hidden')
|
|
getRef('fix_invalid_button').classList.remove('hidden')
|
|
}
|
|
else {
|
|
getRef('send_button').classList.remove('hidden')
|
|
getRef('fix_invalid_button').classList.add('hidden')
|
|
}
|
|
if (1040 - this.value.length) {
|
|
getRef('show_character_count').textContent = `${1040 - this.value.length} /1040`
|
|
}
|
|
else
|
|
getRef('show_character_count').textContent = `You can only add FLO data upto 1040 characters.`
|
|
checkFloData()
|
|
})
|
|
function checkFloData() {
|
|
const floDataText = getRef('flo_data_textarea').value;
|
|
if (/^[\x20-\x7E]*$/.test(floDataText)) {
|
|
getRef('send_button').classList.remove('hidden')
|
|
getRef('fix_invalid_button').classList.add('hidden')
|
|
getRef('flo_data_status').textContent = ''
|
|
}
|
|
else {
|
|
getRef('flo_data_status').textContent = 'FLO data contains invalid characters. Use "Fix" to remove invalid characters.'
|
|
}
|
|
}
|
|
function removeInvalid() {
|
|
const floDataText = getRef('flo_data_textarea').value;
|
|
getRef('flo_data_textarea').value = floDataText.replace(/\s/g, " ").replace(/[^\x20-\x7E]/g, '');
|
|
checkFloData()
|
|
}
|
|
|
|
const selectedColors = [
|
|
'--dark-red',
|
|
'--red',
|
|
'--kinda-pink',
|
|
'--purple',
|
|
'--shady-blue',
|
|
'--nice-blue',
|
|
'--maybe-cyan',
|
|
'--teal',
|
|
'--mint-green',
|
|
'--greenish-yellow',
|
|
'--yellowish-green',
|
|
'--dark-teal',
|
|
'--orange',
|
|
'--tangerine',
|
|
'--redish-orange',
|
|
]
|
|
|
|
async function getContractInfo(name, address) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!name) {
|
|
name = floGlobals.smartContracts[0].contractName
|
|
address = floGlobals.smartContracts[0].contractAddress
|
|
}
|
|
fetchJSON(`${floGlobals.tokenApiUrl}/api/v2/smartContractInfo?contractName=${name}&contractAddress=${address}`)
|
|
.then(info => {
|
|
console.log(info)
|
|
const {
|
|
contractInfo: {
|
|
contractType,
|
|
numberOfDeposits,
|
|
numberOfParticipants,
|
|
priceType,
|
|
oracle_address,
|
|
contractSubtype,
|
|
status,
|
|
expiryTime,
|
|
payeeAddress,
|
|
userChoices,
|
|
tokenIdentification,
|
|
acceptingToken,
|
|
sellingToken,
|
|
contractAmount,
|
|
minimumsubscriptionamount,
|
|
maximumsubscriptionamount,
|
|
totalHonorAmount,
|
|
totalParticipationAmount,
|
|
price
|
|
}, contractAddress,
|
|
contractName
|
|
} = info
|
|
resolve({
|
|
contractName,
|
|
contractAddress,
|
|
contractType,
|
|
contractSubtype,
|
|
status,
|
|
expiryTime,
|
|
payeeAddress,
|
|
userChoices,
|
|
token: tokenIdentification,
|
|
acceptingToken,
|
|
sellingToken,
|
|
participationFees: contractAmount,
|
|
minimumsubscriptionamount,
|
|
maximumsubscriptionamount,
|
|
numberOfDeposits,
|
|
numberOfParticipants,
|
|
priceType,
|
|
oracle_address,
|
|
totalHonorAmount,
|
|
totalParticipationAmount,
|
|
price
|
|
})
|
|
}).catch(error => {
|
|
reject(error)
|
|
})
|
|
})
|
|
}
|
|
|
|
function getScDetails(name, address) {
|
|
return floGlobals.smartContracts[floGlobals.scMap.get(`${name}_${address}`)] || {}
|
|
}
|
|
|
|
function filterSmartContracts(options) {
|
|
const { type, subType, dynamic = false } = options || {}
|
|
let filteredSmartContracts = (floGlobals.smartContracts || [])
|
|
.filter(sc => sc.status === 'active')
|
|
if (type) {
|
|
filteredSmartContracts = filteredSmartContracts.filter(sc => sc.contractType === type)
|
|
if (type === 'continuos-event' && dynamic)
|
|
filteredSmartContracts = filteredSmartContracts.filter(sc => sc.oracle_address)
|
|
}
|
|
if (subType)
|
|
filteredSmartContracts = filteredSmartContracts.filter(sc => sc.contractSubType === subType)
|
|
return filteredSmartContracts
|
|
}
|
|
|
|
function handleSmartContractSelection(actionType) {
|
|
const selectedSmartContract = document.getElementById('selected_smart_contract').value
|
|
const [contractName, contractAddress] = selectedSmartContract.split('_')
|
|
window.location.hash = `#/smartcontracts/${actionType}?scName=${contractName}&scAddress=${contractAddress}`
|
|
}
|
|
function deposit() {
|
|
const selectedSmartContract = document.getElementById('selected_smart_contract').value
|
|
const [contractName, contractAddress] = (selectedSmartContract).split('_')
|
|
const { contractType, contractSubType, acceptingToken, sellingToken, tokenIdentification } = getScDetails(contractName, contractAddress)
|
|
const depositAmount = parseFloat(document.getElementById('deposit_amount').value.trim())
|
|
const depositExpiration = document.getElementById('deposit_expiration').value
|
|
const depositorPrivateKey = document.getElementById('depositor_private_key').value.trim()
|
|
const depositorAddress = floCrypto.getFloID(depositorPrivateKey)
|
|
const floData = `Deposit ${depositAmount} ${sellingToken}# to ${contractName}@ its FLO address being ${contractAddress}$ with deposit-conditions: (1) expiryTime= ${new Date(depositExpiration).toString()}`
|
|
console.log(floData)
|
|
buttonLoader('deposit_button', true)
|
|
floTokenAPI.getBalance(depositorAddress, sellingToken).then(balance => {
|
|
if (balance < depositAmount) {
|
|
buttonLoader('deposit_button', false)
|
|
return notify(`Insufficient balance.\n You have ${balance} ${sellingToken}`, 'error')
|
|
}
|
|
getConfirmation('Deposit', {
|
|
message: `Are you sure you want to deposit ${depositAmount} ${sellingToken} in ${contractName}?`,
|
|
confirmText: 'Deposit',
|
|
}).then(res => {
|
|
if (!res) return
|
|
floBlockchainAPI.sendTx(depositorAddress, contractAddress, floGlobals.sendAmt, depositorPrivateKey, floData).then(txid => {
|
|
showTransactionResult(true, txid, {
|
|
title: `${sellingToken} tokens deposited`,
|
|
description: `If your tokens are not exchanged before the expiry time, you will get them back.`
|
|
})
|
|
getRef('smart_contract_deposit_form').reset()
|
|
document.getElementById('deposit_button').disabled = true
|
|
}).catch(error => {
|
|
showTransactionResult(false, error)
|
|
console.error(error)
|
|
|
|
}).finally(() => {
|
|
buttonLoader('deposit_button', false)
|
|
})
|
|
})
|
|
}).catch(error => {
|
|
notify(`Error getting balance.`, 'error')
|
|
buttonLoader('deposit_button', false)
|
|
console.error(error)
|
|
})
|
|
}
|
|
|
|
function participate(e) {
|
|
const selectedSmartContract = document.getElementById('selected_smart_contract').value
|
|
const [contractName, contractAddress] = (selectedSmartContract).split('_')
|
|
const participationAmount = parseFloat(document.getElementById('participation_amount').value.trim())
|
|
const { contractType, contractSubType, acceptingToken, tokenIdentification } = getScDetails(contractName, contractAddress)
|
|
const participantPrivateKey = document.getElementById('participant_private_key').value.trim()
|
|
const participantAddress = floCrypto.getFloID(participantPrivateKey)
|
|
let floData
|
|
let title = 'Participation successful'
|
|
switch (contractType) {
|
|
case 'one-time-event': {
|
|
// check min,max subscription and participation amount
|
|
switch (contractSubType) {
|
|
case 'time-trigger':
|
|
floData = `send ${participationAmount} ${tokenIdentification}# to ${contractName}@`
|
|
description = html`You have participated in the event. Check out details <a href=${`https://ranchimall.github.io/floscout/#/contract/${contractName}-${contractAddress}`} target='_blank'>here</a>`
|
|
break
|
|
case 'external-trigger':
|
|
const userChoice = getRef('smart_contract_participate_form').querySelector('input[name="choice"]:checked').value
|
|
if (!userChoice)
|
|
return notify('Please select a choice', 'error')
|
|
floData = `send ${participationAmount} ${tokenIdentification}# to ${contractName}@ to FLO address ${contractAddress} with the userchoice: ${userChoice}`
|
|
description = html`You have casted your vote. Check out details <a href=${`https://ranchimall.github.io/floscout/#/contract/${contractName}-${contractAddress}`} target='_blank'>here</a>`
|
|
break
|
|
}
|
|
}
|
|
break;
|
|
case 'continuos-event': {
|
|
switch (contractSubType) {
|
|
case 'tokenswap':
|
|
floData = `send ${participationAmount} ${acceptingToken}# to ${contractName}@`
|
|
title = 'Token swap initiated'
|
|
description = html`Check your transactions and token balance <a href=${`https://ranchimall.github.io/flowallet/#/transactions/${participantAddress}`} target='_blank'>here</a>`
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
break
|
|
}
|
|
console.log(floData)
|
|
buttonLoader('participate_button', true)
|
|
floTokenAPI.getBalance(participantAddress, acceptingToken || tokenIdentification).then(balance => {
|
|
if (balance < participationAmount) {
|
|
buttonLoader('participate_button', false)
|
|
return notify(`Insufficient balance. You have ${balance} ${acceptingToken || tokenIdentification}`, 'error')
|
|
}
|
|
getConfirmation('Participate', {
|
|
message: `Are you sure you want to participate in ${contractName} with ${participationAmount} ${acceptingToken || tokenIdentification}?`,
|
|
confirmText: 'Participate',
|
|
}).then(res => {
|
|
if (!res) return
|
|
floBlockchainAPI.sendTx(participantAddress, contractAddress, floGlobals.sendAmt, participantPrivateKey, floData).then(txid => {
|
|
showTransactionResult(true, txid, {
|
|
title,
|
|
description
|
|
})
|
|
getRef('smart_contract_participate_form').reset()
|
|
document.getElementById('participate_button').disabled = true
|
|
}).catch(error => {
|
|
showTransactionResult(false, error)
|
|
console.error(error)
|
|
}).finally(() => {
|
|
buttonLoader('participate_button', false)
|
|
})
|
|
})
|
|
}).catch(error => {
|
|
notify(`Error getting balance.`, 'error')
|
|
buttonLoader('participate_button', false)
|
|
console.error(error)
|
|
})
|
|
}
|
|
|
|
function updatePrice(e) {
|
|
const selectedSmartContract = document.getElementById('selected_smart_contract').value
|
|
const [contractName, contractAddress] = (selectedSmartContract).split('_')
|
|
const oraclePrivateKey = document.getElementById('oracle_private_key').value.trim()
|
|
const oracleAddress = document.getElementById('oracle_address').value
|
|
if (!floCrypto.verifyPrivKey(oraclePrivateKey, oracleAddress)) {
|
|
return notify(`Private key doesn't match with Oracle address`, 'error')
|
|
}
|
|
const updatedPrice = parseFloat(document.getElementById('updated_price').value.trim())
|
|
const floData = ` {"price-update":{"contract-name": "${contractName}", "contract-address": "${contractAddress}", "price": ${updatedPrice}}} `
|
|
console.log(floData)
|
|
getConfirmation('Update price', {
|
|
message: `Are you sure you want to update the price of ${contractName} to ${updatedPrice} FLO?`,
|
|
confirmText: 'Update',
|
|
cancelText: 'Cancel'
|
|
}).then((res) => {
|
|
if (!res) return
|
|
buttonLoader(e.target.closest('button'), true)
|
|
floBlockchainAPI.writeData(oracleAddress, floData, oraclePrivateKey, contractAddress).then((txid) => {
|
|
showTransactionResult(true, txid, {
|
|
title: 'Price update initiated',
|
|
})
|
|
getRef('smart_contract_update_form').reset()
|
|
document.getElementById('update_price_button').disabled = true
|
|
}).catch((error) => {
|
|
showTransactionResult(false, error)
|
|
console.error(error)
|
|
}).finally(() => {
|
|
buttonLoader(e.target.closest('button'), false)
|
|
})
|
|
})
|
|
}
|
|
|
|
function triggerContract(e) {
|
|
const selectedSmartContract = document.getElementById('selected_smart_contract').value
|
|
const [contractName, contractAddress] = (selectedSmartContract).split('_')
|
|
const triggerPrivateKey = document.getElementById('trigger_private_key').value.trim()
|
|
const triggerAddress = floCrypto.getFloID(triggerPrivateKey)
|
|
const triggerOutcome = getRef('smart_contract_trigger_form').querySelector('input[name="outcome"]:checked').value
|
|
const floData = `${contractName}@ triggerCondition:"${triggerOutcome}"`
|
|
console.log(floData)
|
|
if (contractAddress !== triggerAddress) {
|
|
return notify(`Private key doesn't match with contract trigger address`, 'error')
|
|
}
|
|
getConfirmation('Trigger contract', {
|
|
message: `Triggering ${contractName} with outcome: ${triggerOutcome}`,
|
|
confirmText: 'Trigger',
|
|
cancelText: 'Cancel'
|
|
}).then((res) => {
|
|
if (!res) return
|
|
buttonLoader('trigger_contract_button', true)
|
|
floBlockchainAPI.writeData(contractAddress, floData, triggerPrivateKey, contractAddress).then((txid) => {
|
|
showTransactionResult(true, txid, {
|
|
title: 'Contract trigger initiated',
|
|
})
|
|
getRef('smart_contract_trigger_form').reset()
|
|
document.getElementById('trigger_contract_button').disabled = true
|
|
}).catch((error) => {
|
|
showTransactionResult(false, error)
|
|
console.error(error)
|
|
}).finally(() => {
|
|
buttonLoader('trigger_contract_button', false)
|
|
})
|
|
})
|
|
}
|
|
|
|
function handlePriceTypeChange(e) {
|
|
switch (e.target.value) {
|
|
case 'predetermined':
|
|
e.target.closest('sm-form').querySelector('.oracle-address-wrapper')?.remove()
|
|
break;
|
|
case 'dynamic':
|
|
e.target.closest('sm-form').querySelector('.oracle-address-wrapper')?.remove()
|
|
e.target.closest('fieldset').after(html.node`
|
|
<div class="grid gap-0-5 oracle-address-wrapper">
|
|
<span class="label">Oracle FLO address</span>
|
|
<sm-input id="contract_oracle_address" data-flo-address error-text="Invalid FLO address" required> </sm-input>
|
|
</div>
|
|
`)
|
|
break;
|
|
}
|
|
}
|
|
|
|
async function createSmartContract() {
|
|
const { type, subtype } = pagesData.params
|
|
const contractName = document.getElementById('contract_name').value.trim().replace(/\s+/g, '-')
|
|
const creatorPrivateKey = document.getElementById('contract_creator_private_key').value.trim()
|
|
const creatorAddress = floCrypto.getFloID(creatorPrivateKey)
|
|
let floData
|
|
let confirmationMessage = ''
|
|
if (floGlobals.scMap.has(`${contractName}_${creatorAddress}`))
|
|
return notify(`Contract with name: ${contractName} and address: ${creatorAddress} already exists`, 'error')
|
|
switch (type) {
|
|
case 'one-time-event':
|
|
const contractAsset = document.getElementById('contract_asset').value;
|
|
const contractExpiration = document.getElementById('contract_expiration').value;
|
|
if (new Date(contractExpiration) < new Date()) {
|
|
return notify(`Contract expiration datetime cannot be in the past`, 'error')
|
|
}
|
|
const contractParticipationAmount = parseFloat(document.getElementById('contract_participation_amount').value.trim()) || 0;
|
|
const contractMinSubAmount = parseFloat(document.getElementById('contract_min_sub_amount').value.trim()) || 0;
|
|
const contractMaxSubAmount = parseFloat(document.getElementById('contract_max_sub_amount').value.trim()) || 0;
|
|
if (contractMinSubAmount > contractMaxSubAmount) {
|
|
return notify(`Contract minimum subscription amount cannot be greater than maximum subscription amount`, 'error')
|
|
}
|
|
switch (subtype) {
|
|
case 'time-trigger':
|
|
const payeeAddressesShare = {}
|
|
document.querySelectorAll('.payee-address-wrapper').forEach((payeeAddressWrapper) => {
|
|
const payeeAddress = payeeAddressWrapper.querySelector('.payee-address').value.trim()
|
|
const payeeShare = parseFloat(payeeAddressWrapper.querySelector('.payee-share').value.trim())
|
|
if (payeeAddressesShare[payeeAddress])
|
|
payeeAddressesShare[payeeAddress] += payeeShare
|
|
else
|
|
payeeAddressesShare[payeeAddress] = payeeShare
|
|
})
|
|
// check if payeeAddresses total share is equal to 100 else add remainder
|
|
const payeeAddressesArray = Object.keys(payeeAddressesShare)
|
|
const totalShare = payeeAddressesArray.reduce((acc, payeeAddress) => acc + payeeAddressesShare[payeeAddress], 0)
|
|
if (totalShare < 100) {
|
|
console.log('total share is not equal to 100')
|
|
const res = await getConfirmation('Total share is not equal to 100', {
|
|
message: `Total share is not equal to 100. Do you want to add remainder to the last payee address?`,
|
|
confirmText: 'Add remainder',
|
|
cancelText: 'Cancel'
|
|
})
|
|
if (!res) return
|
|
const remainder = 100 - totalShare
|
|
const lastPayeeAddress = payeeAddressesArray[payeeAddressesArray.length - 1]
|
|
payeeAddressesShare[lastPayeeAddress] += remainder
|
|
const lastPayeeInput = document.getElementById('payee_container').lastElementChild.querySelector('.payee-share')
|
|
if (lastPayeeInput) {
|
|
lastPayeeInput.value = payeeAddressesShare[lastPayeeAddress]
|
|
lastPayeeInput.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}
|
|
return
|
|
} else if (totalShare > 100) {
|
|
return notify(`Total share cannot be greater than 100`, 'error')
|
|
}
|
|
const payeeAddressesShareString = Object.entries(payeeAddressesShare).map(([payeeAddress, payeeShare]) => `${payeeAddress}:${payeeShare}`).join(':')
|
|
console.log(payeeAddressesShareString)
|
|
floData = `Create a smart contract of the name ${contractName}@ of the type one-time-event* using asset ${contractAsset}# at the FLO address ${creatorAddress}$ with contract-conditions: (1) expiryTime= ${new Date(contractExpiration).toString()} (2) payeeAddress= ${payeeAddressesShareString} ${contractParticipationAmount ? `(3) contractamount = ${contractParticipationAmount}` : ''} ${contractMinSubAmount ? `(4) minimumsubscriptionamount = ${contractMinSubAmount}` : ''} ${contractMaxSubAmount ? `(5) maximumsubscriptionamount = ${contractMaxSubAmount}` : ''} end-contract-conditions`
|
|
if (floData.length > 1040) return notify(`Too many payee addresses! remove some and try again`, 'error')
|
|
confirmationMessage = `Name: ${contractName} \nType: One-time event \nSubtype: Time trigger \nAsset: ${contractAsset} \nExpiration: ${new Date(contractExpiration).toString()} \nPayee addresses: ${Object.entries(payeeAddressesShare).reduce((str, [address, share]) => `${address}: ${share}%`, '')} \nParticipation amount: ${contractParticipationAmount} ${contractAsset} \nMin subscription amount: ${contractMinSubAmount} ${contractAsset} \nMax subscription amount: ${contractMaxSubAmount} ${contractAsset}`
|
|
break;
|
|
case 'external-trigger':
|
|
const userChoices = new Set()
|
|
document.querySelectorAll('.user-choice').forEach((userChoice) => {
|
|
const userChoiceValue = userChoice.value.trim()
|
|
if (userChoiceValue !== '')
|
|
userChoices.add(userChoiceValue)
|
|
})
|
|
floData = `Create a smart contract of the name ${contractName}@ of the type one-time-event* using asset ${contractAsset}# at the FLO address ${creatorAddress}$ with contract-conditions:(1) expiryTime= ${new Date(contractExpiration).toString()} (2) userchoices= ${[...userChoices].join(' | ')} ${contractParticipationAmount ? `(3) contractamount = ${contractParticipationAmount}` : ''} ${contractMinSubAmount ? `(4) minimumsubscriptionamount = ${contractMinSubAmount}` : ''} ${contractMaxSubAmount ? `(5) maximumsubscriptionamount = ${contractMaxSubAmount}` : ''} end-contract-conditions`
|
|
if (floData.length > 1040) return notify(`Too many participant choices! remove some and try again`, 'error')
|
|
confirmationMessage = `Name: ${contractName} \nType: One-time event \nSubtype: External trigger \nAsset: ${contractAsset} \nExpiration: ${new Date(contractExpiration).toString()} \nParticipant choices: ${[...userChoices].join(' | ')} \nParticipation amount: ${contractParticipationAmount} ${contractAsset} \nMin subscription amount: ${contractMinSubAmount} ${contractAsset} \nMax subscription amount: ${contractMaxSubAmount} ${contractAsset}`
|
|
break;
|
|
}
|
|
break;
|
|
case 'continuous-event':
|
|
switch (subtype) {
|
|
case 'tokenswap':
|
|
const priceType = document.querySelector('input[name="price-type"]:checked').value
|
|
const participationToken = document.getElementById('contract_input_token').value
|
|
const depositToken = document.getElementById('contract_output_token').value
|
|
if (participationToken === depositToken) return notify(`Participation and deposit token cannot be same`, 'error')
|
|
const initialPrice = parseFloat(document.getElementById('contract_initial_price').value.trim()) || 0;
|
|
let oracleAddress
|
|
if (priceType === 'dynamic')
|
|
oracleAddress = document.getElementById('contract_oracle_address').value.trim()
|
|
floData = `Create Smart Contract with the name ${contractName}@ of the type continuous-event* at the address ${creatorAddress}$ with contract-conditions : (1) subtype = tokenswap (2) accepting_token = ${participationToken}# (3) selling_token = ${depositToken}# (4) price = '${initialPrice}' (5) priceType = ${priceType} ${oracleAddress ? `(6) oracle_address = ${oracleAddress}` : ''} end-contract-conditions`
|
|
confirmationMessage = `Name: ${contractName} \nType: Continuous event \nSubtype: Token swap \nDeposit token: ${depositToken} \nParticipation token: ${participationToken} \nInitial price: ${initialPrice} ${participationToken} per ${depositToken} \nPrice type: ${priceType} ${oracleAddress ? `\nOracle address: ${oracleAddress}` : ''}`
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
console.log(floData)
|
|
getConfirmation('Create smart contract', {
|
|
message: `Are you sure you want to create a smart contract with the following details? \n\n${confirmationMessage}`,
|
|
confirmText: 'Create',
|
|
cancelText: 'Cancel'
|
|
}).then((res) => {
|
|
if (!res) return
|
|
buttonLoader('create_contract_button', true)
|
|
floBlockchainAPI.writeData(creatorAddress, floData, creatorPrivateKey, creatorAddress).then((txid) => {
|
|
showTransactionResult(true, txid, {
|
|
title: 'Smart contract creation initiated',
|
|
description: html`Check details about your smart contract <a href=${`https://ranchimall.github.io/floscout/#/contract/${contractName}-${creatorAddress}`} target="_blank" rel="noopener noreferrer">here</a>.
|
|
<br> It may take some time for the smart contract to be created.`
|
|
})
|
|
getRef('smart_contract_creation_form').reset()
|
|
document.getElementById('create_contract_button').disabled = true
|
|
}).catch((error) => {
|
|
showTransactionResult(false, error)
|
|
console.error(error)
|
|
}).finally(() => {
|
|
buttonLoader('create_contract_button', false)
|
|
})
|
|
})
|
|
}
|
|
|
|
function addPayeeAddress() {
|
|
document.getElementById('payee_container').append(html.node`
|
|
<li class="payee-address-wrapper">
|
|
<sm-input class="flex w-100 payee-address" placeholder="FLO address" error-text="Invalid FLO address" animate data-flo-address required> </sm-input>
|
|
<sm-input class="payee-share" placeholder="Share (%)" value="100" type="number" min="0" max="100" step="0.01" error-text="Share should be between 0-100" animate required> </sm-input>
|
|
<button class="button icon-only" onclick=${removePayee}>
|
|
<svg class="icon" 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="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
</button>
|
|
</li>
|
|
`)
|
|
document.getElementById('payee_container').querySelectorAll('.payee-share').forEach((input) => {
|
|
input.value = parseFloat((100 / document.getElementById('payee_container').querySelectorAll('.payee-share').length).toFixed(2))
|
|
})
|
|
document.getElementById('payee_container').lastElementChild.querySelector('.payee-address').customValidation = floCrypto.validateFloID
|
|
}
|
|
function removePayee(e) {
|
|
e.target.closest('li').remove()
|
|
document.getElementById('payee_container').querySelectorAll('.payee-share').forEach((input) => {
|
|
input.value = parseFloat((100 / document.getElementById('payee_container').querySelectorAll('.payee-share').length).toFixed(2))
|
|
})
|
|
}
|
|
|
|
function addChoice(e) {
|
|
const choiceNo = document.getElementById('choices_container').children.length + 1
|
|
document.getElementById('choices_container').append(html.node`
|
|
<div class="choice-wrapper">
|
|
<sm-input class="user-choice" pattern="^[a-zA-Z0-9 ]+$" placeholder=${`Choice ${choiceNo}`} error-text="Only alphabet and numbers are allowed" required> </sm-input>
|
|
<button class="button icon-only" onclick=${removeChoice}>
|
|
<svg class="icon" 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="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
</button>
|
|
</dic>
|
|
`)
|
|
}
|
|
function removeChoice(e) {
|
|
e.target.closest('.choice-wrapper').remove()
|
|
document.getElementById('choices_container').querySelectorAll('.user-choice').forEach((input, index) => {
|
|
input.placeholder = `Choice ${index + 1}`
|
|
})
|
|
}
|
|
|
|
function loader(show = true) {
|
|
if (show) {
|
|
getRef('loader').classList.remove('hidden')
|
|
} else {
|
|
getRef('loader').animate([
|
|
{ transform: 'translateY(0)' },
|
|
{ transform: 'translateY(-100%)' }
|
|
], {
|
|
duration: 300,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () =>
|
|
getRef('loader').classList.add('hidden')
|
|
}
|
|
}
|
|
function replaceDash(str) {
|
|
return str.replace(/-/g, ' ')
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |