Initial commit

This commit is contained in:
AlphaX-Projects
2021-12-25 14:39:47 +01:00
commit 2f823927a5
3687 changed files with 37381 additions and 0 deletions

View File

@@ -0,0 +1,843 @@
import { LitElement, html, css } from 'lit-element'
import { render } from 'lit-html'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-dialog'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@vaadin/vaadin-grid/vaadin-grid.js'
import '@vaadin/vaadin-grid/theme/material/all-imports.js'
import '@github/time-elements'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class GroupManagement extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
publicGroups: { type: Array },
joinedGroups: { type: Array },
recipientPublicKey: { type: String },
selectedAddress: { type: Object },
manageGroupObj: { type: Object },
joinGroupObj: { type: Object },
leaveGroupObj: { type: Object },
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
error: { type: Boolean },
message: { type: String },
removeError: { type: Boolean },
removeMessage: { type: String }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
#group-management-page {
background: #fff;
padding: 12px 24px;
}
mwc-textfield {
width:100%;
}
.red {
--mdc-theme-primary: red;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
.divCard {
border: 1px solid #eee;
padding: 1em;
/** box-shadow: 0 1px 1px 0 rgba(0,0,0,0.14), 0 2px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20); **/
box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20);
margin-bottom: 2em;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
span {
font-size: 18px;
word-break: break-all;
}
select {
padding: 13px 20px;
width: 100%;
font-size: 14px;
color: #555;
font-weight: 400;
}
.title {
font-weight:600;
font-size:12px;
line-height: 32px;
opacity: 0.66;
}
.itemList {
padding:0;
}
/* .itemList > * {
padding-left:24px;
padding-right:24px;
} */
`
}
constructor() {
super()
this.selectedAddress = {}
this.publicGroups = []
this.joinedGroups = []
this.manageGroupObj = {}
this.joinGroupObj = {}
this.leaveGroupObj = {}
this.recipientPublicKey = ''
this.btnDisable = false
this.isLoading = false
}
render() {
return html`
<div id="group-management-page">
<div style="min-height:48px; display: flex; padding-bottom: 6px; margin: 2px;">
<h2 style="margin: 0; flex: 1; padding-top: .1em; display: inline;">Qortal Groups</h2>
<mwc-button style="float:right;" @click=${() => this.shadowRoot.querySelector('#createGroupDialog').show()}><mwc-icon>add</mwc-icon>Create Group</mwc-button>
</div>
<div class="divCard">
<h3 style="margin: 0; margin-bottom: 1em; text-align: center;">Public Groups</h3>
<vaadin-grid id="publicGroupsGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.publicGroups)}" aria-label="Public Open Groups" .items="${this.publicGroups}" height-by-rows>
<vaadin-grid-column path="groupName"></vaadin-grid-column>
<vaadin-grid-column header="Description" path="description"></vaadin-grid-column>
<vaadin-grid-column path="owner"></vaadin-grid-column>
<vaadin-grid-column width="9.8rem" flex-grow="0" header="Action" .renderer=${(root, column, data) => {
render(html`<mwc-button @click=${() => this.joinGroup(data.item)}><mwc-icon>queue</mwc-icon>Join</mwc-button>`, root)
}}></vaadin-grid-column>
</vaadin-grid>
</vaadin-grid>
${this.isEmptyArray(this.publicGroups) ? html`
No Open Public Groups available!
`: ''}
</div>
<div class="divCard">
<h3 style="margin: 0; margin-bottom: 1em; text-align: center;">Your Joined Groups</h3>
<vaadin-grid id="joinedGroupsGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.joinedGroups)}" aria-label="Joined Groups" .items="${this.joinedGroups}" height-by-rows>
<vaadin-grid-column header="Name" path="groupName"></vaadin-grid-column>
<vaadin-grid-column header="Description" path="description"></vaadin-grid-column>
<vaadin-grid-column width="9.8rem" flex-grow="0" header="Role" .renderer=${(root, column, data) => {
render(html`${this.renderRole(data.item)}`, root)
}}></vaadin-grid-column>
<vaadin-grid-column width="9.8rem" flex-grow="0" header="Action" .renderer=${(root, column, data) => {
render(html`${this.renderManageButton(data.item)}`, root)
}}></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.joinedGroups) ? html`
Not a member of any groups!
`: ''}
</div>
<!-- Create Group Dialog -->
<mwc-dialog id="createGroupDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>Create a New Group</h1>
<hr>
</div>
<mwc-textfield style="width:100%;" ?disabled="${this.isLoading}" label="Group Name" id="groupNameInput"></mwc-textfield>
<p style="margin-bottom:0;">
<mwc-textfield style="width:100%;" ?disabled="${this.isLoading}" label="Description" id="groupDescInput"></mwc-textfield>
</p>
<p>
Group Type:
<select required validationMessage="This Field is Required" id="groupTypeInput" label="Group Type">
<option value="reject" selected>Select an option</option>
<option value="1">Public</option>
<option value="0">Private</option>
</select>
</p>
<p>
Group Approval Threshold (number / percentage of Admins that must approve a transaction):
<select required validationMessage="This Field is Required" id="groupApprovalInput" label="Group Type">
<option value="reject" selected>Select an option</option>
<option value="0">NONE</option>
<option value="1">ONE</option>
<option value="20">20%</option>
<option value="40">40%</option>
<option value="60">60%</option>
<option value="80">80%</option>
<option value="100">100%</option>
</select>
</p>
<p>
Minimum Block delay for Group Transaction Approvals:
<select required validationMessage="This Field is Required" id="groupMinDelayInput" label="Group Type">
<option value="reject" selected>Select an option</option>
<option value="5">5 minutes</option>
<option value="10">10 minutes</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="180">3 hours</option>
<option value="300">5 hours</option>
<option value="420">7 hours</option>
<option value="720">12 hours</option>
<option value="1440">1 day</option>
<option value="4320">3 days</option>
<option value="7200">5 days</option>
<option value="10080">7 days</option>
</select>
</p>
<p>
Maximum Block delay for Group Transaction Approvals:
<select required validationMessage="This Field is Required" id="groupMaxDelayInput" label="Group Type">
<option value="reject" selected>Select an option</option>
<option value="60">1 hour</option>
<option value="180">3 hours</option>
<option value="300">5 hours</option>
<option value="420">7 hours</option>
<option value="720">12 hours</option>
<option value="1440">1 day</option>
<option value="4320">3 days</option>
<option value="7200">5 days</option>
<option value="10080">7 days</option>
<option value="14400">10 days</option>
<option value="21600">15 days</option>
</select>
</p>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
Creating Group &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Creating Group"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.isLoading}"
slot="primaryAction"
@click=${this.createGroup}
>
Create
</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
<!-- Join Group Dialog -->
<mwc-dialog id="joinDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>Join Group Request</h1>
<hr>
</div>
<div class="itemList">
<span class="title">Group Name</span>
<br>
<div><span>${this.joinGroupObj.groupName}</span></div>
<span class="title">Description</span>
<br>
<div><span>${this.joinGroupObj.description}</span></div>
<span class="title">Owner</span>
<br>
<div><span>${this.joinGroupObj.owner}</span></div>
<span class="title">Date Created</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.joinGroupObj.created)}></time-ago></span></div>
${!this.joinGroupObj.updated ? "" : html`<span class="title">Date Updated</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.joinGroupObj.updated)}></time-ago></span></div>`}
</div>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
Joining &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Joining"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.isLoading}"
slot="primaryAction"
@click=${() => this._joinGroup(this.joinGroupObj.groupId, this.joinGroupObj.groupName)}
>
Join
</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
<!-- Leave Group Dialog -->
<mwc-dialog id="leaveDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>Leave Group Request</h1>
<hr>
</div>
<div class="itemList">
<span class="title">Group Name</span>
<br>
<div><span>${this.leaveGroupObj.groupName}</span></div>
<span class="title">Description</span>
<br>
<div><span>${this.leaveGroupObj.description}</span></div>
<span class="title">Owner</span>
<br>
<div><span>${this.leaveGroupObj.owner}</span></div>
<span class="title">Date Created</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.leaveGroupObj.created)}></time-ago></span></div>
${!this.leaveGroupObj.updated ? "" : html`<span class="title">Date Updated</span>
<br>
<div><span><time-ago datetime=${this.timeIsoString(this.leaveGroupObj.updated)}></time-ago></span></div>`}
</div>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.isLoading}">
<!-- loading message -->
Leaving &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.isLoading}"
alt="Leaving"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.isLoading}"
slot="primaryAction"
@click=${() => this._leaveGroup(this.leaveGroupObj.groupId, this.leaveGroupObj.groupName)}
>
Leave Group
</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
<!-- Manage Group Owner Dialog -->
<mwc-dialog id="manageGroupOwnerDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div>Manage Group Owner: ${this.manageGroupObj.groupName}</div>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
<!-- Manage Group Admin Dialog -->
<mwc-dialog id="manageGroupAdminDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div>Manage Group Admin: ${this.manageGroupObj.groupName}</div>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
</div>
`
}
resetDefaultSettings() {
this.error = false
this.message = ''
this.isLoading = false
}
manageGroupOwner(groupObj) {
this.resetDefaultSettings()
this.manageGroupObj = groupObj
this.shadowRoot.querySelector('#manageGroupOwnerDialog').show()
}
manageGroupAdmin(groupObj) {
this.resetDefaultSettings()
this.manageGroupObj = groupObj
this.shadowRoot.querySelector('#manageGroupAdminDialog').show()
}
joinGroup(groupObj) {
this.resetDefaultSettings()
this.joinGroupObj = groupObj
this.shadowRoot.querySelector('#joinDialog').show()
}
leaveGroup(groupObj) {
this.resetDefaultSettings()
this.leaveGroupObj = groupObj
this.shadowRoot.querySelector('#leaveDialog').show()
}
timeIsoString(timestamp) {
let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp
let time = new Date(myTimestamp)
return time.toISOString()
}
renderRole(groupObj) {
if (groupObj.owner === this.selectedAddress.address) {
return "Owner"
} else if (groupObj.isAdmin === true) {
return "Admin"
} else {
return "Member"
}
}
renderManageButton(groupObj) {
if (groupObj.owner === this.selectedAddress.address) {
// render owner actions btn to modal
return html`<mwc-button @click=${() => this.manageGroupOwner(groupObj)}><mwc-icon>create</mwc-icon>Manage</mwc-button>`
} else if (groupObj.isAdmin === true) {
// render admin actions modal
return html`<mwc-button @click=${() => this.manageGroupAdmin(groupObj)}><mwc-icon>create</mwc-icon>Manage</mwc-button>`
} else {
// render member leave group modal
return html`<mwc-button @click=${() => this.leaveGroup(groupObj)}><mwc-icon>exit_to_app</mwc-icon>Leave</mwc-button>`
}
}
_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()
}
firstUpdated() {
// Call getNamesGrid
// this.getNamesGrid()
const getOpenPublicGroups = async () => {
let openG = await parentEpml.request('apiCall', {
url: `/groups?limit=0&reverse=true`
})
let myGs = openG.filter(myG => myG.isOpen === true)
return myGs
}
const getJoinedGroups = async () => {
let joinedG = await parentEpml.request('apiCall', {
url: `/groups/member/${this.selectedAddress.address}`
})
return joinedG
}
const getOpen_JoinedGroups = async () => {
// this.publicGroups = []
// this.joinedGroups = []
let _joinedGroups = await getJoinedGroups()
let _publicGroups = await getOpenPublicGroups()
let results = _publicGroups.filter(myOpenGroup => {
let value = _joinedGroups.some(myJoinedGroup => myOpenGroup.groupId === myJoinedGroup.groupId)
return !value
});
this.publicGroups = results
this.joinedGroups = _joinedGroups
setTimeout(getOpen_JoinedGroups, this.config.user.nodeSettings.pingInterval)
}
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)
}
}
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(getOpen_JoinedGroups, 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.imReady()
}
async createGroup(e) {
// Reset Default Settings...
this.resetDefaultSettings()
const groupNameInput = this.shadowRoot.getElementById("groupNameInput").value
const groupDescInput = this.shadowRoot.getElementById("groupDescInput").value
const groupTypeInput = this.shadowRoot.getElementById("groupTypeInput").value
const groupApprovalInput = this.shadowRoot.getElementById("groupApprovalInput").value
const groupMinDelayInput = this.shadowRoot.getElementById("groupMinDelayInput").value
const groupMaxDelayInput = this.shadowRoot.getElementById("groupMaxDelayInput").value
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
let _groupTypeInput = parseInt(groupTypeInput)
let _groupApprovalInput = parseInt(groupApprovalInput)
let _groupMinDelayInput = parseInt(groupMinDelayInput)
let _groupMaxDelayInput = parseInt(groupMaxDelayInput)
this.resetDefaultSettings()
let myTransaction = await makeTransactionRequest(_groupTypeInput, _groupApprovalInput, _groupMinDelayInput, _groupMaxDelayInput, lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (_groupTypeInput, _groupApprovalInput, _groupMinDelayInput, _groupMaxDelayInput, lastRef) => {
let myTxnrequest = await parentEpml.request('transaction', {
type: 22,
nonce: this.selectedAddress.nonce,
params: {
registrantAddress: this.selectedAddress.address,
rGroupName: groupNameInput,
rGroupDesc: groupDescInput,
rGroupType: _groupTypeInput,
rGroupApprovalThreshold: _groupApprovalInput,
rGroupMinimumBlockDelay: _groupMinDelayInput,
rGroupMaximumBlockDelay: _groupMaxDelayInput,
lastReference: lastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = 'Group Creation Successful!'
this.error = false
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
if (groupNameInput.length < 3) {
this.error = true
this.message = "Invalid Group Name"
this.isLoading = false
} else if (groupDescInput.length < 3) {
this.error = true
this.message = "Invalid Group Description"
this.isLoading = false
} else if (groupTypeInput === 'reject') {
this.error = true
this.message = "Select a Group Type"
this.isLoading = false
} else if (groupApprovalInput === 'reject') {
this.error = true
this.message = "Select a Group Approval Threshold"
this.isLoading = false
} else if (groupMinDelayInput === 'reject') {
this.error = true
this.message = "Select a Minimum Block delay for Group Transaction Approvals"
this.isLoading = false
} else if (groupMaxDelayInput === 'reject') {
this.error = true
this.message = "Select a Maximum Block delay for Group Transaction Approvals"
this.isLoading = false
} else {
this.error = false
// Call validateReceiver
validateReceiver()
}
// this.resetDefaultSettings()
}
async _joinGroup(groupId, groupName) {
// Reset Default Settings...
this.resetDefaultSettings()
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
this.resetDefaultSettings()
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let myTxnrequest = await parentEpml.request('transaction', {
type: 31,
nonce: this.selectedAddress.nonce,
params: {
registrantAddress: this.selectedAddress.address,
rGroupName: groupName,
rGroupId: groupId,
lastReference: lastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = 'Join Group Request Sent Successfully!'
this.error = false
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
this.resetDefaultSettings()
}
async _leaveGroup(groupId, groupName) {
// Reset Default Settings...
this.resetDefaultSettings()
this.isLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
const validateReceiver = async () => {
let lastRef = await getLastRef();
this.resetDefaultSettings()
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let myTxnrequest = await parentEpml.request('transaction', {
type: 32,
nonce: this.selectedAddress.nonce,
params: {
registrantAddress: this.selectedAddress.address,
rGroupName: groupName,
rGroupId: groupId,
lastReference: lastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = 'Leave Group Request Sent Successfully!'
this.error = false
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
this.resetDefaultSettings()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
}
window.customElements.define('group-management', GroupManagement)

View File

@@ -0,0 +1,289 @@
import { LitElement, html, css } from 'lit-element'
// import { render } from 'lit-html'
// import { Epml } from '../../../src/epml.js'
import { Epml } from '../../../../epml.js'
import '@polymer/paper-spinner/paper-spinner-lite.js'
// import * as thing from 'time-elements'
// import '@vaadin/vaadin-grid/vaadin-grid.js'
// import '@vaadin/vaadin-grid/theme/material/all-imports.js'
// import '@material/mwc-icon'
// import '@material/mwc-textfield'
// import '@material/mwc-button'
// import '@material/mwc-dialog'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class GroupTransaction extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
upTime: { type: String },
mintingAccounts: { type: Array },
peers: { type: Array },
addMintingAccountLoading: { type: Boolean },
removeMintingAccountLoading: { type: Boolean },
addPeerLoading: { type: Boolean },
confPeerLoading: { type: Boolean },
addMintingAccountKey: { type: String },
removeMintingAccountKey: { type: String },
addPeerMessage: { type: String },
confPeerMessage: { type: String },
addMintingAccountMessage: { type: String },
removeMintingAccountMessage: { type: String },
tempMintingAccount: { type: Object },
selectedAddress: { type: Object }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#group-transaction-page {
background:#fff;
}
mwc-textfield {
width:100%;
}
.red {
--mdc-theme-primary: red;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
.group-transaction-card {
/* margin:12px; */
padding:12px 24px;
background:#fff;
border-radius:2px;
box-shadow: 11;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
`
}
constructor() {
super()
this.upTime = ""
this.mintingAccounts = []
this.peers = []
this.addPeerLoading = false
this.confPeerLoading = false
this.addMintingAccountLoading = false
this.removeMintingAccountLoading = false
this.addMintingAccountKey = ''
this.removeMintingAccountKey = ''
this.addPeerMessage = ''
this.confPeerMessage = ''
this.addMintingAccountMessage = ''
this.removeMintingAccountMessage = ''
this.tempMintingAccount = {}
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
}
render() {
return html`
<div id="group-transaction-page">
<div class="group-transaction-card">
<h2>Group Transaction</h2>
<p>${this.addMintingAccountMessage}</p>
</div>
</div>
`
}
// getMintingAccountGrid() {
// const myGrid = this.shadowRoot.querySelector('#mintingAccountsGrid')
// myGrid.addEventListener('click', (e) => {
// this.tempMintingAccount = myGrid.getEventContext(e).item
// this.shadowRoot.querySelector('#removeMintingAccountDialog').show()
// })
// }
// addPeer(e) {
// this.addPeerLoading = true
// const addPeerAddress = this.shadowRoot.querySelector('#addPeerAddress').value
// parentEpml.request('apiCall', {
// url: `/peers`,
// method: 'POST',
// body: addPeerAddress
// }).then(res => {
// this.addPeerMessage = res.message
// this.addPeerLoading = false
// })
// }
// addMintingAccount(e) {
// this.addMintingAccountLoading = true
// this.addMintingAccountMessage = "Loading..."
// this.addMintingAccountKey = this.shadowRoot.querySelector('#addMintingAccountKey').value
// parentEpml.request('apiCall', {
// url: `/admin/mintingaccounts`,
// method: 'POST',
// body: this.addMintingAccountKey
// }).then(res => {
// if (res === true) {
// this.updateMintingAccounts()
// this.addMintingAccountKey = ''
// this.addMintingAccountMessage = 'Minting Node Added Successfully!'
// this.addMintingAccountLoading = false
// } else {
// this.addMintingAccountKey = ''
// this.addMintingAccountMessage = 'Failed to Add Minting Node!' // Corrected an error here thanks to crow (-_-)
// this.addMintingAccountLoading = false
// }
// })
// }
// updateMintingAccounts() {
// parentEpml.request('apiCall', {
// url: `/admin/mintingaccounts`
// }).then(res => {
// this.mintingAccounts = []
// setTimeout(() => { this.mintingAccounts = res }, 1)
// })
// // setTimeout(updateMintingAccounts, this.config.user.nodeSettings.pingInterval) // Perhaps should be slower...?
// }
// removeMintingAccount(e) {
// this.removeMintingAccountLoading = true
// this.removeMintingAccountMessage = "Loading..."
// this.removeMintingAccountKey = this.shadowRoot.querySelector('#removeMintingAccountKey').value
// this.mintingAccounts.forEach(mintingAccount => {
// if (this.tempMintingAccount.recipientAccount === mintingAccount.recipientAccount) {
// parentEpml.request('apiCall', {
// url: `/admin/mintingaccounts`,
// method: 'DELETE',
// body: this.removeMintingAccountKey
// }).then(res => {
// if (res === true) {
// this.updateMintingAccounts()
// this.removeMintingAccountKey = ''
// this.removeMintingAccountMessage = 'Minting Node Removed Successfully!'
// this.removeMintingAccountLoading = false
// } else {
// this.removeMintingAccountKey = ''
// this.removeMintingAccountMessage = 'Failed to Remove Minting Node!'
// this.removeMintingAccountLoading = false
// }
// })
// }
// })
// }
firstUpdated() {
// Call getMintingAccountGrid
// this.getMintingAccountGrid()
// Call updateMintingAccounts
// this.updateMintingAccounts()
const getGroupIdFromURL = () => {
let tempUrl = document.location.href
let decodeTempUrl = decodeURI(tempUrl)
let splitedUrl = decodeTempUrl.split('?')
let myGroupId = splitedUrl[1]
this.addMintingAccountMessage = myGroupId
console.log(myGroupId);
}
getGroupIdFromURL()
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(getGroupIdFromURL, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
})
parentEpml.imReady()
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
}
window.customElements.define('group-transaction', GroupTransaction)

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<style>
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background-color: #fff;
}
</style>
</head>
<body>
<group-transaction></group-transaction>
<script src="group-transaction.js"></script>
</body>
</html>

