Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSV exports & massively improve scaling #163

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading