KYC revocation changes

-- listing approved addresses
-- revocation is done with selection UI
-- added search option
This commit is contained in:
sairaj mote 2023-04-01 02:52:56 +05:30
parent 82e2b0993e
commit 80b7d70d86
7 changed files with 473 additions and 101 deletions

View File

@ -367,6 +367,10 @@ sm-chip[selected] {
--background: var(--accent-color);
}
sm-input {
--border-radius: 0.5rem;
}
.multi-state-button {
display: grid;
text-align: center;
@ -381,6 +385,43 @@ sm-chip[selected] {
width: 100%;
}
#confirmation_popup,
#prompt_popup {
flex-direction: column;
}
#confirmation_popup h4,
#prompt_popup h4 {
font-size: 1.2rem;
margin-bottom: 1rem;
}
#confirmation_popup .flex,
#prompt_popup .flex {
margin-top: 1rem;
}
.popup__header {
position: relative;
display: grid;
gap: 0.5rem;
width: 100%;
padding: 0 1.5rem;
align-items: center;
}
.popup__header > * {
grid-row: 1;
}
.popup__header h3,
.popup__header h4 {
grid-column: 1/-1;
justify-self: center;
align-self: center;
}
.popup__header__close {
grid-column: 1;
margin-left: -1rem;
justify-self: flex-start;
}
#loading {
position: fixed;
display: grid;
@ -414,6 +455,10 @@ header {
.address-input sm-input {
width: 100%;
}
.address-input button {
margin-bottom: auto;
height: 3.2rem;
}
#verification_result {
display: grid;
@ -444,6 +489,31 @@ header {
min-width: 5rem;
}
.revoke-card {
list-style: none;
}
.revoke-card label {
gap: 1rem;
padding: 0.8rem 0;
font-weight: 500;
cursor: pointer;
}
.revoke-card input {
accent-color: var(--accent-color);
height: 1.3em;
width: 1.3em;
}
.revoke-card span:last-of-type {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
font-weight: 400;
}
@media screen and (min-width: 40rem) {
sm-popup {
--width: 24rem;
}
}
.error {
color: var(--danger-color);
}

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -350,6 +350,9 @@ sm-chip {
--background: var(--accent-color);
}
}
sm-input {
--border-radius: 0.5rem;
}
.multi-state-button {
display: grid;
text-align: center;
@ -363,6 +366,41 @@ sm-chip {
width: 100%;
}
}
#confirmation_popup,
#prompt_popup {
flex-direction: column;
h4 {
font-size: 1.2rem;
margin-bottom: 1rem;
}
.flex {
margin-top: 1rem;
}
}
.popup__header {
position: relative;
display: grid;
gap: 0.5rem;
width: 100%;
padding: 0 1.5rem;
align-items: center;
& > * {
grid-row: 1;
}
h3,
h4 {
grid-column: 1/-1;
justify-self: center;
align-self: center;
}
&__close {
grid-column: 1;
margin-left: -1rem;
justify-self: flex-start;
}
}
#loading {
position: fixed;
display: grid;
@ -394,6 +432,10 @@ header {
sm-input {
width: 100%;
}
button {
margin-bottom: auto;
height: 3.2rem;
}
}
#verification_section {
@ -429,6 +471,32 @@ header {
min-width: 5rem;
}
}
#approved_kyc_addresses {
}
.revoke-card {
list-style: none;
label {
gap: 1rem;
padding: 0.8rem 0;
font-weight: 500;
cursor: pointer;
}
input {
accent-color: var(--accent-color);
height: 1.3em;
width: 1.3em;
}
span:last-of-type {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
font-weight: 400;
}
}
@media screen and (min-width: 40rem) {
sm-popup {
--width: 24rem;
}
}
.error {
color: var(--danger-color);
}

View File

