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 @@
+
+
+
+
+
+
+
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)