Compare commits

..

2 Commits

Author SHA1 Message Date
bumi a75bcc6a59 Add configurable network name
This is required for the infura provider
2018-04-22 20:25:58 +02:00
bumi f5dc8f86cf Use infura hosted provider if web3 is not available
If the user does not have metamask or similar (no web3 provider is
availale) we will load data from the infura node.

No signer is available for the infura node but the ethers provider would
throw an error if we try to access it - so this moves the decision of
choosing the provider to the ethProvider function.
2018-04-22 18:41:51 +02:00
20 changed files with 5484 additions and 4952 deletions
+8 -9
View File
@@ -22,8 +22,7 @@ You will need the following things properly installed on your computer.
## Running / Development
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
* `ember serve`
* Visit your app at [http://localhost:4200](http://localhost:4200).
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
@@ -57,13 +56,13 @@ _(You need collaborator permissions on the 5apps Deploy project.)_
## Working with locally deployed contracts
The smart contracts and their JavaScript wrapper library are developed in the
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
[truffle-kredits](https://github.com/67P/truffle-kredits) repo/package.
You can run `kredits-web` on your machine, against a local, simulated Ethereum
network, provided e.g. by [ganache](http://truffleframework.com/ganache/) or
[ganache-cli](https://github.com/trufflesuite/ganache-cli).
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools
[truffle-kredits](https://github.com/67P/truffle-kredits) holds all the tools
to start and set up such a simulated network, as well as to deploy smart
contracts to it.
@@ -76,22 +75,22 @@ Run a local IPFS deamon in offline mode.
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
* `ipfs daemon --offline`
#### 2. kredits-contracts
#### 2. truffle-kredits
Get your local Ethereum development node running.
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
* Clone [truffle-kredits](https://github.com/67P/truffle-kredits)
* `npm install`
* `npm run ganache` - which is basically: `ganache-cli -p 7545 -i 100` (we use the non-default port for local networks and a fixed network id)
* `npm run bootstrap` - bootstrap runs fresh migrations, adds some seed data and writes the address/abi information to JSON that will be used by kredits-web
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine
* `npm link` - make the `truffle-kredits` module linkable as `kredits-contracts` on your machine
#### 3. kredits-web
With IPFS and Ethereum/ganache running, you can now start this Ember app.
* `npm link kredits-contracts` - link the local `kredits-contracts` package
* `npm run start:local` - NETWORK_ID=100 and WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings
* `npm link kredits-contracts` - link the local `truffle-kredits` package (it will become `kredits-contracts` soon)
* `npm start`
#### IPFS
+7 -17
View File
@@ -1,30 +1,20 @@
import Component from '@ember/component';
import { inject } from '@ember/service';
import { empty } from '@ember/object/computed';
export default Component.extend({
kredits: inject(),
tagName: 'ul',
classNames: ['proposal-list'],
selectedProposals: [],
submitButtonDisabled: empty('selectedProposals'),
actions: {
confirm() {
this.confirmProposals(this.selectedProposals)
.then(() => {
this.selectedProposals = [];
});
},
toggleSelect(proposalId, selected) {
if (selected) {
this.selectedProposals.removeObject(proposalId);
confirm(proposalId) {
if (this.contractInteractionEnabled) {
this.confirmProposal(proposalId);
} else {
this.selectedProposals.addObject(proposalId);
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
}
},
}
}
});
@@ -1,29 +0,0 @@
import Component from '@ember/component';
import tag from 'ember-awesome-macros/tag';
import { computed } from '@ember/object';
import { reads } from '@ember/object/computed';
import { inject } from '@ember/service';
export default Component.extend({
kredits: inject(),
tagName: 'li',
classNames: ['proposal-list-item'],
attributeBindings: ['data-proposal-id', 'title'],
title: tag`(${'proposal.kind'}) ${'proposal.description'}`,
'data-proposal-id': reads('proposal.id'),
canBeVoted: computed('voterIds.[]', 'kredits.currentUser', function() {
let { isExecuted, voterIds } = this.proposal;
let { currentUser } = this.kredits;
voterIds = voterIds.map((id) => id.toString());
return currentUser.isCore && !isExecuted && !voterIds.includes(currentUser.id);
}),
selected: computed('selectedProposals.[]', function() {
return this.selectedProposals.includes(this.proposal.id);
})
});
@@ -1,8 +0,0 @@
<span class="category {{proposal.kind}}">♥</span>
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
for <span class="recipient">{{proposal.contributor.name}}</span>
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
{{#if canBeVoted}}
<button {{action toggleSelect proposal.id selected}} class="{{if selected 'selected' ''}}">+1</button>
{{/if}}
+10 -12
View File
@@ -1,14 +1,12 @@
{{#each proposals as |proposal|}}
{{proposal-list/item
proposal=proposal
selectedProposals=selectedProposals
toggleSelect=(action 'toggleSelect')
}}
{{/each}}
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
<span class="category {{proposal.kind}}">♥</span>
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
for <span class="recipient">{{proposal.contributor.name}}</span>
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
{{#if confirmProposals}}
<p class="actions">
<input type="submit" disabled={{submitButtonDisabled}}
value="Confirm selected" {{action 'confirm'}}>
</p>
{{/if}}
{{#unless proposal.isExecuted}}
<button {{action "confirm" proposal.id}}>+1</button>
{{/unless}}
</li>
{{/each}}
+4 -12
View File
@@ -1,7 +1,6 @@
import Controller from '@ember/controller';
import { alias, filter, filterBy, sort } from '@ember/object/computed';
import { inject as injectService } from '@ember/service';
import RSVP from 'rsvp';
export default Controller.extend({
kredits: injectService(),
@@ -21,17 +20,10 @@ export default Controller.extend({
proposalsOpenSorted: sort('proposalsOpen', 'proposalsSorting'),
actions: {
confirmProposals(proposalIds) {
if (this.kredits.currentUser.isCore) {
return this.kredits.batchVote(proposalIds)
.then((transaction) => {
window.confirm('Vote submitted to Ethereum blockhain: '+transaction.hash);
return transaction;
});
} else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
return RSVP.reject();
}
confirmProposal(proposalId) {
this.kredits.vote(proposalId).then(transaction => {
window.confirm('Vote submitted to Ethereum blockhain: '+transaction.hash);
});
},
save(contributor) {
+1
View File
@@ -7,6 +7,7 @@ const Router = EmberRouter.extend({
});
Router.map(function() {
this.route('spinner');
this.route('proposals', function() {
this.route('new');
});
+4
View File
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
+16 -25
View File
@@ -4,7 +4,7 @@ import RSVP from 'rsvp';
import Service from '@ember/service';
import { computed } from '@ember/object';
import { notEmpty } from '@ember/object/computed';
import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty } from '@ember/utils';
import config from 'kredits-web/config/environment';
@@ -16,6 +16,8 @@ export default Service.extend({
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null,
currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'),
accountNeedsUnlock: computed('currentUserAccounts', function() {
return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
@@ -27,6 +29,7 @@ export default Service.extend({
return new RSVP.Promise((resolve) => {
let ethProvider;
let networkId;
let networkName;
if (typeof window.web3 !== 'undefined') {
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
networkId = parseInt(window.web3.version.network);
@@ -36,41 +39,30 @@ export default Service.extend({
);
ethProvider.listAccounts().then((accounts) => {
this.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({
ethProvider,
ethSigner
});
resolve(ethProvider, ethProvider.getSigner());
});
} else {
console.debug('[kredits] Creating new instance from npm module class');
networkId = parseInt(config.contractMetadata.networkId);
console.debug(`[kredits] networkId=${networkId} providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(
config.web3ProviderUrl,
{ chainId: networkId }
);
resolve({
ethProvider: ethProvider,
ethSigner: null
});
networkName = config.contractMetadata.networkName;
console.log(networkName);
ethProvider = new ethers.providers.InfuraProvider({ chainId: networkId, name: networkName });
resolve(ethProvider, null);
}
});
},
setup() {
return this.getEthProvider().then((providerAndSigner) => {
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
ipfsConfig: config.ipfs
});
return this.getEthProvider().then((ethProvider, ethSigner) => {
let kredits = new Kredits(ethProvider, ethSigner);
return kredits
.init()
.then((kredits) => {
this.set('kredits', kredits);
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
this.getCurrentUser.then((contributorData) => {
this.set('currentUser', Contributor.create(contributorData));
this.set('currentUser', contributorData);
});
}
return kredits;
@@ -132,11 +124,10 @@ export default Service.extend({
});
},
batchVote(proposalIds) {
proposalIds = proposalIds.map((id) => parseInt(id))
console.debug('[kredits] vote for', proposalIds);
vote(proposalId) {
console.debug('[kredits] vote for', proposalId);
return this.kredits.Operator.functions.batchVote(proposalIds)
return this.kredits.Operator.functions.vote(proposalId)
.then((data) => {
console.debug('[kredits] vote response', data);
return data;
-91
View File
@@ -1,91 +0,0 @@
button, input[type=submit] {
display: inline-block;
border: 1px solid rgba(22, 21, 40, 1);
background-color: rgba(22, 21, 40, 0.6);
color: $primaryColor;
border-radius: 3px;
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
&:hover {
background-color: rgba(22, 21, 40, 0.8);
}
&:disabled {
cursor: not-allowed;
background-color: transparent;
}
}
input[type=submit] {
padding: 0.6rem 2rem;
}
input[type=text], select {
width: 100%;
padding: 1rem;
border: none;
border-bottom: 1px solid rgba(255,255,255,0.2);
background-color: rgba(22, 21, 40, 0.3);
color: #fff;
font-size: 1.2rem;
&:focus, &.valid {
background-color: rgba(22, 21, 40, 0.6);
}
@include placeholder {
color: rgba(238, 238, 238, 0.5);
}
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0;
background-color: rgba(22, 21, 40, 0.6);
background-image:
linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position:
calc(100% - 1.5rem) calc(1rem + 0.5rem),
calc(100% - 1rem) calc(1rem + 0.5rem);
background-size:
0.5rem 0.5rem,
0.5rem 0.5rem;
background-repeat: no-repeat;
&:invalid {
color: rgba(238, 238, 238, 0.5);
}
}
input[type=checkbox] {
display: none;
}
label.checkbox {
line-height: 3.2rem;
font-size: 1.2rem;
&::before {
display: inline-block;
margin-right: 0.8rem;
height: 3.2rem;
width: 3.2rem;
font-size: 2rem;
background-color: rgba(22, 21, 40, 0.3);
border-bottom: 1px solid rgba(255,255,255,0.2);
text-align: center;
vertical-align: middle;
content: '';
color: rgba(255,255,255,0.2);
}
}
input[type=checkbox]:checked + label.checkbox {
&::before {
background-color: rgba(22, 21, 40, 0.6);
color: #fff;
}
}
+15 -1
View File
@@ -84,7 +84,21 @@ section {
}
}
@import "forms";
button, input[type=submit] {
display: inline-block;
border: 1px solid rgba(22, 21, 40, 1);
background-color: rgba(22, 21, 40, 0.6);
color: $primaryColor;
border-radius: 3px;
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
&:hover {
background-color: rgba(22, 21, 40, 0.8);
}
}
@import "components/loading-spinner";
@import "components/contributor-list";
@@ -1,6 +1,7 @@
section#add-contributor, section#add-proposal {
form {
p {
margin-bottom: 1rem;
@@ -13,6 +14,81 @@ section#add-contributor, section#add-proposal {
}
}
}
input[type=text], select {
width: 100%;
padding: 1rem;
border: none;
border-bottom: 1px solid rgba(255,255,255,0.2);
background-color: rgba(22, 21, 40, 0.3);
color: #fff;
font-size: 1.2rem;
&:focus, &.valid {
background-color: rgba(22, 21, 40, 0.6);
}
@include placeholder {
color: rgba(238, 238, 238, 0.5);
}
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0;
background-color: rgba(22, 21, 40, 0.6);
background-image:
linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position:
calc(100% - 1.5rem) calc(1rem + 0.5rem),
calc(100% - 1rem) calc(1rem + 0.5rem);
background-size:
0.5rem 0.5rem,
0.5rem 0.5rem;
background-repeat: no-repeat;
&:invalid {
color: rgba(238, 238, 238, 0.5);
}
}
input[type=submit] {
padding: 0.6rem 2rem;
&:disabled {
background-color: transparent;
}
}
input[type=checkbox] {
display: none;
}
label.checkbox {
line-height: 3.2rem;
font-size: 1.2rem;
&::before {
display: inline-block;
margin-right: 0.8rem;
height: 3.2rem;
width: 3.2rem;
font-size: 2rem;
background-color: rgba(22, 21, 40, 0.3);
border-bottom: 1px solid rgba(255,255,255,0.2);
text-align: center;
vertical-align: middle;
content: '';
color: rgba(255,255,255,0.2);
}
}
input[type=checkbox]:checked + label.checkbox {
&::before {
background-color: rgba(22, 21, 40, 0.6);
color: #fff;
}
}
}
}
@@ -46,10 +46,6 @@ ul.proposal-list {
height: 2rem;
line-height: 2rem;
padding: 0 0.6rem;
&.selected {
background-color: rgba(224,104,251, 0.6);
}
}
}
+5 -3
View File
@@ -2,7 +2,7 @@
<section id="user-account">
{{#if kredits.currentUser }}
{{ kredits.currentUser.name }}
{{#if kredits.currentUser.isCore }}
{{#if kredits.currentUserIsCore }}
(core)
{{/if}}
Account: {{kredits.currentUser.account}}
@@ -32,7 +32,8 @@
<div class="content">
{{!-- TODO: We need a better naming for kredits.hasAccounts --}}
{{proposal-list proposals=proposalsOpenSorted
confirmProposals=(action 'confirmProposals')}}
confirmProposal=(action "confirmProposal")
contractInteractionEnabled=kredits.hasAccounts}}
{{!-- TODO: We need a better naming --}}
{{#if kredits.hasAccounts}}
@@ -49,7 +50,8 @@
<h2>Closed Proposals</h2>
</header>
<div class="content">
{{proposal-list proposals=proposalsClosedSorted}}
{{proposal-list proposals=proposalsClosedSorted
confirmProposal=(action "confirmProposal")}}
{{!-- TODO: We need a better naming --}}
{{#if kredits.hasAccounts}}
+1
View File
@@ -0,0 +1 @@
{{loading-spinner}}
+7 -4
View File
@@ -35,9 +35,9 @@ module.exports = function(environment) {
]
},
contractMetadata: { networkId: '42' },
contractMetadata: { networkId: '42', networkName: 'kovan' },
web3ProviderUrl: 'https://kovan.infura.io/keUVk6OMaAvpmRF3m57n',
web3ProviderUrl: 'https://parity.kosmos.org:8545',
ipfs: {
host: 'ipfs.kosmos.org',
@@ -52,8 +52,8 @@ module.exports = function(environment) {
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
ENV.contractMetadata['networkId'] = '42';
ENV.web3ProviderUrl = 'https://kovan.infura.io/keUVk6OMaAvpmRF3m57n';
ENV.contractMetadata['networkId'] = '100';
ENV.web3ProviderUrl = 'http://localhost:7545';
ENV.ipfs = {
host: 'localhost',
@@ -81,6 +81,9 @@ module.exports = function(environment) {
if (process.env.NETWORK_ID) {
ENV.contractMetadata['networkId'] = process.env.NETWORK_ID;
}
if (process.env.NETWORK_NAME) {
ENV.contractMetadata['networkName'] = process.env.NETWORK_NAME;
}
if (process.env.WEB3_PROVIDER_URL) {
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
}
+5315 -4733
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -13,7 +13,7 @@
"scripts": {
"lint:js": "eslint ./*.js app config lib server tests",
"start": "ember serve",
"start:local": "NETWORK_ID=100 WEB3_PROVIDER_URL=http://localhost:7545 ember s",
"kovan": "NETWORK_ID=42 ember serve",
"test": "ember test",
"build": "ember build",
"build-prod": "ember build --environment production",
@@ -27,7 +27,7 @@
"ember-ajax": "^3.0.0",
"ember-awesome-macros": "0.41.0",
"ember-browserify": "^1.1.13",
"ember-cli": "^3.1.4",
"ember-cli": "~3.1.2",
"ember-cli-app-version": "^3.0.0",
"ember-cli-babel": "^6.6.0",
"ember-cli-dependency-checker": "^2.0.0",
@@ -39,7 +39,7 @@
"ember-cli-sass": "^7.2.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^2.0.0",
"ember-cli-update": "^0.21.4",
"ember-cli-update": "^0.21.2",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.0.0",
"ember-macro-helpers": "0.17.0",
@@ -51,7 +51,7 @@
"eslint-plugin-ember": "^5.0.0",
"ethers": "3.0.15",
"kosmos-schemas": "^1.1.2",
"kredits-contracts": "^3.0.2",
"kredits-contracts": "github:67P/truffle-kredits",
"loader.js": "^4.2.3",
"tv4": "^1.3.0"
},
+11
View File
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | spinner', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:spinner');
assert.ok(route);
});
});