Skip to content

Commit

Permalink
Add CSV exports & massively improve scaling (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
JSKitty authored Aug 3, 2023
1 parent 7698b74 commit f4b5eaa
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 16 deletions.
2 changes: 1 addition & 1 deletion index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ <h3 class="modal-title" id="redeemCodeModalTitle" style="text-align: center; wid
<td class="text-center"><b> Manage </b></td>
<td class="text-center"><b> Promo Code </b></td>
<td class="text-center"><b> Amount </b></td>
<td class="text-center"><b> State </b></td>
<td class="text-center"><b> State </b><i onclick="MPW.promosToCSV()" style="margin-left: 5px;" class="fa-solid fa-lg fa-file-csv ptr"></i></td>
</tr>
</thead>
<tbody id="redeemCodeCreatePendingList" style="text-align: center; vertical-align: middle;">
Expand Down
1 change: 1 addition & 0 deletions scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export {
sweepPromoCode,
deletePromoCode,
openPromoQRScanner,
promosToCSV,
} from './promos';
export { renderWalletBreakdown } from './charting';
export { hexToBytes, bytesToHex, dSHA256 } from './utils.js';
Expand Down
26 changes: 26 additions & 0 deletions scripts/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ export function writeToUint8(arr, bytes, pos) {
while (pos < arrLen) arr[pos++] = bytes[i++];
}

/** Convert a 2D array into a CSV string */
export function arrayToCSV(data) {
return data
.map(
(row) =>
row
.map(String) // convert every value to String
.map((v) => v.replaceAll('"', '""')) // escape double colons
.map((v) => `"${v}"`) // quote it
.join(',') // comma-separated
)
.join('\r\n'); // rows starting on new lines
}

/** Download contents as a file */
export function downloadBlob(content, filename, contentType) {
// Create a blob
const blob = new Blob([content], { type: contentType });

// Create a link to download it
const pom = document.createElement('a');
pom.href = URL.createObjectURL(blob);
pom.setAttribute('download', filename);
pom.click();
}

/* --- NOTIFICATIONS --- */
// Alert - Do NOT display arbitrary / external errors, the use of `.innerHTML` allows for input styling at this cost.
// Supported types: success, info, warning
Expand Down
104 changes: 89 additions & 15 deletions scripts/promos.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { cChainParams, COIN } from './chain_params';
import { Database } from './database';
import { doms, getBalance, restoreWallet, sweepAddress } from './global';
import {
arrayToCSV,
createAlert,
downloadBlob,
getAlphaNumericRand,
} from './misc';
import { ALERTS, translation } from './i18n';
import { createAlert, getAlphaNumericRand } from './misc';
import { getNetwork } from './network';
import { scanQRCode } from './scanner';
import { createAndSendTransaction } from './transactions';
Expand Down Expand Up @@ -39,6 +44,12 @@ export class PromoWallet {
this.time = time instanceof Date ? time : new Date(time);
}

/** A flag to show if this UTXO has successfully synced UTXOs previously */
fSynced = false;

/** A lock to prevent this Promo from synchronisation races */
fLock = false;

/**
* Synchronise UTXOs and return the balance of the Promo Code
* @param {boolean} - Whether to use UTXO Cache, or sync from network
Expand All @@ -60,6 +71,10 @@ export class PromoWallet {
* @returns {Promise<Array<object>>}
*/
async getUTXOs(fFull = false) {
// For shallow syncs, don't allow racing: but Full syncs are allowed to bypass for Tx creation
if (!fFull && this.fLock) return this.utxos;
this.fLock = true;

// If we don't have it, derive the public key from the promo code's WIF
if (!this.address) {
this.address = deriveAddress({ pkBytes: this.pkBytes });
Expand All @@ -82,7 +97,9 @@ export class PromoWallet {
}
}

// Return the UTXO set
// Unlock, mark as synced and return the UTXO set
this.fLock = false;
this.fSynced = true;
return this.utxos;
}
}
Expand Down Expand Up @@ -151,7 +168,7 @@ export async function setPromoMode(fMode) {

// Show smooth table animation
setTimeout(() => {
doms.domPromoTable.style.maxHeight = '600px';
doms.domPromoTable.style.maxHeight = 'min-content';
}, 100);
}
}
Expand All @@ -169,7 +186,7 @@ export function promoConfirm() {

// Show smooth table animation
setTimeout(() => {
doms.domPromoTable.style.maxHeight = '600px';
doms.domPromoTable.style.maxHeight = 'min-content';
}, 100);

createPromoCode(
Expand Down Expand Up @@ -296,6 +313,14 @@ export async function deletePromoCode(strCode) {
const db = await Database.getInstance();
await db.removePromo(strCode);

// And splice from post-creation memory too, if it exists
const nMemIndex = arrPromoCodes.findIndex(
(cCode) => cCode.code === strCode
);
if (nMemIndex >= 0) {
arrPromoCodes.splice(nMemIndex, 1);
}

// Re-render promos
await updatePromoCreationTick();
}
Expand All @@ -307,6 +332,11 @@ export async function deletePromoCode(strCode) {
* @property {string} html - The HTML string returned in the response.
*/

/** An in-memory representation of all created Promo Wallets
* @type {Array<PromoWallet>}
*/
let arrPromoCodes = [];

/**
* Render locally-saved Promo Codes in the created list
* @type {Promise<RenderedPromoPair>} - The code count and HTML pair
Expand All @@ -318,17 +348,36 @@ export async function renderSavedPromos() {
// Finished or 'Saved' codes are hoisted to the top, static
const db = await Database.getInstance();
const arrCodes = await db.getAllPromos();
for (const cCode of arrCodes) {

// Render each code; sorted by Newest First, Oldest Last.
for (const cDiskCode of arrCodes.sort((a, b) => b.time - a.time)) {
// Move on-disk promos to a memory representation for quick state computation
let cCode = arrPromoCodes.find((code) => code.code === cDiskCode.code);
if (!cCode) {
// Push this disk promo to memory
cCode = cDiskCode;
arrPromoCodes.push(cCode);
}

// Sync only the balance of the code (not full data)
await cCode.getUTXOs(false);
cCode.getUTXOs(false);
const nBal = (await cCode.getBalance(true)) / COIN;

// A code younger than ~2 minutes without a balance will just say 'confirming', since Blockbook does not return a balance for NEW codes
const fNew = cCode.time.getTime() > Date.now() - 120000;
// A code younger than ~3 minutes without a balance will just say 'confirming', since Blockbook does not return a balance for NEW codes
const fNew = cCode.time.getTime() > Date.now() - 60000 * 3;

// If this code is allowed to be deleted or not
const fCannotDelete = fNew || nBal > 0;
const fCannotDelete = !cCode.fSynced || fNew || nBal > 0;

// Status calculation (defaults to 'fNew' condition)
let strStatus = 'Confirming...';
if (!fNew) {
if (cCode.fSynced) {
strStatus = nBal > 0 ? 'Unclaimed' : 'Claimed';
} else {
strStatus = 'Syncing';
}
}
strHTML += `
<tr>
<td>${
Expand All @@ -347,13 +396,13 @@ export async function renderSavedPromos() {
cCode.code
}</code></td>
<td>${
fNew ? '...' : nBal + ' ' + cChainParams.current.TICKER
fNew || !cCode.fSynced
? '...'
: nBal + ' ' + cChainParams.current.TICKER
}</td>
<td><a class="ptr active" style="margin-right: 10px;" href="${
getNetwork().strUrl + '/address/' + cCode.address
}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-up-right-from-square"></i></a>${
fNew ? 'Confirming...' : nBal > 0 ? 'Unclaimed' : 'Claimed'
}</td>
}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-up-right-from-square"></i></a>${strStatus}</td>
</tr>
`;
}
Expand All @@ -362,6 +411,30 @@ export async function renderSavedPromos() {
return { codes: arrCodes.length, html: strHTML };
}

/** Export and download all PIVX Promos data in to a CSV format */
export async function promosToCSV() {
const arrCSV = [
// Titles
['Promo Code', 'PIV (Remaining)', 'Funding Address'],
// Content
];

// Push each code in to the CSV
for (const cCode of arrPromoCodes) {
arrCSV.push([
cCode.code,
(await cCode.getBalance(true)) / COIN,
cCode.address,
]);
}

// Encode it
const cCSV = arrayToCSV(arrCSV);

// Download it
downloadBlob(cCSV, 'promos.csv', 'text/csv;charset=utf-8;');
}

/**
* Handle the Promo Workers, Code Rendering, and update or prompt the UI appropriately
* @param {boolean} fRecursive - Whether this call is self-initiated or not
Expand Down Expand Up @@ -428,14 +501,15 @@ export async function updatePromoCreationTick(fRecursive = false) {
}

// Render the table row
strHTML += `
strHTML =
`
<tr>
<td><i class="fa-solid fa-ban ptr" onclick="MPW.deletePromoCode('${cThread.code}')"></i></td>
<td><code class="wallet-code active" style="display: inline !important;">${cThread.code}</code></td>
<td>${cThread.amount} ${cChainParams.current.TICKER}</td>
<td>${strState}</td>
</tr>
`;
` + strHTML;
}

// Render the compiled HTML
Expand Down

0 comments on commit f4b5eaa

Please sign in to comment.