diff --git a/.gitignore b/.gitignore
index b23d2558d..5be211036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
## Custom
config/production/settings.json
config/production/decentraland.json
+config/production/colorado.json
config/staging/settings.json
+
.vscode
_deprecations
diff --git a/config/development/settings.json b/config/development/settings.json
index 56a4e8265..f2a3b71a2 100644
--- a/config/development/settings.json
+++ b/config/development/settings.json
@@ -57,7 +57,6 @@
"codename": "Quixote",
"config":
{
- "blockchainLogin": false,
"commentRanking": true,
"proposalDrafting": true,
"appCache": false,
@@ -71,7 +70,32 @@
"checkList": true
},
"displayLanding": false,
- "allowWebVotes": true
+ "allowWebVotes": true,
+ "defaultRules": {
+ "quadraticVoting": false,
+ "balanceVoting": false,
+ "pollVoting": false
+ },
+ "interface": {
+ "showTransactions": false,
+ "adminBallotCreatorOnly": {
+ "active": false,
+ "email": "hello@democracy.earth"
+ }
+ },
+ "governance": {
+ "dictatorship": true,
+ "dictator": {
+ "userId": "y2RPht3xHDLf7KN3u",
+ "publicAddress": ""
+ }
+ },
+ "loginOptions": {
+ "blockstack": true,
+ "metamask": true,
+ "email": true,
+ "blockchainLogin": false
+ }
}
},
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 922c3d90e..8a57f88b7 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -200,7 +200,7 @@
"new-profile-pic": "Profile picture updated.",
"valid-country": "The typed nationality does not exist.",
"missing-name": "At least a first name is required.",
- "missing-username": "Username is required without spaces.",
+ "missing-username": "This username is invalid.",
"digital-citizen": "Digital",
"pending-verification": "E-mail not verified. Resend link?",
"user-not-found": "User not found.",
@@ -230,6 +230,12 @@
"hours-compressed" : "hours",
"minutes-compressed" : "min",
"seconds-compressed" : "sec",
+ "years-singular": "year",
+ "months-singular": "month",
+ "days-singular": "day",
+ "hours-singular": "hour",
+ "minutes-singular": "min",
+ "seconds-singular": "sec",
"latest-proposals": "Latest Proposals",
"all-proposals": "All Proposals",
"drafts": "Drafts",
@@ -426,6 +432,7 @@
"remove-all-votes": "remove all votes",
"empty-values-ballot": "No values found on ballot.",
"not-enough-funds": "Not enough funds.",
+ "not-enough-funds-qv": "Not enough funds. The cost of voting for this option is {{qvCost}}",
"invalid-transaction": "Self voting is not allowed.",
"events": "Events",
"actions": "Actions",
@@ -519,7 +526,7 @@
"hashtag-tag-description": "All posts tagged #{{hashtag}} on {{collective}}.",
"country-tag-title": "{{country}} posts on {{collective}}.",
"country-tag-description": "All posts from citizens of {{country}} on {{collective}}.",
- "recent-activity": "Recent Transactions",
+ "recent-activity": "Recent Activity",
"bitcoin": "Bitcoin",
"fund-organization": "Fund Organization",
"btc": "BTC",
@@ -547,7 +554,7 @@
"valid-coin": "This cryptocurrency does not exist.",
"from": "from",
"holding": "holding",
- "valid-email-domain": "with a {{domain}} valid e-mail",
+ "valid-email-domain": "with a verified {{domain}} e-mail address",
"electorate-incompatible-warning": "To vote on this proposal you must meet the requirements.",
"log-in-network": "Login to network.",
"authenticate-self": "Authenticate",
@@ -566,7 +573,7 @@
"post-idea": "Post Idea",
"happening-now": "Happening Now",
"requisites": "Requisites",
- "post-new-idea": "A new idea...",
+ "post-new-idea": "A new decision...",
"insufficient-votes": "Unable to find the required token balance in your wallet",
"tokenless-post": "No tokens specified for voting here",
"get-tokens": "Get tokens",
@@ -599,9 +606,9 @@
"transaction-broadcast": "{{token}} Transaction sent to blockchain.",
"ethereum-metamask": "Ethereum",
"no-chain-found": "No blockchain address found.",
- "metamask-confirm-transaction": "Confirm transaction in your blockchain wallet.
Delegate responsible for post tokens:",
+ "metamask-confirm-transaction": "Confirm transaction in your blockchain wallet.
To be included Delegate list:",
"ticket": "ticket",
- "transaction-status-confirmed-onchain": "Confirmed on chain transaciton",
+ "transaction-status-confirmed-onchain": "Confirmed on chain transaction",
"transaction-status-signed-offchain": "Signed off chain stake",
"transaction-status-pending-onchain": "Pending on chain transaction",
"transaction-status-fail-onchain": "Failed on chain transaction",
@@ -670,5 +677,52 @@
"token-erc-20": "ERC 20 Token",
"token-blockchain": "Blockchain Activity",
"token-saft": "SAFT Purchase",
- "off-chain": "off chain"
+ "off-chain": "off chain",
+ "voting-editor-offchain-toggle": "Off-chain",
+ "voting-editor-balance-toggle": "Coin vote",
+ "voting-editor-quadratic-toggle": "Quadratic vote",
+ "voting-editor-poll-voting": "Include poll",
+ "advanced-rules": "Advanced",
+ "voting-editor-balance-tooltip": "Account balance gets tallied after signing with private key.",
+ "voting-editor-quadratic-tooltip": "The cost of voting for a choice increases at a quadratic rate.",
+ "voting-editor-poll-tooltip": "Include a poll with multiple options on this post.",
+ "poll-choice": "Poll choice {{number}}",
+ "poll-default-title-0": "Downvote 👎",
+ "poll-default-title-1": "Upvote 👍",
+ "poll-inside": "Election Poll",
+ "ticker-rule-quadratic": "",
+ "ticker-rule-balance": "Balance",
+ "decision-rules": "Decision Mechanism",
+ "remove-vote": "Remove vote from this choice.",
+ "election-rule-quadratic": "Quadratic Vote",
+ "election-rule-balance": "Coin Vote",
+ "token-budget": "Budget",
+ "closing-date": "Closing",
+ "always-on-tooltip": "An Always On decision does not have a closing date.",
+ "decision-deadline": "Decision Deadline",
+ "blockchain-time": "Blockchain Time",
+ "blockchain-time-daily": "24 hours",
+ "blockchain-time-weekly": "7 days",
+ "blockchain-time-monthly": "30 days",
+ "blockchain-time-quarterly": "1 quarter",
+ "blockchain-time-annual": "1 year",
+ "duration": "Duration",
+ "blockchain-time-closing-criteria": "Poll ends when {{blockchain}} reaches a block height of {{height}} blocks estimated on {{date}}.",
+ "blockchain-time-always-on": "This poll never ends and tallies an ongoing result.",
+ "countdown-expiration": "Poll ends in {{days}} {{hours}} {{minutes}} {{seconds}} (in {{blocks}})",
+ "blocks-compressed": "blocks",
+ "blocks-singular": "block",
+ "poll-closed-after-time": "Poll ended after {{days}} (at block {{height}})",
+ "poll-never-ends": "This poll never ends (always on).",
+ "height-compressed": "blocks",
+ "height-singular": "block",
+ "poll-is-closed": "This poll is no longer open for voting.",
+ "feeds": "Feeds",
+ "poll-hypothetical": "Poll will end in {{days}} {{hours}} {{minutes}} {{seconds}} (in {{blocks}})",
+ "coinvote-signature": "A total of {{quantity}} {{ticker}} will be tallied in support of {{choice}} in the proposal available on {{url}}. No tokens will be spent.",
+ "metamask-confirm-tally": "Sign vote with your private key.
To be included on Delegate list:",
+ "transaction-tally": "{{token}} Signature verified, account balance tallied.",
+ "transaction-tally-denied": "{{token}} Signature denied, no votes tallied.",
+ "already-voted": "Already Voted",
+ "already-voted-detail": "Your account balance has already been tallied on this decision."
}
diff --git a/imports/api/blockchain/modules/web3Util.js b/imports/api/blockchain/modules/web3Util.js
index d8c7e4f18..d52448c74 100644
--- a/imports/api/blockchain/modules/web3Util.js
+++ b/imports/api/blockchain/modules/web3Util.js
@@ -2,8 +2,10 @@ import { Meteor } from 'meteor/meteor';
import Web3 from 'web3';
import abi from 'human-standard-token-abi';
import { BigNumber } from 'bignumber.js';
+
import { token } from '/lib/token';
+const numeral = require('numeral');
// Set web3 provider
let web3;
@@ -172,23 +174,79 @@ const _getTokenData = async (_publicAddress) => {
let _balance;
for (let i = 0; i < token.coin.length; i++) {
- _balance = await _getTokenBalance(_publicAddress, token.coin[i].contractAddress);
- if (_balance.toNumber() !== 0) {
- const withoutDecimal = _removeDecimal(_balance.toNumber(), token.coin[i].decimals);
- const tokenObj = {
- balance: withoutDecimal.toNumber(),
- placed: 0,
- available: withoutDecimal.toNumber(),
- token: token.coin[i].code,
- publicAddress: _publicAddress,
- };
-
- tokenData.push(tokenObj);
+ if (token.coin[i].type === 'ERC20') {
+ _balance = await _getTokenBalance(_publicAddress, token.coin[i].contractAddress);
+ if (_balance.toNumber() !== 0) {
+ const withoutDecimal = _removeDecimal(_balance.toNumber(), token.coin[i].decimals);
+ const tokenObj = {
+ balance: withoutDecimal.toNumber(),
+ placed: 0,
+ available: withoutDecimal.toNumber(),
+ token: token.coin[i].code,
+ publicAddress: _publicAddress,
+ };
+
+ tokenData.push(tokenObj);
+ }
}
}
return tokenData;
};
+/**
+* @summary shows balance in currency not decimals
+* @param {object} value value to be changed
+* @param {string} token currency being used
+* @returns {number}
+*/
+const _currencyValue = (value, tokenCode) => {
+ switch (tokenCode) {
+ case 'WEI':
+ return _wei2eth(value.toString());
+ // case 'VOTE':
+ // return adjustDecimal(value);
+ default:
+ return value;
+ }
+};
+
+
+/**
+* @summary format currency display according to crypto rules
+* @param {string} value value to be changed
+* @param {string} tokenCode currency being used
+* @returns {string} formatted number
+*/
+const _formatCryptoValue = (value, tokenCode) => {
+ let tokenFinal;
+ if (!tokenCode) { tokenFinal = 'ETH'; } else { tokenFinal = tokenCode; }
+ return numeral(_currencyValue(value, tokenFinal)).format(_getCoin(tokenFinal).format);
+};
+
+
+/**
+* @summary get the token balance a user has for a given contract coin
+* @param {object} user with token
+* @param {object} contract to be checked
+* @return {string} the balance quantity
+*/
+const _getBalance = (user, contract) => {
+ let result;
+ for (let i = 0; i < user.profile.wallet.reserves.length; i += 1) {
+ const coin = _getCoin(user.profile.wallet.reserves[i].token);
+ if (coin.code === contract.blockchain.coin.code) {
+ if (coin.code === 'ETH') {
+ result = _formatCryptoValue(_removeDecimal(Meteor.user().profile.wallet.reserves[i].balance, coin.decimals).toNumber(), coin.code);
+ console.log(result);
+ } else {
+ result = _formatCryptoValue(Meteor.user().profile.wallet.reserves[i].balance, coin.code);
+ }
+ return result;
+ }
+ }
+ return undefined;
+}
+
export const wei2eth = _wei2eth;
export const getEthBalance = _getEthBalance;
export const getWeiBalance = _getWeiBalance;
@@ -199,3 +257,4 @@ export const smallNumber = _smallNumber;
export const addDecimal = _addDecimal;
export const getCoin = _getCoin;
export const getTokenData = _getTokenData;
+export const getBalance = _getBalance;
diff --git a/imports/api/contracts/Contracts.js b/imports/api/contracts/Contracts.js
index 1be4a0c83..8ce247eff 100644
--- a/imports/api/contracts/Contracts.js
+++ b/imports/api/contracts/Contracts.js
@@ -52,6 +52,11 @@ Schema.Tally = new SimpleSchema({
type: Number,
optional: true,
},
+ 'voter.$.qVotes': {
+ type: Number,
+ optional: true,
+ decimal: true,
+ },
'voter.$.ballotList': {
type: [String],
optional: true,
@@ -62,6 +67,66 @@ Schema.Tally = new SimpleSchema({
},
});
+
+Schema.Poll = new SimpleSchema({
+ contractId: {
+ type: String,
+ optional: true,
+ },
+ totalStaked: {
+ type: String,
+ optional: true,
+ },
+});
+
+Schema.Closing = new SimpleSchema({
+ blockchain: {
+ type: String,
+ defaultValue: 'ETH',
+ },
+ height: {
+ type: Number,
+ defaultValue: 0,
+ },
+ calendar: {
+ type: Date,
+ autoValue() {
+ const creationDate = new Date();
+ if (this.isInsert) {
+ creationDate.setDate(creationDate.getDate() + 1);
+ }
+ return creationDate;
+ },
+ },
+ delta: {
+ type: Number,
+ defaultValue: 0,
+ },
+ urgency: {
+ type: Number,
+ optional: true,
+ },
+});
+
+Schema.Rules = new SimpleSchema({
+ alwaysOn: {
+ type: Boolean,
+ defaultValue: true,
+ },
+ quadraticVoting: {
+ type: Boolean,
+ defaultValue: Meteor.settings.public.app.config.defaultRules.quadraticVoting,
+ },
+ balanceVoting: {
+ type: Boolean,
+ defaultValue: Meteor.settings.public.app.config.defaultRules.balanceVoting,
+ },
+ pollVoting: {
+ type: Boolean,
+ defaultValue: Meteor.settings.public.app.config.defaultRules.pollVoting,
+ },
+});
+
Schema.Constituency = new SimpleSchema({
kind: {
type: String,
@@ -127,7 +192,7 @@ Schema.Contract = new SimpleSchema({
kind: {
// kind of contract
type: String,
- allowedValues: ['DRAFT', 'VOTE', 'DELEGATION', 'MEMBERSHIP', 'DISCIPLINE'],
+ allowedValues: ['DRAFT', 'VOTE', 'DELEGATION', 'MEMBERSHIP', 'DISCIPLINE', 'POLL'],
autoValue() {
if (this.isInsert) {
if (this.field('kind').value === undefined) {
@@ -150,7 +215,7 @@ Schema.Contract = new SimpleSchema({
// URL inside the instance of .Earth
type: String,
autoValue() {
- const slug = convertToSlug(this.field('title').value);
+ let slug = convertToSlug(this.field('title').value);
if (this.isInsert) {
if (this.field('kind').value === 'DELEGATION') {
if (this.field('keyword').value !== undefined) {
@@ -159,6 +224,9 @@ Schema.Contract = new SimpleSchema({
return 'delegation';
}
if (this.field('title').value !== undefined) {
+ if (this.field('kind'.value === 'POLL') && this.field('keyword').value) {
+ slug = this.field('keyword').value;
+ }
if (Contracts.findOne({ keyword: slug }) === undefined) {
if (this.field('title').value !== '') {
const time = this.field('createdAt').value;
@@ -526,10 +594,25 @@ Schema.Contract = new SimpleSchema({
return Math.floor(Math.random() * 10000000000000000);
},
},
- quadraticVoting: {
- type: Boolean,
+ rules: {
+ type: Schema.Rules,
+ optional: true,
+ },
+ poll: {
+ type: [Schema.Poll],
+ defaultValue: [],
+ },
+ pollId: {
+ type: String,
+ optional: true,
+ },
+ pollChoiceId: {
+ type: String,
+ optional: true,
+ },
+ closing: {
+ type: Schema.Closing,
optional: true,
- defaultValue: false,
},
});
diff --git a/imports/api/server/publications.js b/imports/api/server/publications.js
index cf7290c50..8a420993a 100644
--- a/imports/api/server/publications.js
+++ b/imports/api/server/publications.js
@@ -79,6 +79,24 @@ Meteor.publish('delegates', function (terms) {
return this.ready();
});
+/**
+* @summary returns list of contracts from a poll
+* @return {Object} querying terms
+*/
+Meteor.publish('poll', function (terms) {
+ check(terms, Object);
+
+ const parameters = query(terms);
+ log(`{ publish: 'poll', user: ${logUser()}, pollId: '${terms.pollId}' }`);
+
+ const pollFeed = Contracts.find(parameters.find, parameters.options);
+ if (pollFeed) {
+ return pollFeed;
+ }
+
+ return this.ready();
+});
+
/**
* @summary files related to a user account
* @return {Object} file
diff --git a/imports/api/transactions/transaction.js b/imports/api/transactions/transaction.js
index ae05cceb0..292f933ff 100644
--- a/imports/api/transactions/transaction.js
+++ b/imports/api/transactions/transaction.js
@@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
+import { TAPi18n } from 'meteor/tap:i18n';
import { rules } from '/lib/const';
import { BigNumber } from 'bignumber.js';
@@ -11,8 +12,7 @@ import { getTime } from '/imports/api/time';
import { Transactions } from '/imports/api/transactions/Transactions';
import { getTotalVoters } from '/imports/ui/modules/ballot';
import { notify } from '/imports/api/notifier/notifier';
-import { getWeiBalance, getTokenData } from '/imports/api/blockchain/modules/web3Util';
-import { smallNumber } from '/imports/api/blockchain/modules/web3Util.js';
+import { getWeiBalance, getTokenData, smallNumber } from '/imports/api/blockchain/modules/web3Util';
/**
* @summary looks at what type of entity (collective or individual) doing transaction
@@ -168,14 +168,14 @@ const _updateWallet = (entityId, entityType, profileSettings) => {
* @param {string} contractId - contractId to be checked
*/
const _getTransactions = (userId, contractId) => {
- // const tx = Transactions.find({ $and: [{ $or: [{ 'output.entityId': userId }, { 'input.entityId': userId }] }, { contractId }] }, { sort: { timestamp: -1 } }).fetch();
- // return tx;
+ // const tx = Transactions.find({ $and: [{ $or: [{ 'output.entityId': userId }, { 'input.entityId': userId }] }, { contractId }] }, { sort: { timestamp: -1 } }).fetch();
+ // return tx;
return _.sortBy(
_.union(
_.filter(Transactions.find({ 'input.entityId': userId }).fetch(), (item) => { return (item.output.entityId === contractId); }, 0),
_.filter(Transactions.find({ 'output.entityId': userId }).fetch(), (item) => { return (item.input.entityId === contractId); }, 0)),
- 'timestamp');
+ 'timestamp');
};
/**
@@ -258,11 +258,14 @@ const _restoredTokens = (quantity, totals) => {
return quantity;
};
-const _transactionMessage = (code) => {
- switch (code) {
+const _transactionMessage = (processingStatus) => {
+ switch (processingStatus.code) {
case 'INSUFFICIENT':
displayNotice('not-enough-funds', true);
return false;
+ case 'INSUFFICIENT-QV':
+ displayNotice(`${TAPi18n.__('not-enough-funds-qv').replace('{{qvCost}}', processingStatus.qvCost)}`, true, true);
+ return false;
case 'INVALID':
displayNotice('invalid-transaction', true);
return false;
@@ -345,38 +348,90 @@ const _processDelegation = (transaction) => {
};
/**
-* @summary processes de transaction after insert and updates wallet of involved parties
-* @param {string} txId - transaction identificator
-* @param {string} success - INSUFFICIENT,
+* @summary updates user wallet when transaction is quadratic, whether voting or revoking,
+* according to the number of votes associated with said user in the contract tally
+* @param {string} senderId
+* @param {object} senderProfile
+* @param {string} receiverId
+* @param {object} receiverProfile
+* @return {object} _userWallet - updated wallet assigned to userProfile.wallet
*/
-const _processTransaction = (ticket) => {
- const txId = ticket;
- const transaction = Transactions.findOne({ _id: txId });
- const senderProfile = _getProfile(transaction.input);
- const receiverProfile = _getProfile(transaction.output);
+const _processQuadraticTransaction = (senderId, senderProfile, receiverId, receiverProfile) => {
+ let _userWallet;
+ let userId;
+ let userProfile;
+ let contract;
+ let revoke = false;
+ let found = false;
- // verify transaction
- if (senderProfile.wallet.available < transaction.input.quantity) {
- return 'INSUFFICIENT';
- } else if (transaction.input.entityId === transaction.output.entityId) {
- return 'INVALID';
+ // Set up
+ if (receiverProfile.tally) {
+ // user is voting
+ _userWallet = senderProfile.wallet;
+ userId = senderId;
+ userProfile = senderProfile;
+ contract = Contracts.findOne({ _id: receiverId });
+ } else if (senderProfile.tally) {
+ // user is revoking votes
+ _userWallet = receiverProfile.wallet;
+ userId = receiverId;
+ userProfile = receiverProfile;
+ contract = Contracts.findOne({ _id: senderId });
+ revoke = true;
}
- // transact
- senderProfile.wallet = _pay(senderProfile.wallet, 'INPUT', transaction, transaction.input.quantity);
- receiverProfile.wallet = _pay(receiverProfile.wallet, 'OUTPUT', transaction, transaction.output.quantity);
+ for (let i = 0; i < contract.tally.voter.length; i += 1) {
+ if (contract.tally.voter[i]._id === userId) {
+ const votesPerUserPerBallot = contract.tally.voter[i].votes;
+ // TODO - here .balance needs to be updated too to be compatible with legacy code
+ if (revoke) {
+ _userWallet.available += Math.pow(votesPerUserPerBallot + 1, 2);
+ _userWallet.placed -= 1;
+ found = true;
+ } else {
+ _userWallet.available -= Math.pow(votesPerUserPerBallot, 2);
+ _userWallet.placed += 1;
+ found = true;
+ }
+ }
+ }
- // update wallets
- _updateWallet(transaction.input.entityId, transaction.input.entityType, senderProfile);
- _updateWallet(transaction.output.entityId, transaction.output.entityType, receiverProfile);
+ if (!found && revoke) {
+ // Edge case of revoking when only one vote has been placed
+ _userWallet.available += 1;
+ _userWallet.placed -= 1;
+ }
- // delegation
- if (transaction.kind === 'DELEGATION') {
- _processDelegation(transaction);
+ return Object.assign(userProfile.wallet, _userWallet);
+};
+
+
+/**
+* @summary get what would be the cost of this vote should it go through
+* @param {string} senderId
+* @param {string} receiverId
+* @return {number} quadraticVoteCost
+*/
+const _getQuadraticVoteCost = (senderId, receiverId) => {
+ const userId = senderId;
+ const contract = Contracts.findOne({ _id: receiverId });
+ let votesPerUserPerBallot;
+ let quadraticVoteCost;
+ let found = false;
+
+ for (let i = 0; i < contract.tally.voter.length; i += 1) {
+ if (contract.tally.voter[i]._id === userId) {
+ votesPerUserPerBallot = contract.tally.voter[i].votes;
+ quadraticVoteCost = Math.pow(votesPerUserPerBallot + 1, 2);
+ found = true;
+ }
}
- // set this transaction as processed
- return Transactions.update({ _id: txId }, { $set: { status: 'CONFIRMED' } });
+ if (!found) {
+ // User is voting for the first time
+ return 1;
+ }
+ return quadraticVoteCost;
};
/**
@@ -651,6 +706,7 @@ const _tally = (transaction) => {
if ((contract.tally.voter[i]._id === transaction.input.entityId) || (contract.tally.voter[i]._id === transaction.output.entityId)) {
found = true;
contract.tally.voter[i].votes += _tallyAddition(transaction);
+ contract.tally.voter[i].qVotes = Math.sqrt(contract.tally.voter[i].votes);
if (contract.tally.voter[i].votes === 0) {
contract.tally.voter.splice(i, 1);
break;
@@ -700,6 +756,66 @@ const _tally = (transaction) => {
Contracts.update({ _id: transaction.contractId }, { $set: { tally: contract.tally, ballot: contract.ballot } });
};
+/**
+* @summary processes the transaction after insert and updates wallet of involved parties
+* @param {string} txId - transaction identificator
+* @param {string} success - INSUFFICIENT,
+*/
+const _processTransaction = (ticket) => {
+ const txId = ticket;
+ const transaction = Transactions.findOne({ _id: txId });
+ const senderProfile = _getProfile(transaction.input);
+ const receiverProfile = _getProfile(transaction.output);
+ const senderId = transaction.input.entityId;
+ const receiverId = transaction.output.entityId;
+
+ // verify transaction
+ if (receiverProfile.rules && receiverProfile.rules.quadraticVoting) {
+ // This is a WEBVOTE quadratic vote, and user is voting (not revoking)
+ const quadraticCost = _getQuadraticVoteCost(senderId, receiverId);
+ if (senderProfile.wallet.available < quadraticCost) {
+ return { code: 'INSUFFICIENT-QV', qvCost: quadraticCost };
+ }
+ } else if (senderProfile.wallet.available < transaction.input.quantity) {
+ return { code: 'INSUFFICIENT', qvCost: null };
+ } else if (transaction.input.entityId === transaction.output.entityId) {
+ return { code: 'INVALID', qvCost: null };
+ }
+
+ // Invoke tally first only if it is a transaction between
+ // an individual and a contract, tally needs to be updated
+ // before processing quadratic vote
+ if (transaction.input.entityType !== 'COLLECTIVE') {
+ _tally(transaction);
+ }
+
+ if (receiverProfile.rules && receiverProfile.rules.quadraticVoting) {
+ // Quadratic transaction, user is sender therefore voting
+ senderProfile.wallet = _processQuadraticTransaction(senderId, senderProfile, receiverId, receiverProfile);
+ receiverProfile.wallet = _pay(receiverProfile.wallet, 'OUTPUT', transaction, transaction.output.quantity);
+ } else if (senderProfile.rules && senderProfile.rules.quadraticVoting) {
+ // Quadratic transaction, user is receiver therefore revoking
+ senderProfile.wallet = _pay(senderProfile.wallet, 'INPUT', transaction, transaction.input.quantity);
+ receiverProfile.wallet = _processQuadraticTransaction(senderId, senderProfile, receiverId, receiverProfile);
+ } else {
+ // Non-quadratic
+ senderProfile.wallet = _pay(senderProfile.wallet, 'INPUT', transaction, transaction.input.quantity);
+ receiverProfile.wallet = _pay(receiverProfile.wallet, 'OUTPUT', transaction, transaction.output.quantity);
+ }
+
+ // update wallets
+ _updateWallet(transaction.input.entityId, transaction.input.entityType, senderProfile);
+ _updateWallet(transaction.output.entityId, transaction.output.entityType, receiverProfile);
+
+ // delegation
+ if (transaction.kind === 'DELEGATION') {
+ _processDelegation(transaction);
+ }
+
+ // set this transaction as processed
+ return Transactions.update({ _id: txId }, { $set: { status: 'CONFIRMED' } });
+};
+
/**
* @summary inserts ticket data to a contract affected by a crypto transaction
* @param {string} _id of contract to update
@@ -822,7 +938,8 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
}
if (senderId === receiverId) {
- _transactionMessage('INVALID');
+ const status = { code: 'INVALID', qvCost: null };
+ _transactionMessage(status);
return null;
}
@@ -856,6 +973,7 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
geo: settings.geo,
};
+
// blockchain transaction
if (settings.kind === 'CRYPTO') {
// input carries information of sender
@@ -877,16 +995,15 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
newTx = Transactions.findOne({ _id: txId });
// adds voter
- _addVoter(newTransaction.output.entityId, senderId, newTx._id)
+ _addVoter(newTransaction.output.entityId, senderId, newTx._id);
// go to interface
if (callback !== undefined) { callback(); }
return txId;
+ } else if (settings.kind === 'VOTE') {
+ // web transaciton
- // web transaciton
- } /*
- else {
// executes the transaction
txId = Transactions.insert(newTransaction);
processing = _processTransaction(txId);
@@ -895,8 +1012,8 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
// NOTE: uncomment for testing
- // console.log(txId);
- // console.log(processing);
+ // console.log(`txId: ${txId}`);
+ // console.log(`processing: ${processing}`);
if (_transactionMessage(processing)) {
@@ -909,16 +1026,11 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
_updateWalletCache(newTx, false);
}
- // update tally in contract excluding subsidy
- if (newTx.kind === 'VOTE' && newTx.input.entityType !== 'COLLECTIVE' && newTx.output.entityType !== 'COLLECTIVE') {
- _tally(newTx);
- }
-
notify(newTx);
return txId;
}
- } */
+ }
return null;
};
@@ -928,22 +1040,26 @@ const _transact = (senderId, receiverId, votes, settings, callback) => {
*/
const _genesisTransaction = (userId) => {
const user = Meteor.users.findOne({ _id: userId });
+ const userTransactions = Transactions.find({ 'output.entityId': userId }).fetch();
// veryfing genesis...
- // TODO this is not right, should check against Transactions collection.
- if (user.profile.wallet !== undefined) {
- if (user.profile.wallet.ledger.length > 0) {
- if (user.profile.wallet.ledger[0].entityType === 'COLLECTIVE') {
- // this user already had a genesis
- return;
- }
- }
+ const genesisCheck = userTransactions.find(function (tx) {
+ return tx.input.entityType === 'COLLECTIVE' && tx.input.quantity === 1000;
+ });
+
+ const userBalance = user.profile.wallet.balance;
+
+ // TODO - add emailListCheck condition if set true from Meteor.settings: user.emails && emailListCheck(user.emails[0].address)
+ if (genesisCheck === undefined && userBalance === 0) {
+ // generate first transaction from collective to new member
+ user.profile.wallet = _generateWalletAddress(user.profile.wallet);
+ Meteor.users.update({ _id: userId }, { $set: { profile: user.profile } });
+ const transactSettings = {
+ kind: 'VOTE',
+ currency: 'WEB VOTE',
+ };
+ _transact(Meteor.settings.public.Collective._id, userId, rules.VOTES_INITIAL_QUANTITY, transactSettings);
}
-
- // generate first transaction from collective to new member
- user.profile.wallet = _generateWalletAddress(user.profile.wallet);
- Meteor.users.update({ _id: userId }, { $set: { profile: user.profile } });
- _transact(Meteor.settings.public.Collective._id, userId, rules.VOTES_INITIAL_QUANTITY);
};
/**
diff --git a/imports/api/users/Wallet.js b/imports/api/users/Wallet.js
index 8fcd0d52a..8d31cc33d 100644
--- a/imports/api/users/Wallet.js
+++ b/imports/api/users/Wallet.js
@@ -14,7 +14,7 @@ const Schema = {};
const _coins = () => {
const coins = [];
coins.push('VOTES'); // backwards compatibility;
- coins.push('NONE'); // for tokenless users
+ // coins.push('WEB VOTE'); // for tokenless users
for (let i = 0; i < token.coin.length; i += 1) {
coins.push(token.coin[i].code);
if (token.coin[i].subcode) {
@@ -43,7 +43,7 @@ Schema.Wallet = new SimpleSchema({
autoValue() {
if (this.isInsert) {
if (this.field('currency').value === undefined) {
- return 'NONE';
+ return 'WEB VOTE';
}
}
},
diff --git a/imports/startup/both/modules/Contract.js b/imports/startup/both/modules/Contract.js
index 3b134c364..f9b72010e 100644
--- a/imports/startup/both/modules/Contract.js
+++ b/imports/startup/both/modules/Contract.js
@@ -107,7 +107,8 @@ const _entangle = (draft) => {
return _getPublicAddress(constituency[i].code);
}
}
- return undefined;
+
+ return draft;
};
/**
@@ -125,40 +126,76 @@ const _contractHasToken = (contract) => {
return false;
};
+const _webVoteChain = (contract) => {
+ const draft = contract;
+
+ draft.blockchain = {
+ coin: { code: 'WEB VOTE' },
+ votePrice: '1',
+ };
+ draft.constituency = [{
+ kind: 'TOKEN',
+ code: 'WEB VOTE',
+ check: 'EQUAL',
+ }];
+
+ return draft;
+};
+
+const _blockstackChain = (contract) => {
+ const draft = contract;
+
+ draft.blockchain = {
+ coin: { code: 'STX' },
+ publicAddress: Meteor.user().profile.wallet.reserves[0].publicAddress,
+ votePrice: '1',
+ };
+ draft.constituency = [{
+ kind: 'TOKEN',
+ code: 'STX',
+ check: 'EQUAL',
+ }];
+ draft.wallet.currency = 'STX';
+
+ return draft;
+};
+
+const _ethereumChain = (contract) => {
+ const draft = contract;
+ // ERC20 tokens
+ if (!_contractHasToken(draft)) {
+ draft.constituency.push(defaultConstituency);
+ }
+ for (let i = 0; i < draft.constituency.length; i += 1) {
+ if (draft.constituency[i].kind === 'TOKEN') {
+ draft.wallet.currency = draft.constituency[i].code;
+ }
+ }
+
+ if (draft.blockchain.coin === undefined) {
+ // set coin.code to whats in wallet.currency
+ draft.blockchain.coin = {};
+ draft.blockchain.coin.code = draft.wallet.currency;
+ } else {
+ draft.blockchain.coin.code = draft.wallet.currency;
+ }
+
+ return draft;
+};
+
/**
* @summary inserts default blockchain data to a contract
* @param {object} contract contract to include chain data
*/
const _chain = (contract) => {
- const draft = contract;
- if (Meteor.user().profile.wallet.reserves[0].token === 'STX') {
- // Blockstack tokens only
- draft.blockchain = {
- coin: { code: 'STX' },
- publicAddress: Meteor.user().profile.wallet.reserves[0].publicAddress,
- votePrice: '1',
- };
- draft.constituency = [{
- kind: 'TOKEN',
- code: 'STX',
- check: 'EQUAL',
- }];
- draft.wallet.currency = 'STX';
- } else {
- // ERC20 tokens
- if (!_contractHasToken(draft)) {
- draft.constituency.push(defaultConstituency);
- }
- for (let i = 0; i < draft.constituency.length; i += 1) {
- if (draft.constituency[i].kind === 'TOKEN') {
- draft.wallet.currency = draft.constituency[i].code;
- }
- }
+ let draft = contract;
- // blockchain
- if (!draft.blockchain.publicAddress) {
- draft.blockchain = _entangle(draft);
- }
+ if (draft.wallet.currency === 'WEB VOTE') {
+ draft = _webVoteChain(draft);
+ } else if (draft.wallet.currency === 'STX') {
+ draft = _blockstackChain(draft);
+ } else {
+ draft = _ethereumChain(draft);
}
return draft;
};
@@ -181,16 +218,13 @@ const _createContract = (newkeyword, newtitle) => {
// sign by author
_sign(contract._id, Meteor.user(), 'AUTHOR');
- // Omit for tokenless users
- if (Meteor.user().profile.wallet.reserves !== undefined) {
- // chain by author
- const chainedContract = _chain(contract);
- Contracts.update({ _id: contract._id }, { $set: {
- blockchain: chainedContract.blockchain,
- wallet: chainedContract.wallet,
- constituency: chainedContract.constituency,
- } });
- }
+ // chain by author
+ const chainedContract = _chain(contract);
+ Contracts.update({ _id: contract._id }, { $set: {
+ blockchain: chainedContract.blockchain,
+ wallet: chainedContract.wallet,
+ constituency: chainedContract.constituency,
+ } });
}
return Contracts.findOne({ keyword: `draft-${Meteor.userId()}` });
// has title & keyword, used for forks
@@ -200,11 +234,99 @@ const _createContract = (newkeyword, newtitle) => {
} else {
Contracts.insert({ keyword: newkeyword, title: newtitle });
}
- return Contracts.find({ keyword: newkeyword }).fetch();
+ return Contracts.findOne({ keyword: newkeyword });
}
return false;
};
+/**
+* @summary saves poll on database
+* @param {object} draft being checked for poll creation.
+* @param {object} pollContract poll data
+*/
+const _savePoll = (draft, pollContract) => {
+ // update db
+ Contracts.update({ _id: pollContract._id }, {
+ $set: {
+ blockchain: draft.blockchain,
+ constituency: draft.constituency,
+ constituencyEnabled: draft.constituencyEnabled,
+ rules: draft.rules,
+ wallet: draft.wallet,
+ kind: pollContract.kind,
+ pollId: pollContract.pollId,
+ pollChoiceId: pollContract.pollChoiceId,
+ signatures: draft.signatures,
+ closing: draft.closing,
+ stage: 'LIVE',
+ },
+ });
+};
+
+/**
+* @summary removes all poll contracts
+* @param {object} draft being checked for poll erasure
+*/
+const _removePoll = (draft) => {
+ for (let k = 0; k < draft.poll.length; k += 1) {
+ Contracts.remove({ _id: draft.poll[k].contractId });
+ }
+};
+
+/**
+* @summary create a basic poll inside a contract
+* @param {object} draft being checked for poll creation.
+* @return {object} draft created with poll settings included
+*/
+const _createPoll = (draft) => {
+ let pollContract;
+ const newDraft = draft;
+
+ // is a draft configured for polling without a poll
+ if (draft.rules && draft.rules.pollVoting === true) {
+ if (draft.poll.length === 0) {
+ const options = [];
+ let pollContractURI;
+ for (let i = 0; i < 2; i += 1) {
+ // creaate uri reference
+ pollContractURI = _contractURI(`${TAPi18n.__('poll-choice').replace('{{number}}', i.toString())} ${document.getElementById('titleContent').innerText} ${TAPi18n.__(`poll-default-title-${i}`)}`);
+
+ // create contract to be used as poll option
+ pollContract = _createContract(pollContractURI, TAPi18n.__(`poll-default-title-${i}`));
+
+ // attach id of parent contract to poll option contract
+ pollContract.pollId = draft._id;
+ pollContract.kind = 'POLL';
+ pollContract.pollChoiceId = i;
+
+ _savePoll(draft, pollContract);
+
+ // add to array to be stored in parent contract
+ options.push({
+ contractId: pollContract._id,
+ totalStaked: '0',
+ });
+ }
+
+ // store array in parent contract
+ newDraft.poll = options;
+
+ return newDraft;
+ } else if (draft.poll.length > 0) {
+ // change info of existing poll
+
+ _removePoll(draft);
+ newDraft.poll = [];
+ newDraft.poll = _createPoll(newDraft).poll;
+ return newDraft;
+ }
+ }
+
+ // return same draft
+ return draft;
+};
+
+
/**
* @summary verifies if there's already a precedent among delegator and delegate
* @param {string} delegatorId - identity assigning the tokens (usually currentUser)
@@ -466,6 +588,8 @@ const _land = (draft) => {
return land;
};
+
+
/**
* @summary publishes a contract and goes to home
* @param {string} contractId - id of the contract to publish
@@ -473,7 +597,6 @@ const _land = (draft) => {
*/
const _publish = (contractId, keyword) => {
let draft = Session.get('draftContract');
-
// status
draft.stage = 'LIVE';
@@ -508,12 +631,7 @@ const _publish = (contractId, keyword) => {
}
// chain
- if (Meteor.user().profile.wallet.reserves) {
- draft = _chain(draft);
- if (draft.wallet.currency) {
- draft.blockchain.coin.code = draft.wallet.currency;
- }
- }
+ draft = _chain(draft);
// db
Contracts.update({ _id: contractId }, { $set: {
@@ -529,9 +647,26 @@ const _publish = (contractId, keyword) => {
constituency: draft.constituency,
wallet: draft.wallet,
blockchain: draft.blockchain,
+ rules: draft.rules,
+ poll: draft.poll,
+ closing: draft.closing,
},
});
+ // polls must live under parent rules
+ if (draft.poll.length > 0) {
+ for (let k = 0; k < draft.poll.length; k += 1) {
+ Contracts.update({ _id: draft.poll[k].contractId }, {
+ $set: {
+ closing: draft.closing,
+ rules: draft.rules,
+ constituency: draft.constituency,
+ constituencyEnabled: draft.constituencyEnabled,
+ },
+ });
+ }
+ }
+
// add reply to counter in contract
if (draft.replyId) {
// count
@@ -602,5 +737,7 @@ export const removeContract = _remove;
export const createDelegation = _newDelegation;
export const getURLDate = _getURLDate;
export const sendDelegationVotes = _sendDelegation;
+export const createPoll = _createPoll;
+export const removePoll = _removePoll;
export const createContract = _createContract;
export const getDelegationContract = _getDelegationContract;
diff --git a/imports/startup/both/modules/User.js b/imports/startup/both/modules/User.js
index 624aaf816..d74ae49e2 100644
--- a/imports/startup/both/modules/User.js
+++ b/imports/startup/both/modules/User.js
@@ -163,6 +163,9 @@ const _createUser = (data) => {
console.log('does user exist?');
console.log(Meteor.user());
+ // TODO - here we could add Meteor.settings check for webvotes bool
+ genesisTransaction(Meteor.user()._id);
+
return resolve(result);
});
});
diff --git a/imports/startup/both/modules/metamask.js b/imports/startup/both/modules/metamask.js
index 7864d0783..fa9b4a665 100644
--- a/imports/startup/both/modules/metamask.js
+++ b/imports/startup/both/modules/metamask.js
@@ -5,6 +5,7 @@ import { Router } from 'meteor/iron:router';
import { Session } from 'meteor/session';
import { $ } from 'meteor/jquery';
+import { Contracts } from '/imports/api/contracts/Contracts';
import { displayModal } from '/imports/ui/modules/modal';
import { transact } from '/imports/api/transactions/transaction';
import { displayNotice } from '/imports/ui/modules/notice';
@@ -14,6 +15,7 @@ import { Transactions } from '/imports/api/transactions/Transactions';
import abi from 'human-standard-token-abi';
+import { debug } from 'util';
const Web3 = require('web3');
const ethUtil = require('ethereumjs-util');
@@ -185,8 +187,8 @@ const _delegate = (sourceId, targetId, contractId, hash, value) => {
tickets: [{
hash,
status: 'PENDING',
- value
- }]
+ value,
+ }],
});
Meteor.users.update({ _id: sourceId }, { $set: { profile: source.profile }});
@@ -201,15 +203,14 @@ const _delegate = (sourceId, targetId, contractId, hash, value) => {
tickets: [{
hash,
status: 'PENDING',
- value
- }]
+ value,
+ }],
});
console.log(target);
Meteor.users.update({ _id: targetId }, { $set: { profile: target.profile }});
}
-}
-
+};
/**
* @summary send crypto with mask;
@@ -329,49 +330,234 @@ const _transactWithMetamask = (from, to, quantity, tokenCode, contractAddress, s
}
};
-if (Meteor.isClient) {
- const handleSignMessage = (publicAddress) => {
- return new Promise((resolve, reject) => {
- web3.eth.personal.sign(
- web3.utils.utf8ToHex(`${TAPi18n.__('metamask-sign-nonce').replace('{{collectiveName}}', Meteor.settings.public.Collective.name)}`),
- publicAddress,
- function (err, signature) {
- if (err) return reject(err);
- return resolve({ signature });
- }
- );
- });
- };
- const verifySignature = (signature, publicAddress) => {
- const msg = `${TAPi18n.__('metamask-sign-nonce').replace('{{collectiveName}}', Meteor.settings.public.Collective.name)}`;
- let res;
-
- // Perform an elliptic curve signature verification with ecrecover
- const msgBuffer = ethUtil.toBuffer(msg);
- const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
- const signatureBuffer = ethUtil.toBuffer(signature.signature);
- const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
- const publicKey = ethUtil.ecrecover(
- msgHash,
- signatureParams.v,
- signatureParams.r,
- signatureParams.s
+const handleSignMessage = (publicAddress, nonce, message) => {
+ return new Promise((resolve, reject) => {
+ web3.eth.personal.sign(
+ web3.utils.utf8ToHex(`${message}`),
+ publicAddress,
+ function (err, signature) {
+ if (err) return reject(err);
+ return resolve({ signature });
+ }
);
- const addressBuffer = ethUtil.publicToAddress(publicKey);
- const address = ethUtil.bufferToHex(addressBuffer);
+ });
+};
+
+const verifySignature = (signature, publicAddress, nonce, message) => {
+ let msg;
+ console.log(message);
+ if (!message) {
+ msg = `${TAPi18n.__('metamask-sign-nonce').replace('{{collectiveName}}', Meteor.settings.public.Collective.name)}`;
+ } else {
+ msg = message;
+ }
+ let res;
+
+ // Perform an elliptic curve signature verification with ecrecover
+ const msgBuffer = ethUtil.toBuffer(msg);
+ const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
+ const signatureBuffer = ethUtil.toBuffer(signature.signature);
+ const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
+ const publicKey = ethUtil.ecrecover(
+ msgHash,
+ signatureParams.v,
+ signatureParams.r,
+ signatureParams.s
+ );
+ const addressBuffer = ethUtil.publicToAddress(publicKey);
+ const address = ethUtil.bufferToHex(addressBuffer);
+
+ // The signature verification is successful if the address found with
+ // ecrecover matches the initial publicAddress
+ console.log(`address.toLowerCase(): ${address.toLowerCase()}`);
+ console.log(`publicAddress.toLowerCase(): ${publicAddress.toLowerCase()}`);
+
+ if (address.toLowerCase() === publicAddress.toLowerCase()) {
+ return 'success';
+ }
+ return res
+ .status(401)
+ .send({ error: TAPi18n.__('metamask-sign-fail') });
+};
- // The signature verification is successful if the address found with
- // ecrecover matches the initial publicAddress
- if (address.toLowerCase() === publicAddress.toLowerCase()) {
- return 'success';
+/**
+* @summary persist in db the coin vote
+* @param {string} from blockchain address
+* @param {string} to blockchain destination
+* @param {string} tokenCode currency
+* @param {string} value the value
+* @param {string} sourceId sender in sovereign
+* @param {string} targetId receiver in sovereign
+* @param {string} delegateId user to delegate into
+*/
+const _transactCoinVote = (sourceId, targetId, tokenCode, from, to, value, delegateId, publicAddress) => {
+ console.log(value);
+ transact(
+ sourceId,
+ targetId,
+ 0,
+ {
+ currency: tokenCode,
+ status: 'PENDING',
+ kind: 'CRYPTO',
+ contractId: targetId,
+ input: {
+ address: from,
+ },
+ output: {
+ address: to,
+ },
+ blockchain: {
+ tickets: [{
+ hash: publicAddress,
+ status: 'CONFIRMED',
+ value,
+ }],
+ coin: {
+ code: tokenCode,
+ },
+ },
+ geo: Meteor.user().profile.country ? Meteor.user().profile.country.code : '',
+ },
+ () => {
+ _delegate(Meteor.userId(), delegateId, targetId, publicAddress, value);
+ displayModal(false, modal);
+ displayNotice(`${TAPi18n.__('transaction-tally').replace('{{token}}', tokenCode)}`, true, true);
+ }
+ );
+};
+
+/**
+* @summary do a coinvote with account stake
+* @param {string} from blockchain address
+* @param {string} to blockchain destination
+* @param {string} quantity amount transacted
+* @param {string} tokenCode currency
+* @param {string} contractAddress
+* @param {string} sourceId sender in sovereign
+* @param {string} targetId receiver in sovereign
+* @param {string} delegateId user to delegate into
+*/
+const _coinvote = (from, to, quantity, tokenCode, contractAddress, sourceId, targetId, delegateId, choice, url) => {
+ if (_web3(true)) {
+ const nonce = Math.floor(Math.random() * 10000);
+ let publicAddress;
+
+ let message = TAPi18n.__('coinvote-signature');
+ message = message.replace('{{quantity}}', quantity);
+ message = message.replace('{{ticker}}', tokenCode);
+ message = message.replace('{{choice}}', choice);
+ message = message.replace('{{url}}', url);
+
+ let value;
+ if (tokenCode === 'ETH') {
+ value = web3.utils.toWei(quantity, _convertToEther(tokenCode)).toString();
+ } else {
+ const quantityWithDecimals = addDecimal(quantity.toNumber(), 18).toString();
+ value = quantityWithDecimals;
}
- return res
- .status(401)
- .send({ error: TAPi18n.__('metamask-sign-fail') });
- };
+ if (Meteor.Device.isPhone()) {
+ return web3.eth.getCoinbase().then(function (coinbaseAddress) {
+ publicAddress = coinbaseAddress.toLowerCase();
+ return handleSignMessage(publicAddress, nonce, message);
+ }).then(function (signature) {
+ const verification = verifySignature(signature, publicAddress, nonce, message);
+
+ if (verification === 'success') {
+ _transactCoinVote(sourceId, targetId, tokenCode, from, to, value, delegateId, publicAddress);
+ } else {
+ console.log(TAPi18n.__('metamask-login-error'));
+ }
+ });
+ }
+
+ // Support privacy-mode in desktop only for now
+ window.ethereum.enable().then(function () {
+ return web3.eth.getCoinbase();
+ }).then(function (coinbaseAddress) {
+ publicAddress = coinbaseAddress.toLowerCase();
+ return handleSignMessage(publicAddress, nonce, message);
+ }).then(function (signature) {
+ const verification = verifySignature(signature, publicAddress, nonce, message);
+
+ if (verification === 'success') {
+ _transactCoinVote(sourceId, targetId, tokenCode, from, to, value, delegateId, publicAddress);
+ } else {
+ console.log(TAPi18n.__('metamask-login-error'));
+ }
+ })
+ .catch((e) => {
+ displayModal(false, modal);
+ displayNotice(`${TAPi18n.__('transaction-tally-denied').replace('{{token}}', tokenCode)}`, true, true);
+ });
+ } else {
+ modal.message = TAPi18n.__('metamask-activate');
+ displayModal(true, modal);
+ }
+};
+
+const _scanCoinVote = (contract) => {
+ if (contract.blockchain && contract.blockchain.tickets) {
+ for (let i = 0; i < contract.blockchain.tickets.length; i += 1) {
+ for (let k = 0; k < Meteor.user().profile.wallet.reserves.length; k += 1) {
+ if (contract.blockchain.tickets[i].hash === Meteor.user().profile.wallet.reserves[k].publicAddress) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+};
+/**
+* @summary checks if user didn't coinvoted already
+* @param {object} contract with the settings
+* @return {boolean} if voted already or not
+*/
+const _verifyCoinVote = (contract) => {
+ let poll;
+ let check;
+ if (contract.rules && contract.rules.balanceVoting) {
+ if (contract.poll && contract.poll.length > 0) {
+ for (let i = 0; i < contract.poll.length; i += 1) {
+ poll = Contracts.findOne({ _id: contract.poll[i].contractId });
+ if (poll) {
+ check = _scanCoinVote(poll);
+ if (check) { break; }
+ }
+ }
+ return check;
+ }
+ return _scanCoinVote(contract);
+ }
+ return false;
+};
+
+
+/**
+* @summary get current height of the blockchain
+*/
+const _getBlockHeight = async () => {
+ let height = 0;
+ if (_web3()) {
+ height = await web3.eth.isSyncing().then(
+ async (res) => {
+ if (!res) {
+ return await web3.eth.getBlockNumber().then((blockNumber) => {
+ return blockNumber;
+ });
+ }
+ return false;
+ }
+ );
+ }
+ return height;
+};
+
+
+if (Meteor.isClient) {
/**
* @summary log in signing public blockchain address with private key
*/
@@ -379,14 +565,13 @@ if (Meteor.isClient) {
if (_web3(false)) {
const nonce = Math.floor(Math.random() * 10000);
let publicAddress;
- Session.set('newLogin', true);
if (Meteor.Device.isPhone()) {
// When mobile, not supporting privacy-mode for now
// https://github.com/DemocracyEarth/sovereign/issues/421
return web3.eth.getCoinbase().then(function (coinbaseAddress) {
publicAddress = coinbaseAddress.toLowerCase();
- return handleSignMessage(publicAddress, nonce);
+ return handleSignMessage(publicAddress, nonce, TAPi18n.__('metamask-sign-nonce').replace('{{collectiveName}}', Meteor.settings.public.Collective.name));
}).then(function (signature) {
const verification = verifySignature(signature, publicAddress, nonce);
@@ -403,6 +588,7 @@ if (Meteor.isClient) {
methodName,
methodArguments,
});
+ Session.set('newLogin', true);
Router.go('/');
},
});
@@ -417,7 +603,7 @@ if (Meteor.isClient) {
return web3.eth.getCoinbase();
}).then(function (coinbaseAddress) {
publicAddress = coinbaseAddress.toLowerCase();
- return handleSignMessage(publicAddress, nonce);
+ return handleSignMessage(publicAddress, nonce, TAPi18n.__('metamask-sign-nonce').replace('{{collectiveName}}', Meteor.settings.public.Collective.name));
}).then(function (signature) {
const verification = verifySignature(signature, publicAddress, nonce);
@@ -493,7 +679,10 @@ if (Meteor.isServer) {
}
export const transactWithMetamask = _transactWithMetamask;
+export const coinvote = _coinvote;
export const getTransactionStatus = _getTransactionStatus;
export const setupWeb3 = _web3;
export const syncBlockchain = _syncBlockchain;
export const hideLogin = _hideLogin;
+export const getBlockHeight = _getBlockHeight;
+export const verifyCoinVote = _verifyCoinVote;
diff --git a/imports/startup/both/routes.js b/imports/startup/both/routes.js
index c4d6e854f..70292c829 100644
--- a/imports/startup/both/routes.js
+++ b/imports/startup/both/routes.js
@@ -119,10 +119,6 @@ Router.route('/', {
},
onAfterAction() {
_boilerPlate();
- if (!setupWeb3(false)) {
- // If user is not logged in to Metamask then logout of Sovereign
- Meteor.logout();
- }
},
});
diff --git a/imports/startup/server/accounts/accounts.js b/imports/startup/server/accounts/accounts.js
index 34d4cdb19..54b2c2620 100644
--- a/imports/startup/server/accounts/accounts.js
+++ b/imports/startup/server/accounts/accounts.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore';
import { Accounts } from 'meteor/accounts-base';
+import { rules } from '/lib/const';
import { convertToSlug } from '/lib/utils';
import { deburr, toLower, camelCase } from 'lodash';
@@ -138,8 +139,12 @@ function normalizeMetamaskUser(profile, user) {
const anonymousUser = 'anonymous' + publicAddress.slice(0,7);
const username = generateAvailableUsername(deburr(toLower(camelCase(anonymousUser))));
+ // other option is to web votes here
const walletInit = {
- currency: 'ETH',
+ currency: 'WEB VOTE',
+ balance: rules.VOTES_INITIAL_QUANTITY,
+ placed: 0,
+ available: rules.VOTES_INITIAL_QUANTITY,
reserves: [{
balance: 0,
placed: 0,
diff --git a/imports/ui/.DS_Store b/imports/ui/.DS_Store
deleted file mode 100644
index 6cc8000c5..000000000
Binary files a/imports/ui/.DS_Store and /dev/null differ
diff --git a/imports/ui/css/earth.css b/imports/ui/css/earth.css
index c1f252c8f..e253277ce 100755
--- a/imports/ui/css/earth.css
+++ b/imports/ui/css/earth.css
@@ -6290,6 +6290,7 @@ blockquote {
.token-wrap {
display: inline-block;
margin-right: 10px;
+ margin-top: 5px;
float: left;
}
diff --git a/imports/ui/modules/split.js b/imports/ui/modules/split.js
index ae49342e2..704817b36 100644
--- a/imports/ui/modules/split.js
+++ b/imports/ui/modules/split.js
@@ -31,7 +31,9 @@ const _saveSplitSettings = (left, right) => {
* @param {boolean} winResize if call is coming from a window resize
*/
const _resizeSplit = (diff, winResize) => {
- if ($('.split-right') && $('.split-left')) {
+ if (!Meteor.settings.public.app.config.interface.showTransactions) {
+ $('.split-left').width('100%');
+ } else if ($('.split-right') && $('.split-left')) {
const contentWidth = $('.right').width();
const agoraWidth = parseInt((contentWidth * RIGHTHALF) - diff, 10);
const contractWidth = parseInt((contentWidth * LEFTHALF) + diff, 10);
@@ -44,7 +46,9 @@ const _resizeSplit = (diff, winResize) => {
};
const _resetSplit = () => {
- if ($('.split-right')) {
+ if (!Meteor.settings.public.app.config.interface.showTransactions) {
+ $('.split-left').width('100%');
+ } else if ($('.split-right')) {
event.preventDefault();
if ($(window).width() < gui.DESKTOP_MIN_WIDTH) {
$('.split-left').width('100%');
diff --git a/imports/ui/templates/components/decision/balance/balance.html b/imports/ui/templates/components/decision/balance/balance.html
index 957ba4fd9..e178b2db3 100644
--- a/imports/ui/templates/components/decision/balance/balance.html
+++ b/imports/ui/templates/components/decision/balance/balance.html
@@ -26,11 +26,5 @@
{{_ 'available'}}
{{/unless}}
- {{#unless disableBar}}
-