commit f9c0e8eb79b2d20ab76d421c51198102eb3f669e
parent 5290ab9b54942bf3fe7951dc8b49533d20d1b84a
Author: lash <dev@holbrook.no>
Date: Mon, 20 Feb 2023 21:11:27 +0000
Add stateful initialization for qr minter
Diffstat:
M | js/qrread.html | | | 191 | ++++++++++++++++++++++++++++++++++--------------------------------------------- |
A | js/qrread.js | | | 171 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | js/qrread_ui.js | | | 150 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | js/style.css | | | 11 | +++++++++++ |
4 files changed, 415 insertions(+), 108 deletions(-)
diff --git a/js/qrread.html b/js/qrread.html
@@ -3,121 +3,96 @@
<title>webcam</title>
<script src="node_modules/jsqr/dist/jsQR.js"></script>
<script src="node_modules/ethers/dist/ethers.umd.min.js"></script>
+ <script src="qrread.js"></script>
+ <script src="qrread_ui.js"></script>
+ <link rel="stylesheet" href="style.css"></link>
<script>
-const privateKey = "5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6";
-const tokenId = "49d2711d67894feeb8fa449530aff40ee969cc09eebfc521c397432ef56cf33d";
-const batchNumber = "0000000000000000000000000000000000000000000000000000000000000000";
-const addressPrePad = "000000000000000000000000";
-const dataPost = tokenId + batchNumber;
-const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
-const wallet_offline = new ethers.Wallet(privateKey);
-const wallet = wallet_offline.connect(provider);
+const MAX_MINT = 4;
-const txBase = {
- to: "0xb7cf275e96d3d0ea54aaa8f60133aa5dd3a6e3af",
- gasLimit: 200000,
- gasPrice: 1,
- data: "0xd824ee4f", // mintFromBatchTo(address,bytes32,uint16)
- value: 0,
- nonce: -1,
- chainId: 5050,
-};
-
-const constraints = {
- audio: false,
- video: {
- width: 800, height: 800,
- }
-};
-
-
-// Access webcam
-async function init() {
- try {
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
- handleSuccess(stream);
- } catch (e) {
- console.error(e);
- }
-}
-
-// Success
-function handleSuccess(stream) {
- const video = document.getElementById('video');
- window.stream = stream;
- video.srcObject = stream;
-}
-
-// Draw image
-
-var scanning = true;
-var canvas;
-var ctx;
-
-// Load init
-window.addEventListener('load', live);
-
-function test() {
- signAndSend("0x7F8301136a596D64f1b7E5C882FCB0FCD0623745");
-}
-
-function live() {
- init();
- canvas = document.getElementById('qr-canvas');
- ctx = canvas.getContext('2d', { willReadFrequently: true });
- scan();
-}
-
-function scan() {
- ctx.drawImage(video, 0, 0, 800, 800);
- const imageData = ctx.getImageData(0, 0, 800, 800).data;
- const code = jsQR(imageData, 800, 800);
- if (code) {
- console.log("Found QR code", code);
- signAndSend(code.data);
- return;
- }
- setTimeout(scan, 10);
-}
-
-async function signAndSend(addr) {
- if (addr.length < 40) {
- console.error('invalid ethereum address (too short)', addr);
- return;
- }
- if (addr.substring(0, 9) == "ethereum:") { // metamask qr
- addr = addr.substring(9);
+//const privateKey = "5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6";
+//const tokenId = "49d2711d67894feeb8fa449530aff40ee969cc09eebfc521c397432ef56cf33d";
+//const batchNumber = "0000000000000000000000000000000000000000000000000000000000000000";
+//const addressPrePad = "000000000000000000000000";
+//const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
+//const wallet_offline = new ethers.Wallet(privateKey);
+//const wallet = wallet_offline.connect(provider);
+//
+window.addEventListener('load', () => {
+ for (let i = 1; i <= MAX_MINT; i<<=1) {
+ const opt = document.createElement('option');
+ opt.setAttribute('value', i);
+ opt.innerHTML = i.toString();
+ document.getElementById('requestAmount').appendChild(opt);
}
- 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;
- }
- console.info('found recipient address', addr);
- let tx = txBase;
- const nonce = await wallet.getTransactionCount();
- addr = addressPrePad + addr;
- tx.data += addr;
- tx.data += dataPost;
- tx.nonce = nonce;
- console.log(tx);
- const txSigned = await wallet.signTransaction(tx);
- console.log(txSigned);
- const r = await wallet.sendTransaction(tx);
- console.log(r);
-}
+ document.getElementById('keyFileSubmit').addEventListener("click", (o) => {
+ const keyFile = document.getElementById("keyFile").value;
+ const keyFilePassword = document.getElementById("keyFilePassword").value;
+ return keyFileHandler(keyFile, keyFilePassword);
+ });
+ document.getElementById('chainSubmit').addEventListener("click", (o) => {
+ const chainId = document.getElementById("chainId").value;
+ const chainRpcUrl = document.getElementById("chainRpcUrl").value;
+ return chainHandler(chainRpcUrl, chainId);
+ });
+ document.getElementById('contractSubmit').addEventListener("click", (o) => {
+ const tokenAddress = document.getElementById("contractAddress").value;
+ return contractHandler(tokenAddress);
+ });
+ document.getElementById('requestSubmit').addEventListener("click", (o) => {
+ const tokenBatch = document.getElementById("tokenBatch").value;
+ const amount = document.getElementById("requestAmount").value;
+ return requestHandler(tokenBatch, amount);
+ });
+});
</script>
</head>
- <div class="video-wrap">
- <video id="video" playsinline autoplay></video>
+ <dl id="settingsView">
+ <dt>Status</dt>
+ <dd><span id="statusText" class="statusBusy">Initializing...</span></dd>
+ </dl>
+ <div class="pane" id="start">
+ <label for="keyFile">Keyfile JSON text</label>
+ <textarea cols="80" rows="24" id="keyFile"></textarea>
+ <label for="keyFilePassword">Keyfile passphrase</label>
+ <input type="password" id="keyFilePassword" />
+ <button id="keyFileSubmit">unlock wallet</button>
+ </div>
+ <div class="pane" id="connect">
+ <label for="chainRpcUrl">RPC URL</label>
+ <input type="text" id="chainRpcUrl" value="http://localhost:8545" />
+ <label for="chainId">Chain ID</label>
+ <input type="text" id="chainId" />
+ <button id="chainSubmit">connect to network</button>
+ </div>
+ <div class="pane" id="contract">
+ <label for="contractAddress">Contract address</label>
+ <input type="text" id="contractAddress" />
+ <button id="contractSubmit">connect to contract</button>
+ </div>
+ <div class="pane" id="product">
+ <label for="requestTokenChooser">Choose token</label>
+ <div id="tokenChooser"></div>
+ <label for="requestAmount">Choose mint amount</label>
+ <select id="requestAmount"></select>
+ <button id="requestSubmit">create request</button>
</div>
- <div class="out">
- <canvas id="qr-canvas" width="800" height="800"></canvas>
+ <div class="pane" id="read">
+ <h2>Scan QR code</h2>
+ <dl>
+ <dt>Token Id</dt>
+ <dd id="scanTokenId"></dd>
+ <dt>Batch</dt>
+ <dd id="scanTokenBatch"></dd>
+ <dt>Amount</dt>
+ <dd id="scanTokenAmount"></dd>
+ </dl>
+ <!--<div class="video-wrap">
+ <video id="video" playsinline autoplay></video>
+ </div>-->
+ <div class="out">
+ <canvas id="qr-canvas" width="800" height="800"></canvas>
+ </div>
</div>
</html>
diff --git a/js/qrread.js b/js/qrread.js
@@ -0,0 +1,171 @@
+var state = 0;
+const STATE = {
+ WALLET_SETTINGS: 1,
+ CHAIN_SETTINGS: 2,
+ CONTRACT_SETTINGS: 4,
+ MINT: 8,
+ READ_WALLET: 16,
+ TX_FLIGHT: 32,
+ TX_RESULT: 64,
+};
+
+var settings = {
+ privateKey: undefined,
+ tokenAddress: undefined,
+ tokenId: undefined,
+ batchNumber: undefined,
+ provider: undefined,
+ wallet: undefined,
+ chainId: undefined,
+ dataPost: undefined,
+ mintAmount: 1,
+};
+
+const txBase = {
+ to: "0xb7cf275e96d3d0ea54aaa8f60133aa5dd3a6e3af",
+ gasLimit: 200000,
+ gasPrice: 1,
+ data: "0xd824ee4f", // mintFromBatchTo(address,bytes32,uint16)
+ value: 0,
+ nonce: -1,
+ chainId: 5050,
+};
+
+function checkState(stateCheck, exact) {
+ masked = state & stateCheck;
+ if (exact) {
+ if (masked != stateCheck) {
+ console.error('fail exact state', state, stateCheck);
+ throw 'fail state transition check (exact)';
+ }
+ }
+ if (masked == 0) {
+ console.error('fail contains state', state, stateCheck);
+ throw 'fail state transition check (partial)';
+ }
+}
+
+function keyFileHandler(v, passphrase) {
+ settings.wallet = ethers.Wallet.fromEncryptedJsonSync(v, passphrase);
+ 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);
+ 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);
+ 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;
+ while (true) {
+ try {
+ const batch = await contract.token(tokenId, j);
+ if (batch.count == 0) {
+ console.debug('skipping unique token', tokenId);
+ break;
+ }
+ 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++;
+ }
+ }
+
+
+ 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);
+ return true;
+}
+
+function requestHandler(tokenBatch, amount) {
+ const v = tokenBatch.split('.');
+ let batchNumberHex = "0000000000000000000000000000000000000000000000000000000000000000" + v[1].toString(16);
+ batchNumberHex = batchNumberHex.slice(-64);
+ settings.dataPost = v[0] + batchNumberHex;
+ settings.tokenId = v[0];
+ settings.batchNumber = v[1];
+ settings.mintAmount = amount;
+ const e = new CustomEvent('uistate', {
+ detail: {
+ delta: STATE.MINT,
+ settings: settings,
+ },
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ window.dispatchEvent(e);
+}
+
+
+
+
+const nftAbi = [{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"bytes32","name":"_declaration","type":"bytes32"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_minter","type":"address"},{"indexed":true,"internalType":"uint48","name":"_count","type":"uint48"},{"indexed":false,"internalType":"bytes32","name":"_tokenId","type":"bytes32"}],"name":"Allocate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_owner","type":"address"},{"indexed":true,"internalType":"address","name":"_approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_owner","type":"address"},{"indexed":true,"internalType":"address","name":"_operator","type":"address"},{"indexed":false,"internalType":"bool","name":"_approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_minter","type":"address"},{"indexed":true,"internalType":"address","name":"_beneficiary","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":true,"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":true,"internalType":"uint256","name":"_tokenId","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"_data","type":"bytes32"}],"name":"TransferWithData","type":"event"},{"inputs":[{"internalType":"bytes32","name":"content","type":"bytes32"},{"internalType":"uint48","name":"count","type":"uint48"}],"name":"allocate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURL","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"declaration","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_truncatedId","type":"bytes32"}],"name":"getDigest","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_data","type":"bytes32"}],"name":"getDigestHex","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"bytes32","name":"_content","type":"bytes32"},{"internalType":"uint16","name":"_batch","type":"uint16"},{"internalType":"uint48","name":"_index","type":"uint48"}],"name":"mintExactFromBatchTo","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"bytes32","name":"_content","type":"bytes32"},{"internalType":"uint16","name":"_batch","type":"uint16"}],"name":"mintFromBatchTo","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"bytes32","name":"_content","type":"bytes32"}],"name":"mintTo","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"mintedToken","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_operator","type":"address"},{"internalType":"bool","name":"_approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_baseString","type":"string"}],"name":"setBaseURL","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_data","type":"bytes32"}],"name":"toURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_data","type":"bytes32"}],"name":"toURL","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"token","outputs":[{"internalType":"uint48","name":"count","type":"uint48"},{"internalType":"uint48","name":"cursor","type":"uint48"},{"internalType":"bool","name":"sparse","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"tokens","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"},{"internalType":"bool","name":"_final","type":"bool"}],"name":"transferOwnership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}];
diff --git a/js/qrread_ui.js b/js/qrread_ui.js
@@ -0,0 +1,150 @@
+const constraints = {
+ audio: false,
+ video: {
+ width: 400, height: 400,
+ }
+};
+
+const addressPrePad = "000000000000000000000000";
+
+
+const video = document.createElement('video');
+video.setAttribute('id', 'video');
+video.setAttribute('autoplay', true);
+video.setAttribute('playsinline', true);
+
+window.addEventListener('uistate', (e) => {
+ console.debug('statechange', e);
+ switch (e.detail.delta) {
+ case STATE.WALLET_SETTINGS:
+ updateSettingsView('Wallet address', e.detail.settings.wallet.address);
+ document.getElementById("start").style.display = "none";
+ document.getElementById("connect").style.display = "block";
+ break;
+ case STATE.CHAIN_SETTINGS:
+ updateSettingsView('RPC', e.detail.settings.provider.connection.url);
+ updateSettingsView('Chain ID', e.detail.settings.chainId);
+ document.getElementById("connect").style.display = "none";
+ document.getElementById("contract").style.display = "block";
+ break;
+ case STATE.CONTRACT_SETTINGS:
+ updateSettingsView('NFT contract address', e.detail.settings.tokenAddress);
+ document.getElementById("contract").style.display = "none";
+ document.getElementById("product").style.display = "block";
+ break;
+ case STATE.MINT:
+ document.getElementById("scanTokenId").innerHTML = settings.tokenId;
+ document.getElementById("scanTokenBatch").innerHTML = settings.batchNumber;
+ document.getElementById("scanTokenAmount").innerHTML = settings.mintAmount;
+ document.getElementById("product").style.display = "none";
+ document.getElementById("read").style.display = "block";
+ live();
+ break;
+ default:
+ throw 'invalid state ' + e.detail.delta;
+ }
+});
+
+window.addEventListener('token', (e) => {
+ const v = e.detail.tokenId + '.' + e.detail.batch;
+ const input = document.createElement('input');
+ input.setAttribute('id', 'tokenBatch');
+ input.setAttribute('name', 'tokenBatch');
+ input.setAttribute('type', 'radio');
+ input.setAttribute('value', v);
+ const label = document.createElement('label');
+ label.setAttribute('for', v);
+ label.innerHTML = v;
+ const ls = document.getElementById('tokenChooser');
+ ls.appendChild(input);
+ ls.appendChild(label);
+});
+
+function updateSettingsView(k, v) {
+ const dl = document.getElementById("settingsView");
+ const dt = document.createElement("dt");
+ dt.innerHTML = k;
+ dl.appendChild(dt);
+ const dd = document.createElement("dd");
+ dd.innerHTML = v;
+ dl.appendChild(dd);
+}
+
+// Access webcam
+async function initCamera() {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ handleSuccess(stream);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+// Success
+function handleSuccess(stream) {
+ //const video = document.getElementById('video');
+ window.stream = stream;
+ video.srcObject = stream;
+}
+
+// Draw image
+
+var scanning = true;
+var canvas;
+var ctx;
+
+// Load init
+
+function test() {
+ signAndSend("0x7F8301136a596D64f1b7E5C882FCB0FCD0623745");
+}
+
+function live() {
+ initCamera();
+ canvas = document.getElementById('qr-canvas');
+ ctx = canvas.getContext('2d', { willReadFrequently: true });
+ scan();
+}
+
+function scan() {
+ ctx.drawImage(video, 0, 0, 400, 400);
+ const imageData = ctx.getImageData(0, 0, 400, 400).data;
+ const code = jsQR(imageData, 400, 400);
+ if (code) {
+ console.log("Found QR code", code);
+ signAndSend(code.data);
+ return;
+ }
+ setTimeout(scan, 10);
+}
+
+async function signAndSend(addr) {
+ if (addr.length < 40) {
+ console.error('invalid ethereum address (too short)', addr);
+ return;
+ }
+ if (addr.substring(0, 9) == "ethereum:") { // metamask qr
+ addr = addr.substring(9);
+ }
+ 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;
+ }
+ console.info('found recipient address', addr);
+ let tx = txBase;
+ const nonce = await settings.wallet.getTransactionCount();
+ addr = addressPrePad + addr;
+ tx.data += addr;
+ tx.data += settings.dataPost;
+ tx.nonce = nonce;
+ console.log(tx);
+ const txSigned = await settings.wallet.signTransaction(tx);
+ console.log(txSigned);
+ const r = await settings.wallet.sendTransaction(tx);
+ console.log(r);
+}
diff --git a/js/style.css b/js/style.css
@@ -0,0 +1,11 @@
+div.pane {
+ display: none;
+}
+
+div#start {
+ display: block;
+}
+
+.statusBusy {
+ color: #00f;
+}