diff --git a/core/language/us.json b/core/language/us.json index 76458f9f..fa6d0460 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -1024,7 +1024,13 @@ "rewarddialog6": "On pressing confirm, the rewardshare will be removed and the minting key will become invalid.", "deployAtdialog1": "You are deploying the AT", "deployAtdialog2": "On pressing confirm, the AT will be deployed!", - "deployAtdialog3": "Initial amount balance" + "deployAtdialog3": "Initial amount balance", + "votedialog1": "You are requesting to vote on the poll below:", + "votedialog2": "On pressing confirm, the vote request will be sent!", + "votedialog3": "You are requesting to create the poll below:", + "votedialog4": "Poll Description", + "votedialog5": "Options", + "votedialog6": "On pressing confirm, the poll will be created!" }, "sponsorshipspage": { "schange1": "Active Sponsorships", diff --git a/crypto/api/transactions/polls/CreatePollTransaction.js b/crypto/api/transactions/polls/CreatePollTransaction.js new file mode 100644 index 00000000..b7d8bb6c --- /dev/null +++ b/crypto/api/transactions/polls/CreatePollTransaction.js @@ -0,0 +1,111 @@ +'use strict' +import TransactionBase from '../TransactionBase.js' +import { QORT_DECIMALS } from '../../constants.js' + +export default class CreatePollTransaction extends TransactionBase { + constructor() { + super() + this.type = 8 + this._options = [] + } + + render(html) { + return html` + ${this._votedialog3} +
+ ${this._rPollName} +
+ ${this._votedialog4} +
+ ${this._rPollDesc} +
+ ${this._votedialog5} +
+ ${this._pollOptions.join(', ')} +
+ ${this._votedialog6} +
+ ${this._feeDialog}: ${this._feeDisplay} +
+ ` + } + + addOption(option) { + const optionBytes = this.constructor.utils.stringtoUTF8Array(option); + const optionLength = this.constructor.utils.int32ToBytes(optionBytes.length); + this._options.push({ length: optionLength, bytes: optionBytes }); + } + + set feeDialog(feeDialog){ + this._feeDialog = feeDialog + } + + set votedialog3(votedialog3) { + this._votedialog3 = votedialog3 + } + + set votedialog4(votedialog4) { + this._votedialog4 = votedialog4 + } + set votedialog5(votedialog5) { + this._votedialog5 = votedialog5 + } + set votedialog6(votedialog6) { + this._votedialog6 = votedialog6 + } + + set fee(fee) { + this._feeDisplay = fee + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + set ownerAddress(ownerAddress) { + this._ownerAddress = ownerAddress instanceof Uint8Array ? ownerAddress : this.constructor.Base58.decode(ownerAddress) + } + + set rPollName(rPollName) { + this._rPollName = rPollName + this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName) + this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length); + + } + + set rPollDesc(rPollDesc) { + this._rPollDesc = rPollDesc + this._rPollDescBytes = this.constructor.utils.stringtoUTF8Array(this._rPollDesc.toLocaleLowerCase()) + this._rPollDescLength = this.constructor.utils.int32ToBytes(this._rPollDescBytes.length) + } + + set rOptions(rOptions) { + const optionsArray = rOptions[0].split(', ').map(opt => opt.trim()); + this._pollOptions = optionsArray + for (let i = 0; i < optionsArray.length; i++) { + this.addOption(optionsArray[i]); + } + + this._rNumberOfOptionsBytes = this.constructor.utils.int32ToBytes(optionsArray.length); + } + + + get params() { + const params = super.params + params.push( + this._ownerAddress, + this._rPollNameLength, + this._rPollNameBytes, + this._rPollDescLength, + this._rPollDescBytes, + this._rNumberOfOptionsBytes + ) + // Push the dynamic options + console.log('this._options', this._options) + for (let i = 0; i < this._options.length; i++) { + params.push(this._options[i].length, this._options[i].bytes); + } + + params.push(this._feeBytes); + + return params + } +} \ No newline at end of file diff --git a/crypto/api/transactions/polls/VoteOnPollTransaction.js b/crypto/api/transactions/polls/VoteOnPollTransaction.js new file mode 100644 index 00000000..9fbba6ff --- /dev/null +++ b/crypto/api/transactions/polls/VoteOnPollTransaction.js @@ -0,0 +1,65 @@ +'use strict' +import TransactionBase from '../TransactionBase.js' +import { QORT_DECIMALS } from '../../constants.js' + +export default class VoteOnPollTransaction extends TransactionBase { + constructor() { + super() + this.type = 9 + } + + render(html) { + return html` + ${this._votedialog1} +
+ ${this._rPollName} +
+ ${this._votedialog2} +
+ ${this._feeDialog}: ${this._feeDisplay} +
+ ` + } + + set feeDialog(feeDialog){ + this._feeDialog = feeDialog + } + + set votedialog1(votedialog1) { + this._votedialog1 = votedialog1 + } + + set votedialog2(votedialog2) { + this._votedialog2 = votedialog2 + } + + set fee(fee) { + this._feeDisplay = fee + this._fee = fee * QORT_DECIMALS + this._feeBytes = this.constructor.utils.int64ToBytes(this._fee) + } + + + set rPollName(rPollName) { + this._rPollName = rPollName + this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName) + this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length) + } + + + set rOptionIndex(rOptionIndex) { + this._rOptionIndex = rOptionIndex + this._rOptionIndexBytes = this.constructor.utils.int32ToBytes(this._rOptionIndex) + } + + get params() { + const params = super.params + params.push( + this._rPollNameLength, + this._rPollNameBytes, + this._rOptionIndexBytes, + this._feeBytes + ) + return params + } +} \ No newline at end of file diff --git a/crypto/api/transactions/transactions.js b/crypto/api/transactions/transactions.js index 0423d3ae..4f0e92fd 100644 --- a/crypto/api/transactions/transactions.js +++ b/crypto/api/transactions/transactions.js @@ -23,6 +23,8 @@ import RewardShareTransaction from './reward-share/RewardShareTransaction.js' import RemoveRewardShareTransaction from './reward-share/RemoveRewardShareTransaction.js' import TransferPrivsTransaction from './TransferPrivsTransaction.js' import DeployAtTransaction from './DeployAtTransaction.js' +import VoteOnPollTransaction from './polls/VoteOnPollTransaction.js' +import CreatePollTransaction from './polls/CreatePollTransaction.js' export const transactionTypes = { 2: PaymentTransaction, @@ -31,6 +33,8 @@ export const transactionTypes = { 5: SellNameTransacion, 6: CancelSellNameTransacion, 7: BuyNameTransacion, + 8: CreatePollTransaction, + 9: VoteOnPollTransaction, 16: DeployAtTransaction, 17: MessageTransaction, 18: ChatTransaction, diff --git a/plugins/plugins/core/components/qdn-action-types.js b/plugins/plugins/core/components/qdn-action-types.js index 59385690..496aba42 100644 --- a/plugins/plugins/core/components/qdn-action-types.js +++ b/plugins/plugins/core/components/qdn-action-types.js @@ -59,4 +59,10 @@ export const OPEN_NEW_TAB = 'OPEN_NEW_TAB' export const NOTIFICATIONS_PERMISSION = 'NOTIFICATIONS_PERMISSION' //SEND_LOCAL_NOTIFICATION -export const SEND_LOCAL_NOTIFICATION = 'SEND_LOCAL_NOTIFICATION' \ No newline at end of file +export const SEND_LOCAL_NOTIFICATION = 'SEND_LOCAL_NOTIFICATION' + +//VOTE_ON_POLL +export const VOTE_ON_POLL= 'VOTE_ON_POLL' + +//CREATE_POLL +export const CREATE_POLL= 'CREATE_POLL' \ No newline at end of file diff --git a/plugins/plugins/core/qdn/browser/browser.src.js b/plugins/plugins/core/qdn/browser/browser.src.js index 0b8086dd..0d411ab7 100644 --- a/plugins/plugins/core/qdn/browser/browser.src.js +++ b/plugins/plugins/core/qdn/browser/browser.src.js @@ -496,6 +496,33 @@ class WebBrowser extends LitElement { return qortFee } + async unitVoteFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=VOTE_ON_POLL` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching vote fee'); + } + + const data = await response.json() + const joinFee = (Number(data) / 1e8).toFixed(8) + return joinFee + } + async unitCreatePollFee() { + const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/transactions/unitfee?txType=CREATE_POLL` + const response = await fetch(url) + if (!response.ok) { + throw new Error('Error when fetching vote fee'); + } + + const data = await response.json() + const joinFee = (Number(data) / 1e8).toFixed(8) + return joinFee + } + async _joinGroup(groupId, groupName) { const joinFeeInput = await this.unitJoinFee() const getLastRef = async () => { @@ -610,6 +637,122 @@ class WebBrowser extends LitElement { } + async _voteOnPoll(pollName, optionIndex) { + const voteFeeInput = await this.unitVoteFee() + 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 myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let votedialog1 = get("transactions.votedialog1") + let votedialog2 = get("transactions.votedialog2") + let feeDialog = get("walletpage.wchange12") + + let myTxnrequest = await parentEpml.request('transaction', { + type: 9, + nonce: this.selectedAddress.nonce, + params: { + fee: voteFeeInput, + voterAddress: this.selectedAddress.address, + rPollName: pollName, + rOptionIndex: optionIndex, + lastReference: lastRef, + votedialog1: votedialog1, + votedialog2: votedialog2, + feeDialog + }, + apiVersion: 2 + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const voteRes = await validateReceiver() + return voteRes + + } + async _createPoll(pollName, pollDescription, options, pollOwnerAddress) { + const voteFeeInput = await this.unitCreatePollFee() + 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 myTransaction = await makeTransactionRequest(lastRef) + const res = getTxnRequestResponse(myTransaction) + return res + } + + const makeTransactionRequest = async (lastRef) => { + let votedialog3 = get("transactions.votedialog3") + let votedialog4 = get("transactions.votedialog4") + let votedialog5 = get("transactions.votedialog5") + let votedialog6 = get("transactions.votedialog6") + let feeDialog = get("walletpage.wchange12") + + let myTxnrequest = await parentEpml.request('transaction', { + type: 8, + nonce: this.selectedAddress.nonce, + params: { + fee: voteFeeInput, + ownerAddress: pollOwnerAddress, + rPollName: pollName, + rPollDesc: pollDescription, + rOptions: options, + lastReference: lastRef, + votedialog3: votedialog3, + votedialog4: votedialog4, + votedialog5: votedialog5, + votedialog6: votedialog6, + feeDialog + }, + apiVersion: 2 + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + if (txnResponse.success === false && txnResponse.message) { + throw new Error(txnResponse.message) + } else if (txnResponse.success === true && !txnResponse.data.error) { + return txnResponse.data + } else if (txnResponse.data && txnResponse.data.message) { + throw new Error(txnResponse.data.message) + } else { + throw new Error('Server error. Could not perform action.') + } + } + const voteRes = await validateReceiver() + return voteRes + + } + firstUpdated() { this.changeTheme(); this.changeLanguage(); @@ -1324,6 +1467,103 @@ class WebBrowser extends LitElement { // If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}` break; } + case actions.VOTE_ON_POLL: { + const requiredFields = ['pollName', 'optionIndex']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const pollName = data.pollName; + const optionIndex = data.optionIndex; + + + let pollInfo = null + try { + pollInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/polls/${pollName}`, + }); + } catch (error) { + const errorMsg = (error && error.message) || 'Poll not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + if (!pollInfo || pollInfo.error) { + const errorMsg = (pollInfo && pollInfo.message) || 'Poll not found'; + let obj = {}; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + try { + this.loader.show(); + const resVoteOnPoll = await this._voteOnPoll(pollName, optionIndex) + response = JSON.stringify(resVoteOnPoll); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to vote on the poll.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); + } + + break; + } + case actions.CREATE_POLL: { + const requiredFields = ['pollName', 'pollDescription', 'pollOptions', 'pollOwnerAddress']; + const missingFields = []; + + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(', '); + const errorMsg = `Missing fields: ${missingFieldsString}` + let data = {}; + data['error'] = errorMsg; + response = JSON.stringify(data); + break + } + const pollName = data.pollName; + const pollDescription = data.pollDescription + const pollOptions = data.pollOptions + const pollOwnerAddress = data.pollOwnerAddress + + try { + this.loader.show(); + const resCreatePoll = await this._createPoll(pollName, pollDescription, pollOptions, pollOwnerAddress) + response = JSON.stringify(resCreatePoll); + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Failed to created poll.'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + } finally { + this.loader.hide(); + } + + break; + } case actions.OPEN_NEW_TAB: { if(!data.qortalLink){ const obj = {};