@ -116,45 +116,6 @@
router.addRoute('verify', async state => {
verify(state.params.address)
})
floGlobals.approvedKyc = {};
function getApprovedKycs() {
return new Promise((resolve, reject) => {
const aggregatorTxs = Object.keys(floGlobals.approvedKycAggregators).map(aggregator => {
return floBlockchainAPI.readAllTxs(aggregator);
});
Promise.all(aggregatorTxs).then(aggregatorData => {
aggregatorData = aggregatorData.flat(1)
.filter(tx => tx.vin[0].addr in floGlobals.approvedKycAggregators && tx.floData.startsWith('KYC'))
.sort((a, b) => a.time - b.time);
for (const tx of aggregatorData) {
const { floData, time, vin, vout } = tx;
const [service, operationType, operationData, validity] = floData.split('|');
switch (operationType) {
case 'APPROVE_KYC':
operationData.split(',').forEach(address => {
floGlobals.approvedKyc[address] = {
validFrom: time * 1000,
validTo: validity || Date.now() + 10000000,
verifiedBy: vin[0].addr
};
});
break;
case 'REVOKE_KYC':
operationData.split(',').forEach(address => {
floGlobals.approvedKyc[address].validTo = time * 1000;
floGlobals.approvedKyc[address].revokedBy = vin[0].addr;
});
break;
default:
return;
}
}
resolve();
}).catch(e => {
reject(e);
})
})
}
function verify(address) {
if (address) {
if (getRef('address_verify').value.trim() !== address)

View File

@ -15,6 +15,14 @@
<body>
<sm-notifications id="notification_drawer"></sm-notifications>
<sm-popup id="confirmation_popup">
<h4 id="confirm_title"></h4>
<p id="confirm_message"></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="loading">
<sm-spinner></sm-spinner>
<h4>Loading</h4>
@ -60,28 +68,65 @@
<sm-chip value="revoke">Revoke</sm-chip>
</sm-chips>
</div>
<sm-form id="kyc_form">
<div class="grid gap-0-5">
<h4>Aggregator credentials</h4>
<sm-input id="aggregator_private_key" placeholder="Private key" error-text="Invalid private key"
type="password" required animate></sm-input>
<p id="aggregator_balance" class="hidden"></p>
<div class="grid gap-0-5">
<h4>Aggregator credentials</h4>
<sm-input id="aggregator_private_key" placeholder="Private key" error-text="Invalid private key"
type="password" required animate></sm-input>
<p id="aggregator_balance" class="hidden"></p>
</div>
<div id="kyc_mode_wrapper">
<sm-form>
<div class="grid gap-0-5">
<h4>KYCs to be approved</h4>
<p class="margin-bottom-1">
Enter the addresses of the KYCs you want to approve. You can add multiple addresses by
clicking the "Add address" button.
</p>
<ul id="kyc_addresses_container" class="grid gap-0-3"></ul>
<button id="add_address" class="button margin-right-auto" onclick="addAddressInput()">Add
address</button>
</div>
<div class="multi-state-button">
<button id="submit_kyc" class="button button--primary" onclick="approveAddresses()"
type="submit" disabled>Approve</button>
</div>
</sm-form>
<div class="grid gap-1 hidden">
<div class="grid gap-0-5">
<h4>Approved KYC addresses</h4>
<p>
To revoke a KYC, select one or multiple KYCs and click on the 'Revoke selected' button.
</p>
</div>
<div class="grid gap-0-5"
style="position: sticky; top: 0; z-index: 2; background-color: rgba(var(--foreground-color),1);">
<sm-input id="search_approved" type="search" placeholder="Search address">
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</sm-input>
<div class="flex gap-0-5 align-center hidden">
<button class="button icon-only" title="Clear selection" onclick="clearSelection()">
<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>
<div id="selected_addresses"></div>
<div class="multi-state-button">
<button id="revoke_kyc_button" class="button button--primary margin-left-auto"
onclick="revokeAddresses()">Revoke selected</button>
</div>
</div>
</div>
<ul id="approved_kyc_addresses" class="grid gap-0-3"></ul>
</div>
<div class="grid gap-0-5">
<h4 id="address_manage_title">KYCs to be approved</h4>
<p class="margin-bottom-1">
Enter the addresses of the KYCs you want to approve or revoke. You can add multiple addresses by
clicking the "Add address" button.
</p>
<ul id="kyc_addresses_container" class="grid gap-0-3"></ul>
<button id="add_address" class="button margin-right-auto" onclick="addAddressInput()">Add
address</button>
</div>
<div class="multi-state-button">
<button id="submit_kyc" class="button button--primary" onclick="submitAddresses()"
type="submit">Approve</button>
</div>
</sm-form>
</div>
</section>
</article>
<template id="key_address_template">
@ -153,20 +198,110 @@
if (potentialTarget) potentialTarget.remove();
}
}
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>
router.addRoute('', state => {
console.log(state)
})
const render = {
approvedKycAddressCard(address) {
const floID = floCrypto.toFloID(address)
const btcID = btcOperator.convert.legacy2bech(floID)
return html`
<li class="revoke-card" data-address=${btcID} data-search-key=${`${btcID}-${floID}`}>
<label class="flex align-center">
<input type="checkbox" value=${btcID}/>
<div class="grid gap-0-3">
<span>BTC: ${btcID}</span>
<span>FLO: ${floID}</span>
</div>
</label>
</li>
`
},
approvedKycs() {
const approvedKycs = Object.keys(floGlobals.approvedKyc)
.filter(address => !floGlobals.approvedKyc[address].revokedBy)
.map(address => render.approvedKycAddressCard(address))
renderElem(getRef('approved_kyc_addresses'), html`${approvedKycs}`)
}
}
const addressesToRevoke = new Set()
getRef('kyc_type').addEventListener('change', e => {
if (e.target.value === 'approve') {
getRef('address_manage_title').textContent = 'KYCs to be approved'
getRef('submit_kyc').textContent = 'Approve'
showChildElement('kyc_mode_wrapper', 0)
} else {
getRef('address_manage_title').textContent = 'KYCs to be revoked'
getRef('submit_kyc').textContent = 'Revoke'
showChildElement('kyc_mode_wrapper', 1)
render.approvedKycs()
}
})
getRef('search_approved').addEventListener('input', e => {
const searchKey = e.target.value.toLowerCase()
getRef('approved_kyc_addresses').querySelectorAll('li').forEach(li => {
if (li.dataset.searchKey.toLowerCase().includes(searchKey)) {
li.classList.remove('hidden')
} else {
li.classList.add('hidden')
}
})
})
getRef('approved_kyc_addresses').addEventListener('change', e => {
if (e.target.checked) {
const addresses = processAddresses([...addressesToRevoke])
const floData = `KYC|REVOKE_KYC|${addresses.join(',')}`
if (floData.length > 1040) {
return notify('Maximum limit reached for this batch. Revoke selected and continue the process.', 'error')
}
addressesToRevoke.add(e.target.value)
} else {
addressesToRevoke.delete(e.target.value)
}
if (addressesToRevoke.size) {
getRef('selected_addresses').textContent = `${addressesToRevoke.size} selected`
getRef('revoke_kyc_button').parentElement.classList.remove('hidden')
} else {
getRef('selected_addresses').textContent = ``
getRef('revoke_kyc_button').parentElement.classList.add('hidden')
}
})
function clearSelection() {
addressesToRevoke.clear()
getRef('selected_addresses').textContent = ``
getRef('revoke_kyc_button').parentElement.classList.add('hidden')
getRef('approved_kyc_addresses').querySelectorAll('input').forEach(input => input.checked = false)
}
getRef('aggregator_private_key').addEventListener('input', e => {
checkBalance(floCrypto.getAddress(e.target.value.trim()))
})
@ -231,56 +366,98 @@
addressInputObserver.observe(getRef('kyc_addresses_container'), {
childList: true
})
function processAddresses() {
const addressInputs = document.querySelectorAll('.kyc-address')
const addresses = new Set()
addressInputs.forEach(input => {
const address = input.value.trim()
if (address !== '') {
let equivalentBtcAddress = address
if (floCrypto.validateFloID(address))
equivalentBtcAddress = btcOperator.convert.legacy2bech(address)
addresses.add(equivalentBtcAddress)
}
function processAddresses(addresses = []) {
const uniqueAddresses = new Set()
addresses.forEach(address => {
let equivalentBtcAddress = address
if (floCrypto.validateFloID(address))
equivalentBtcAddress = btcOperator.convert.legacy2bech(address)
uniqueAddresses.add(equivalentBtcAddress)
})
return [...addresses]
return [...uniqueAddresses]
}
function submitAddresses() {
function approveAddresses() {
const aggregatorPrivateKey = getRef('aggregator_private_key').value.trim()
if (!aggregatorPrivateKey) {
return notify('Enter aggregator private key', 'error')
}
const aggregatorAddress = floCrypto.getAddress(aggregatorPrivateKey)
if (!floGlobals.approvedKycAggregators.hasOwnProperty(aggregatorAddress)) {
notify('KYC aggregator address is not approved', 'error')
return
return notify('KYC aggregator address is not approved', 'error')
}
const addresses = processAddresses()
const addressInputs = [...document.querySelectorAll('.kyc-address')]
.filter(input => input.value.trim() !== '')
const addresses = processAddresses(addressInputs)
if (addresses.length === 0) {
notify('No addresses to process', 'error')
return
return notify('Enter at least one address to approve', 'error')
}
const kycType = getRef('kyc_type').value
const floData = `KYC|${kycType === 'approve' ? 'APPROVE_KYC' : 'REVOKE_KYC'}|${addresses.join(',')}`
const floData = `KYC|APPROVE_KYC|${addresses.join(',')}`
if (floData.length > 1040) {
notify('Too many addresses. Try removing one and resubmitting.', 'error')
return
return notify('Too many addresses. Try removing one and resubmitting.', 'error')
}
buttonLoader(getRef('submit_kyc'), true)
floBlockchainAPI.writeData(aggregatorAddress, floData, aggregatorPrivateKey, aggregatorAddress).then(txId => {
notify(`${kycType === 'approve' ? 'Approval' : 'Revoke'} request submitted. TXID: ${txId}`, 'success')
getRef('kyc_addresses_container').innerHTML = '';
getRef('kyc_form').reset()
addAddressInput()
}).catch(e => {
notify(e, 'error')
}).finally(() => {
buttonLoader(getRef('submit_kyc'), false)
setTimeout(() => {
checkBalance()
}, 1000)
getConfirmation('Approve entered addresses?', {
confirmText: 'Approve',
}).then((res) => {
if (!res) return
buttonLoader(getRef('submit_kyc'), true)
floBlockchainAPI.writeData(aggregatorAddress, floData, aggregatorPrivateKey, aggregatorAddress).then(txId => {
notify(`Approval request submitted. TXID: ${txId}`, 'success')
getRef('kyc_addresses_container').innerHTML = '';
getRef('aggregator_private_key').value = ''
addAddressInput()
}).catch(e => {
notify(e, 'error')
}).finally(() => {
buttonLoader(getRef('submit_kyc'), false)
setTimeout(() => {
checkBalance()
}, 1000)
})
})
}
function revokeAddresses() {
const aggregatorPrivateKey = getRef('aggregator_private_key').value.trim()
if (aggregatorPrivateKey === '') {
return notify('Please enter KYC aggregator private key', 'error')
}
const aggregatorAddress = floCrypto.getAddress(aggregatorPrivateKey)
if (!floGlobals.approvedKycAggregators.hasOwnProperty(aggregatorAddress)) {
return notify('KYC aggregator address is not approved', 'error')
}
const addresses = processAddresses([...addressesToRevoke])
if (addresses.length === 0) {
return notify('Select at least one address to revoke', 'error')
}
getConfirmation('Revoke selected addresses?', {
confirmText: 'Revoke',
}).then((res) => {
if (!res) return
const floData = `KYC|REVOKE_KYC|${addresses.join(',')}`
buttonLoader(getRef('revoke_kyc_button'), true)
floBlockchainAPI.writeData(aggregatorAddress, floData, aggregatorPrivateKey, aggregatorAddress).then(txId => {
notify(`Revoke request submitted. TXID: ${txId}`, 'success')
getRef('aggregator_private_key').value = ''
addressesToRevoke.forEach(address => {
if (!floGlobals.approvedKyc.hasOwnProperty(address)) return
floGlobals.approvedKyc[address].revokedBy = aggregatorAddress
floGlobals.approvedKyc[address].validTo = Date.now()
})
addressesToRevoke.clear()
render.approvedKycs()
}).catch(e => {
notify(e, 'error')
}).finally(() => {
buttonLoader(getRef('revoke_kyc_button'), false)
setTimeout(() => {
checkBalance()
}, 1000)
})
})
}
window.onload = async () => {
try {
await getApprovedAggregators()
await getApprovedKycs()
router.routeTo(window.location.hash)
getRef('aggregator_private_key').customValidation = floCrypto.getPubKeyHex
addAddressInput()

View File

@ -19,6 +19,61 @@ function getRef(elementId) {
}
}
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
return getRef(popupId).show({ pinned })
}
// hides the popup or modal
function closePopup(options = {}) {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide(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
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')
const { closed } = openPopup('confirmation_popup')
confirmButton.onclick = () => {
closePopup({ payload: true })
}
cancelButton.onclick = () => {
closePopup()
}
closed.then((payload) => {
confirmButton.onclick = null
cancelButton.onclick = null
if (payload)
resolve(true)
else
resolve(false)
})
})
}
// 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);
};
}
class Router {
constructor(options = {}) {
@ -83,8 +138,8 @@ function loading(show = true) {
}
}
floGlobals.approvedKycAggregators = {};
function getApprovedAggregators() {
floGlobals.approvedKycAggregators = {};
return new Promise((resolve, reject) => {
floBlockchainAPI.readAllTxs(floGlobals.masterAddress).then(txs => {
txs.filter(tx => floCrypto.isSameAddr(tx.vin[0].addr, floGlobals.masterAddress) && tx.floData.startsWith('KYC'))
@ -116,4 +171,44 @@ function getApprovedAggregators() {
reject(e);
})
})
}
function getApprovedKycs() {
floGlobals.approvedKyc = {};
return new Promise((resolve, reject) => {
const aggregatorTxs = Object.keys(floGlobals.approvedKycAggregators).map(aggregator => {
return floBlockchainAPI.readAllTxs(aggregator);
});
Promise.all(aggregatorTxs).then(aggregatorData => {
aggregatorData = aggregatorData.flat(1)
.filter(tx => tx.vin[0].addr in floGlobals.approvedKycAggregators && tx.floData.startsWith('KYC'))
.sort((a, b) => a.time - b.time);
for (const tx of aggregatorData) {
const { floData, time, vin, vout } = tx;
const [service, operationType, operationData, validity] = floData.split('|');
switch (operationType) {
case 'APPROVE_KYC':
operationData.split(',').forEach(address => {
floGlobals.approvedKyc[address] = {
validFrom: time * 1000,
validTo: validity || Date.now() + 10000000,
verifiedBy: vin[0].addr
};
});
break;
case 'REVOKE_KYC':
operationData.split(',').forEach(address => {
floGlobals.approvedKyc[address].validTo = time * 1000;
floGlobals.approvedKyc[address].revokedBy = vin[0].addr;
});
break;
default:
return;
}
}
resolve();
}).catch(e => {
reject(e);
})
})
}

File diff suppressed because one or more lines are too long