craft-nft

A standalone NFT implementation for real-world arts and crafts assets
Log | Files | Refs | README

commit 0dfad1abb4bd8def7bb5b85c3a35f902aa72daa6
parent 99b8085cec819c64828246cf88844ed279e54c4b
Author: lash <dev@holbrook.no>
Date:   Tue, 21 Feb 2023 11:48:04 +0000

add status field updates

Diffstat:
Mjs/qrread.html | 20++++++++++++++++++++
Mjs/qrread.js | 220++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mjs/qrread_ui.js | 103++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mjs/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; + }