mirror of https://github.com/qortal/qortal-ui
AlphaX-Projects
3 years ago
committed by
GitHub
2 changed files with 506 additions and 0 deletions
@ -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> |
@ -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…
Reference in new issue