From 7683003873e2d7cf24d277e3892f93a6e47076ea Mon Sep 17 00:00:00 2001 From: AlphaX-Projects <77661270+AlphaX-Projects@users.noreply.github.com> Date: Mon, 3 Jan 2022 14:06:48 -0800 Subject: [PATCH] Add Puzzle --- .../plugins/core/puzzles/index.html | 45 ++ .../plugins/core/puzzles/puzzles.src.js | 461 ++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 qortal-ui-plugins/plugins/core/puzzles/index.html create mode 100644 qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js diff --git a/qortal-ui-plugins/plugins/core/puzzles/index.html b/qortal-ui-plugins/plugins/core/puzzles/index.html new file mode 100644 index 00000000..e328bade --- /dev/null +++ b/qortal-ui-plugins/plugins/core/puzzles/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js b/qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js new file mode 100644 index 00000000..8e00ec59 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js @@ -0,0 +1,461 @@ +import { LitElement, html, css } from 'lit-element' +import { render } from 'lit-html' +import { Epml } from '../../../epml.js' + +// Not sure if these are imported in the proper way: +import nacl from '../../../../qortal-ui-crypto/api/deps/nacl-fast.js' +import Base58 from '../../../../qortal-ui-crypto/api/deps/Base58.js' +import publicKeyToAddress from '../../../../qortal-ui-crypto/api/wallet/publicKeyToAddress.js' + +import '@material/mwc-icon' +import '@material/mwc-button' +import '@material/mwc-textfield' +import '@material/mwc-dialog' +import '@material/mwc-slider' + +import '@polymer/paper-spinner/paper-spinner-lite.js' +import '@vaadin/vaadin-grid/vaadin-grid.js' +import '@vaadin/vaadin-grid/theme/material/all-imports.js' + +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) +const DEFAULT_FEE = 0.001 +const PAYMENT_TX_TYPE = 2 + +class Puzzles extends LitElement { + static get properties() { + return { + loading: { type: Boolean }, + invalid: { type: Boolean }, + puzzles: { type: Array }, + solved: { type: Object }, + selectedAddress: { type: Object }, + selectedPuzzle: { type: Object }, + error: { type: Boolean }, + message: { type: String } + } + } + + static get styles() { + return css` + * { + --mdc-theme-primary: rgb(3, 169, 244); + --mdc-theme-secondary: var(--mdc-theme-primary); + --paper-input-container-focus-color: var(--mdc-theme-primary); + } + #puzzle-page { + background: #fff; + padding: 12px 24px; + } + + h2 { + margin:0; + } + + h2, h3, h4, h5 { + color:#333; + font-weight: 400; + } + + .red { + --mdc-theme-primary: #F44336; + } + + .clue { + font-family: monospaced; + font-size: smaller; + } + ` + } + + constructor() { + super() + this.loading = false + this.invalid = true + this.puzzles = [] + this.solved = {} + this.selectedAddress = {} + this.selectedPuzzle = {} + this.error = false + this.message = '' + } + + render() { + return html` +
+
+

Puzzles

