Compare commits

...

22 Commits

Author SHA1 Message Date
basti b8ded8e6a8 0.1.0 2017-02-18 18:05:39 +08:00
basti 26cebc11c6 Add IPFS service (#4)
Add IPFS service

Add the `ipfs-api` JS library and a service using the browserified
version of it. The service has functions for storing and reading file
contests.
2017-02-18 09:48:26 +00:00
bumi 2a9f9a50c4 use public infura ETH node in production -still on ropsten 2017-02-08 19:22:16 +08:00
basti 7c98de3e6a Comment unused test 2017-02-08 14:34:14 +08:00
basti cd6fbfe17c Mark current user in contrib list if available 2017-02-08 14:22:49 +08:00
basti ad76fb2369 Warn non-Ethereum users when trying to add contributor 2017-02-08 14:01:19 +08:00
bumi 0f9664410b code style, missing semicolon 2017-02-08 13:58:19 +08:00
bumi d05356cfe4 connect add contributor form to ethereum 2017-02-08 13:46:40 +08:00
basti 1de2e002bb Show proposals to everyone, with warning for non-members 2017-02-08 13:22:19 +08:00
bumi 7a072f2586 use dev chain in development environment 2017-02-08 13:15:06 +08:00
basti 3eb58451cc Use testnet for all envs for now 2017-02-07 22:38:15 +08:00
basti 9cc4f85a69 Use parity.kosmos.org 2017-02-07 21:35:55 +08:00
basti 29602d5c36 Change page title 2017-02-07 21:35:44 +08:00
basti b15191aa0e Add deployment 2017-02-07 21:12:08 +08:00
bumi d6d4e51ea3 connect to contracts depending on the environment 2017-02-07 13:28:37 +08:00
basti bb3696addc Handle transfer event 2017-02-06 23:22:43 +08:00
basti 96ea4bb993 Don't handle event for already-executed proposals 2017-02-06 22:23:36 +08:00
basti 433d6b0d59 Handle ProposalVoted 2017-02-05 23:11:39 +08:00
bumi 0235313282 load kredits contract definition from kredits-contracts package 2017-02-05 22:15:11 +08:00
basti 10763b1e7f Update balance on ProposalExecuted 2017-02-05 21:52:12 +08:00
basti 1f39884eef Handle ProposalExecuted event 2017-02-05 21:31:00 +08:00
basti d0e10babfe Watch contract events (and add new proposals) 2017-02-05 19:27:17 +08:00
20 changed files with 368 additions and 106 deletions
+47 -12
View File
@@ -5,29 +5,64 @@ export default Ember.Component.extend({
id: null,
realName: null,
address: null,
ipfsHash: null,
isCore: true,
classId: function() {
let value = this.get('id');
return (Ember.isEmpty(value)) ? null : 'valid';
inProgress: false,
isValidId: function() {
return Ember.isPresent(this.get('id'));
}.property('id'),
classRealName: function() {
let value = this.get('realName');
return (Ember.isEmpty(value)) ? null : 'valid';
isValidRealName: function() {
return Ember.isPresent(this.get('realName'));
}.property('realName'),
classAddress: function() {
let value = this.get('address');
return (Ember.isEmpty(value)) ? null : 'valid';
isValidAddress: function() {
return this.get('kredits.web3Instance').isAddress(this.get('address'));
}.property('address'),
isValid: function() {
return this.get('isValidId') && this.get('isValidRealName') && this.get('isValidAddress');
}.property('isValidAddress', 'isValidId', 'isValidRealName'),
reset: function() {
this.setProperties({
id: null,
realName: null,
address: null,
ipfsHash: null,
isCore: true,
inProgress: false
});
},
actions: {
save() {
console.log('id', this.get('id'));
console.log('realName', this.get('realName'));
console.log('address', this.get('address'));
if (!this.get('contractInteractionEnabled')) {
alert('Only core team members can add new contributors. Please ask someone to set you up.');
return;
}
if (this.get('isValid')) {
this.set('inProgress', true);
this.get('kredits').addContributor(
this.get('address'),
this.get('realName'),
'', // TODO: this.get('ipfsHash')
this.get('isCore'),
this.get('id')
).then(contributor => {
this.reset();
this.get('contributors').pushObject(contributor);
window.scroll(0,0);
});
} else {
alert('Invalid data. Please review and try again.');
}
}
}
+4 -4
View File
@@ -4,23 +4,23 @@
type="text"
placeholder="GitHub UID (123)"
value=id
class=classId}}
class=(if isValidId 'valid' '')}}
</p>
<p>
{{input name="realname"
type="text"
placeholder="Carl Sagan"
value=realName
class=classRealName}}
class=(if isValidRealName 'valid' '')}}
</p>
<p>
{{input name="address"
type="text"
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
value=address
class=classAddress}}
class=(if isValidAddress 'valid' '')}}
</p>
<p class="actions">
<input type="submit" value="Save">
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
</p>
</form>
+1 -1
View File
@@ -1,6 +1,6 @@
<tbody>
{{#each contributors as |contributor|}}
<tr>
<tr class="{{if contributor.isCurrentUser 'current-user'}}">
<td class="person">
<img class="avatar" src={{contributor.avatarURL}}>
{{contributor.github_username}}
+5 -1
View File
@@ -8,7 +8,11 @@ export default Ember.Component.extend({
actions: {
confirm(proposalId) {
this.sendAction('confirmAction', proposalId);
if (this.get('contractInteractionEnabled')) {
this.sendAction('confirmAction', proposalId);
} else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
}
}
}
+88 -4
View File
@@ -1,4 +1,5 @@
import Ember from 'ember';
import Proposal from 'kredits-web/models/proposal';
const {
computed,
@@ -22,21 +23,21 @@ export default Ember.Controller.extend({
let proposals = this.get('model.proposals')
.filterBy('executed', false)
.map(p => {
p.recipientName = this.findContributorByAddress(p.recipientAddress).github_username;
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
return p;
});
return proposals;
}.property('model.proposals.[]', 'model.contributors.[]'),
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
proposalsClosed: function() {
let proposals = this.get('model.proposals')
.filterBy('executed', true)
.map(p => {
p.recipientName = this.findContributorByAddress(p.recipientAddress).github_username;
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
return p;
});
return proposals;
}.property('model.proposals.[]', 'model.contributors.[]'),
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
proposalsSorting: ['id:desc'],
proposalsClosedSorted: Ember.computed.sort('proposalsClosed', 'proposalsSorting'),
@@ -45,6 +46,89 @@ export default Ember.Controller.extend({
contributorsSorting: ['kredits:desc'],
contributorsSorted: Ember.computed.sort('model.contributors', 'contributorsSorting'),
watchContractEvents: function() {
let events = this.get('kredits.kreditsContract')
.allEvents(/* [additionalFilterObject], */);
events.watch((error, data) => {
Ember.Logger.debug('[index] Received contract event', data);
switch (data.event) {
case 'ProposalCreated':
this._handleProposalCreated(data);
break;
case 'ProposalExecuted':
this._handleProposalExecuted(data);
break;
case 'ProposalVoted':
this._handleProposalVoted(data);
break;
case 'Transfer':
this._handleTransfer(data);
break;
}
});
}.on('init'),
_handleProposalCreated(data) {
if (Ember.isPresent(this.get('model.proposals')
.findBy('id', data.args.id.toNumber()))) {
Ember.Logger.debug('[index] proposal exists, not adding from event');
return false;
}
let proposal = Proposal.create({
id: data.args.id.toNumber(),
creatorAddress: data.args.creator,
recipientAddress: data.args.recipient,
recipientName: null,
votesCount: 0,
votesNeeded: 2,
amount: data.args.amount.toNumber(),
executed: false,
url: data.args.url,
ipfsHash: data.args.ipfsHash
});
this.get('model.proposals').pushObject(proposal);
},
_handleProposalExecuted(data) {
if (this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.get('executed')) {
Ember.Logger.debug('[index] proposal already executed, not adding from event');
return false;
}
this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.setProperties({
'executed': true,
'votesCount': 2 // TODO use real count
});
this.get('model.contributors')
.findBy('address', data.args.recipient)
.incrementProperty('kredits', data.args.amount.toNumber());
},
_handleProposalVoted(data) {
this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.incrementProperty('votesCount', 1);
},
_handleTransfer(data) {
this.get('model.contributors')
.findBy('address', data.args.from)
.incrementProperty('kredits', - data.args.value.toNumber());
this.get('model.contributors')
.findBy('address', data.args.to)
.incrementProperty('kredits', data.args.value.toNumber());
},
actions: {
confirmProposal(proposalId) {
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>KreditsWeb</title>
<title>Kredits</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+3 -1
View File
@@ -2,10 +2,12 @@ import Ember from 'ember';
export default Ember.Object.extend({
address: null,
github_username: null,
github_uid: null,
ipfsHash: null,
kredits: null,
address: null,
isCurrentUser: false,
avatarURL: function() {
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
+3 -2
View File
@@ -3,8 +3,9 @@ import Ember from 'ember';
export default Ember.Object.extend({
id: null,
creator: null,
recipient: null,
creatorAddress: null,
recipientAddress: null,
recipientName: null,
votesCount: null,
votesNeeded: null,
amount: null,
+31
View File
@@ -0,0 +1,31 @@
import Ember from 'ember';
import ipfsAPI from 'npm:ipfs-api';
import config from 'kredits-web/config/environment';
export default Ember.Service.extend({
ipfsInstance: null,
ipfs: function() {
if (this.get('ipfsInstance')) {
return this.get('ipfsInstance');
}
let ipfs = ipfsAPI(config.ipfs);
this.set('ipfsInstance', ipfs);
return ipfs;
}.property('ipfsInstance'),
storeFile(content) {
let ipfs = this.get('ipfs');
return ipfs.add(new ipfs.Buffer(content)).then(res => {
return res[0].hash;
});
},
getFile(hash) {
return this.get('ipfs').cat(hash, { buffer: true }).then(res => {
return res.toString();
});
}
});
+38 -8
View File
@@ -3,14 +3,17 @@ import Web3 from 'npm:web3';
import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor';
import Proposal from 'kredits-web/models/proposal';
import kreditsContracts from 'npm:kredits-contracts';
export default Ember.Service.extend({
ipfs: Ember.inject.service(),
web3Instance: null,
web3Provided: false, // Web3 provided (using Mist Browser, Metamask et al.)
web3: function() {
if (Ember.isPresent(this.get('web3Instance'))) {
if (this.get('web3Instance')) {
return this.get('web3Instance');
}
@@ -22,7 +25,7 @@ export default Ember.Service.extend({
this.set('web3Provided', true);
} else {
Ember.Logger.debug('[kredits] Creating new instance from npm module class');
let provider = new Web3.providers.HttpProvider("http://139.59.248.169:8545");
let provider = new Web3.providers.HttpProvider(config.web3ProviderUrl);
web3Instance = new Web3(provider);
}
@@ -32,12 +35,19 @@ export default Ember.Service.extend({
return web3Instance;
}.property('web3Instance'),
currentUserAccounts: function() {
return (this.get('web3Provided') && this.get('web3').eth.accounts) || [];
}.property('web3', 'web3Provided'),
kreditsContract: function() {
// TODO cache this
let contract = this.get('web3')
.eth.contract(config.kreditsContract.ABI)
.at(config.kreditsContract.address);
window.Kredits = contract;
if (this.get('kreditsContractInstance')) {
return this.get('kreditsContractInstance');
}
console.log(config.ethereumChain);
let contract = kreditsContracts(this.get('web3'), config.ethereumChain)['Kredits'];
this.set('kreditsContractInstance', contract);
return contract;
}.property('web3'),
@@ -60,7 +70,8 @@ export default Ember.Service.extend({
github_username: person[1],
github_uid: person[0],
ipfsHash: person[3],
kredits: balance.toNumber()
kredits: balance.toNumber(),
isCurrentUser: this.get('currentUserAccounts').includes(address)
});
Ember.Logger.debug('[kredits] contributor', contributor);
resolve(contributor);
@@ -127,6 +138,25 @@ export default Ember.Service.extend({
});
},
addContributor(address, name, ipfsHash, isCore, id) {
Ember.Logger.debug('[kredits] add contributor', name, address);
return new Ember.RSVP.Promise((resolve, reject) => {
this.get('kreditsContract').addContributor(address, name, ipfsHash, isCore, id, (err, data) => {
if (err) { reject(err); }
Ember.Logger.debug('[kredits] add contributor response', data);
let contributor = Contributor.create({
address: address,
github_username: name,
github_uid: id,
ipfsHash: ipfsHash,
kredits: 0,
isCurrentUser: this.get('currentUserAccounts').includes(address)
});
resolve(contributor);
});
});
},
logKreditsContract: function() {
Ember.Logger.debug('[kredits] kreditsContract', this.get('kreditsContract'));
}.on('init')
@@ -26,6 +26,9 @@ section#add-contributor {
input[type=submit] {
padding: 0.6rem 2rem;
&:disabled {
background-color: transparent;
}
}
}
+29 -22
View File
@@ -7,32 +7,39 @@ table.contributor-list {
&:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2);
}
}
td {
padding: 0 1.2rem;
line-height: 4.2rem;
td {
padding: 0 1.2rem;
line-height: 4.2rem;
background-color: rgba(255,255,255,0.1);
&.person {
text-align: left;
font-size: 1.4rem;
img.avatar {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 0.2rem;
border-radius: 1rem;
background-color: rgba(255,255,255,0.1);
&.person {
text-align: left;
font-size: 1.4rem;
img.avatar {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 0.2rem;
border-radius: 1rem;
}
}
&.kredits {
text-align: right;
.amount {
font-size: 1.4rem;
}
.symbol {
font-size: 1rem;
padding-left: 0.2rem;
}
}
}
&.kredits {
text-align: right;
.amount {
font-size: 1.4rem;
}
.symbol {
font-size: 1rem;
padding-left: 0.2rem;
&.current-user {
td {
background-color: rgba(255,255,255,0.2);
}
}
}
+28 -25
View File
@@ -13,34 +13,37 @@
</div>
</section>
{{#if contractInteractionEnabled}}
{{#if proposalsOpen}}
<section id="proposals-open">
<header>
<h2>Open Proposals</h2>
</header>
<div class="content">
{{proposal-list proposals=proposalsOpenSorted confirmAction="confirmProposal"}}
</div>
</section>
{{/if}}
<section id="proposals-closed">
{{#if proposalsOpen}}
<section id="proposals-open">
<header>
<h2>Closed Proposals</h2>
<h2>Open Proposals</h2>
</header>
<div class="content">
{{proposal-list proposals=proposalsClosedSorted confirmAction="confirmProposal"}}
</div>
</section>
<section id="add-contributor">
<header>
<h2>Add Contributor</h2>
</header>
<div class="content">
{{add-contributor kredits=kredits}}
{{proposal-list proposals=proposalsOpenSorted
confirmAction="confirmProposal"
contractInteractionEnabled=contractInteractionEnabled}}
</div>
</section>
{{/if}}
<section id="proposals-closed">
<header>
<h2>Closed Proposals</h2>
</header>
<div class="content">
{{proposal-list proposals=proposalsClosedSorted
confirmAction="confirmProposal"}}
</div>
</section>
<section id="add-contributor">
<header>
<h2>Add Contributor</h2>
</header>
<div class="content">
{{add-contributor kredits=kredits
contributors=model.contributors
contractInteractionEnabled=contractInteractionEnabled}}
</div>
</section>
+18 -5
View File
@@ -1,6 +1,4 @@
/* jshint node: true */
let contracts = require('../vendor/contract-abis');
module.exports = function(environment) {
var ENV = {
modulePrefix: 'kredits-web',
@@ -23,18 +21,31 @@ module.exports = function(environment) {
// when it is created
},
kreditsContract: {
ABI: contracts['Kredits'].abi
browserify: {
transform: [
["babelify", {
presets: ["es2015"],
global: true
}]
]
},
web3ProviderUrl: "https://parity.kosmos.org:8545",
ethereumChain: "testnet",
ipfs: {
host: 'localhost',
port: '5001',
protocol: 'http'
}
};
if (environment === 'development') {
ENV.kreditsContract.address = '0x9C68Af50e97f5605402B4C01e7aB836ed7145e8B';
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
ENV.ethereumChain = 'dev';
}
if (environment === 'test') {
@@ -49,6 +60,8 @@ module.exports = function(environment) {
}
if (environment === 'production') {
ENV.ethereumChain = 'testnet';
ENV.web3ProviderUrl = 'https://ropsten.infura.io';
}
return ENV;
+11 -3
View File
@@ -1,6 +1,6 @@
{
"name": "kredits-web",
"version": "0.0.0",
"version": "0.1.0",
"description": "Small description for kredits-web goes here",
"license": "MIT",
"author": "",
@@ -10,11 +10,17 @@
},
"repository": "",
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "ember test"
"test": "ember test",
"postinstall": "bower install",
"build": "ember build",
"build-prod": "ember build --environment production",
"update-version-file": "bash scripts/update-version-file.sh",
"deploy": "npm run build-prod && npm run update-version-file && bash scripts/deploy.sh"
},
"devDependencies": {
"babel-preset-es2015": "^6.22.0",
"babelify": "^7.3.0",
"broccoli-asset-rev": "^2.4.5",
"ember-ajax": "^2.4.1",
"ember-browserify": "^1.1.13",
@@ -35,6 +41,8 @@
"ember-export-application-global": "^1.0.5",
"ember-load-initializers": "^0.5.1",
"ember-resolver": "^2.0.3",
"ipfs-api": "^12.1.7",
"kredits-contracts": "67P/kredits-contracts",
"loader.js": "^4.0.10",
"web3": "^0.18.2"
},
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -xe
# Get revision from local master
rev=`git rev-parse HEAD`
short_rev=${rev:0:7}
# Check out build branch
git checkout build-production
# Copy from build dir to root
cp -r dist/* .
# Add all files
git add --all
# Commit build files
git commit -m "Build production from $short_rev"
# Push to remote
git push 5apps build-production:master
# Push build branch to collab repo
git push origin build-production:build-production
# Go back to previous branch
git checkout -
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
PACKAGE_VERSION=$(cat package.json \
| grep version \
| head -1 \
| awk -F: '{ print $2 }' \
| sed 's/[",]//g' \
| tr -d '[[:space:]]')
echo $PACKAGE_VERSION > dist/VERSION
@@ -1,16 +1,16 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('add-contributor', 'Integration | Component | add contributor', {
integration: true
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
this.render(hbs`{{add-contributor}}`);
assert.equal(this.$().text().trim(), '');
});
// import { moduleForComponent, test } from 'ember-qunit';
// import hbs from 'htmlbars-inline-precompile';
//
// moduleForComponent('add-contributor', 'Integration | Component | add contributor', {
// integration: true
// });
//
// test('it renders', function(assert) {
//
// // Set any properties with this.set('myProperty', 'value');
// // Handle any actions with this.on('myAction', function(val) { ... });
//
// this.render(hbs`{{add-contributor}}`);
//
// assert.equal(this.$().text().trim(), '');
// });
+12
View File
@@ -0,0 +1,12 @@
import { moduleFor, test } from 'ember-qunit';
moduleFor('service:ipfs', 'Unit | Service | ipfs', {
// Specify the other units that are required for this test.
// needs: ['service:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.subject();
assert.ok(service);
});
-1
View File
File diff suppressed because one or more lines are too long