View File

@@ -0,0 +1,46 @@
<!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-color: #fff;
}
</style>
</head>
<body>
<group-management></group-management>
<script src="group-management.js"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
<h1>Core plugin</h1>

View File

@@ -0,0 +1,116 @@
import { parentEpml } from './connect.js'
import './streams/streams.js'
let config = {}
let haveRegisteredNodeManagement = false
parentEpml.ready().then(() => {
// THOUGHTS: DONE: The request to register urls should be made once...
// pluginUrlsConf
let pluginUrlsConf = [
{
url: 'minting',
domain: 'core',
page: 'minting/index.html',
title: 'Minting Details',
icon: 'info',
menus: [],
parent: false
},
{
url: 'wallet',
domain: 'core',
page: 'wallet/index.html',
title: 'Wallet',
icon: 'account_balance_wallet',
menus: [],
parent: false
},
{
url: 'send-coin',
domain: 'core',
page: 'send-coin/index.html',
title: 'Send Coin',
icon: 'send',
menus: [],
parent: false
},
{
url: 'trade-portal',
domain: 'core',
page: 'trade-portal/index.html',
title: 'Trade Portal',
icon: 'toc',
menus: [],
parent: false
},
{
url: 'reward-share',
domain: 'core',
page: 'reward-share/index.html',
title: 'Reward Share',
icon: 'call_split',
menus: [],
parent: false
},
{
url: 'name-registration',
domain: 'core',
page: 'name-registration/index.html',
title: 'Name Registration',
icon: 'assignment_ind',
menus: [],
parent: false
},
{
url: 'q-chat',
domain: 'core',
page: 'messaging/q-chat/index.html',
title: 'Q-Chat',
icon: 'message',
menus: [],
parent: false
},
{
url: 'group-management',
domain: 'core',
page: 'group-management/index.html',
title: 'Group Management',
icon: 'group',
menus: [],
parent: false
}
]
const registerPlugins = (pluginInfo) => {
parentEpml.request('registerUrl', pluginInfo)
}
parentEpml.subscribe('config', c => {
config = JSON.parse(c)
// Only register node management if node management is enabled and it hasn't already been registered
if (!haveRegisteredNodeManagement && config.user.knownNodes[config.user.node].enableManagement) {
haveRegisteredNodeManagement = true
let nodeManagementConf = {
url: 'node-management',
domain: 'core',
page: 'node-management/index.html',
title: 'Node Management',
icon: 'cloud',
menus: [],
parent: false
}
let _pluginUrlsConf = [...pluginUrlsConf, nodeManagementConf]
registerPlugins(_pluginUrlsConf)
} else {
registerPlugins(pluginUrlsConf)
}
})
})

View File

@@ -0,0 +1,100 @@
import { LitElement, html, css } from 'lit-element'
class ChainMessaging extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#chain-messaging-page {
background:#fff;
}
`
}
constructor() {
super()
// ...
}
render() {
return html`
<div id="chain-messaging-page">
<h2 style="text-align: center; margin-top: 3rem;">Coming Soon!</h2>
</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)
}
}
}
// _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
}
}
window.customElements.define('chain-messaging', ChainMessaging)

View 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-color: #fff;
}
</style>
</head>
<body>
<chain-messaging></chain-messaging>
<script src="chain-messaging.js"></script>
</body>
</html>

View 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);
}
body {
margin: 0;
font-family: "Roboto", sans-serif;
background-color: #fff;
}
</style>
</head>
<body>
<q-messaging></q-messaging>
<script src="messaging.js"></script>
</body>
</html>

View File

