forked from Qortal/qortal-ui
Add Puzzle
This commit is contained in:
parent
b101a5c0a3
commit
7683003873
45
qortal-ui-plugins/plugins/core/puzzles/index.html
Normal file
45
qortal-ui-plugins/plugins/core/puzzles/index.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/font/material-icons.css">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
--scrollbarBG: #a1a1a1;
|
||||||
|
--thumbBG: #6a6c75;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--thumbBG);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<puzzles-info></puzzles-info>
|
||||||
|
<script type="module" src="puzzles.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
461
qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js
Normal file
461
qortal-ui-plugins/plugins/core/puzzles/puzzles.src.js
Normal file
@ -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`
|
||||||
|
<div id="puzzle-page">
|
||||||
|
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
|
||||||
|
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">Puzzles</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<vaadin-grid id="puzzlesGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.puzzles)}" .items="${this.puzzles}" height-by-rows>
|
||||||
|
<vaadin-grid-column auto-width header="Reward" .renderer=${(root, column, data) => {
|
||||||
|
if (data.item.isSolved) {
|
||||||
|
render(html`<span style="font-size: smaller;">SOLVED by ${data.item.winner}</span>`, root)
|
||||||
|
} else {
|
||||||
|
render(html`<span>${data.item.reward} QORT</span>`, root)
|
||||||
|
}
|
||||||
|
}}></vaadin-grid-column>
|
||||||
|
<vaadin-grid-column auto-width path="name"></vaadin-grid-column>
|
||||||
|
<vaadin-grid-column auto-width path="description"></vaadin-grid-column>
|
||||||
|
<vaadin-grid-column auto-width path="clue" style="font-family: monospace; font-size: smaller;"></vaadin-grid-column>
|
||||||
|
<vaadin-grid-column width="12em" header="Action" .renderer=${(root, column, data) => {
|
||||||
|
if (data.item.isSolved) {
|
||||||
|
render(html``, root)
|
||||||
|
} else {
|
||||||
|
render(html`<mwc-button @click=${() => this.guessPuzzle(data.item)}><mwc-icon>queue</mwc-icon>Guess</mwc-button>`, root)
|
||||||
|
}
|
||||||
|
}}></vaadin-grid-column>
|
||||||
|
</vaadin-grid>
|
||||||
|
|
||||||
|
<mwc-dialog id="puzzleGuessDialog" scrimClickAction="${this.loading ? '' : 'close'}">
|
||||||
|
<div>Enter your guess to solve this puzzle and win ${this.selectedPuzzle.reward} QORT:</div>
|
||||||
|
<br>
|
||||||
|
<div id="puzzleGuessName">Name: ${this.selectedPuzzle.name}</div>
|
||||||
|
<div id="puzzleGuessDescription">Description: ${this.selectedPuzzle.description}</div>
|
||||||
|
<div id="puzzleGuessClue" ?hidden=${!this.selectedPuzzle.clue}>Clue: <span class="clue">${this.selectedPuzzle.clue}</span></div>
|
||||||
|
<br>
|
||||||
|
<div id="puzzleGuessInputHint" style="font-size: smaller;">Your guess needs to be 43 or 44 characters and <b>not</b> include 0 (zero), I (upper i), O (upper o) or l (lower L).</div>
|
||||||
|
<mwc-textfield style="width:100%" ?disabled="${this.loading}" label="Your Guess" id="puzzleGuess" pattern="[1-9A-HJ-NP-Za-km-z]{43,44}" style="font-family: monospace;" maxLength="44" charCounter="true" autoValidate="true"></mwc-textfield>
|
||||||
|
<div style="text-align:right; height:36px;">
|
||||||
|
<span ?hidden="${!this.loading}">
|
||||||
|
<!-- loading message -->
|
||||||
|
Claiming your reward...
|
||||||
|
<paper-spinner-lite
|
||||||
|
style="margin-top:12px;"
|
||||||
|
?active="${this.loading}"
|
||||||
|
alt="Claiming puzzle reward"></paper-spinner-lite>
|
||||||
|
</span>
|
||||||
|
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
|
||||||
|
${this.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mwc-button
|
||||||
|
?disabled="${this.loading || this.invalid}"
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this.submitPuzzleGuess}>
|
||||||
|
Submit
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
?disabled="${this.loading}"
|
||||||
|
slot="secondaryAction"
|
||||||
|
dialogAction="cancel"
|
||||||
|
class="red">
|
||||||
|
Close
|
||||||
|
</mwc-button>
|
||||||
|
</mwc-dialog>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
Loading…
x
Reference in New Issue
Block a user