commit 0dfad1abb4bd8def7bb5b85c3a35f902aa72daa6
parent 99b8085cec819c64828246cf88844ed279e54c4b
Author: lash <dev@holbrook.no>
Date: Tue, 21 Feb 2023 11:48:04 +0000
add status field updates
Diffstat:
M | js/qrread.html | | | 20 | ++++++++++++++++++++ |
M | js/qrread.js | | | 220 | ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- |
M | js/qrread_ui.js | | | 103 | ++++++++++++++++++++++++++++++++++++++++++++----------------------------------- |
M | js/style.css | | | 21 | ++++++++++++++++++++- |
4 files changed, 233 insertions(+), 131 deletions(-)
diff --git a/js/qrread.html b/js/qrread.html
@@ -20,11 +20,15 @@ window.addEventListener('load', () => {
document.getElementById('keyFileSubmit').addEventListener("click", (o) => {
const keyFile = document.getElementById("keyFile").value;
const keyFilePassword = document.getElementById("keyFilePassword").value;
+ const submit = document.getElementById('keyFileSubmit');
+ submit.setAttribute('disabled', 1);
return keyFileHandler(keyFile, keyFilePassword);
});
document.getElementById('chainSubmit').addEventListener("click", (o) => {
const chainId = document.getElementById("chainId").value;
const chainRpcUrl = document.getElementById("chainRpcUrl").value;
+ const submit = document.getElementById('chainSubmit');
+ submit.setAttribute('disabled', 1);
return chainHandler(chainRpcUrl, chainId);
});
document.getElementById('contractSubmit').addEventListener("click", (o) => {
@@ -58,6 +62,19 @@ window.addEventListener('load', () => {
window.dispatchEvent(e);
window.dispatchEvent(ee);
});
+ document.getElementById('scanConfirm').addEventListener("click", (o) => {
+ const e = new CustomEvent('uistate', {
+ detail: {
+ delta: STATE.SCAN_CONFIRM,
+ settings: settings,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ });
+ setStatus('waiting for wallet', STATUS_BUSY);
});
</script>
@@ -108,6 +125,9 @@ window.addEventListener('load', () => {
<label for="scanAddress">Recipient address</label>
<input type="text" id="scanAddress" size="42" />
<ol id="txList"></ol>
+ <button id="scanManualMint">mint</button>
+ <button id="scanConfirm">confirm</button>
<button id="scanAbort">abort</button>
+ <button id="scanReturn">return</button>
</div>
</html>
diff --git a/js/qrread.js b/js/qrread.js
@@ -7,7 +7,8 @@ const STATE = {
SCAN_START: 16,
SCAN_RESULT: 32,
SCAN_STOP: 64,
- SCAN_DONE: 128,
+ SCAN_CONFIRM: 128,
+ SCAN_DONE: 256,
};
var settings = {
@@ -21,6 +22,7 @@ var settings = {
dataPost: undefined,
mintAmount: 1,
minedAmount: 0,
+ failedAmount: 0,
recipient: undefined,
};
@@ -48,111 +50,159 @@ function checkState(stateCheck, exact) {
}
}
-function keyFileHandler(v, passphrase) {
- settings.wallet = ethers.Wallet.fromEncryptedJsonSync(v, passphrase);
+async function signAndSend() {
+ let addr = settings.recipient;
+ console.info('found recipient address', addr);
+ let tx = txBase;
+ let nonce = await settings.wallet.getTransactionCount();
+ addr = addressPrePad + addr;
+ tx.data += addr;
+ tx.data += settings.dataPost;
+
+ for (let i = 0; i < settings.mintAmount; i++) {
+ setStatus('signing and sending transaction ' + (i + 1) + ' of ' + settings.mintAmount + '...', STATUS_BUSY);
+ let txCopy = tx;
+ txCopy.nonce = nonce;
+ const txSigned = await settings.wallet.signTransaction(tx);
+ console.log(txSigned);
+ const txr = await settings.wallet.sendTransaction(txCopy);
+ setStatus('sent transaction ' + (i + 1) + ' of ' + settings.mintAmount, STATUS_OK);
+ const e = new CustomEvent('tx', {
+ detail: {
+ settings: settings,
+ tx: txr,
+ mintAmount: settings.mintAmount,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ console.debug(txr);
+ nonce++;
+ }
+}
+
+async function keyFileHandler(v, passphrase) {
+ setStatus('unlocking keyfile...', STATUS_BUSY);
console.debug('wallet', settings.wallet);
- state |= STATE.WALLET_SETTINGS;
- const e = new CustomEvent('uistate', {
- detail: {
- delta: STATE.WALLET_SETTINGS,
- settings: settings,
- },
- bubbles: true,
- cancelable: true,
- composed: false,
- });
- window.dispatchEvent(e);
+ // make sure dom updates are executed before unlock
+ setTimeout(() => {
+ settings.wallet = ethers.Wallet.fromEncryptedJsonSync(v, passphrase);
+ state |= STATE.WALLET_SETTINGS;
+ const e = new CustomEvent('uistate', {
+ detail: {
+ delta: STATE.WALLET_SETTINGS,
+ settings: settings,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ setStatus('keyfile unlocked', STATUS_OK);
+ }, 0);
return true;
}
async function chainHandler(rpc, chainId) {
- settings.provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
- settings.wallet = settings.wallet.connect(settings.provider);
- const network = await settings.provider.getNetwork();
- console.debug('connected to network', network, settings.provider);
- if (network.chainId != chainId) {
- throw 'chainId mismatch, requested ' + chainId + ', got ' + network.chainId;
- }
- settings.chainId = chainId;
- state |= STATE.CHAIN_SETTINGS;
- const e = new CustomEvent('uistate', {
- detail: {
- delta: STATE.CHAIN_SETTINGS,
- settings: settings,
- },
- bubbles: true,
- cancelable: true,
- composed: false,
- });
- window.dispatchEvent(e);
+ setStatus('connecting to network', STATUS_BUSY);
+ setTimeout(async () => {
+ settings.provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
+ settings.wallet = settings.wallet.connect(settings.provider);
+ const network = await settings.provider.getNetwork();
+ console.debug('connected to network', network, settings.provider);
+ if (network.chainId != chainId) {
+ throw 'chainId mismatch, requested ' + chainId + ', got ' + network.chainId;
+ }
+ settings.chainId = chainId;
+ state |= STATE.CHAIN_SETTINGS;
+ const e = new CustomEvent('uistate', {
+ detail: {
+ delta: STATE.CHAIN_SETTINGS,
+ settings: settings,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ setStatus('connected to network', STATUS_OK);
+ }, 0);
return true;
}
async function contractHandler(contractAddress) {
checkState(STATE.WALLET_SETTINGS | STATE.NETWORK_SETTINGS, true);
- const contract = new ethers.Contract(contractAddress, nftAbi, settings.provider);
- let i = 0;
- let tokens = [];
- while (true) {
- try {
- const tokenId = await contract.tokens(i);
- tokens.push(tokenId);
- } catch(e) {
- break;
- }
- i++;
- }
-
- for (let i = 0; i < tokens.length; i++) {
- const tokenId = tokens[i];
- const uri = await contract.tokenURI(ethers.BigNumber.from(tokenId));
- let j = 0;
+ setStatus('scanning contract for tokens...', STATUS_BUSY);
+ setTimeout(async () => {
+ const contract = new ethers.Contract(contractAddress, nftAbi, settings.provider);
+ let i = 0;
+ let tokens = [];
while (true) {
try {
- const batch = await contract.token(tokenId, j);
- if (batch.count == 0) {
- console.debug('skipping unique token', tokenId);
+ const tokenId = await contract.tokens(i);
+ tokens.push(tokenId);
+ } catch(e) {
+ break;
+ }
+ i++;
+ }
+
+ let c = 0;
+ for (let i = 0; i < tokens.length; i++) {
+ const tokenId = tokens[i];
+ const uri = await contract.tokenURI(ethers.BigNumber.from(tokenId));
+ let j = 0;
+ while (true) {
+ try {
+ const batch = await contract.token(tokenId, j);
+ if (batch.count == 0) {
+ console.debug('skipping unique token', tokenId);
+ break;
+ } else if (batch.sparse) {
+ console.debug('skip sparse token', tokenId);
+ j++;
+ continue;
+ }
+ const e = new CustomEvent('token', {
+ detail: {
+ tokenId: tokenId,
+ batch: j,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ c++;
+ } catch {
break;
- } else if (batch.sparse) {
- console.debug('skip sparse token', tokenId);
- j++;
- continue;
}
- const e = new CustomEvent('token', {
- detail: {
- tokenId: tokenId,
- batch: j,
- },
- bubbles: true,
- cancelable: true,
- composed: false,
- });
- window.dispatchEvent(e);
- console.debug('bat', batch);
- } catch {
- break;
+ j++;
}
- j++;
}
- }
-
+
- settings.tokenAddress = contractAddress;
- state |= STATE.CONTRACT_SETTINGS;
- const e = new CustomEvent('uistate', {
- detail: {
- delta: STATE.CONTRACT_SETTINGS,
- settings: settings,
- },
- bubbles: true,
- cancelable: true,
- composed: false,
- });
- window.dispatchEvent(e);
+ settings.tokenAddress = contractAddress;
+ state |= STATE.CONTRACT_SETTINGS;
+ const e = new CustomEvent('uistate', {
+ detail: {
+ delta: STATE.CONTRACT_SETTINGS,
+ settings: settings,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+ setStatus('found ' + c + ' available token batches in contract', STATUS_OK);
+ }, 0);
return true;
}
function requestHandler(tokenBatch, amount) {
+ setStatus('scan QR code or manually enter address...', STATUS_BUSY);
const v = tokenBatch.split('.');
let batchNumberHex = "0000000000000000000000000000000000000000000000000000000000000000" + v[1].toString(16);
batchNumberHex = batchNumberHex.slice(-64);
diff --git a/js/qrread_ui.js b/js/qrread_ui.js
@@ -13,6 +13,28 @@ video.setAttribute('id', 'video');
video.setAttribute('autoplay', true);
video.setAttribute('playsinline', true);
+const STATUS_ERROR = 1;
+const STATUS_BUSY = 2;
+const STATUS_OK = 3;
+
+function setStatus(s, typ) {
+ const em = document.getElementById('statusText');
+ em.innerHTML = s;
+ switch (typ) {
+ case STATUS_ERROR:
+ em.setAttribute('class', 'statusError');
+ break;
+ case STATUS_BUSY:
+ em.setAttribute('class', 'statusBusy');
+ break;
+ case STATUS_OK:
+ em.setAttribute('class', 'statusOk');
+ break;
+ default:
+ em.setAttribute('class', 'statusBusy');
+ }
+}
+
window.addEventListener('uistate', (e) => {
console.debug('statechange', e);
switch (e.detail.delta) {
@@ -20,6 +42,7 @@ window.addEventListener('uistate', (e) => {
updateSettingsView('Wallet address', e.detail.settings.wallet.address);
document.getElementById("start").style.display = "none";
document.getElementById("connect").style.display = "block";
+ document.getElementById("keyFileSubmit").style.display = "none";
break;
case STATE.CHAIN_SETTINGS:
updateSettingsView('RPC', e.detail.settings.provider.connection.url);
@@ -38,17 +61,29 @@ window.addEventListener('uistate', (e) => {
document.getElementById("scanTokenAmount").innerHTML = settings.mintAmount;
document.getElementById("product").style.display = "none";
document.getElementById("read").style.display = "block";
+ document.getElementById("scanConfirm").style.display = "none";
+ document.getElementById("scanReturn").style.display = "none";
+ document.getElementById("scanAbort").style.display = "block";
+ document.getElementById("scanManualMint").style.display = "block";
live();
break;
case STATE.SCAN_RESULT:
document.getElementById('scanAddress').value = e.detail.settings.recipient;
+ document.getElementById("scanManualMint").style.display = "none";
+ document.getElementById("scanConfirm").style.display = "block";
break;
case STATE.SCAN_STOP:
window.stream.getTracks().forEach(track => track.stop());
break;
+ case STATE.SCAN_CONFIRM:
+ document.getElementById("scanConfirm").style.display = "none";
+ signAndSend();
+ break;
case STATE.SCAN_DONE:
document.getElementById("read").style.display = "none";
document.getElementById("product").style.display = "block";
+ document.getElementById("scanAbort").style.display = "none";
+ document.getElementById("scanReturn").style.display = "block";
break;
default:
@@ -82,6 +117,7 @@ window.addEventListener('tx', (e) => {
l.innerHTML = e.detail.tx.hash;
const r = document.createElement('span');
r.setAttribute('id', 'status.' + e.detail.tx.hash);
+ r.setAttribute('class', 'statusBusy');
r.innerHTML = 'status: pending';
li.appendChild(l);
li.appendChild(r);
@@ -89,14 +125,26 @@ window.addEventListener('tx', (e) => {
watchTx(e.detail.tx);
});
-async function watchTx(tx) {
+async function watchTx(tx, i) {
const rcpt = await settings.provider.waitForTransaction(tx.hash);
const txRow = document.getElementById('status.' + tx.hash);
console.debug('rcpt', rcpt);
+ settings.minedAmount++;
if (rcpt.status == 1) {
+ txRow.setAttribute('class', 'statusOk');
txRow.innerHTML = 'status: confirmed';
+ setStatus('transaction ' + i + ' of ' + settings.mintAmount + ' confirmed', STATUS_OK);
} else {
+ txRow.setAttribute('class', 'statusError');
txRow.innerHTML = 'status: failed';
+ setStatus('transaction ' + i + ' of ' + settings.mintAmount + ' failed', STATUS_ERROR);
+ settings.failedAmount++;
+ }
+ if (settings.failedAmount > 0) {
+ setStatus('some transactions failed', STATUS_ERROR);
+ }
+ else {
+ setStatus('token minting successully completed', STATUS_OK);
}
}
@@ -112,6 +160,7 @@ function updateSettingsView(k, v) {
// Access webcam
async function initCamera() {
+ console.debug('starting camera');
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
handleSuccess(stream);
@@ -128,17 +177,10 @@ function handleSuccess(stream) {
}
// Draw image
-
-var scanning = true;
var canvas;
var ctx;
// Load init
-
-function test() {
- signAndSend("0x7F8301136a596D64f1b7E5C882FCB0FCD0623745");
-}
-
function live() {
initCamera();
canvas = document.getElementById('qr-canvas');
@@ -147,6 +189,7 @@ function live() {
}
function scan() {
+ setStatus('waiting for address', STATUS_BUSY);
ctx.drawImage(video, 0, 0, 400, 400);
const imageData = ctx.getImageData(0, 0, 400, 400).data;
const code = jsQR(imageData, 400, 400);
@@ -163,6 +206,12 @@ function scan() {
if (addr.substring(0, 2) == "0x") {
addr = addr.substring(2);
}
+ const re = new RegExp("^[0-9a-fA-F]{40}$");
+ const m = addr.match(re);
+ if (m === null) {
+ console.error('invalid ethereum address (invalid hex or too long)', addr);
+ return;
+ }
settings.recipient = addr;
const e = new CustomEvent('uistate', {
detail: {
@@ -174,45 +223,9 @@ function scan() {
composed: false,
});
window.dispatchEvent(e);
- signAndSend(addr);
+ setStatus('confirm address...', STATUS_BUSY);
return;
}
setTimeout(scan, 10);
}
-async function signAndSend(addr) {
-
- const re = new RegExp("^[0-9a-fA-F]{40}$");
- const m = addr.match(re);
- if (m === null) {
- console.error('invalid ethereum address (invalid hex or too long)', addr);
- return;
- }
- console.info('found recipient address', addr);
- let tx = txBase;
- let nonce = await settings.wallet.getTransactionCount();
- addr = addressPrePad + addr;
- tx.data += addr;
- tx.data += settings.dataPost;
-
- for (let i = 0; i < settings.mintAmount; i++) {
- let txCopy = tx;
- txCopy.nonce = nonce;
- const txSigned = await settings.wallet.signTransaction(tx);
- console.log(txSigned);
- const txr = await settings.wallet.sendTransaction(txCopy);
- const e = new CustomEvent('tx', {
- detail: {
- settings: settings,
- tx: txr,
- mintAmount: settings.mintAmount,
- },
- bubbles: true,
- cancelable: true,
- composed: false,
- });
- window.dispatchEvent(e);
- console.debug(txr);
- nonce++;
- }
-}
diff --git a/js/style.css b/js/style.css
@@ -7,5 +7,24 @@ div#start {
}
.statusBusy {
- color: #00f;
+ color: #ffa500;
+}
+
+.statusError {
+ color: #c00;
+}
+
+.statusOk {
+ color: #0c0;
+}
+
+.statusInfo {
+ color: #00c;
+}
+
+
+#scanConfirm,
+#scanReturn {
+ display: none;
+
}