@@ -0,0 +1,223 @@
import { LitElement, html, css } from 'lit-element'
import { Epml } from '../../../epml.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class Messaging extends LitElement {
static get properties() {
return {}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
}
#page {
background: #fff;
padding: 12px 24px;
}
h2 {
margin:0;
font-weight: 400;
}
h3, h4, h5 {
color:#333;
font-weight: 400;
}
.major-title {
color: rgb(3, 169, 244);
margin-top: 1rem;
font-weight: 400;
font-size: 28px;
text-align: center;
}
.sub-title {
color: rgb(3, 169, 244);
margin-top: .5rem;
font-weight: 400;
/* font-size: 19px; */
text-align: center;
}
.sub-title:hover {
cursor: pointer;
}
.sub-url {
font-size: 19px;
text-decoration: underline;
}
.sub-url:hover {
cursor: pointer;
}
.divCard {
border: 1px solid #eee;
padding: 1em;
/** box-shadow: 0 1px 1px 0 rgba(0,0,0,0.14), 0 2px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20); **/
box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20);
margin-bottom: 1.5rem;
}
p {
color:#333;
}
ul, ul li {
color:#333;
}
`
}
constructor() {
super()
}
render() {
return html`
<div id="page">
<div class="divCard">
<h2 class="major-title">Welcome to the Qortal Messaging System!</h2>
<p style="font-size: 19px;">With this system, you are able to accomplish multiple types of messaging available to you in Qortal:</p>
<ul>
<li class="sub-url" @click=${() => this.getUrl('chain-messaging')}><strong>Chain Based Messaging</strong></li>
<li class="sub-url" @click=${() => this.getUrl('q-chat')}><strong>Q-Chat</strong></li>
</ul>
</div>
<div class="divCard">
<h3 class="sub-title" @click=${() => this.getUrl('chain-messaging')}>Chain-Based Messaging</h3>
<p style="font-size: 17px;">A long-term message that is stored <strong>ON CHAIN</strong>.
These messages <strong>are able</strong> to be <strong>sent to groups or individual accounts</strong>, and are essentially <strong>the 'e-mail' of Qortal</strong>.
Use these messages if you intend on the message being a <strong>PERMANENT message</strong> that stays when and where you send it.</p>
<!-- <span style="display: block; text-align: center"><strong>- more info -</strong></span> -->
<ul>
<li style="font-size: 17px; padding: 10px;">There are no @ in Qortal Chain Messaging, only 'registered names'. As the registered names on the chain can only be registered ONCE. Therefore, there are NO DUPLICATES.</li>
<li style="font-size: 17px; padding: 10px;">Chain Messages LAST FOREVER</li>
<li style="font-size: 17px; padding: 10px;"><strong>Encyption is DEFAULT</strong>, unless chosen to be NOT encrypted. (also known as a 'public message' which are readable by an api call from anyone.)</li>
<li style="font-size: 17px; padding: 10px;">'Attachments' will be possible in the future, with the use of the Qortal Data System.</li>
<li style="font-size: 17px; padding: 10px;">Public Messages can be used for 'verification purposes' such as 'copyrights' or 'I did it first' notations. The terms 'copyright' and 'patent' are a thing of the past, if you did it first, put in a public message, and it is by default proven.</li>
</ul>
</div>
<div class="divCard">
<h3 class="sub-title" @click=${() => this.getUrl('q-chat')}>Q-Chat</h3>
<p style="font-size: 17px;">Is a custom chat system that is UNLIKE ANY OTHER in existence. It is the FIRST OF ITS KIND IN THE WORLD.
It is a real-time, blockchain-based chat system that utilizes a memory-based PoW (Proof Of Work) algorithm, to implement a specialized transaction that is 'temporary', on the Qortal blockchain.
Q-Chat messages will DISSAPEAR AFTER 24 HOURS and therefore are not a great choice for things you wish to be permanent.</p>
<ul>
<li style="font-size: 17px; padding: 10px;"><strong>In the future, there will be a 'pinning' system</strong>, that will allow you to convert, or send messages by default with, the ability to stay forever on the Qortal Blockchain. So that if you DO decide that you like a message enough to keep it forever, you may do so.</li>
<li style="font-size: 17px; padding: 10px;"><strong>Q-chat messages are encrypted</strong> (at the moment in June, 2020, Q-chat PM's are encrypted, but the group messages are base58 encoded, meaning they aren't plain text, but if you're smart you can decode them. However, IN THE NEAR FUTURE, ALL MESSAGES REGARDLESS OF GROUP OR PM WILL BE DEFAULT ENCRYPTED WITH PUB/PRIV KEY OF YOUR QORTAL ACCOUNT.</li>
<li style="font-size: 17px; padding: 10px;">Uses a UNIQUE memory-based PoW algorithm, to send messages FREE (no transaction fee)</li>
<li style="font-size: 17px; padding: 10px;">Text-based messaging for the future.</li>
</ul>
</div>
</div>
`
}
getUrl(pageId) {
this.onPageNavigation(`/app/${pageId}`)
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
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)
}
}
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) {
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.imReady()
}
_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()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('q-messaging', Messaging)

View File

@@ -0,0 +1,46 @@
<!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-color: #fff;
overflow: hidden;
}
</style>
</head>
<body>
<q-chat></q-chat>
<script type="module" src="q-chat.js"></script>
</body>
</html>

View File

@@ -0,0 +1,710 @@
import { LitElement, html, css } from 'lit-element'
import { render } from 'lit-html'
import { Epml } from '../../../../epml.js'
// Components
import '../../components/ChatWelcomePage.js'
import '../../components/ChatHead.js'
import '../../components/ChatPage.js'
import '@polymer/paper-spinner/paper-spinner-lite.js'
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-dialog'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class Chat extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
chatTitle: { type: String },
chatHeads: { type: Array },
chatHeadsObj: { type: Object },
chatId: { type: String },
messages: { type: Array },
btnDisable: { type: Boolean },
isLoading: { type: Boolean },
balance: { type: Number }
}
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite{
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
ul {
list-style: none;
padding: 0;
}
.container {
margin: 0 auto;
width: 100%;
background: #fff;
}
.people-list {
width: 20vw;
float: left;
height: 100vh;
overflow-y: hidden;
border-right: 3px #ddd solid;
}
.people-list .search {
padding: 20px;
}
.people-list .create-chat {
border-radius: 3px;
border: none;
display: inline-block;
padding: 14px;
color: white;
background: #6a6c75;
width: 90%;
font-size: 15px;
text-align: center;
cursor: pointer;
}
.people-list .create-chat:hover {
opacity: .8;
box-shadow: 0 3px 5px rgba(0, 0, 0, .2);
}
.people-list ul {
padding: 0;
height: 85vh;
overflow-y: auto;
overflow-x: hidden;
}
.chat {
width: 80vw;
height: 100vh;
float: left;
background: #fff;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
color: #434651;
box-sizing: border-box;
}
.chat .new-message-bar {
display: flex;
flex: 0 1 auto;
align-items: center;
justify-content: space-between;
padding: 0px 25px;
font-size: 14px;
font-weight: 500;
top: 0;
position: absolute;
left: 20vw;
right: 0;
z-index: 5;
background: #6a6c75;
color: white;
border-radius: 0 0 8px 8px;
min-height: 25px;
transition: opacity .15s;
text-transform: capitalize;
opacity: .85;
cursor: pointer;
}
.chat .new-message-bar:hover {
opacity: .75;
transform: translateY(-1px);
box-shadow: 0 3px 7px rgba(0, 0, 0, .2);
}
.hide-new-message-bar {
display: none !important;
}
.chat .chat-history {
position: absolute;
top: 0;
right: 0;
bottom: 100%;
left: 20vw;
border-bottom: 2px solid white;
overflow-y: hidden;
height: 100vh;
box-sizing: border-box;
}
.chat .chat-message {
padding: 10px;
height: 10%;
display: inline-block;
width: 100%;
background-color: #eee;
}
.chat .chat-message textarea {
width: 90%;
border: none;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
}
.chat .chat-message button {
float: right;
color: #94c2ed;
font-size: 16px;
text-transform: uppercase;
border: none;
cursor: pointer;
font-weight: bold;
background: #f2f5f8;
padding: 10px;
margin-top: 4px;
margin-right: 4px;
}
.chat .chat-message button:hover {
color: #75b1e8;
}
.online,
.offline,
.me {
margin-right: 3px;
font-size: 10px;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.red {
--mdc-theme-primary: red;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
.title {
font-weight:600;
font-size:12px;
line-height: 32px;
opacity: 0.66;
}
.input {
width: 100%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 100%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatTitle = ""
this.chatHeads = []
this.chatHeadsObj = {}
this.chatId = ''
this.balance = 1
this.messages = []
this.btnDisable = false
this.isLoading = false
this.showNewMesssageBar = this.showNewMesssageBar.bind(this)
this.hideNewMesssageBar = this.hideNewMesssageBar.bind(this)
}
render() {
return html`
<style>
</style>
<div class="container clearfix">
<div class="people-list" id="people-list">
<div class="search">
<div class="create-chat" @click=${() => this.shadowRoot.querySelector('#startChatDialog').show()}>New Private Message</div>
</div>
<ul class="list">
${this.isEmptyArray(this.chatHeads) ? "Loading..." : this.renderChatHead(this.chatHeads)}
</ul>
</div>
<div class="chat">
<div id="newMessageBar" class="new-message-bar hide-new-message-bar clearfix" @click=${ () => this.scrollToBottom()}>
<span style="flex: 1;">New Message</span>
<span>(Click to scroll down) <mwc-icon style="font-size: 16px; vertical-align: bottom;">keyboard_arrow_down</mwc-icon></span>
</div>
<div class="chat-history">
${window.parent.location.pathname !== "/app/q-chat" ? html`${this.renderChatPage(this.chatId)}` : html`${this.renderChatWelcomePage()}`}
</div>
</div>
<!-- Start Chatting Dialog -->
<mwc-dialog id="startChatDialog" scrimClickAction="${this.isLoading ? '' : 'close'}">
<div style="text-align:center">
<h1>New Private Message</h1>
<hr>
</div>
<p>Type the name or address of who you want to chat with to send a private message!</p>
<textarea class="input" ?disabled=${this.isLoading} id="sendTo" placeholder="Name / Address" rows="1"></textarea>
<p style="margin-bottom:0;">
<textarea class="textarea" @keydown=${(e) => this._textArea(e)} ?disabled=${this.isLoading} id="messageBox" placeholder="Message..." rows="1"></textarea>
</p>
<mwc-button
?disabled="${this.isLoading}"
slot="primaryAction"
@click=${this._sendMessage}
>
${this.isLoading === false ? "Send" : html`<paper-spinner-lite active></paper-spinner-lite>`}
</mwc-button>
<mwc-button
?disabled="${this.isLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
</div>
`
}
renderChatWelcomePage() {
return html`<chat-welcome-page myAddress=${JSON.stringify(this.selectedAddress)}></chat-welcome-page>`
}
renderChatHead(chatHeadArr) {
let tempUrl = document.location.href
let splitedUrl = decodeURI(tempUrl).split('?')
let activeChatHeadUrl = splitedUrl[1] === undefined ? '' : splitedUrl[1]
return chatHeadArr.map(eachChatHead => {
return html`<chat-head activeChatHeadUrl=${activeChatHeadUrl} chatInfo=${JSON.stringify(eachChatHead)}></chat-head>`
})
}
renderChatPage(chatId) {
// Check for the chat ID from and render chat messages
// Else render Welcome to Q-CHat
// TODO: DONE: Do the above in the ChatPage
return html`<chat-page .hideNewMesssageBar=${this.hideNewMesssageBar} .showNewMesssageBar=${this.showNewMesssageBar} myAddress=${window.parent.reduxStore.getState().app.selectedAddress.address} chatId=${chatId}></chat-page>`
}
setChatHeads(chatObj) {
let groupList = chatObj.groups.map(group => group.groupId === 0 ? { groupId: group.groupId, url: `group/${group.groupId}`, groupName: "Qortal General Chat", timestamp: group.timestamp === undefined ? 2 : group.timestamp } : { ...group, timestamp: group.timestamp === undefined ? 1 : group.timestamp, url: `group/${group.groupId}` })
let directList = chatObj.direct.map(dc => {
return { ...dc, url: `direct/${dc.address}` }
})
const compareNames = (a, b) => {
return a.groupName.localeCompare(b.groupName)
}
groupList.sort(compareNames)
let chatHeadMasterList = [...groupList, ...directList]
const compareArgs = (a, b) => {
return b.timestamp - a.timestamp
}
this.chatHeads = chatHeadMasterList.sort(compareArgs)
}
getChatHeadFromState(chatObj) {
if (chatObj === undefined) {
return
} else {
this.chatHeadsObj = chatObj
this.setChatHeads(chatObj)
}
}
firstUpdated() {
const stopKeyEventPropagation = (e) => {
e.stopPropagation();
return false;
}
this.shadowRoot.getElementById('sendTo').addEventListener('keydown', stopKeyEventPropagation);
this.shadowRoot.getElementById('messageBox').addEventListener('keydown', stopKeyEventPropagation);
const getDataFromURL = () => {
let tempUrl = document.location.href
let splitedUrl = decodeURI(tempUrl).split('?')
let urlData = splitedUrl[1]
if (urlData !== undefined) {
this.chatId = urlData
}
}
const runFunctionsAfterPageLoad = () => {
// Functions to exec after render while waiting for page info...
getDataFromURL()
try {
let key = `${window.parent.reduxStore.getState().app.selectedAddress.address.substr(0, 10)}_chat-heads`
let localChatHeads = localStorage.getItem(key)
this.setChatHeads(JSON.parse(localChatHeads))
} catch (e) {
// TODO: Could add error handling in case there's a weird one... (-_-)
return
}
// Clear Interval...
if (this.selectedAddress.address !== undefined) {
clearInterval(runFunctionsAfterPageLoadInterval)
return
}
}
let runFunctionsAfterPageLoadInterval = setInterval(runFunctionsAfterPageLoad, 100);
window.addEventListener("contextmenu", (event) => {
event.preventDefault();
this._textMenu(event)
});
window.addEventListener("click", () => {
parentEpml.request('closeCopyTextMenu', null)
parentEpml.request('closeFramePasteMenu', null)
});
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
parentEpml.request('closeFramePasteMenu', null)
}
}
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) {
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('chat_heads', chatHeads => {
chatHeads = JSON.parse(chatHeads)
// setTimeout(() => {
this.getChatHeadFromState(chatHeads)
// }, 5000)
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {
this.balance = res
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
})
parentEpml.imReady()
}
_sendMessage() {
this.isLoading = true
const recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
if (recipient.length === 0) {
this.isLoading = false
} else if (messageText.length === 0) {
this.isLoading = false
} else {
this.sendMessage()
}
}
async sendMessage(e) {
this.isLoading = true
const _recipient = this.shadowRoot.getElementById('sendTo').value
const messageBox = this.shadowRoot.getElementById('messageBox')
const messageText = messageBox.value
let recipient
const validateName = async (receiverName) => {
let myRes
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`
})
if (myNameRes.error === 401) {
myRes = false
} else {
myRes = myNameRes
}
return myRes
}
const myNameRes = await validateName(_recipient)
if (!myNameRes) {
recipient = _recipient
} else {
recipient = myNameRes.owner
}
let _reference = new Uint8Array(64);
window.crypto.getRandomValues(_reference);
let sendTimestamp = Date.now()
let reference = window.parent.Base58.encode(_reference)
const getAddressPublicKey = async () => {
let isEncrypted
let _publicKey
let addressPublicKey = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/publickey/${recipient}`
})
if (addressPublicKey.error === 102) {
_publicKey = false
parentEpml.request('showSnackBar', "Invalid Name / Address, Check the name / address and retry...")
this.isLoading = false
} else if (addressPublicKey !== false) {
isEncrypted = 1
_publicKey = addressPublicKey
sendMessageRequest(isEncrypted, _publicKey)
} else {
isEncrypted = 0
_publicKey = this.selectedAddress.address
sendMessageRequest(isEncrypted, _publicKey)
}
};
const sendMessageRequest = async (isEncrypted, _publicKey) => {
let chatResponse = await parentEpml.request('chat', {
type: 18,
nonce: this.selectedAddress.nonce,
params: {
timestamp: sendTimestamp,
recipient: recipient,
recipientPublicKey: _publicKey,
message: messageText,
lastReference: reference,
proofOfWorkNonce: 0,
isEncrypted: isEncrypted,
isText: 1
}
})
_computePow(chatResponse)
}
const _computePow = async (chatBytes) => {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
const chatBytesArray = new Uint8Array(_chatBytesArray)
const chatBytesHash = new window.parent.Sha256().process(chatBytesArray).finish().result
const hashPtr = window.parent.sbrk(32, window.parent.heap);
const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
hashAry.set(chatBytesHash);
const difficulty = this.balance === 0 ? 14 : 8;
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
let _response = await parentEpml.request('sign_chat', {
nonce: this.selectedAddress.nonce,
chatBytesArray: chatBytesArray,
chatNonce: nonce
})
getSendChatResponse(_response)
}
const getSendChatResponse = (response) => {
if (response === true) {
messageBox.value = ""
parentEpml.request('showSnackBar', "Message Sent Successfully!")
this.isLoading = false
} else if (response.error) {
parentEpml.request('showSnackBar', response.message)
this.isLoading = false
} else {
parentEpml.request('showSnackBar', "Sending failed, Please retry...")
this.isLoading = false
}
}
// Exec..
getAddressPublicKey()
}
_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()
}
_textArea(e) {
if (e.keyCode === 13 && !e.shiftKey) this._sendMessage()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
onPageNavigation(pageUrl) {
parentEpml.request('setPageUrl', pageUrl)
}
isEmptyArray(arr) {
if (!arr) { return true }
return arr.length === 0
}
scrollToBottom() {
const viewElement = this.shadowRoot.querySelector('chat-page').shadowRoot.querySelector('chat-scroller').shadowRoot.getElementById('viewElement');
viewElement.scroll({ top: viewElement.scrollHeight, left: 0, behavior: 'smooth' })
}
showNewMesssageBar() {
this.shadowRoot.getElementById('newMessageBar').classList.remove('hide-new-message-bar')
}
hideNewMesssageBar() {
this.shadowRoot.getElementById('newMessageBar').classList.add('hide-new-message-bar')
}
}
window.customElements.define('q-chat', Chat)

View 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>
<minting-info></minting-info>
<script type="module" src="minting-info.js"></script>
</body>
</html>

View File

@@ -0,0 +1,733 @@
import { LitElement, html, css } from "lit-element";
import { render } from "lit-html";
import { Epml } from "../../../epml.js";
import "@material/mwc-icon";
import "@material/mwc-button";
import "@material/mwc-dialog";
import "@material/mwc-textfield";
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent });
class MintingInfo extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
loading: { type: Boolean },
adminInfo: { type: Array },
nodeInfo: { type: Array },
sampleBlock: { type: Array },
addressInfo: { type: Array },
addressLevel: { type: Array },
}
}
static get styles() {
return css`
@keyframes moveInBottom {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translate(0);
}
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
h4 {
font-weight:600;
font-size:20px;
line-height: 28px;
color:#000000;
}
.header-title {
display: block;
overflow: hidden;
font-size: 40px;
color: black;
font-weight: 400;
text-align: center;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
cursor: inherit;
margin-top: 2rem;
}
.level-black {
font-size: 32px;
color: black;
font-weight: 400;
text-align: center;
margin-top: 2rem;
}
.level-blue {
display: inline-block;
font-size: 32px;
color: #03a9f4;
font-weight: 400;
text-align: center;
margin-top: 2rem;
}
.sub-main {
position: relative;
text-align: center;
width: 100%;
}
.center-box {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, 0%);
text-align: center;
}
.content-box {
border: 1px solid #a1a1a1;
border-radius: 10px;
padding: 10px 25px;
text-align: center;
display: inline-block;
min-width: 250px;
margin-left: 10px;
margin-bottom: 5px;
}
.help-icon {
color: #f44336;
}
.details {
display: flex;
font-size: 18px;
}
.title {
font-weight:600;
font-size:20px;
line-height: 28px;
opacity: 0.66;
color: #03a9f4;
}
.sub-title {
font-weight:600;
font-size:20px;
line-height: 24px;
opacity: 0.66;
}
.input {
width: 90%;
border: none;
display: inline-block;
font-size: 20px;
padding: 10px 20px;
border-radius: 5px;
resize: none;
background: #eee;
}
.textarea {
width: 90%;
border: none;
display: inline-block;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
height: 120px;
resize: none;
background: #eee;
}
.not-minter {
display: inline-block;
font-size: 32px;
color: #03a9f4;
font-weight: 400;
margin-top: 2rem;
}
.blue {
color: #03a9f4;
}
.black {
color: #000000;
}
.red {
color: #f44336;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
`
}
constructor() {
super()
this.selectedAddress = window.parent.reduxStore.getState().app.selectedAddress.address
this.adminInfo = [];
this.nodeInfo = [];
this.sampleBlock = [];
this.addressInfo = [];
this.addressLevel = [];
}
render() {
if (this.renderMintingPage() === "false") {
return html`
<div>
<div>
<span class="header-title">General Minting Details</span>
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<div class="sub-main">
<div class="center-box">
<div>
<span class="level-black">Blockchain Statistics</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">Avg. Qortal Blocktime</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._averageBlockTime()} Seconds</h4>
</div>
<div class="content-box">
<span class="title">Avg. Blocks Per Day</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._timeCalc()} Blocks</h4>
</div>
<div class="content-box">
<span class="title">Avg. Created QORT Per Day</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._dayReward()} QORT</h4>
</div><br><br><br>
<div>
<span class="level-black">${this.renderActivateHelp()}</span>
<hr style="width: 50%;color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
</div>
</div>
<!-- Activate Account Dialog -->
<mwc-dialog id="activateAccountDialog">
<div style="text-align:center">
<h2>Activate Your Account</h2>
<hr>
</div>
<div>
<h3>Introduction</h3><br />
To activate your account you must have an transaction in your account.
You can ask within Q-Chat or wherever else and someone will happily send you some small QORT.
After somebody send you some QORT, logout and after login again. Your account is activated.
</div>
<mwc-button slot="primaryAction" dialogAction="cancel" class="red-button">Close</mwc-button>
</mwc-dialog>
</div>
`} else {
return html`
<div>
<div>
<span class="header-title">General Minting Details</span>
<hr style="color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div>
<div class="sub-main">
<div class="center-box">
<div>
<span class="level-black">Blockchain Statistics</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">Avg. Qortal Blocktime</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._averageBlockTime()} Seconds</h4>
</div>
<div class="content-box">
<span class="title">Avg. Blocks Per Day</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._timeCalc()} Blocks</h4>
</div>
<div class="content-box">
<span class="title">Avg. Created QORT Per Day</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._dayReward()} QORT</h4>
</div><br><br><br>
<div>
<span class="level-black">${this.renderMintingHelp()}</span>
<hr style="width: 50%;color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">Current Status</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4><span class=${this.cssMinting}>${this._mintingStatus()}</span></h4>
</div>
<div class="content-box">
<span class="title">Current Level</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>Level ${this.addressInfo.level}</h4>
</div>
<div class="content-box">
<span class="title">Blocks To Next Level</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._levelUpBlocks()} Blocks</h4>
</div><br>
<div>
<span class="level-black">If you continue minting 24/7 you will reach level <div class="level-blue">${this._levelUp()}</div> in <div class="level-blue">${this._levelUpDays()}</div> days !</span>
</div><br><br><br>
<div>
<span class="level-black">Minting Rewards Info</span>
<hr style="width: 50%; color: #eee; border-radius: 80%; margin-bottom: 2rem;">
</div><br>
<div class="content-box">
<span class="title">Current Tier</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._currentTier()}</h4>
</div>
<div class="content-box">
<span class="title">Total Minters in The Tier</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countLevels()} Minters</h4>
</div>
<div class="content-box">
<span class="title">Tier Share Per Block</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._tierPercent()} %</h4>
</div>
<div class="content-box">
<span class="title">Est. Reward Per Block</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countReward()} QORT</h4>
</div>
<div class="content-box">
<span class="title">Est. Reward Per Day</span>
<hr style="color: #eee; border-radius: 90%; margin-bottom: 1rem;">
<h4>${this._countRewardDay()} QORT</h4>
</div>
</div>
</div>
<!-- Become A Minter Dialog -->
<mwc-dialog id="becomeMinterDialog">
<div style="text-align:center">
<h2>Become A Minter</h2>
<hr>
</div>
<div>
<h3>Introduction</h3><br />
In Qortal, in order to become a minter and begin earning QORT rewards with your increase in Minter Level, you must first become sponsored.
A sponsor in Qortal is any other minter of level 5 or higher, or a Qortal Founder. You will obtain a sponsorship key from the sponsor, and use that key to get to level 1.
Once you have reached level 1, you will be able to create your own minting key and start earning rewards for helping secure the Qortal Blockchain.
</div>
<div>
<h3>Sponsorship</h3><br />
Your sponsor will issue you a Sponsorship Key which you will use to add to your node, and begin minting (for no rewards until reaching level 1.)
Once you reach level 1, you create/assign your own Minting Key and begin earning rewards. You have XXXX blocks remaining in your sponsorship period.
<br />
Simply reach out to a minter in Qortal who is high enough level to issue a sponsorship key, obtain that key, then come back here and input the key to begin your minting journey !
</div>
<mwc-button slot="primaryAction" dialogAction="cancel" class="red-button">Close</mwc-button>
</mwc-dialog>
</div>
`}
}
firstUpdated() {
const getAdminInfo = () => {
parentEpml.request("apiCall", {url: `/admin/info`}).then((res) => {
setTimeout(() => {this.adminInfo = res;}, 1);
});
setTimeout(getAdminInfo, 30000);
};
const getNodeInfo = () => {
parentEpml.request("apiCall", {url: `/admin/status`}).then((res) => {
this.nodeInfo = res;
// Now look up the sample block
getSampleBlock()
});
setTimeout(getNodeInfo, 30000);
};
const getSampleBlock = () => {
let callBlock = parseFloat(this.nodeInfo.height) - 10000;
parentEpml.request("apiCall", {url: `/blocks/byheight/${callBlock}`}).then((res) => {
setTimeout(() => {this.sampleBlock = res;}, 1);
});
};
const getAddressInfo = () => {
parentEpml.request('apiCall', {url: `/addresses/${window.parent.reduxStore.getState().app.selectedAddress.address}`}).then((res) => {
setTimeout(() => {this.addressInfo = res;}, 1);
});
setTimeout(getAddressInfo, 30000);
};
const getAddressLevel = () => {
parentEpml.request('apiCall', {url: `/addresses/online/levels`}).then((res) => {
setTimeout(() => {this.addressLevel = res;}, 1);
});
setTimeout(getAddressLevel, 30000);
};
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.ready().then(() => {
parentEpml.subscribe("config", async c => {
if (!configLoaded) {
setTimeout(getAdminInfo, 1);
setTimeout(getNodeInfo, 1);
setTimeout(getAddressInfo, 1);
setTimeout(getAddressLevel, 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.imReady();
}
renderMintingPage() {
if (this.addressInfo.error === 124) {
return "false"
} else {
return "true"
}
}
renderActivateHelp() {
if (this.renderMintingPage() === "false") {
return html `Activate Account Details <div class="level-blue">==></div> Not Activated<br><mwc-button class="red-button" @click=${() => this.shadowRoot.querySelector("#activateAccountDialog").show()}><mwc-icon class="help-icon">help_outline</mwc-icon>&nbsp;Press For Help</mwc-button>`;
} else {
return "No Details";
}
}
_averageBlockTime() {
let avgBlockString = (this.adminInfo.currentTimestamp - this.sampleBlock.timestamp).toString();
let averageTimeString = ((avgBlockString / 1000) / 10000).toFixed(2);
let averageBlockTimeString = (averageTimeString).toString();
return "" + averageBlockTimeString;
}
_timeCalc() {
let timeString = (this.adminInfo.currentTimestamp - this.sampleBlock.timestamp).toString();
let averageString = ((timeString / 1000) / 10000).toFixed(2);
let averageBlockDay = (86400 / averageString).toFixed(2);
let averageBlockDayString = (averageBlockDay).toString();
return "" + averageBlockDayString;
}
_dayReward() {
let rewardString = (this._timeCalc() * this._blockReward()).toFixed(2);
let rewardDayString = (rewardString).toString();
return "" + rewardDayString ;
}
_mintingStatus() {
if (this.nodeInfo.isMintingPossible === true && this.nodeInfo.isSynchronizing === true) {
this.cssMinting = "blue"
return "Minting"
} else if (this.nodeInfo.isMintingPossible === true && this.nodeInfo.isSynchronizing === false) {
this.cssMinting = "blue"
return "Minting"
} else if (this.nodeInfo.isMintingPossible === false && this.nodeInfo.isSynchronizing === true) {
this.cssMinting = "red"
return `(Synchronizing... ${this.nodeInfo.syncPercent !== undefined ? this.nodeInfo.syncPercent + '%' : ''})`
} else if (this.nodeInfo.isMintingPossible === false && this.nodeInfo.isSynchronizing === false) {
this.cssMinting = "red"
return "Not Minting"
} else {
return "No Status"
}
}
renderMintingHelp() {
if (this._mintingStatus() === "Not Minting") {
return html `Minting Account Details <div class="level-blue">==></div> Not A Minter<br><mwc-button class="red-button" @click=${() => this.shadowRoot.querySelector("#becomeMinterDialog").show()}><mwc-icon class="help-icon">help_outline</mwc-icon>&nbsp;Press For Help</mwc-button>`;
} else {
return "Minting Account Details";
}
}
_levelUpDays() {
let countDays = ((this._blocksNeed() - (this.addressInfo.blocksMinted + this.addressInfo.blocksMintedAdjustment)) / this._timeCalc()).toFixed(2);
let countString = (countDays).toString();
return "" + countString;
}
_levelUpBlocks() {
let countBlocksString = (this._blocksNeed() - (this.addressInfo.blocksMinted + this.addressInfo.blocksMintedAdjustment)).toString();
return "" + countBlocksString;
}
_blocksNeed() {
if (this.addressInfo.level === 0) {
return "7200"
} else if (this.addressInfo.level === 1) {
return "72000"
} else if (this.addressInfo.level === 2) {
return "201600"
} else if (this.addressInfo.level === 3) {
return "374400"
} else if (this.addressInfo.level === 4) {
return "618400"
} else if (this.addressInfo.level === 5) {
return "964000"
} else if (this.addressInfo.level === 6) {
return "1482400"
} else if (this.addressInfo.level === 7) {
return "2173600"
} else if (this.addressInfo.level === 8) {
return "3037600"
} else if (this.addressInfo.level === 9) {
return "4074400"
}
}
_levelUp() {
if (this.addressInfo.level === 0) {
return "1"
} else if (this.addressInfo.level === 1) {
return "2"
} else if (this.addressInfo.level === 2) {
return "3"
} else if (this.addressInfo.level === 3) {
return "4"
} else if (this.addressInfo.level === 4) {
return "5"
} else if (this.addressInfo.level === 5) {
return "6"
} else if (this.addressInfo.level === 6) {
return "7"
} else if (this.addressInfo.level === 7) {
return "8"
} else if (this.addressInfo.level === 8) {
return "9"
} else if (this.addressInfo.level === 9) {
return "10"
}
}
_currentTier() {
if (this.addressInfo.level === 0) {
return "Tier 0 (Level 0)"
} else if (this.addressInfo.level === 1) {
return "Tier 1 (Level 1 + 2)"
} else if (this.addressInfo.level === 2) {
return "Tier 1 (Level 1 + 2)"
} else if (this.addressInfo.level === 3) {
return "Tier 2 (Level 3 + 4)"
} else if (this.addressInfo.level === 4) {
return "Tier 2 (Level 3 + 4)"
} else if (this.addressInfo.level === 5) {
return "Tier 3 (Level 5 + 6)"
} else if (this.addressInfo.level === 6) {
return "Tier 3 (Level 5 + 6)"
} else if (this.addressInfo.level === 7) {
return "Tier 4 (Level 7 + 8)"
} else if (this.addressInfo.level === 8) {
return "Tier 4 (Level 7 + 8)"
} else if (this.addressInfo.level === 9) {
return "Tier 5 (Level 9 + 10)"
} else if (this.addressInfo.level === 10) {
return "Tier 5 (Level 9 + 10)"
}
}
_tierPercent() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
return "5"
} else if (this.addressInfo.level === 2) {
return "5"
} else if (this.addressInfo.level === 3) {
return "10"
} else if (this.addressInfo.level === 4) {
return "10"
} else if (this.addressInfo.level === 5) {
return "15"
} else if (this.addressInfo.level === 6) {
return "15"
} else if (this.addressInfo.level === 7) {
return "20"
} else if (this.addressInfo.level === 8) {
return "20"
} else if (this.addressInfo.level === 9) {
return "25"
} else if (this.addressInfo.level === 10) {
return "25"
}
}
_blockReward() {
if (this.nodeInfo.height < 259201) {
return "5.00"
} else if (this.nodeInfo.height < 518401) {
return "4.75"
} else if (this.nodeInfo.height < 777601) {
return "4.50"
} else if (this.nodeInfo.height < 1036801) {
return "4.25"
} else if (this.nodeInfo.height < 1296001) {
return "4.00"
} else if (this.nodeInfo.height < 1555201) {
return "3.75"
} else if (this.nodeInfo.height < 1814401) {
return "3.50"
} else if (this.nodeInfo.height < 2073601) {
return "3.25"
} else if (this.nodeInfo.height < 2332801) {
return "3.00"
} else if (this.nodeInfo.height < 2592001) {
return "2.75"
} else if (this.nodeInfo.height < 2851201) {
return "2.50"
} else if (this.nodeInfo.height < 3110401) {
return "2.25"
} else {
return "2.00"
}
}
_countLevels() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
let countTier10 = (this.addressLevel[0].count + this.addressLevel[1].count).toString();
return "" + countTier10;
} else if (this.addressInfo.level === 2) {
let countTier11 = (this.addressLevel[0].count + this.addressLevel[1].count).toString();
return "" + countTier11;
} else if (this.addressInfo.level === 3) {
let countTier20 = (this.addressLevel[2].count + this.addressLevel[3].count).toString();
return "" + countTier20;
} else if (this.addressInfo.level === 4) {
let countTier21 = (this.addressLevel[2].count + this.addressLevel[3].count).toString();
return "" + countTier21;
} else if (this.addressInfo.level === 5) {
let countTier30 = (this.addressLevel[4].count + this.addressLevel[5].count).toString();
return "" + countTier30;
} else if (this.addressInfo.level === 6) {
let countTier31 = (this.addressLevel[4].count + this.addressLevel[5].count).toString();
return "" + countTier31;
} else if (this.addressInfo.level === 10) {
let countTier101 = (this.addressLevel[6].count).toString();
return "" + countTier101;
}
}
_countReward() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
let countReward10 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[0].count + this.addressLevel[1].count)).toFixed(8);
let countReward11 = (countReward10).toString();
return "" + countReward11;
} else if (this.addressInfo.level === 2) {
let countReward20 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[0].count + this.addressLevel[1].count)).toFixed(8);
let countReward21 = (countReward20).toString();
return "" + countReward21;
} else if (this.addressInfo.level === 3) {
let countReward30 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[2].count + this.addressLevel[3].count)).toFixed(8);
let countReward31 = (countReward30).toString();
return "" + countReward31;
} else if (this.addressInfo.level === 4) {
let countReward40 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[2].count + this.addressLevel[3].count)).toFixed(8);
let countReward41 = (countReward40).toString();
return "" + countReward41;
} else if (this.addressInfo.level === 5) {
let countReward50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[4].count + this.addressLevel[5].count)).toFixed(8);
let countReward51 = (countReward50).toString();
return "" + countReward51;
} else if (this.addressInfo.level === 6) {
let countReward60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[4].count + this.addressLevel[5].count)).toFixed(8);
let countReward61 = (countReward60).toString();
return "" + countReward61;
} else if (this.addressInfo.level === 10) {
let countReward100 = ((this._blockReward() / 100 * this._tierPercent()) / this.addressLevel[6].count).toFixed(8);
let countReward101 = (countReward100).toString();
return "" + countReward101;
}
}
_countRewardDay() {
if (this.addressInfo.level === 0) {
return "0"
} else if (this.addressInfo.level === 1) {
let countRewardDay10 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[0].count + this.addressLevel[1].count) * this._timeCalc()).toFixed(8);
let countRewardDay11 = (countRewardDay10).toString();
return "" + countRewardDay11;
} else if (this.addressInfo.level === 2) {
let countRewardDay20 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[0].count + this.addressLevel[1].count) * this._timeCalc()).toFixed(8);
let countRewardDay21 = (countRewardDay20).toString();
return "" + countRewardDay21;
} else if (this.addressInfo.level === 3) {
let countRewardDay30 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[2].count + this.addressLevel[3].count) * this._timeCalc()).toFixed(8);
let countRewardDay31 = (countRewardDay30).toString();
return "" + countRewardDay31;
} else if (this.addressInfo.level === 4) {
let countRewardDay40 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[2].count + this.addressLevel[3].count) * this._timeCalc()).toFixed(8);
let countRewardDay41 = (countRewardDay40).toString();
return "" + countRewardDay41;
} else if (this.addressInfo.level === 5) {
let countRewardDay50 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[4].count + this.addressLevel[5].count) * this._timeCalc()).toFixed(8);
let countRewardDay51 = (countRewardDay50).toString();
return "" + countRewardDay51;
} else if (this.addressInfo.level === 6) {
let countRewardDay60 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[4].count + this.addressLevel[5].count) * this._timeCalc()).toFixed(8);
let countRewardDay61 = (countRewardDay60).toString();
return "" + countRewardDay61;
} else if (this.addressInfo.level === 10) {
let countRewardDay100 = ((this._blockReward() / 100 * this._tierPercent()) / (this.addressLevel[6].count) * this._timeCalc()).toFixed(8);
let countRewardDay101 = (countRewardDay100).toString();
return "" + countRewardDay101;
}
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
isEmptyArray(arr) {
if (!arr) return true;
return arr.length === 0;
}
}
window.customElements.define('minting-info', MintingInfo)

View File

@@ -0,0 +1 @@
This needs work. Should have been done simpler.

View 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>
<name-registration></name-registration>
<script type="module" src="name-registration.js"></script>
</body>
</html>

View File

@@ -0,0 +1,332 @@
// import '@webcomponents/webcomponentsjs/webcomponents-loader.js'
// /* Es6 browser but transpi;led code */
// import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'
import { LitElement, html, css } from 'lit-element'
import { Epml } from '../../../epml.js'
import '@material/mwc-icon'
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-dialog'
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 })
class NameRegistration extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
names: { type: Array },
recipientPublicKey: { type: String },
selectedAddress: { type: Object },
btnDisable: { type: Boolean },
registerNameLoading: { type: Boolean },
error: { type: Boolean },
message: { type: String },
removeError: { type: Boolean },
removeMessage: { 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);
}
#name-registration-page {
background: #fff;
padding: 12px 24px;
}
.divCard {
border: 1px solid #eee;
padding: 1em;
/** box-shadow: 0 1px 1px 0 rgba(0,0,0,0.14), 0 2px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20); **/
box-shadow: 0 .3px 1px 0 rgba(0,0,0,0.14), 0 1px 1px -1px rgba(0,0,0,0.12), 0 1px 2px 0 rgba(0,0,0,0.20);
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.names = []
this.recipientPublicKey = ''
this.btnDisable = false
this.registerNameLoading = false
}
render() {
return html`
<div id="name-registration-page">
<div style="min-height:48px; display: flex; padding-bottom: 6px; margin: 2px;">
<h2 style="margin: 0; flex: 1; padding-top: .1em; display: inline;">Name Registration</h2>
<mwc-button style="float:right;" @click=${() => this.shadowRoot.querySelector('#registerNameDialog').show()}><mwc-icon>add</mwc-icon>Register Name</mwc-button>
</div>
<div class="divCard">
<h3 style="margin: 0; margin-bottom: 1em; text-align: center;">Registered Names</h3>
<vaadin-grid id="namesGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.names)}" aria-label="Peers" .items="${this.names}" height-by-rows>
<vaadin-grid-column path="name"></vaadin-grid-column>
<vaadin-grid-column path="owner"></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.names) ? html`
No names registered by this account!
`: ''}
</div>
<!-- Register Name Dialog -->
<mwc-dialog id="registerNameDialog" scrimClickAction="${this.registerNameLoading ? '' : 'close'}">
<div>Register a Name!</div>
<br>
<mwc-textfield style="width:100%;" ?disabled="${this.registerNameLoading}" label="Name" id="nameInput"></mwc-textfield>
<p style="margin-bottom:0;">
<mwc-textfield style="width:100%;" ?disabled="${this.registerNameLoading}" label="Description (optional)" id="descInput"></mwc-textfield>
</p>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.registerNameLoading}">
<!-- loading message -->
Doing something delicious &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.registerNameLoading}"
alt="Registering Name"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.registerNameLoading}"
slot="primaryAction"
@click=${this.registerName}
>
Register
</mwc-button>
<mwc-button
?disabled="${this.registerNameLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
</div>
`
}
// getNamesGrid() {
// const myGrid = this.shadowRoot.querySelector('#namesGrid')
// myGrid.addEventListener('click', (e) => {
// this.tempMintingAccount = myGrid.getEventContext(e).item
// this.shadowRoot.querySelector('#removeRewardShareDialog').show()
// })
// }
firstUpdated() {
// Call getNamesGrid
// this.getNamesGrid()
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 fetchNames = () => {
// console.log('=========================================')
parentEpml.request('apiCall', {
url: `/names/address/${this.selectedAddress.address}?limit=0&reverse=true`
}).then(res => {
setTimeout(() => { this.names = res }, 1)
})
setTimeout(fetchNames, this.config.user.nodeSettings.pingInterval)
}
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(fetchNames, 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.imReady()
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
async registerName(e) {
this.error = false
this.message = ''
const nameInput = this.shadowRoot.getElementById("nameInput").value
const descInput = this.shadowRoot.getElementById("descInput").value
// Check for valid...^
this.registerNameLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
// Get Account Details
const validateName = async () => {
let isValid = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${nameInput}`
})
return isValid
};
const validateReceiver = async () => {
let nameInfo = await validateName();
let lastRef = await getLastRef();
if (nameInfo.error === 401) {
this.error = false
this.message = ''
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
} else {
this.error = true
this.message = `Name Already Exists!`
}
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let myTxnrequest = await parentEpml.request('transaction', {
type: 3,
nonce: this.selectedAddress.nonce,
params: {
name: nameInput,
value: descInput,
lastReference: lastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = 'Name Registration Successful!'
this.error = false
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
this.registerNameLoading = false
}
_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
}
}
window.customElements.define('name-registration', NameRegistration)

View 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-color: #fff;
}
</style>
</head>
<body>
<node-management></node-management>
<script src="node-management.js"></script>
</body>
</html>

