craft-nft

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

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:
Mjs/qrread.html | 191++++++++++++++++++++++++++++++++++---------------------------------------------
Ajs/qrread.js | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajs/qrread_ui.js | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajs/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; +}