Added balance checking history

This commit is contained in:
sairaj mote 2023-11-10 03:02:12 +05:30
parent 6fa3d33e6e
commit 56e8e28d7a
7 changed files with 642 additions and 89 deletions

View File

@ -198,10 +198,16 @@ button:disabled {
}
.icon-only {
padding: 0.5rem;
height: 100%;
padding: 0;
padding: 0.4rem;
border-radius: 0.3rem;
aspect-ratio: 1/1;
}
.icon-only .icon {
height: 1em;
width: 1em;
}
a:-webkit-any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
@ -712,11 +718,14 @@ menu {
}
#main_header {
grid-area: header;
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
gap: 1rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.3);
grid-column: 1/-1;
}
#logo {
@ -796,11 +805,53 @@ theme-toggle {
main {
display: grid;
min-height: calc(100% - 5rem);
height: 100%;
grid-template-rows: auto auto 1fr;
grid-template-areas: "header" "main" "search-history";
}
aside {
grid-area: search-history;
view-transition-name: search-history;
padding-bottom: 1.5rem;
}
aside > * {
padding: 0 1rem;
}
aside h4 {
padding: 1rem;
}
.contact {
display: grid;
gap: 0.5rem;
align-items: center;
border: solid thin rgba(var(--text-color), 0.3);
padding: 0.5rem;
border-radius: 0.5rem;
}
.contact sm-chips {
background-color: rgba(var(--text-color), 0.06);
padding: 0.2rem 0.3rem;
border-radius: 0.5rem;
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.contact sm-chip {
--padding: 0.3rem 0.5rem;
font-size: 0.8rem;
--border-radius: 0.3rem;
}
.contact sm-copy {
font-size: 0.9rem;
font-weight: 500;
}
#main_section {
align-content: center;
grid-area: main;
margin-top: 3vw;
align-content: start;
padding: 1.5rem;
}
#main_section > * {
@ -853,6 +904,22 @@ main {
.popup__header {
padding: 1rem 1.5rem 0 1.5rem;
}
main {
grid-template-columns: 22rem 1fr;
grid-template-areas: "header header" "search-history main";
grid-template-rows: auto 1fr;
}
aside {
border-right: solid thin rgba(var(--text-color), 0.3);
overflow-y: auto;
}
aside h4 {
position: -webkit-sticky;
position: sticky;
top: 0;
background-color: rgba(var(--foreground-color), 1);
z-index: 1;
}
#input_wrapper {
grid-template-columns: 1fr auto;
}

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -185,9 +185,15 @@ button:disabled {
}
.icon-only {
padding: 0.5rem;
height: 100%;
padding: 0;
padding: 0.4rem;
border-radius: 0.3rem;
aspect-ratio: 1/1;
.icon {
height: 1em;
width: 1em;
}
}
a:any-link:focus-visible {
@ -658,11 +664,14 @@ menu {
}
}
#main_header {
grid-area: header;
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
gap: 1rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.3);
grid-column: 1/-1;
}
#logo {
color: inherit;
@ -737,10 +746,48 @@ theme-toggle {
}
main {
display: grid;
min-height: calc(100% - 5rem);
height: 100%;
grid-template-rows: auto auto 1fr;
grid-template-areas: "header" "main" "search-history";
}
aside {
grid-area: search-history;
view-transition-name: search-history;
& > * {
padding: 0 1rem;
}
h4 {
padding: 1rem;
}
padding-bottom: 1.5rem;
}
.contact {
display: grid;
gap: 0.5rem;
align-items: center;
border: solid thin rgba(var(--text-color), 0.3);
padding: 0.5rem;
border-radius: 0.5rem;
sm-chips {
background-color: rgba(var(--text-color), 0.06);
padding: 0.2rem 0.3rem;
border-radius: 0.5rem;
width: fit-content;
}
sm-chip {
--padding: 0.3rem 0.5rem;
font-size: 0.8rem;
--border-radius: 0.3rem;
}
sm-copy {
font-size: 0.9rem;
font-weight: 500;
}
}
#main_section {
align-content: center;
grid-area: main;
margin-top: 3vw;
align-content: start;
padding: 1.5rem;
& > * {
max-width: 36rem;
@ -791,6 +838,21 @@ main {
.popup__header {
padding: 1rem 1.5rem 0 1.5rem;
}
main {
grid-template-columns: 22rem 1fr;
grid-template-areas: "header header" "search-history main";
grid-template-rows: auto 1fr;
}
aside {
border-right: solid thin rgba(var(--text-color), 0.3);
overflow-y: auto;
h4 {
position: sticky;
top: 0;
background-color: rgba(var(--foreground-color), 1);
z-index: 1;
}
}
#input_wrapper {
grid-template-columns: 1fr auto;
}

View File

@ -15,6 +15,15 @@
<body>
<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>
<main>
<header id="main_header">
<div id="logo" class="app-brand">
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
@ -83,12 +92,17 @@
</button>
<theme-toggle></theme-toggle>
</header>
<main>
<aside class="flex flex-direction-column">
<h4>
Searched addresses
</h4>
<ul id="searched_addresses_list" class="grid gap-0-5"></ul>
</aside>
<section id="main_section" class="grid gap-1-5">
<h2>
Check USDC/USDT balance
</h2>
<sm-form>
<sm-form oninvalid="handleInvalidSearch()">
<div id="input_wrapper">
<sm-input id="private_key_input" class="password-field flex-1" placeholder="FLO/BTC private key"
type="password" data-private-key animate required>
@ -218,6 +232,7 @@
<script src="scripts/tap_combined.js" type="text/javascript"></script>
<script src="scripts/keccak.js" type="text/javascript"></script>
<script src="scripts/floEthereum.js" type="text/javascript"></script>
<script src="scripts/compactIDB.js" type="text/javascript"></script>
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="text/javascript"> </script>
<script src="scripts/usdc_balance.js" type="text/javascript"> </script>
<script>
@ -240,6 +255,48 @@
function getRef(elementId) {
return document.getElementById(elementId)
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
getRef(popupId).show({ pinned })
return getRef(popupId);
}
// hides the popup or modal
function closePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
document.addEventListener('popupopened', async e => {
switch (e.target.id) {
case 'saved_ids_popup':
const allSavedIds = await getArrayOfSavedIds()
const renderedIds = renderContactPickerList(allSavedIds)
renderElem(getRef('saved_ids_picker_list'), html`${renderedIds}`)
getRef('search_saved_ids_picker').focusIn()
break;
}
})
document.addEventListener('popupclosed', e => {
zIndex--
switch (e.target.id) {
case 'saved_ids_popup':
renderElem(getRef('saved_ids_picker_list'), html``)
getRef('search_saved_ids_picker').value = ''
floGlobals.addressSelectorTarget = null
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;
}
})
//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
@ -257,6 +314,31 @@
}
return getRef("notification_drawer").push(message, { icon, ...options });
}
// 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 createRipple(event, target) {
const circle = document.createElement("span");
const diameter = Math.max(target.clientWidth, target.clientHeight);
@ -290,13 +372,16 @@
}
function buttonLoader(id, show) {
const button = typeof id === 'string' ? document.getElementById(id) : id;
button.disabled = show;
if (!button) return
if (!button.dataset.hasOwnProperty('wasDisabled'))
button.dataset.wasDisabled = button.disabled
const animOptions = {
duration: 200,
fill: 'forwards',
easing: 'ease'
}
if (show) {
button.disabled = true
button.parentNode.append(document.createElement('sm-spinner'))
button.animate([
{
@ -307,7 +392,17 @@
},
], animOptions)
} else {
button.getAnimations().forEach(anim => anim.cancel())
button.disabled = button.dataset.wasDisabled === 'true';
button.animate([
{
clipPath: 'circle(0)',
},
{
clipPath: 'circle(100%)',
},
], animOptions).onfinish = (e) => {
button.removeAttribute('data-original-state')
}
const potentialTarget = button.parentNode.querySelector('sm-spinner')
if (potentialTarget) potentialTarget.remove();
}
@ -332,7 +427,7 @@
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
return {
isValid: floCrypto.getPubKeyHex(value),
errorText: `Invalid private key.<br> It usually starts with "L".`
errorText: `Invalid private key.<br> It usually starts with "L" or "R".`
}
}
}
@ -363,9 +458,16 @@
createRipple(e, e.target.closest("button, .interactive"));
}
});
compactIDB.initDB('floEthereum', {
contacts: {}
}).then((result) => {
renderSearchedAddressList()
}).catch((error) => {
console.error(error)
})
connectToMetaMask().then(() => {
// setMetaMaskStatus(window.ethereum.isConnected())
window.ethereum.on('networkChanged', (networkId) => {
window.ethereum.on('chainChanged', (networkId) => {
if (networkId !== '1') {
getRef('error__title').textContent = 'Please switch MetaMask to Ethereum Mainnet'
getRef('main_section').classList.add('hidden')
@ -414,18 +516,60 @@
});
}
})
function checkBalance() {
function renderSearchedAddressList() {
compactIDB.readAllData('contacts').then(contacts => {
if (Object.keys(contacts).length === 0) {
renderElem(getRef('searched_addresses_list'), html`<li class="flex align-center justify-center">
<p>Your searched addresses will appear here for easier access in future.</p>
</li>`)
return
}
const renderedContacts = []
for (const floAddress in contacts) {
const { ethAddress } = contacts[floAddress]
renderedContacts.push(html`
<li class="contact" .dataset=${{ floAddress, ethAddress }}>
<sm-chips onchange=${e => e.target.closest('.contact').querySelector('sm-copy').value = e.target.value}>
<sm-chip value=${floAddress} selected>FLO</sm-chip>
<sm-chip value=${ethAddress}>ETH</sm-chip>
</sm-chips>
<sm-copy value="${floAddress}"></sm-copy>
<div class="flex align-center space-between gap-0-5">
<button class="button button--small" onclick=${() => deleteContact(floAddress)}>
Delete
</button>
<button class="button button--colored button--small" onclick=${() => checkBalance(ethAddress, floAddress)}>Check balance</button>
</div>
</li>`)
}
renderElem(getRef('searched_addresses_list'), html`${renderedContacts}`)
}).catch((error) => {
console.error(error)
})
}
function checkBalance(ethAddress, floAddress) {
if (!ethAddress) {
const keyToConvert = document.querySelector('[data-private-key]').value.trim()
if (/^[0-9a-fA-F]{64}$/.test(keyToConvert)) {
keyToConvert = coinjs.privkey2wif(keyToConvert)
}
const ethPrivateKey = coinjs.wif2privkey(keyToConvert).privkey;
const ethAddress = floEthereum.ethAddressFromPrivateKey(ethPrivateKey)
const floAddress = floCrypto.getFloID(keyToConvert)
ethAddress = floEthereum.ethAddressFromPrivateKey(ethPrivateKey)
floAddress = floCrypto.getFloID(keyToConvert)
}
if (!ethAddress) return
buttonLoader('check_balance_button', true)
Promise.all([checkUSDCBalance(ethAddress), checkUSDTBalance(ethAddress)]).then(([usdcBalance, usdtBalance]) => {
console.log(usdcBalance, usdtBalance)
compactIDB.readData('contacts', floAddress).then(result => {
if (result) return
compactIDB.addData('contacts', {
ethAddress,
}, floAddress).then(() => {
renderSearchedAddressList()
}).catch((error) => {
console.error(error)
})
})
getRef('eth_address').value = ethAddress
getRef('flo_address').value = floAddress
getRef('usdc_balance').textContent = `${ethers.utils.formatUnits(usdcBalance, 6)} USDC`
@ -451,6 +595,27 @@
buttonLoader('check_balance_button', false)
})
}
function handleInvalidSearch() {
if (document.startViewTransition)
document.startViewTransition(() => {
getRef('eth_balance_wrapper').classList.add('hidden')
})
else {
getRef('eth_balance_wrapper').classList.add('hidden')
}
}
async function deleteContact(floAddress) {
const confirmed = await getConfirmation('Delete contact', {
message: 'Are you sure you want to delete this contact?'
})
if (!confirmed) return
compactIDB.removeData('contacts', floAddress).then(() => {
renderSearchedAddressList()
}).catch((error) => {
console.error(error)
})
}
</script>
</body>

257
scripts/compactIDB.js Normal file
View File

@ -0,0 +1,257 @@
(function (EXPORTS) { //compactIDB v2.1.2
/* Compact IndexedDB operations */
'use strict';
const compactIDB = EXPORTS;
var defaultDB;
const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
if (!indexedDB) {
console.error("Your browser doesn't support a stable version of IndexedDB.");
return;
}
compactIDB.setDefaultDB = dbName => defaultDB = dbName;
Object.defineProperty(compactIDB, 'default', {
get: () => defaultDB,
set: dbName => defaultDB = dbName
});
function getDBversion(dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
resolve(db.version)
db.close()
}).catch(error => reject(error))
})
}
function upgradeDB(dbName, createList = null, deleteList = null) {
return new Promise((resolve, reject) => {
getDBversion(dbName).then(version => {
var idb = indexedDB.open(dbName, version + 1);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onupgradeneeded = (event) => {
let db = event.target.result;
if (createList instanceof Object) {
if (Array.isArray(createList)) {
let tmp = {}
createList.forEach(o => tmp[o] = {})
createList = tmp
}
for (let o in createList) {
let obs = db.createObjectStore(o, createList[o].options || {});
if (createList[o].indexes instanceof Object)
for (let i in createList[o].indexes)
obs.createIndex(i, i, createList[o].indexes || {});
}
}
if (Array.isArray(deleteList))
deleteList.forEach(o => db.deleteObjectStore(o));
resolve('Database upgraded')
}
idb.onsuccess = (event) => event.target.result.close();
}).catch(error => reject(error))
})
}
compactIDB.initDB = function (dbName, objectStores = {}) {
return new Promise((resolve, reject) => {
if (!(objectStores instanceof Object))
return reject('ObjectStores must be an object or array')
defaultDB = defaultDB || dbName;
var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onsuccess = (event) => {
var db = event.target.result;
let cList = Object.values(db.objectStoreNames);
var obs = {},
a_obs = {},
d_obs = [];
if (!Array.isArray(objectStores))
var obs = objectStores
else
objectStores.forEach(o => obs[o] = {})
let nList = Object.keys(obs)
for (let o of nList)
if (!cList.includes(o))
a_obs[o] = obs[o]
for (let o of cList)
if (!nList.includes(o))
d_obs.push(o)
if (!Object.keys(a_obs).length && !d_obs.length)
resolve("Initiated IndexedDB");
else
upgradeDB(dbName, a_obs, d_obs)
.then(result => resolve(result))
.catch(error => reject(error))
db.close();
}
});
}
const openDB = compactIDB.openDB = function (dbName = defaultDB) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB");
idb.onupgradeneeded = (event) => {
event.target.result.close();
deleteDB(dbName).then(_ => null).catch(_ => null).finally(_ => reject("Datebase not found"))
}
idb.onsuccess = (event) => resolve(event.target.result);
});
}
const deleteDB = compactIDB.deleteDB = function (dbName = defaultDB) {
return new Promise((resolve, reject) => {
var deleteReq = indexedDB.deleteDatabase(dbName);;
deleteReq.onerror = (event) => reject("Error deleting database!");
deleteReq.onsuccess = (event) => resolve("Database deleted successfully");
});
}
compactIDB.writeData = function (obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let writeReq = (key ? obs.put(data, key) : obs.put(data));
writeReq.onsuccess = (evt) => resolve(`Write data Successful`);
writeReq.onerror = (evt) => reject(
`Write data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.addData = function (obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let addReq = (key ? obs.add(data, key) : obs.add(data));
addReq.onsuccess = (evt) => resolve(`Add data successful`);
addReq.onerror = (evt) => reject(
`Add data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.removeData = function (obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let delReq = obs.delete(key);
delReq.onsuccess = (evt) => resolve(`Removed Data ${key}`);
delReq.onerror = (evt) => reject(
`Remove data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.clearData = function (obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
let clearReq = obs.clear();
clearReq.onsuccess = (evt) => resolve(`Clear data Successful`);
clearReq.onerror = (evt) => reject(`Clear data Unsuccessful`);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.readData = function (obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
let getReq = obs.get(key);
getReq.onsuccess = (evt) => resolve(evt.target.result);
getReq.onerror = (evt) => reject(
`Read data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
compactIDB.readAllData = function (obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var tmpResult = {}
let curReq = obs.openCursor();
curReq.onsuccess = (evt) => {
var cursor = evt.target.result;
if (cursor) {
tmpResult[cursor.primaryKey] = cursor.value;
cursor.continue();
} else
resolve(tmpResult);
}
curReq.onerror = (evt) => reject(
`Read-All data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`
);
db.close();
}).catch(error => reject(error));
});
}
/* compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var filteredResult = {}
let keyRange;
if(options.lowerKey!==null && options.upperKey!==null)
keyRange = IDBKeyRange.bound(options.lowerKey, options.upperKey);
else if(options.lowerKey!==null)
keyRange = IDBKeyRange.lowerBound(options.lowerKey);
else if (options.upperKey!==null)
keyRange = IDBKeyRange.upperBound(options.upperBound);
else if (options.atKey)
let curReq = obs.openCursor(keyRange, )
}).catch(error => reject(error))
})
}*/
compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) {
options.lowerKey = options.atKey || options.lowerKey || 0
options.upperKey = options.atKey || options.upperKey || false
options.patternEval = options.patternEval || ((k, v) => true);
options.limit = options.limit || false;
options.reverse = options.reverse || false;
options.lastOnly = options.lastOnly || false
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
var filteredResult = {}
let curReq = obs.openCursor(
options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey),
options.lastOnly || options.reverse ? "prev" : "next");
curReq.onsuccess = (evt) => {
var cursor = evt.target.result;
if (!cursor || (options.limit && options.limit <= Object.keys(filteredResult).length))
return resolve(filteredResult); //reached end of key list or limit reached
else if (options.patternEval(cursor.primaryKey, cursor.value)) {
filteredResult[cursor.primaryKey] = cursor.value;
options.lastOnly ? resolve(filteredResult) : cursor.continue();
} else
cursor.continue();
}
curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`);
db.close();
}).catch(error => reject(error));
});
}
})(window.compactIDB = {});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long