View File

@@ -0,0 +1,506 @@
import { LitElement, html, css } from "lit-element";
import { render } from 'lit-html'
import { Epml } from "../../../epml.js";
import "@polymer/paper-spinner/paper-spinner-lite.js";
import "@vaadin/vaadin-grid/vaadin-grid.js";
import "@vaadin/vaadin-grid/theme/material/all-imports.js";
import "@material/mwc-icon";
import "@material/mwc-textfield";
import "@material/mwc-button";
import "@material/mwc-dialog";
const parentEpml = new Epml({ type: "WINDOW", source: window.parent });
class NodeManagement extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
upTime: { type: String },
mintingAccounts: { type: Array },
peers: { type: Array },
addMintingAccountLoading: { type: Boolean },
removeMintingAccountLoading: { type: Boolean },
addPeerLoading: { type: Boolean },
confPeerLoading: { type: Boolean },
addMintingAccountKey: { type: String },
removeMintingAccountKey: { type: String },
addPeerMessage: { type: String },
confPeerMessage: { type: String },
addMintingAccountMessage: { type: String },
removeMintingAccountMessage: { type: String },
tempMintingAccount: { type: Object },
nodeConfig: { type: Object },
nodeDomain: { type: String },
};
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
paper-spinner-lite {
height: 24px;
width: 24px;
--paper-spinner-color: var(--mdc-theme-primary);
--paper-spinner-stroke-width: 2px;
}
#node-management-page {
background: #fff;
}
mwc-textfield {
width: 100%;
}
.red {
--mdc-theme-primary: #F44336;
}
.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
mwc-button.red-button {
--mdc-theme-primary: red;
--mdc-theme-on-primary: white;
}
.node-card {
/* margin:12px; */
padding: 12px 24px;
background: #fff;
border-radius: 2px;
box-shadow: 11;
}
h2 {
margin: 0;
}
h2,
h3,
h4,
h5 {
color: #333;
font-weight: 400;
}
[hidden] {
display: hidden !important;
visibility: none !important;
}
.details {
display: flex;
font-size: 18px;
}
`;
}
constructor() {
super();
this.upTime = "";
this.mintingAccounts = [];
this.peers = [];
this.addPeerLoading = false;
this.confPeerLoading = false;
this.addMintingAccountLoading = false;
this.removeMintingAccountLoading = false;
this.addMintingAccountKey = "";
this.addPeerMessage = "";
this.confPeerMessage = "";
this.addMintingAccountMessage = "";
this.tempMintingAccount = {};
this.config = {
user: {
node: {},
},
};
this.nodeConfig = {};
this.nodeDomain = "";
}
render() {
return html`
<div id="node-management-page">
<div class="node-card">
<h2>Node management for: ${this.nodeDomain}</h2>
<span><br />Node has been online for: ${this.upTime}</span>
<br /><br />
<div id="minting">
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
<h3
style="margin: 0; flex: 1; padding-top: 8px; display: inline;"
>
Node's minting accounts
</h3>
<mwc-button
style="float:right;"
@click=${() =>
this.shadowRoot
.querySelector("#addMintingAccountDialog")
.show()}
><mwc-icon>add</mwc-icon>Add minting account</mwc-button
>
</div>
<!-- Add Minting Account Dialog -->
<mwc-dialog
id="addMintingAccountDialog"
scrimClickAction="${this.addMintingAccountLoading ? "" : "close"}"
>
<div>
If you would like to mint with your own account you will need to
create a rewardshare transaction to yourself (with rewardshare
percent set to 0), and then mint with the rewardshare key it
gives you.
</div>
<br />
<mwc-textfield
?disabled="${this.addMintingAccountLoading}"
label="Rewardshare key"
id="addMintingAccountKey"
></mwc-textfield>
<div
style="text-align:right; height:36px;"
?hidden=${this.addMintingAccountMessage === ""}
>
<span ?hidden="${this.addMintingAccountLoading}">
${this.addMintingAccountMessage} &nbsp;
</span>
<span ?hidden="${!this.addMintingAccountLoading}">
<!-- loading message -->
Doing something delicious &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.addMintingAccountLoading}"
alt="Adding minting account"
></paper-spinner-lite>
</span>
</div>
<mwc-button
?disabled="${this.addMintingAccountLoading}"
slot="primaryAction"
@click=${this.addMintingAccount}
>
Add
</mwc-button>
<mwc-button
?disabled="${this.addMintingAccountLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red"
>
Close
</mwc-button>
</mwc-dialog>
<vaadin-grid id="mintingAccountsGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.mintingAccounts)}" aria-label="Minting Accounts" .items="${this.mintingAccounts}" height-by-rows>
<vaadin-grid-column auto-width header="Minting Account" path="mintingAccount"></vaadin-grid-column>
<vaadin-grid-column auto-width header="Recipient Account" path="recipientAccount"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="Action" .renderer=${(root, column, data) => {
render(html`<mwc-button class="red" ?disabled=${this.removeMintingAccountLoading} @click=${() => this.removeMintingAccount(data.item.publicKey)}><mwc-icon>create</mwc-icon>Remove</mwc-button>`, root)
}}></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.mintingAccounts) ? html` No minting accounts found for this node ` : ""}
</div>
<br />
<div id="peers">
<div style="min-height: 48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">
<span>Peers connected to node</span>
<span>(${this.peers.length})</span>
</h3>
<mwc-button @click=${() => this.shadowRoot.querySelector("#addPeerDialog").show()}><mwc-icon>add</mwc-icon>Add peer</mwc-button>
</div>
<mwc-dialog id="addPeerDialog" scrimClickAction="${this.addPeerLoading ? "" : "close"}">
<div>Type the peer you wish to add's address below</div>
<br />
<mwc-textfield ?disabled="${this.addPeerLoading}" label="Peer Address" id="addPeerAddress" ></mwc-textfield>
<div style="text-align:right; height:36px;" ?hidden=${this.addPeerMessage === ""}>
<span ?hidden="${this.addPeerLoading}"> ${this.addPeerMessage} &nbsp;</span>
<span ?hidden="${!this.addPeerLoading}">
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.addPeerLoading}"
alt="Adding minting account"
></paper-spinner-lite>
</span>
</div>
<mwc-button
?disabled="${this.addPeerLoading}"
@click="${this.addPeer}"
slot="primaryAction"
>
Add
</mwc-button>
<mwc-button
slot="secondaryAction"
dialogAction="cancel"
?disabled="${this.addPeerLoading}"
class="red"
>
Close
</mwc-button>
</mwc-dialog>
<vaadin-grid id="peersGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.peers)}" aria-label="Peers" .items="${this.peers}" height-by-rows>
<vaadin-grid-column path="address"></vaadin-grid-column>
<vaadin-grid-column path="lastHeight"></vaadin-grid-column>
<vaadin-grid-column path="version" header="Build Version"></vaadin-grid-column>
<vaadin-grid-column path="age" header="Connected for"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="Action" .renderer=${(root, column, data) => {
render(html`<mwc-button class="red" @click=${() => this.removePeer(data.item.address, data.index)}><mwc-icon>delete</mwc-icon>Remove</mwc-button><mwc-button class="green" @click=${() => this.forceSyncPeer(data.item.address, data.index)}>Force Sync</mwc-button>`, root)
}}></vaadin-grid-column>
</vaadin-grid>
${this.isEmptyArray(this.peers) ? html` Node has no connected peers ` : ""}
</div>
<br />
</div>
</div>
`;
}
forceSyncPeer (peerAddress, rowIndex) {
parentEpml
.request("apiCall", {
url: `/admin/forcesync`,
method: "POST",
body: peerAddress,
})
.then((res) => {
parentEpml.request('showSnackBar', "Starting Sync with Peer: " + peerAddress );
});
}
removePeer(peerAddress, rowIndex) {
parentEpml
.request("apiCall", {
url: `/peers`,
method: "DELETE",
body: peerAddress,
})
.then((res) => {
parentEpml.request('showSnackBar', "Successfully removed Peer: " + peerAddress );
this.peers.splice(rowIndex, 1);
});
}
onPageNavigation(pageUrl) {
parentEpml.request("setPageUrl", pageUrl);
}
addPeer(e) {
this.addPeerLoading = true;
const addPeerAddress = this.shadowRoot.querySelector("#addPeerAddress")
.value;
parentEpml
.request("apiCall", {
url: `/peers`,
method: "POST",
body: addPeerAddress,
})
.then((res) => {
this.addPeerMessage = res.message;
this.addPeerLoading = false;
});
}
addMintingAccount(e) {
this.addMintingAccountLoading = true;
this.addMintingAccountMessage = "Loading...";
this.addMintingAccountKey = this.shadowRoot.querySelector(
"#addMintingAccountKey"
).value;
parentEpml
.request("apiCall", {
url: `/admin/mintingaccounts`,
method: "POST",
body: this.addMintingAccountKey,
})
.then((res) => {
if (res === true) {
this.updateMintingAccounts();
this.addMintingAccountKey = "";
this.addMintingAccountMessage = "Minting Node Added Successfully!";
this.addMintingAccountLoading = false;
} else {
this.addMintingAccountKey = "";
this.addMintingAccountMessage = "Failed to Add Minting Node!"; // Corrected an error here thanks to crow (-_-)
this.addMintingAccountLoading = false;
}
});
}
updateMintingAccounts() {
parentEpml.request("apiCall", {
url: `/admin/mintingaccounts`,
}).then((res) => {
setTimeout(() => this.mintingAccounts = res, 1);
});
}
_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()
}
removeMintingAccount(publicKey) {
this.removeMintingAccountLoading = true;
parentEpml.request("apiCall", {
url: `/admin/mintingaccounts`,
method: "DELETE",
body: publicKey,
}).then((res) => {
if (res === true) {
this.updateMintingAccounts();
this.removeMintingAccountLoading = false;
parentEpml.request('showSnackBar', "Successfully Removed Minting Account!");
} else {
this.removeMintingAccountLoading = false;
parentEpml.request('showSnackBar', "Failed to Remove Minting Account!");
}
});
}
firstUpdated() {
// Call updateMintingAccounts
this.updateMintingAccounts();
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)
}
// Calculate HH MM SS from Milliseconds...
const convertMsToTime = (milliseconds) => {
let day, hour, minute, seconds;
seconds = Math.floor(milliseconds / 1000);
minute = Math.floor(seconds / 60);
seconds = seconds % 60;
hour = Math.floor(minute / 60);
minute = minute % 60;
day = Math.floor(hour / 24);
hour = hour % 24;
if (isNaN(day)) {
return "offline";
}
return day + "d " + hour + "h " + minute + "m";
};
const getNodeUpTime = () => {
parentEpml
.request("apiCall", {
url: `/admin/uptime`,
})
.then((res) => {
this.upTime = "";
setTimeout(() => {
this.upTime = convertMsToTime(res);
}, 1);
});
setTimeout(getNodeUpTime, this.config.user.nodeSettings.pingInterval);
};
const updatePeers = () => {
parentEpml
.request("apiCall", {
url: `/peers`,
})
.then((res) => {
setTimeout(() => {
this.peers = res;
}, 1);
});
setTimeout(updatePeers, this.config.user.nodeSettings.pingInterval);
};
const getNodeConfig = () => {
parentEpml.request("getNodeConfig").then((res) => {
setTimeout(() => {
this.nodeConfig = res;
}, 1);
let myNode = window.parent.reduxStore.getState().app.nodeConfig
.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
this.nodeDomain = myNode.domain + ":" + myNode.port;
});
setTimeout(getNodeConfig, 1000);
};
let configLoaded = false;
parentEpml.ready().then(() => {
parentEpml.subscribe("config", async c => {
if (!configLoaded) {
setTimeout(getNodeUpTime, 1);
setTimeout(updatePeers, 1);
setTimeout(this.updateMintingAccounts, 1);
setTimeout(getNodeConfig, 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.imReady();
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
isEmptyArray(arr) {
if (!arr) return true;
return arr.length === 0;
}
}
window.customElements.define("node-management", NodeManagement);

View 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>
<reward-share></reward-share>
<script type="module" src="reward-share.js"></script>
</body>
</html>

View File

@@ -0,0 +1,562 @@
import { LitElement, html, css } from 'lit-element'
import { render } from 'lit-html'
import { Epml } from '../../../epml.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 })
class RewardShare extends LitElement {
static get properties() {
return {
loading: { type: Boolean },
rewardShares: { type: Array },
recipientPublicKey: { type: String },
selectedAddress: { type: Object },
btnDisable: { type: Boolean },
createRewardShareLoading: { type: Boolean },
removeRewardShareLoading: { type: Boolean },
rewardSharePercentage: { type: Number },
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);
}
#reward-share-page {
background: #fff;
padding: 12px 24px;
}
h2 {
margin:0;
}
h2, h3, h4, h5 {
color:#333;
font-weight: 400;
}
.red {
--mdc-theme-primary: #F44336;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.rewardShares = []
this.recipientPublicKey = ''
this.rewardSharePercentage = 0
this.btnDisable = false
this.createRewardShareLoading = false
this.removeRewardShareLoading = false
}
render() {
return html`
<div id="reward-share-page">
<div style="min-height:48px; display: flex; padding-bottom: 6px;">
<h3 style="margin: 0; flex: 1; padding-top: 8px; display: inline;">Rewardshares involving this account</h3>
<mwc-button style="float:right;" @click=${() => this.shadowRoot.querySelector('#createRewardShareDialog').show()}><mwc-icon>add</mwc-icon>Create reward share</mwc-button>
</div>
<vaadin-grid id="accountRewardSharesGrid" style="height:auto;" ?hidden="${this.isEmptyArray(this.rewardShares)}" .items="${this.rewardShares}" height-by-rows>
<vaadin-grid-column auto-width path="mintingAccount"></vaadin-grid-column>
<vaadin-grid-column auto-width path="sharePercent"></vaadin-grid-column>
<vaadin-grid-column auto-width path="recipient"></vaadin-grid-column>
<vaadin-grid-column width="12em" header="Action" .renderer=${(root, column, data) => {
render(html`${this.renderRemoveRewardShareButton(data.item)}`, root)
}}></vaadin-grid-column>
</vaadin-grid>
<mwc-dialog id="createRewardShareDialog" scrimClickAction="${this.createRewardShareLoading ? '' : 'close'}">
<div>Level 1 - 4 can create a Self Share and Level 5 or above can create a Reward Share!</div>
<br>
<mwc-textfield style="width:100%;" ?disabled="${this.createRewardShareLoading}" label="Recipient Public Key" id="recipientPublicKey"></mwc-textfield>
<p style="margin-bottom:0;">
Reward share percentage: ${this.rewardSharePercentage}
<!-- <mwc-textfield style="width:36px;" ?disabled="${this.createRewardShareLoading}" id="createRewardShare"></mwc-textfield> -->
</p>
<mwc-slider
@change="${e => this.rewardSharePercentage = this.shadowRoot.getElementById('rewardSharePercentageSlider').value}"
id="rewardSharePercentageSlider"
style="width:100%;"
step="1"
pin
markers
max="100"
value="${this.rewardSharePercentage}">
</mwc-slider>
<div style="text-align:right; height:36px;">
<span ?hidden="${!this.createRewardShareLoading}">
<!-- loading message -->
Doing something delicious &nbsp;
<paper-spinner-lite
style="margin-top:12px;"
?active="${this.createRewardShareLoading}"
alt="Adding minting account"></paper-spinner-lite>
</span>
<span ?hidden=${this.message === ''} style="${this.error ? 'color:red;' : ''}">
${this.message}
</span>
</div>
<mwc-button
?disabled="${this.createRewardShareLoading}"
slot="primaryAction"
@click=${this.createRewardShare}
>
<!--dialogAction="add"-->
Add
</mwc-button>
<mwc-button
?disabled="${this.createRewardShareLoading}"
slot="secondaryAction"
dialogAction="cancel"
class="red">
Close
</mwc-button>
</mwc-dialog>
${this.isEmptyArray(this.rewardShares) ? html`
Account is not involved in any reward shares
`: ''}
</div>
`
}
renderRemoveRewardShareButton(rewardShareObject) {
if (rewardShareObject.mintingAccount === this.selectedAddress.address) {
return html`<mwc-button class="red" ?disabled=${this.removeRewardShareLoading} @click=${() => this.removeRewardShare(rewardShareObject)}><mwc-icon>create</mwc-icon>Remove</mwc-button>`
} else {
return ""
}
}
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("recipientPublicKey")
const updateRewardshares = () => {
parentEpml.request('apiCall', {
url: `/addresses/rewardshares?involving=${this.selectedAddress.address}`
}).then(res => {
setTimeout(() => { this.rewardShares = res }, 1)
})
setTimeout(updateRewardshares, this.config.user.nodeSettings.pingInterval)
}
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(updateRewardshares, 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 createRewardShare(e) {
this.error = false
this.message = ''
const recipientPublicKey = this.shadowRoot.getElementById("recipientPublicKey").value
const percentageShare = this.shadowRoot.getElementById("rewardSharePercentageSlider").value
// Check for valid...^
this.createRewardShareLoading = true
let recipientAddress = window.parent.base58PublicKeyToAddress(recipientPublicKey)
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
// Get Account Details
const getAccountDetails = async () => {
let myAccountDetails = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/${this.selectedAddress.address}`
})
return myAccountDetails
};
// Get Reward Relationship if it already exists
const getRewardShareRelationship = async (minterAddr) => {
let isRewardShareExisting = false
let myRewardShareArray = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/rewardshares?minters=${minterAddr}&recipients=${recipientAddress}`
})
isRewardShareExisting = myRewardShareArray.length !== 0 ? true : false
return isRewardShareExisting
// THOUGHTS: At this point, I think I dont wanna further do any check...
// myRewardShareArray.forEach(rewsh => {
// if (rewsh.mintingAccount) {
// }
// })
}
// Validate Reward Share by Level
const validateReceiver = async () => {
let accountDetails = await getAccountDetails();
let lastRef = await getLastRef();
let isExisting = await getRewardShareRelationship(this.selectedAddress.address)
// Check for creating self share at different levels (also adding check for flags...)
if (accountDetails.flags === 1) {
this.error = false
this.message = ''
let myTransaction = await makeTransactionRequest(lastRef)
if (isExisting === true) {
this.error = true
this.message = `Cannot Create Multiple Reward Shares!`
} else {
// Send the transaction for confirmation by the user
this.error = false
this.message = ''
getTxnRequestResponse(myTransaction)
}
} else if (accountDetails.address === recipientAddress) {
if (accountDetails.level >= 1 && accountDetails.level <= 4) {
this.error = false
this.message = ''
let myTransaction = await makeTransactionRequest(lastRef)
if (isExisting === true) {
this.error = true
this.message = `Cannot Create Multiple Self Shares!`
} else {
// Send the transaction for confirmation by the user
this.error = false
this.message = ''
getTxnRequestResponse(myTransaction)
}
} else if (accountDetails.level >= 5) {
this.error = false
this.message = ''
let myTransaction = await makeTransactionRequest(lastRef)
if (isExisting === true) {
this.error = true
this.message = `Cannot Create Multiple Self Shares!`
} else {
// Send the transaction for confirmation by the user
this.error = false
this.message = ''
getTxnRequestResponse(myTransaction)
}
} else {
this.error = true
this.message = `CANNOT CREATE SELF SHARE! at level ${accountDetails.level}`
}
} else { //Check for creating reward shares
if (accountDetails.level >= 5) {
this.error = false
this.message = ''
let myTransaction = await makeTransactionRequest(lastRef)
if (isExisting === true) {
this.error = true
this.message = `Cannot Create Multiple Reward Shares!`
} else {
// Send the transaction for confirmation by the user
this.error = false
this.message = ''
getTxnRequestResponse(myTransaction)
}
} else {
this.error = true
this.message = `CANNOT CREATE REWARD SHARE! at level ${accountDetails.level}`
}
}
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let mylastRef = lastRef
let myTxnrequest = await parentEpml.request('transaction', {
type: 38,
nonce: this.selectedAddress.nonce,
params: {
recipientPublicKey,
percentageShare,
lastReference: mylastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.error = true
this.message = txnResponse.message
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.message = 'Reward Share Successful!'
this.error = false
} else {
this.error = true
this.message = txnResponse.data.message
throw new Error(txnResponse)
}
}
validateReceiver()
this.createRewardShareLoading = false
}
async removeRewardShare(rewardShareObject) {
const myPercentageShare = -1
// Check for valid...^
this.removeRewardShareLoading = true
// Get Last Ref
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`
})
return myRef
};
// Remove Reward Share
const removeReceiver = async () => {
let lastRef = await getLastRef();
let myTransaction = await makeTransactionRequest(lastRef)
getTxnRequestResponse(myTransaction)
}
// Make Transaction Request
const makeTransactionRequest = async (lastRef) => {
let mylastRef = lastRef
let myTxnrequest = await parentEpml.request('transaction', {
type: 381,
nonce: this.selectedAddress.nonce,
params: {
rewardShareKeyPairPublicKey: rewardShareObject.rewardSharePublicKey,
recipient: rewardShareObject.recipient,
percentageShare: myPercentageShare,
lastReference: mylastRef,
}
})
return myTxnrequest
}
// FAILED txnResponse = {success: false, message: "User declined transaction"}
// SUCCESS txnResponse = { success: true, data: true }
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.removeRewardShareLoading = false
parentEpml.request('showSnackBar', txnResponse.message)
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.removeRewardShareLoading = false
parentEpml.request('showSnackBar', 'Reward Share Removed Successfully!')
} else {
this.removeRewardShareLoading = false
parentEpml.request('showSnackBar', txnResponse.data.message)
throw new Error(txnResponse)
}
}
removeReceiver()
}
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('reward-share', RewardShare)