+
+ + + { + if (data.item.isSolved) { + render(html`SOLVED by ${data.item.winner}`, root) + } else { + render(html`${data.item.reward} QORT`, root) + } + }}> + + + + { + if (data.item.isSolved) { + render(html``, root) + } else { + render(html` this.guessPuzzle(data.item)}>queueGuess`, root) + } + }}> + + + +
Enter your guess to solve this puzzle and win ${this.selectedPuzzle.reward} QORT:
+
+
Name: ${this.selectedPuzzle.name}
+
Description: ${this.selectedPuzzle.description}
+
Clue: ${this.selectedPuzzle.clue}
+
+
Your guess needs to be 43 or 44 characters and not include 0 (zero), I (upper i), O (upper o) or l (lower L).
+ +
+ + + Claiming your reward...   + + + + ${this.message} + +
+ + + Submit + + + Close + +
+
+ ` + } + + firstUpdated() { + window.addEventListener("contextmenu", (event) => { + event.preventDefault(); + this._textMenu(event) + }); + + window.addEventListener("click", () => { + parentEpml.request('closeCopyTextMenu', null) + }); + + window.onkeyup = (e) => { + if (e.keyCode === 27) { + parentEpml.request('closeCopyTextMenu', null) + } + } + + const textBox = this.shadowRoot.getElementById("puzzleGuess") + + // keep track of input validity so we can enabled/disable submit button + textBox.validityTransform = (newValue, nativeValidity) => { + this.invalid = !nativeValidity.valid + return nativeValidity + } + + const getPuzzleGroupMembers = async () => { + return await parentEpml.request('apiCall', { + url: `/groups/members/165` + }) + } + + const getBalance = async(address) => { + return await parentEpml.request('apiCall', { + url: `/addresses/balance/${address}` + }) + } + + const getName = async(memberAddress) => { + let _names = await parentEpml.request('apiCall', { + url: `/names/address/${memberAddress}` + }) + + if (_names.length === 0) return ""; + + return _names[0].name + } + + const getNameInfo = async(name) => { + // We have to explicitly encode '#' to stop them being interpreted as in-page references + name = name.replaceAll('#', '%23') + + return await parentEpml.request('apiCall', { + url: `/names/${name}` + }) + } + + const getFirstOutgoingPayment = async(sender) => { + let _payments = await parentEpml.request('apiCall', { + url: `/transactions/search?confirmationStatus=CONFIRMED&limit=20&txType=PAYMENT&address=${sender}` + }) + + return _payments.find(payment => payment.creatorAddress === sender) + } + + const updatePuzzles = async () => { + let _puzzleGroupMembers = await getPuzzleGroupMembers() + + let _puzzles = [] + + await Promise.all(_puzzleGroupMembers.members + .sort((a, b) => b.joined - a.joined) + .map(async (member) => { + let _puzzleAddress = member.member + + if (member.isAdmin) return + + // Already solved? No need to refresh info + if (this.solved[_puzzleAddress]) { + _puzzles.push(this.solved[_puzzleAddress]) + return + } + + let _name = await getName(_puzzleAddress) + // No name??? + if (_name === "") return + + let _reward = await getBalance(_puzzleAddress) + let _isSolved = _reward < 1.0; + let _nameInfo = await getNameInfo(_name) + + let _nameData = JSON.parse(_nameInfo.data) + + let _puzzle = { + reward: _reward, + address: _puzzleAddress, + name: _name, + description: _nameData.description, + isSolved: _isSolved + } + + if (!_isSolved && _nameData.clue) + _puzzle.clue = _nameData.clue; + + if (_isSolved) { + // Info on winner + let _payment = await getFirstOutgoingPayment(_puzzleAddress) + _puzzle.winner = _payment.recipient + // Does winner have a name? + let _winnerName = await getName(_puzzle.winner) + if (_winnerName) _puzzle.winner = _winnerName + // Add to 'solved' map to prevent info refresh as it'll never change + this.solved[_puzzleAddress] = _puzzle + } + + _puzzles.push(_puzzle); + })) + + this.puzzles = _puzzles; + + setTimeout(updatePuzzles, 20000) + } + + let configLoaded = false + + parentEpml.ready().then(() => { + parentEpml.subscribe('selected_address', async selectedAddress => { + this.selectedAddress = {} + selectedAddress = JSON.parse(selectedAddress) + if (!selectedAddress || Object.entries(selectedAddress).length === 0) return + this.selectedAddress = selectedAddress + }) + + parentEpml.subscribe('config', c => { + if (!configLoaded) { + setTimeout(updatePuzzles, 1) + configLoaded = true + } + this.config = JSON.parse(c) + }) + + parentEpml.subscribe('copy_menu_switch', async value => { + if (value === 'false' && window.getSelection().toString().length !== 0) { + this.clearSelection() + } + }) + + parentEpml.subscribe('frame_paste_menu_switch', async res => { + res = JSON.parse(res) + + if (res.isOpen === false && this.isPasteMenuOpen === true) { + this.pasteToTextBox(textBox) + this.isPasteMenuOpen = false + } + }) + }) + + parentEpml.imReady() + + textBox.addEventListener('contextmenu', (event) => { + const getSelectedText = () => { + var text = ""; + + if (typeof window.getSelection != "undefined") { + text = window.getSelection().toString(); + } else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") { + text = this.shadowRoot.selection.createRange().text; + } + + return text; + } + + const checkSelectedTextAndShowMenu = () => { + let selectedText = getSelectedText(); + + if (selectedText && typeof selectedText === 'string') { + // ... + } else { + this.pasteMenu(event) + this.isPasteMenuOpen = true + + // Prevent Default and Stop Event Bubbling + event.preventDefault() + event.stopPropagation() + } + } + + checkSelectedTextAndShowMenu() + }) + } + + async guessPuzzle(puzzle) { + this.selectedPuzzle = puzzle + this.shadowRoot.getElementById("puzzleGuess").value = '' + this.shadowRoot.getElementById("puzzleGuess").checkValidity() + this.message = '' + this.invalid = true + + this.shadowRoot.querySelector('#puzzleGuessDialog').show() + } + + async submitPuzzleGuess(e) { + this.loading = true + this.error = false + + // Check for valid guess + const guess = this.shadowRoot.getElementById("puzzleGuess").value + + let _rawGuess = Base58.decode(guess) + let _keyPair = nacl.sign.keyPair.fromSeed(_rawGuess) + + let _guessAddress = publicKeyToAddress(_keyPair.publicKey) + + console.log("Guess '" + _guessAddress + "' vs puzzle's address '" + this.selectedPuzzle.address + "'") + if (_guessAddress !== this.selectedPuzzle.address) { + this.error = true + this.message = 'Guess incorrect!' + this.loading = false + return + } + + // Get Last Ref + const getLastRef = async (address) => { + let myRef = await parentEpml.request('apiCall', { + url: `/addresses/lastreference/${address}` + }) + return myRef + } + + let lastRef = await getLastRef(_guessAddress) + let amount = this.selectedPuzzle.reward - DEFAULT_FEE; + let recipientAddress = this.selectedAddress.address + let txnParams = { + recipient: recipientAddress, + amount: amount, + lastReference: lastRef, + fee: DEFAULT_FEE + } + + // Mostly copied from qortal-ui-core/src/plugins/routes.js + let txnResponse = await parentEpml.request('standaloneTransaction', { + type: 2, + keyPair: { + publicKey: _keyPair.publicKey, + privateKey: _keyPair.secretKey + }, + params: txnParams + }) + + if (txnResponse.success) { + this.message = 'Reward claim submitted - check wallet for reward!' + } else { + this.error = true + if (txnResponse.data) { + this.message = "Error while claiming reward: " + txnResponse.data.message + } else { + this.message = "Error while claiming reward: " + txnResponse.message + } + } + + this.loading = false + } + + pasteToTextBox(textBox) { + // Return focus to the window + window.focus() + + navigator.clipboard.readText().then(clipboardText => { + + textBox.value += clipboardText + textBox.focus() + }); + } + + pasteMenu(event) { + let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY } + parentEpml.request('openFramePasteMenu', eventObject) + } + + _textMenu(event) { + const getSelectedText = () => { + var text = ""; + if (typeof window.getSelection != "undefined") { + text = window.getSelection().toString(); + } else if (typeof this.shadowRoot.selection != "undefined" && this.shadowRoot.selection.type == "Text") { + text = this.shadowRoot.selection.createRange().text; + } + return text; + } + + const checkSelectedTextAndShowMenu = () => { + let selectedText = getSelectedText(); + if (selectedText && typeof selectedText === 'string') { + let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY } + let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true } + parentEpml.request('openCopyTextMenu', textMenuObject) + } + } + + checkSelectedTextAndShowMenu() + } + + isEmptyArray(arr) { + if (!arr) { return true } + return arr.length === 0 + } + + clearSelection() { + window.getSelection().removeAllRanges() + window.parent.getSelection().removeAllRanges() + } +} + +window.customElements.define('puzzles-info', Puzzles)