View File

@@ -0,0 +1,44 @@
<!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;
}
</style>
</head>
<body>
<send-coin-page></send-coin-page>
<script src="send-coin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,858 @@
import { LitElement, html, css } from 'lit-element'
import { render } from 'lit-html'
import { Epml } from '../../../epml'
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-select'
import '@material/mwc-list/mwc-list-item.js'
import '@material/mwc-slider'
import '@polymer/paper-progress/paper-progress.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class SendMoneyPage extends LitElement {
static get properties() {
return {
addresses: { type: Array },
amount: { type: Number },
errorMessage: { type: String },
sendMoneyLoading: { type: Boolean },
btnDisable: { type: Boolean },
data: { type: Object },
selectedAddress: { type: Object },
recipient: { type: String },
isValidAmount: { type: Boolean },
balance: { type: Number },
qortBalance: { type: Number },
btcBalance: { type: Number },
ltcBalance: { type: Number },
dogeBalance: { type: Number },
selectedCoin: { type: String },
satFeePerByte: { type: Number },
}
}
static get observers() {
return ['_kmxKeyUp(amount)']
}
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);
}
#sendMoneyWrapper {
/* Extra 3px for left border */
/* overflow: hidden; */
}
/* #sendMoneyWrapper>* {
width: auto !important;
padding: 0 15px;
} */
#sendMoneyWrapper paper-button {
float: right;
}
#sendMoneyWrapper .buttons {
/* --paper-button-ink-color: var(--paper-green-500);
color: var(--paper-green-500); */
width: auto !important;
}
.address-item {
--paper-item-focused: {
background: transparent;
}
--paper-item-focused-before: {
opacity: 0;
}
}
.address-balance {
font-size: 42px;
font-weight: 100;
}
.show-transactions {
cursor: pointer;
}
.address-icon {
border-radius: 50%;
border: 5px solid;
/*border-left: 4px solid;*/
padding: 8px;
}
mwc-textfield {
margin: 0;
}
.selectedBalance {
display: none;
font-size: 14px;
}
.selectedBalance .balance {
font-size: 22px;
font-weight: 100;
}
.coinName::before {
content: "";
display: inline-block;
height: 25px;
width: 25px;
position: absolute;
background-repeat: no-repeat;
background-size: cover;
left: 10px;
top: 10px;
}
.qort.coinName:before {
background-image: url('/img/qort.png');
}
.btc.coinName:before {
background-image: url('/img/btc.png');
}
.ltc.coinName:before {
background-image: url('/img/ltc.png');
}
.doge.coinName:before {
background-image: url('/img/doge.png');
}
.coinName {
display: inline-block;
height: 25px;
padding-left: 25px;
}
paper-progress {
--paper-progress-active-color: var(--mdc-theme-primary);
}
`
}
render() {
return html`
<div id="sendMoneyWrapper" style="width:auto; padding:10px; background: #fff; height:100vh;">
<div class="layout horizontal center" style=" padding:12px 15px;">
<paper-card style="width:100%; max-width:740px;">
<div style="background-color: ${this.selectedAddress.color}; margin:0; color: ${this.textColor(this.selectedAddress.textColor)};">
<h3 style="margin:0; padding:8px 0;">Send Coin</h3>
<div class="selectedBalance">
<span id="balance"></span> available for transfer from
<span id="address"></span>
</div>
</div>
</paper-card>
<p>
<mwc-select id="coinType" label="Select Coin" index="0" @selected=${(e) => this.selectCoin(e)} style="min-width: 130px; max-width:100%; width:100%;">
<mwc-list-item value="qort"></span> <span class="coinName qort">QORT</span></mwc-list-item>
<mwc-list-item value="btc"> <span class="coinName btc">BTC</span></mwc-list-item>
<mwc-list-item value="ltc"> <span class="coinName ltc">LTC</span></mwc-list-item>
<mwc-list-item value="doge"> <span class="coinName doge">DOGE</span></mwc-list-item>
</mwc-select>
</p>
<p>
<mwc-textfield
style="width:100%;"
id="amountInput"
required
label="Amount (QORT)"
@input=${(e) => {
this._checkAmount(e)
}}
type="number"
auto-validate="false"
value="${this.amount}"
>
</mwc-textfield>
</p>
<p>
<mwc-textfield style="width:100%;" label="To (address or name)" id="recipient" type="text" value="${this.recipient}"></mwc-textfield>
</p>
<div style="${this.selectedCoin === 'invalid' || this.selectedCoin === 'qort' ? 'visibility: hidden; margin-bottom: -5em;' : 'visibility: visible; margin-bottom: 0;'}">
<p style="margin-bottom:0;">Fee per byte: ${(this.satFeePerByte / 1e8).toFixed(8)} ${this.selectedCoin === 'invalid' ? 'QORT' : this.selectedCoin.toLocaleUpperCase()}</p>
<mwc-slider
@change="${(e) => (this.satFeePerByte = e.target.value)}"
id="feeSlider"
style="width:100%;"
step="1"
min="10"
max="100"
?disabled=${this.selectedCoin === 'invalid' || this.selectedCoin === 'qort' ? true : false}
value="${this.satFeePerByte}"
>
</mwc-slider>
</div>
<p style="color:red">${this.errorMessage}</p>
<p style="color:green;word-break: break-word;">${this.successMessage}</p>
${this.sendMoneyLoading ? html` <paper-progress indeterminate style="width:100%; margin:4px;"></paper-progress> ` : ''}
<div class="buttons">
<div>
<mwc-button ?disabled=${this.btnDisable} style="width:100%;" raised icon="send" @click=${(e) => this.doSend(e)}>Send &nbsp;</mwc-button>
</div>
</div>
</div>
</div>
`
}
_floor(num) {
return Math.floor(num)
}
// Helper Functions (Re-Used in Most part of the UI )
/**
* Check and Validate Amount Helper Function
* @param { Event } e
*
* @description Gets called oninput in an input element
*/
_checkAmount(e) {
const targetAmount = e.target.value
const target = e.target
if (targetAmount.length === 0) {
this.isValidAmount = false
this.btnDisable = true
// Quick Hack to lose and regain focus inorder to display error message without user having to click outside the input field
e.target.blur()
e.target.focus()
e.target.invalid = true
e.target.validationMessage = 'Invalid Amount!'
} else {
this.btnDisable = false
}
e.target.blur()
e.target.focus()
e.target.validityTransform = (newValue, nativeValidity) => {
if (newValue.includes('-') === true) {
this.btnDisable = true
target.validationMessage = 'Invalid Amount!'
return {
valid: false,
}
} else if (!nativeValidity.valid) {
if (newValue.includes('.') === true) {
let myAmount = newValue.split('.')
if (myAmount[1].length > 8) {
this.btnDisable = true
target.validationMessage = 'Invalid Amount!'
} else {
return {
valid: true,
}
}
}
} else {
this.btnDisable = false
}
}
}
textColor(color) {
return color == 'light' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.87)'
}
pasteToTextBox(elementId) {
// Return focus to the window
window.focus()
navigator.clipboard.readText().then((clipboardText) => {
let element = this.shadowRoot.getElementById(elementId)
element.value += clipboardText
element.focus()
})
}
pasteMenu(event, elementId) {
let eventObject = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY, elementId }
parentEpml.request('openFramePasteMenu', eventObject)
}
doSend(e) {
if (this.selectedCoin === 'invalid') {
parentEpml.request('showSnackBar', 'Invalid Selection!')
} else if (this.selectedCoin === 'qort') {
this.sendQort()
} else if (this.selectedCoin === 'btc') {
this.sendBtc()
} else if (this.selectedCoin === 'ltc') {
this.sendLtc()
} else if (this.selectedCoin === 'doge') {
this.sendDoge()
}
}
async sendQort() {
const amount = this.shadowRoot.getElementById('amountInput').value
let recipient = this.shadowRoot.getElementById('recipient').value
this.sendMoneyLoading = true
this.btnDisable = true
if (parseFloat(amount) + parseFloat(0.001) > parseFloat(this.balance)) {
this.sendMoneyLoading = false
this.btnDisable = false
parentEpml.request('showSnackBar', 'Insufficient Funds!')
return false
}
if (parseFloat(amount) <= 0) {
this.sendMoneyLoading = false
this.btnDisable = false
parentEpml.request('showSnackBar', 'Invalid Amount!')
return false
}
if (recipient.length === 0) {
this.sendMoneyLoading = false
this.btnDisable = false
parentEpml.request('showSnackBar', 'Receiver cannot be empty!')
return false
}
const getLastRef = async () => {
let myRef = await parentEpml.request('apiCall', {
type: 'api',
url: `/addresses/lastreference/${this.selectedAddress.address}`,
})
return myRef
}
const validateName = async (receiverName) => {
let myRes
let myNameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`,
})
if (myNameRes.error === 401) {
myRes = false
} else {
myRes = myNameRes
}
return myRes
}
const validateAddress = async (receiverAddress) => {
let myAddress = await window.parent.validateAddress(receiverAddress)
return myAddress
}
const validateReceiver = async (recipient) => {
let lastRef = await getLastRef()
let isAddress
try {
isAddress = await validateAddress(recipient)
} catch (err) {
isAddress = false
}
if (isAddress) {
let myTransaction = await makeTransactionRequest(recipient, lastRef)
getTxnRequestResponse(myTransaction)
} else {
let myNameRes = await validateName(recipient)
if (myNameRes !== false) {
let myNameAddress = myNameRes.owner
let myTransaction = await makeTransactionRequest(myNameAddress, lastRef)
getTxnRequestResponse(myTransaction)
} else {
console.error('INVALID_RECEIVER')
this.errorMessage = 'INVALID_RECEIVER'
this.sendMoneyLoading = false
this.btnDisable = false
}
}
}
const makeTransactionRequest = async (receiver, lastRef) => {
let myReceiver = receiver
let mylastRef = lastRef
let myTxnrequest = await parentEpml.request('transaction', {
type: 2,
nonce: this.selectedAddress.nonce,
params: {
recipient: myReceiver,
amount: amount,
lastReference: mylastRef,
fee: 0.001,
},
})
return myTxnrequest
}
const getTxnRequestResponse = (txnResponse) => {
if (txnResponse.success === false && txnResponse.message) {
this.errorMessage = txnResponse.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
} else if (txnResponse.success === true && !txnResponse.data.error) {
this.shadowRoot.getElementById('amountInput').value = ''
this.shadowRoot.getElementById('recipient').value = ''
this.errorMessage = ''
this.recipient = ''
this.amount = 0
this.successMessage = 'Transaction Successful!'
this.sendMoneyLoading = false
this.btnDisable = false
} else {
this.errorMessage = txnResponse.data.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
}
}
validateReceiver(recipient)
}
async sendBtc() {
const amount = this.shadowRoot.getElementById('amountInput').value
let recipient = this.shadowRoot.getElementById('recipient').value
const xprv58 = this.selectedAddress.btcWallet.derivedMasterPrivateKey
this.sendMoneyLoading = true
this.btnDisable = true
const makeRequest = async () => {
const opts = {
xprv58: xprv58,
receivingAddress: recipient,
bitcoinAmount: amount,
feePerByte: (this.satFeePerByte / 1e8).toFixed(8),
}
const response = await parentEpml.request('sendBtc', opts)
return response
}
const manageResponse = (response) => {
if (response.length === 64) {
this.shadowRoot.getElementById('amountInput').value = 0
this.shadowRoot.getElementById('recipient').value = ''
this.errorMessage = ''
this.recipient = ''
this.amount = 0
this.successMessage = 'Transaction Successful!'
this.sendMoneyLoading = false
this.btnDisable = false
} else if (response === false) {
this.errorMessage = 'Transaction Failed!'
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
} else {
this.errorMessage = response.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(response)
}
}
// Call makeRequest
const res = await makeRequest()
manageResponse(res)
}
async sendLtc() {
const amount = this.shadowRoot.getElementById('amountInput').value
let recipient = this.shadowRoot.getElementById('recipient').value
const xprv58 = this.selectedAddress.ltcWallet.derivedMasterPrivateKey
this.sendMoneyLoading = true
this.btnDisable = true
const makeRequest = async () => {
const opts = {
xprv58: xprv58,
receivingAddress: recipient,
litecoinAmount: amount,
feePerByte: (this.satFeePerByte / 1e8).toFixed(8),
}
const response = await parentEpml.request('sendLtc', opts)
return response
}
const manageResponse = (response) => {
if (response.length === 64) {
this.shadowRoot.getElementById('amountInput').value = 0
this.shadowRoot.getElementById('recipient').value = ''
this.errorMessage = ''
this.recipient = ''
this.amount = 0
this.successMessage = 'Transaction Successful!'
this.sendMoneyLoading = false
this.btnDisable = false
} else if (response === false) {
this.errorMessage = 'Transaction Failed!'
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
} else {
this.errorMessage = response.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(response)
}
}
// Call makeRequest
const res = await makeRequest()
manageResponse(res)
}
async sendDoge() {
const amount = this.shadowRoot.getElementById('amountInput').value
let recipient = this.shadowRoot.getElementById('recipient').value
const xprv58 = this.selectedAddress.dogeWallet.derivedMasterPrivateKey
this.sendMoneyLoading = true
this.btnDisable = true
const makeRequest = async () => {
const opts = {
xprv58: xprv58,
receivingAddress: recipient,
dogecoinAmount: amount,
feePerByte: (this.satFeePerByte / 1e8).toFixed(8),
}
const response = await parentEpml.request('sendDoge', opts)
return response
}
const manageResponse = (response) => {
if (response.length === 64) {
this.shadowRoot.getElementById('amountInput').value = 0
this.shadowRoot.getElementById('recipient').value = ''
this.errorMessage = ''
this.recipient = ''
this.amount = 0
this.successMessage = 'Transaction Successful!'
this.sendMoneyLoading = false
this.btnDisable = false
} else if (response === false) {
this.errorMessage = 'Transaction Failed!'
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(txnResponse)
} else {
this.errorMessage = response.message
this.sendMoneyLoading = false
this.btnDisable = false
throw new Error(response)
}
}
// Call makeRequest
const res = await makeRequest()
manageResponse(res)
}
_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()
}
updateAccountBalance() {
clearTimeout(this.updateAccountBalanceTimeout)
parentEpml
.request('apiCall', {
url: `/addresses/balance/${this.selectedAddress.address}`,
})
.then((res) => {
this.qortBalance = res
this.updateAccountBalanceTimeout = setTimeout(() => this.updateAccountBalance(), 4000)
})
}
constructor() {
super()
this.recipient = ''
this.errorMessage = ''
this.sendMoneyLoading = false
this.btnDisable = false
this.selectedAddress = {}
this.amount = 0
this.satFeePerByte = 100000 // TODO: Set to 0.001 QORT (100000 in sats)
this.btcSatMinFee = 20
this.btcSatMaxFee = 150
this.btcDefaultFee = 100 // 0.000001 BTC per byte
this.ltcSatMinFee = 10
this.ltcSatMaxFee = 100
this.ltcDefaultFee = 30 // 0.0000003 LTC per byte
this.dogeSatMinFee = 100000
this.dogeSatMaxFee = 1000000
this.dogeDefaultFee = 500000 // 0.005 DOGE per byte
this.isValidAmount = false
this.qortBalance = 0
this.btcBalance = 0
this.ltcBalance = 0
this.dogeBalance = 0
this.selectedCoin = 'invalid'
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
const addr = selectedAddress.address
this.updateAccountBalance()
})
parentEpml.subscribe('config', (c) => {
if (!configLoaded) {
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(res.elementId)
this.isPasteMenuOpen = false
}
})
})
}
firstUpdated() {
// Get BTC Balance
this.updateBTCAccountBalance()
// Get LTC Balance
this.updateLTCAccountBalance()
// Get DOGE Balance
this.updateDOGEAccountBalance()
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)
}
}
// TODO: Rewrite the context menu event listener to support more elements (for now, I'll do write everything out manually )
this.shadowRoot.getElementById('amountInput').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, 'amountInput')
this.isPasteMenuOpen = true
// Prevent Default and Stop Event Bubbling
event.preventDefault()
event.stopPropagation()
}
}
checkSelectedTextAndShowMenu()
})
this.shadowRoot.getElementById('recipient').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, 'recipient')
this.isPasteMenuOpen = true
// Prevent Default and Stop Event Bubbling
event.preventDefault()
event.stopPropagation()
}
}
checkSelectedTextAndShowMenu()
})
}
selectCoin(e) {
const coinType = this.shadowRoot.getElementById('coinType').value
this.selectedCoin = coinType
this.amount = 0
this.recipient = ''
this.shadowRoot.getElementById('amountInput').value = 0
this.shadowRoot.getElementById('recipient').value = ''
this.successMessage = ''
this.errorMessage = ''
if (coinType === 'qort') {
this.shadowRoot.getElementById('balance').textContent = `${this.qortBalance} QORT`
this.shadowRoot.getElementById('address').textContent = this.selectedAddress.address
this.shadowRoot.querySelector('.selectedBalance').style.display = 'block'
this.shadowRoot.getElementById('amountInput').label = 'Amount (QORT)'
this.shadowRoot.getElementById('recipient').label = 'To (address or name)'
this.satFeePerByte = 100000
} else if (coinType === 'btc') {
this.shadowRoot.getElementById('balance').textContent = `${this.btcBalance} BTC`
this.shadowRoot.getElementById('address').textContent = this.selectedAddress.btcWallet.address
this.shadowRoot.querySelector('.selectedBalance').style.display = 'block'
this.shadowRoot.getElementById('amountInput').label = 'Amount (BTC)'
this.shadowRoot.getElementById('recipient').label = 'To (BTC address)'
this.shadowRoot.getElementById('feeSlider').min = this.btcSatMinFee
this.shadowRoot.getElementById('feeSlider').max = this.btcSatMaxFee
this.satFeePerByte = this.btcDefaultFee
} else if (coinType === 'ltc') {
this.shadowRoot.getElementById('balance').textContent = `${this.ltcBalance} LTC`
this.shadowRoot.getElementById('address').textContent = this.selectedAddress.ltcWallet.address
this.shadowRoot.querySelector('.selectedBalance').style.display = 'block'
this.shadowRoot.getElementById('amountInput').label = 'Amount (LTC)'
this.shadowRoot.getElementById('recipient').label = 'To (LTC address)'
this.shadowRoot.getElementById('feeSlider').min = this.ltcSatMinFee
this.shadowRoot.getElementById('feeSlider').max = this.ltcSatMaxFee
this.satFeePerByte = this.ltcDefaultFee
} else if (coinType === 'doge') {
this.shadowRoot.getElementById('balance').textContent = `${this.dogeBalance} DOGE`
this.shadowRoot.getElementById('address').textContent = this.selectedAddress.dogeWallet.address
this.shadowRoot.querySelector('.selectedBalance').style.display = 'block'
this.shadowRoot.getElementById('amountInput').label = 'Amount (DOGE)'
this.shadowRoot.getElementById('recipient').label = 'To (DOGE address)'
this.shadowRoot.getElementById('feeSlider').min = this.dogeSatMinFee
this.shadowRoot.getElementById('feeSlider').max = this.dogeSatMaxFee
this.satFeePerByte = this.dogeDefaultFee
} else {
this.selectedCoin = 'invalid'
}
}
updateBTCAccountBalance() {
parentEpml
.request('apiCall', {
url: `/crosschain/btc/walletbalance`,
method: 'POST',
body: window.parent.reduxStore.getState().app.selectedAddress.btcWallet.derivedMasterPublicKey,
})
.then((res) => {
if (isNaN(Number(res))) {
parentEpml.request('showSnackBar', 'Failed to Fetch BTC Balance. Try again!')
} else {
this.btcBalance = (Number(res) / 1e8).toFixed(8)
}
})
}
updateLTCAccountBalance() {
parentEpml
.request('apiCall', {
url: `/crosschain/ltc/walletbalance`,
method: 'POST',
body: window.parent.reduxStore.getState().app.selectedAddress.ltcWallet.derivedMasterPublicKey,
})
.then((res) => {
if (isNaN(Number(res))) {
parentEpml.request('showSnackBar', 'Failed to Fetch LTC Balance. Try again!')
} else {
this.ltcBalance = (Number(res) / 1e8).toFixed(8)
}
})
}
updateDOGEAccountBalance() {
parentEpml
.request('apiCall', {
url: `/crosschain/doge/walletbalance`,
method: 'POST',
body: window.parent.reduxStore.getState().app.selectedAddress.dogeWallet.derivedMasterPublicKey,
})
.then((res) => {
if (isNaN(Number(res))) {
parentEpml.request('showSnackBar', 'Failed to Fetch DOGE Balance. Try again!')
} else {
this.dogeBalance = (Number(res) / 1e8).toFixed(8)
}
})
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('send-coin-page', SendMoneyPage)

View File

@@ -0,0 +1,63 @@
import { parentEpml } from '../connect.js'
import { EpmlStream } from 'epml'
const BLOCK_CHECK_INTERVAL = 3000 // You should be runn off config.user.nodeSettings.pingInterval...
const BLOCK_CHECK_TIMEOUT = 3000
export const BLOCK_STREAM_NAME = 'new_block'
const onNewBlockFunctions = []
let mostRecentBlock = {
height: -1
}
export const onNewBlock = newBlockFn => onNewBlockFunctions.push(newBlockFn)
export const check = () => {
const c = doCheck()
c.then(() => {
setTimeout(() => check(), BLOCK_CHECK_INTERVAL)
})
c.catch(() => {
setTimeout(() => check(), BLOCK_CHECK_INTERVAL)
})
}
const doCheck = async () => {
let timeout = setTimeout(() => {
throw new Error('Block check timed out')
}, BLOCK_CHECK_TIMEOUT)
let _nodeStatus = {}
const block = await parentEpml.request('apiCall', {
url: '/blocks/last'
})
const _nodeInfo = await parentEpml.request('apiCall', {
url: '/admin/info'
})
let nodeConfig = await parentEpml.request('getNodeConfig')
if (nodeConfig.node === 0 || nodeConfig.node === 1) {
_nodeStatus = await parentEpml.request('apiCall', {
url: '/admin/status'
})
}
let appInfo = {
block: block,
nodeInfo: _nodeInfo,
nodeStatus: _nodeStatus
}
parentEpml.request('updateAppInfo', appInfo)
clearTimeout(timeout)
if (block.height > mostRecentBlock.height) {
mostRecentBlock = block
onNewBlockFunctions.forEach(fn => fn(mostRecentBlock))
}
}

View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<meta charset="UTF-8">
<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>
<trade-portal></trade-portal>
<script type="module" src="trade-portal.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
<!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;
background: white;
font-family: 'Roboto', sans-serif;
}
</style>
</head>
<body>
<multi-wallet></multi-wallet>
<script src="wallet-app.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff