Merge pull request #42 from 67P/kredits-module
Refactor contract interaction in its own module
This commit was merged in pull request #42.
This commit is contained in:
@@ -53,7 +53,7 @@ export default Component.extend({
|
|||||||
saved.then(() => {
|
saved.then(() => {
|
||||||
this.reset();
|
this.reset();
|
||||||
window.scroll(0,0);
|
window.scroll(0,0);
|
||||||
window.alert('Contributor added.');
|
window.alert('Proposal added.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ export default Controller.extend({
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.get('kredits.kreditsContract')
|
let contract = this.get('kredits.kredits').Operator.contract;
|
||||||
.then((contract) => {
|
contract.onproposalvoted = this._handleProposalVoted.bind(this);
|
||||||
contract.onproposalvoted = this._handleProposalVoted.bind(this);
|
contract.onproposalcreated = this._handleProposalCreated.bind(this);
|
||||||
contract.onproposalcreated = this._handleProposalCreated.bind(this);
|
contract.onproposalexecuted = this._handleProposalExecuted.bind(this);
|
||||||
contract.onproposalexecuted = this._handleProposalExecuted.bind(this);
|
// TODO: transfer on the token contract
|
||||||
// TODO: transfer on the token contract
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
contributors: alias('model.contributors'),
|
contributors: alias('model.contributors'),
|
||||||
@@ -50,7 +48,7 @@ export default Controller.extend({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
proposal = this.get('kredits').getProposalById(proposalId);
|
proposal = this.get('kredits.kredits').Operator.getById(proposalId);
|
||||||
this.get('proposals').pushObject(proposal);
|
this.get('proposals').pushObject(proposal);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ export default Controller.extend({
|
|||||||
|
|
||||||
this.get('contributors')
|
this.get('contributors')
|
||||||
.findBy('id', recipientId.toString())
|
.findBy('id', recipientId.toString())
|
||||||
.incrementProperty('balance', amount.toNumber());
|
.incrementProperty('balance', amount);
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleProposalVoted(proposalId, voter, totalVotes) {
|
_handleProposalVoted(proposalId, voter, totalVotes) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default class Base {
|
||||||
|
constructor(contract) {
|
||||||
|
this.contract = contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
get functions() {
|
||||||
|
return this.contract.functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import ethers from 'npm:ethers';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
|
import Kredits from '../kredits';
|
||||||
|
import ContributorSerializer from '../serializers/contributor';
|
||||||
|
|
||||||
|
import Base from './base';
|
||||||
|
|
||||||
|
export default class Contributor extends Base {
|
||||||
|
all() {
|
||||||
|
return this.functions.contributorsCount()
|
||||||
|
.then((count) => {
|
||||||
|
count = count.toNumber();
|
||||||
|
let contributors = [];
|
||||||
|
|
||||||
|
for (let id = 1; id <= count; id++) {
|
||||||
|
contributors.push(this.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RSVP.all(contributors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id) {
|
||||||
|
id = ethers.utils.bigNumberify(id);
|
||||||
|
|
||||||
|
return this.functions.getContributorById(id)
|
||||||
|
.then((data) => {
|
||||||
|
// TODO: remove as soon as the contract provides the id
|
||||||
|
data.id = id;
|
||||||
|
// TODO: rename address to account
|
||||||
|
data.address = data.account;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
// Fetch IPFS data if available
|
||||||
|
.then((data) => {
|
||||||
|
return Kredits.ipfs.catAndMerge(data, ContributorSerializer.deserialize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add(contributorAttr) {
|
||||||
|
let json = ContributorSerializer.serialize(contributorAttr);
|
||||||
|
// TODO: validate against schema
|
||||||
|
|
||||||
|
return Kredits.ipfs
|
||||||
|
.add(json)
|
||||||
|
.then((ipfsHashAttr) => {
|
||||||
|
let contributor = [
|
||||||
|
contributorAttr.address,
|
||||||
|
ipfsHashAttr.ipfsHash,
|
||||||
|
ipfsHashAttr.hashFunction,
|
||||||
|
ipfsHashAttr.hashSize,
|
||||||
|
contributorAttr.isCore,
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('[kredits] addContributor', ...contributor);
|
||||||
|
return this.functions.addContributor(...contributor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Contributor from './contributor';
|
||||||
|
import Operator from './operator';
|
||||||
|
import Token from './token';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Contributors: Contributor,
|
||||||
|
Operator,
|
||||||
|
Token
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import ethers from 'npm:ethers';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
|
import Kredits from '../kredits';
|
||||||
|
import ContributionSerializer from '../serializers/contribution';
|
||||||
|
|
||||||
|
import Base from './base';
|
||||||
|
|
||||||
|
export default class Operator extends Base {
|
||||||
|
all() {
|
||||||
|
return this.functions.proposalsCount()
|
||||||
|
.then((count) => {
|
||||||
|
count = count.toNumber();
|
||||||
|
let proposals = [];
|
||||||
|
|
||||||
|
for (let id = 0; id < count; id++) {
|
||||||
|
proposals.push(this.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RSVP.all(proposals);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id) {
|
||||||
|
id = ethers.utils.bigNumberify(id);
|
||||||
|
|
||||||
|
return this.functions.proposals(id)
|
||||||
|
.then((data) => {
|
||||||
|
// TODO: remove as soon as the contract provides the id
|
||||||
|
data.id = id;
|
||||||
|
// TODO: rename creatorAddress to creator
|
||||||
|
data.creatorAddress = data.creator;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
// Fetch IPFS data if available
|
||||||
|
.then((data) => {
|
||||||
|
return Kredits.ipfs.catAndMerge(data, ContributionSerializer.deserialize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addProposal(proposalAttr) {
|
||||||
|
let json = ContributionSerializer.serialize(proposalAttr);
|
||||||
|
// TODO: validate against schema
|
||||||
|
|
||||||
|
return Kredits.ipfs
|
||||||
|
.add(json)
|
||||||
|
.then((ipfsHashAttr) => {
|
||||||
|
let proposal = [
|
||||||
|
proposalAttr.recipientId,
|
||||||
|
proposalAttr.amount,
|
||||||
|
ipfsHashAttr.ipfsHash,
|
||||||
|
ipfsHashAttr.hashFunction,
|
||||||
|
ipfsHashAttr.hashSize,
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('[kredits] addProposal', ...proposal);
|
||||||
|
return this.functions.addProposal(...proposal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Base from './base';
|
||||||
|
|
||||||
|
export default class Token extends Base {
|
||||||
|
}
|
||||||
@@ -1,10 +1 @@
|
|||||||
import ContributionSerializer from './serializers/contribution';
|
export { default } from './kredits';
|
||||||
import ContributorSerializer from './serializers/contributor';
|
|
||||||
import { fromBytes32, toBytes32 } from './utils/multihash';
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContributionSerializer,
|
|
||||||
ContributorSerializer,
|
|
||||||
fromBytes32,
|
|
||||||
toBytes32
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import ethers from 'npm:ethers';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
|
import abis from 'contracts/abis';
|
||||||
|
import addresses from 'contracts/addresses';
|
||||||
|
|
||||||
|
import contracts from './contracts';
|
||||||
|
import IPFS from './utils/ipfs';
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function capitalize(word) {
|
||||||
|
let [first, ...rest] = word;
|
||||||
|
return `${first.toUpperCase()}${rest.join('')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Kredits {
|
||||||
|
constructor(provider, signer, addresses) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.signer = signer;
|
||||||
|
|
||||||
|
// Initialize our registry contract
|
||||||
|
this.addresses = addresses;
|
||||||
|
this.contracts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static setup(provider, signer, ipfsConfig) {
|
||||||
|
this.ipfsConfig = ipfsConfig;
|
||||||
|
this.ipfs = new IPFS(ipfsConfig);
|
||||||
|
|
||||||
|
let registryContract = this.initRegistryContract(provider);
|
||||||
|
|
||||||
|
let addresses = Object.keys(contracts).reduce((mem, name) => {
|
||||||
|
let contractName = capitalize(name);
|
||||||
|
mem[contractName] = registryContract.functions.getProxyFor(contractName);
|
||||||
|
return mem;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return RSVP.hash(addresses)
|
||||||
|
.then((addresses) => {
|
||||||
|
return new Kredits(provider, signer, addresses);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static initRegistryContract(provider) {
|
||||||
|
let address = addresses['Registry'][provider.chainId];
|
||||||
|
let abi = abis['Registry'];
|
||||||
|
console.log('Initialize registry contract:', address, abi, provider);
|
||||||
|
return new ethers.Contract(address, abi, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
get Contributor() {
|
||||||
|
// TODO: rename to contributor
|
||||||
|
return this.contractFor('contributors');
|
||||||
|
}
|
||||||
|
|
||||||
|
get Operator() {
|
||||||
|
return this.contractFor('operator');
|
||||||
|
}
|
||||||
|
|
||||||
|
get Token() {
|
||||||
|
return this.contractFor('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be private
|
||||||
|
contractFor(name) {
|
||||||
|
if (this.contracts[name]) {
|
||||||
|
return this.contracts[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
let contractName = capitalize(name);
|
||||||
|
let address = this.addresses[contractName];
|
||||||
|
let contract = new ethers.Contract(address, abis[contractName], this.signer);
|
||||||
|
this.contracts[name] = new contracts[contractName](contract);
|
||||||
|
|
||||||
|
return this.contracts[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ export default class Contributor {
|
|||||||
description,
|
description,
|
||||||
details,
|
details,
|
||||||
url,
|
url,
|
||||||
} = JSON.parse(serialized);
|
} = JSON.parse(serialized.toString('utf8'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind,
|
kind,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default class Contributor {
|
|||||||
kind,
|
kind,
|
||||||
url,
|
url,
|
||||||
accounts,
|
accounts,
|
||||||
} = JSON.parse(serialized);
|
} = JSON.parse(serialized.toString('utf8'));
|
||||||
|
|
||||||
let github_username, github_uid, wiki_username;
|
let github_username, github_uid, wiki_username;
|
||||||
let github = accounts.find((a) => a.site === 'github.com');
|
let github = accounts.find((a) => a.site === 'github.com');
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import ipfsAPI from 'npm:ipfs-api';
|
||||||
|
import multihashes from 'npm:multihashes';
|
||||||
|
|
||||||
|
export default class IPFS {
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this._ipfsAPI = ipfsAPI(config);
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
catAndMerge(data, deserialize) {
|
||||||
|
// if no hash details are found simply return the data; nothing to merge
|
||||||
|
if (!data.hashSize || data.hashSize === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return this.cat(data)
|
||||||
|
.then(deserialize)
|
||||||
|
.then((attributes) => {
|
||||||
|
return Object.assign({}, data, attributes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add(data) {
|
||||||
|
return this._ipfsAPI
|
||||||
|
.add(new this._ipfsAPI.Buffer(data))
|
||||||
|
.then((res) => {
|
||||||
|
return this.decodeHash(res[0].hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(hashData) {
|
||||||
|
let ipfsHash = hashData; // default - if it is a string
|
||||||
|
if (hashData.hasOwnProperty('hashSize')) {
|
||||||
|
ipfsHash = this.encodeHash(hashData);
|
||||||
|
}
|
||||||
|
return this._ipfsAPI.cat(ipfsHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeHash(ipfsHash) {
|
||||||
|
let multihash = multihashes.decode(multihashes.fromB58String(ipfsHash));
|
||||||
|
return {
|
||||||
|
ipfsHash: '0x' + multihashes.toHexString(multihash.digest),
|
||||||
|
hashSize: multihash.length,
|
||||||
|
hashFunction: multihash.code,
|
||||||
|
sourceHash: ipfsHash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeHash(hashData) {
|
||||||
|
let digest = this._ipfsAPI.Buffer.from(hashData.ipfsHash.slice(2), 'hex');
|
||||||
|
return multihashes.encode(digest, hashData.hashFunction, hashData.hashSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import bs58 from 'npm:bs58';
|
|
||||||
import NpmBuffer from 'npm:buffer';
|
|
||||||
const Buffer = NpmBuffer.Buffer;
|
|
||||||
|
|
||||||
function fromBytes32({ digest, hashFunction, hashSize }) {
|
|
||||||
if (hashSize === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashBytes = Buffer.from(digest.slice(2), 'hex');
|
|
||||||
const multiHashBytes = new (hashBytes.constructor)(2 + hashBytes.length);
|
|
||||||
|
|
||||||
multiHashBytes[0] = hashFunction;
|
|
||||||
multiHashBytes[1] = hashSize;
|
|
||||||
multiHashBytes.set(hashBytes, 2);
|
|
||||||
|
|
||||||
return bs58.encode(multiHashBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toBytes32(string) {
|
|
||||||
const decoded = bs58.decode(string);
|
|
||||||
|
|
||||||
return {
|
|
||||||
digest: `0x${decoded.slice(2).toString('hex')}`,
|
|
||||||
hashFunction: decoded[0],
|
|
||||||
hashSize: decoded[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
fromBytes32,
|
|
||||||
toBytes32
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import computed from 'ember-computed';
|
import computed from 'ember-computed';
|
||||||
import EmberObject from 'ember-object';
|
import EmberObject from 'ember-object';
|
||||||
|
import bignumber from 'kredits-web/utils/cps/bignumber';
|
||||||
|
|
||||||
export default EmberObject.extend({
|
export default EmberObject.extend({
|
||||||
// Contract
|
// Contract
|
||||||
id: null,
|
id: bignumber('idRaw', 'toString'),
|
||||||
// TODO: Should we rename it to account like in the contract?
|
// TODO: Should we rename it to account like in the contract?
|
||||||
address: null,
|
address: null,
|
||||||
balance: 0,
|
balance: bignumber('balanceRaw', 'toNumber'),
|
||||||
isCore: false,
|
isCore: false,
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import EmberObject from 'ember-object';
|
import EmberObject from 'ember-object';
|
||||||
import { alias } from 'ember-computed';
|
import { alias } from 'ember-computed';
|
||||||
|
import bignumber from 'kredits-web/utils/cps/bignumber';
|
||||||
|
|
||||||
export default EmberObject.extend({
|
export default EmberObject.extend({
|
||||||
// Contract
|
// Contract
|
||||||
id: null,
|
id: bignumber('idRaw', 'toString'),
|
||||||
creatorAddress: null,
|
creatorAddress: null,
|
||||||
recipientId: null,
|
recipientId: bignumber('recipientIdRaw', 'toString'),
|
||||||
amount: null,
|
amount: bignumber('amountRaw', 'toNumber'),
|
||||||
votesCount: null,
|
votesCount: bignumber('votesCountRaw', 'toNumber'),
|
||||||
votesNeeded: null,
|
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
|
||||||
executed: null,
|
executed: null,
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default Route.extend({
|
|||||||
beforeModel(transition) {
|
beforeModel(transition) {
|
||||||
const kredits = this.get('kredits');
|
const kredits = this.get('kredits');
|
||||||
|
|
||||||
return kredits.initEthProvider().then(() => {
|
return kredits.setup().then(() => {
|
||||||
if (kredits.get('accountNeedsUnlock')) {
|
if (kredits.get('accountNeedsUnlock')) {
|
||||||
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
|
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
|
||||||
transition.retry();
|
transition.retry();
|
||||||
|
|||||||
+1
-2
@@ -9,8 +9,7 @@ export default Ember.Route.extend({
|
|||||||
newContributor.set('kind', 'person');
|
newContributor.set('kind', 'person');
|
||||||
|
|
||||||
let kredits = this.get('kredits');
|
let kredits = this.get('kredits');
|
||||||
let totalSupply = kredits.get('tokenContract')
|
let totalSupply = kredits.get('kredits').Token.functions.totalSupply();
|
||||||
.then((contract) => contract.totalSupply());
|
|
||||||
|
|
||||||
return Ember.RSVP.hash({
|
return Ember.RSVP.hash({
|
||||||
contributors: kredits.getContributors(),
|
contributors: kredits.getContributors(),
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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 => {
|
|
||||||
Ember.Logger.debug('[ipfs] stored content in IPFS', content, res[0].hash);
|
|
||||||
return res[0].hash;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getFile(hash) {
|
|
||||||
return this.get('ipfs').cat(hash).then(res => {
|
|
||||||
return res.toString('utf8');
|
|
||||||
}, err => {
|
|
||||||
Ember.Logger.warn('[ipfs] error trying to fetch file', hash, err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
+60
-262
@@ -1,36 +1,21 @@
|
|||||||
import ethers from 'npm:ethers';
|
import ethers from 'npm:ethers';
|
||||||
|
import Kredits from 'kredits-web/lib/kredits';
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import Service from 'ember-service';
|
import Service from 'ember-service';
|
||||||
import injectService from 'ember-service/inject';
|
|
||||||
import computed, { alias } from 'ember-computed';
|
import computed, { alias } from 'ember-computed';
|
||||||
import { isEmpty, isPresent } from 'ember-utils';
|
import { isEmpty, isPresent } from 'ember-utils';
|
||||||
|
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
import abis from 'contracts/abis';
|
|
||||||
import addresses from 'contracts/addresses';
|
|
||||||
import {
|
|
||||||
ContributionSerializer,
|
|
||||||
ContributorSerializer,
|
|
||||||
fromBytes32,
|
|
||||||
toBytes32
|
|
||||||
} from 'kredits-web/lib/kredits';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getOwner,
|
getOwner,
|
||||||
Logger: {
|
Logger: {
|
||||||
debug,
|
debug
|
||||||
error
|
|
||||||
}
|
}
|
||||||
} = Ember;
|
} = Ember;
|
||||||
|
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
|
|
||||||
ipfs: injectService(),
|
|
||||||
|
|
||||||
ethProvider: null,
|
ethProvider: null,
|
||||||
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
@@ -48,7 +33,7 @@ export default Service.extend({
|
|||||||
// this is called called in the routes beforeModel(). So it is initialized before everything else
|
// this is called called in the routes beforeModel(). So it is initialized before everything else
|
||||||
// and we can rely on the ethProvider and the potential currentUserAccounts to be available
|
// and we can rely on the ethProvider and the potential currentUserAccounts to be available
|
||||||
initEthProvider: function() {
|
initEthProvider: function() {
|
||||||
return new Ember.RSVP.Promise((resolve) => {
|
return new RSVP.Promise((resolve) => {
|
||||||
let ethProvider;
|
let ethProvider;
|
||||||
let networkId;
|
let networkId;
|
||||||
if (typeof window.web3 !== 'undefined') {
|
if (typeof window.web3 !== 'undefined') {
|
||||||
@@ -58,14 +43,7 @@ export default Service.extend({
|
|||||||
ethProvider.listAccounts().then((accounts) => {
|
ethProvider.listAccounts().then((accounts) => {
|
||||||
this.set('currentUserAccounts', accounts);
|
this.set('currentUserAccounts', accounts);
|
||||||
this.set('ethProvider', ethProvider);
|
this.set('ethProvider', ethProvider);
|
||||||
if (accounts.length > 0) {
|
resolve(ethProvider);
|
||||||
this.get('getCurrentUser').then((contributorData) => {
|
|
||||||
this.set('currentUser', contributorData);
|
|
||||||
resolve(ethProvider);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(ethProvider);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
debug('[kredits] Creating new instance from npm module class');
|
debug('[kredits] Creating new instance from npm module class');
|
||||||
@@ -79,36 +57,24 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
registryContract: computed('ethProvider', function() {
|
setup() {
|
||||||
let networkId = this.get('ethProvider').chainId;
|
return this.initEthProvider().then((ethProvider) => {
|
||||||
let registry = new ethers.Contract(
|
let signer = ethProvider.getSigner();
|
||||||
addresses['Registry'][networkId],
|
return Kredits.setup(ethProvider, signer, config.ipfs).then((kredits) => {
|
||||||
abis['Registry'],
|
this.set('kredits', kredits);
|
||||||
this.get('ethProvider')
|
|
||||||
);
|
|
||||||
return registry;
|
|
||||||
}),
|
|
||||||
|
|
||||||
contributorsContract: computed('ethProvider', function() {
|
// TODO: Cleanup
|
||||||
return this.contractFor('Contributors');
|
if (this.get('currentUserAccounts').length > 0) {
|
||||||
}),
|
this.get('getCurrentUser').then((contributorData) => {
|
||||||
|
this.set('currentUser', contributorData);
|
||||||
kreditsContract: computed('ethProvider', function() {
|
});
|
||||||
return this.contractFor('Operator');
|
}
|
||||||
}),
|
return kredits;
|
||||||
|
});
|
||||||
tokenContract: computed('ethProvider', function() {
|
});
|
||||||
return this.contractFor('Token');
|
|
||||||
}),
|
|
||||||
|
|
||||||
contractFor(name) {
|
|
||||||
return this.get('registryContract').functions.getProxyFor(name)
|
|
||||||
.then((address) => {
|
|
||||||
debug('[kredits] get contract', name, address);
|
|
||||||
return new ethers.Contract(address, abis[name], this.get('ethProvider').getSigner());
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: Only assign valid attributes
|
||||||
buildModel(name, attributes) {
|
buildModel(name, attributes) {
|
||||||
debug('[kredits] build', name, attributes);
|
debug('[kredits] build', name, attributes);
|
||||||
let model = getOwner(this).lookup(`model:${name}`);
|
let model = getOwner(this).lookup(`model:${name}`);
|
||||||
@@ -122,237 +88,69 @@ export default Service.extend({
|
|||||||
return model;
|
return model;
|
||||||
},
|
},
|
||||||
|
|
||||||
getContributorById(id) {
|
|
||||||
id = ethers.utils.bigNumberify(id);
|
|
||||||
|
|
||||||
return this.get('contributorsContract')
|
|
||||||
.then((contract) => contract.getContributorById(id))
|
|
||||||
.then(this.reassembleIpfsHash)
|
|
||||||
// Set basic data
|
|
||||||
.then(({ account: address, balance, ipfsHash, isCore }) => {
|
|
||||||
let isCurrentUser = this.get('currentUserAccounts').includes(address);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
address,
|
|
||||||
balance: balance.toNumber(),
|
|
||||||
ipfsHash,
|
|
||||||
isCore,
|
|
||||||
isCurrentUser,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// Fetch IPFS data if available
|
|
||||||
.then((data) => {
|
|
||||||
return this.fetchAndMergeIpfsData(data, ContributorSerializer);
|
|
||||||
})
|
|
||||||
.then((attributes) => {
|
|
||||||
return this.buildModel('contributor', attributes);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getContributors() {
|
|
||||||
return this.get('contributorsContract')
|
|
||||||
.then((contract) => contract.contributorsCount())
|
|
||||||
.then((count) => {
|
|
||||||
count = count.toNumber();
|
|
||||||
debug('[kredits] contributors count:', count);
|
|
||||||
let contributors = [];
|
|
||||||
|
|
||||||
for(var id = 1; id <= count; id++) {
|
|
||||||
contributors.push(this.getContributorById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return RSVP.all(contributors);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
reassembleIpfsHash(data) {
|
|
||||||
let { ipfsHash: digest, hashFunction, hashSize } = data;
|
|
||||||
data.ipfsHash = fromBytes32({ digest, hashFunction, hashSize });
|
|
||||||
delete data.hashFunction;
|
|
||||||
delete data.hashSize;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the contributor's profile data from IPFS and returns the attributes
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
fetchAndMergeIpfsData(data, Serializer) {
|
|
||||||
let ipfsHash = data.ipfsHash;
|
|
||||||
|
|
||||||
if (!ipfsHash) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.get('ipfs')
|
|
||||||
.getFile(ipfsHash)
|
|
||||||
.then(Serializer.deserialize)
|
|
||||||
.then((attributes) => {
|
|
||||||
debug('[kredits] fetched ipfs data:', attributes);
|
|
||||||
return Object.assign({}, data, attributes);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
error('[kredits] error trying to fetch', ipfsHash, err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getProposalById(id) {
|
|
||||||
id = ethers.utils.bigNumberify(id);
|
|
||||||
|
|
||||||
return this.get('kreditsContract')
|
|
||||||
.then((contract) => contract.proposals(id))
|
|
||||||
.then(this.reassembleIpfsHash)
|
|
||||||
// Set basic data
|
|
||||||
.then(({
|
|
||||||
creator: creatorAddress,
|
|
||||||
recipientId,
|
|
||||||
votesCount,
|
|
||||||
votesNeeded,
|
|
||||||
amount,
|
|
||||||
executed,
|
|
||||||
ipfsHash,
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
creatorAddress,
|
|
||||||
recipientId: recipientId.toString(),
|
|
||||||
votesCount: votesCount.toNumber(),
|
|
||||||
votesNeeded: votesNeeded.toNumber(),
|
|
||||||
amount: amount.toNumber(),
|
|
||||||
executed,
|
|
||||||
ipfsHash
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// Fetch IPFS data if available
|
|
||||||
.then((data) => {
|
|
||||||
return this.fetchAndMergeIpfsData(data, ContributionSerializer);
|
|
||||||
})
|
|
||||||
.then((attributes) => {
|
|
||||||
return this.buildModel('proposal', attributes);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getProposals() {
|
|
||||||
return this.get('kreditsContract')
|
|
||||||
.then((contract) => contract.proposalsCount())
|
|
||||||
.then((count) => {
|
|
||||||
count = count.toNumber();
|
|
||||||
debug('[kredits] proposals count:', count);
|
|
||||||
let proposals = [];
|
|
||||||
|
|
||||||
for(var i = 0; i < count; i++) {
|
|
||||||
proposals.push(this.getProposalById(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return RSVP.all(proposals);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
vote(proposalId) {
|
|
||||||
debug('[kredits] vote for', proposalId);
|
|
||||||
|
|
||||||
return this.get('kreditsContract')
|
|
||||||
.then((contract) => contract.vote(proposalId))
|
|
||||||
.then((data) => {
|
|
||||||
debug('[kredits] vote response', data);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: extract common logic to module
|
|
||||||
addContributor(attributes) {
|
addContributor(attributes) {
|
||||||
debug('[kredits] add contributor', attributes);
|
debug('[kredits] add contributor', attributes);
|
||||||
|
|
||||||
let json = ContributorSerializer.serialize(attributes);
|
return this.get('kredits').Contributor.add(attributes)
|
||||||
// TODO: validate against schema
|
|
||||||
|
|
||||||
return this.get('ipfs')
|
|
||||||
.storeFile(json)
|
|
||||||
// Set ipfsHash
|
|
||||||
.then((ipfsHash) => {
|
|
||||||
attributes.ipfsHash = ipfsHash;
|
|
||||||
return attributes;
|
|
||||||
})
|
|
||||||
.then((attributes) => {
|
|
||||||
return this.get('contributorsContract')
|
|
||||||
.then((contract) => {
|
|
||||||
let { address, isCore, ipfsHash } = attributes;
|
|
||||||
let { digest, hashFunction, hashSize } = toBytes32(ipfsHash);
|
|
||||||
|
|
||||||
let contributor = [
|
|
||||||
address,
|
|
||||||
digest,
|
|
||||||
hashFunction,
|
|
||||||
hashSize,
|
|
||||||
isCore,
|
|
||||||
];
|
|
||||||
debug('[kredits] addContributor', ...contributor);
|
|
||||||
return contract.addContributor(...contributor);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
debug('[kredits] add contributor response', data);
|
debug('[kredits] add contributor response', data);
|
||||||
return this.buildModel('contributor', attributes);
|
return this.buildModel('contributor', attributes);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getContributors() {
|
||||||
|
return this.get('kredits').Contributor.all()
|
||||||
|
.then((contributors) => {
|
||||||
|
return contributors.map((contributor) => {
|
||||||
|
return this.buildModel('contributor', contributor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
addProposal(attributes) {
|
addProposal(attributes) {
|
||||||
debug('[kredits] add proposal', attributes);
|
debug('[kredits] add proposal', attributes);
|
||||||
|
|
||||||
let json = ContributionSerializer.serialize(attributes);
|
return this.get('kredits').Operator.addProposal(attributes)
|
||||||
// TODO: validate against schema
|
|
||||||
|
|
||||||
return this.get('ipfs')
|
|
||||||
.storeFile(json)
|
|
||||||
// Set ipfsHash
|
|
||||||
.then((ipfsHash) => {
|
|
||||||
delete attributes.contributorIpfsHash;
|
|
||||||
attributes.ipfsHash = ipfsHash;
|
|
||||||
return attributes;
|
|
||||||
})
|
|
||||||
.then((attributes) => {
|
|
||||||
return this.get('kreditsContract')
|
|
||||||
.then((contract) => {
|
|
||||||
let { recipientId, amount, ipfsHash } = attributes;
|
|
||||||
let { digest, hashFunction, hashSize } = toBytes32(ipfsHash);
|
|
||||||
|
|
||||||
let proposal = [
|
|
||||||
recipientId,
|
|
||||||
amount,
|
|
||||||
digest,
|
|
||||||
hashFunction,
|
|
||||||
hashSize,
|
|
||||||
];
|
|
||||||
debug('[kredits] addProposal', ...proposal);
|
|
||||||
return contract.addProposal(...proposal);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
debug('[kredits] add proposal response', data);
|
debug('[kredits] add proposal response', data);
|
||||||
return this.buildModel('proposal', attributes);
|
return this.buildModel('proposal', attributes);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getProposals() {
|
||||||
|
return this.get('kredits').Operator.all()
|
||||||
|
.then((proposals) => {
|
||||||
|
return proposals.map((proposal) => {
|
||||||
|
return this.buildModel('proposal', proposal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
vote(proposalId) {
|
||||||
|
debug('[kredits] vote for', proposalId);
|
||||||
|
|
||||||
|
return this.get('kredits').Operator.functions.vote(proposalId)
|
||||||
|
.then((data) => {
|
||||||
|
debug('[kredits] vote response', data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: Cleanup
|
||||||
getCurrentUser: computed('ethProvider', function() {
|
getCurrentUser: computed('ethProvider', function() {
|
||||||
if (isEmpty(this.get('currentUserAccounts'))) {
|
if (isEmpty(this.get('currentUserAccounts'))) {
|
||||||
return RSVP.resolve();
|
return RSVP.resolve();
|
||||||
}
|
}
|
||||||
return this.get('contributorsContract')
|
return this.get('kredits').Contributor
|
||||||
.then((contract) => {
|
.functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
|
||||||
return contract.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
|
.then((id) => {
|
||||||
.then((id) => {
|
id = id.toNumber();
|
||||||
id = id.toNumber();
|
// check if the user is a contributor or not
|
||||||
// check if the user is a contributor or not
|
if (id === 0) {
|
||||||
if (id === 0) {
|
return RSVP.resolve();
|
||||||
return RSVP.resolve();
|
} else {
|
||||||
} else {
|
return this.get('kredits').Contributor.getById(id);
|
||||||
return this.getContributorById(id);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{#if currentUser.isCore}}
|
{{#if kredits.currentUser.isCore}}
|
||||||
{{add-contributor contributors=model.contributors save=(action 'save')}}
|
{{add-contributor contributors=model.contributors save=(action 'save')}}
|
||||||
{{else}}
|
{{else}}
|
||||||
Only core team members can add new contributors. Please ask someone to set you up.
|
Only core team members can add new contributors. Please ask someone to set you up.
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import computed from 'ember-computed';
|
||||||
|
|
||||||
|
export default function(dependentKey, converterMethod) {
|
||||||
|
return computed(dependentKey, {
|
||||||
|
get () {
|
||||||
|
return this.get(dependentKey)[converterMethod]();
|
||||||
|
},
|
||||||
|
set (key, value) {
|
||||||
|
this.set(dependentKey, value);
|
||||||
|
return value[converterMethod]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ const {
|
|||||||
} = Ember;
|
} = Ember;
|
||||||
|
|
||||||
moduleFor('controller:index', 'Unit | Controller | index', {
|
moduleFor('controller:index', 'Unit | Controller | index', {
|
||||||
needs: ['service:ipfs', 'service:kredits']
|
needs: ['service:kredits']
|
||||||
});
|
});
|
||||||
|
|
||||||
let addFixtures = function(controller) {
|
let addFixtures = function(controller) {
|
||||||
@@ -23,6 +23,9 @@ let addFixtures = function(controller) {
|
|||||||
{ github_username: "trinity", github_uid: "123", balance: 5000 },
|
{ github_username: "trinity", github_uid: "123", balance: 5000 },
|
||||||
{ github_username: "mouse", github_uid: "696", balance: 0 }
|
{ github_username: "mouse", github_uid: "696", balance: 0 }
|
||||||
].forEach(fixture => {
|
].forEach(fixture => {
|
||||||
|
// we expect a bignumer but I don't want to add the bignumber dependency here... so this is some hack to return an object that looks good enough for the test
|
||||||
|
let fakeBignumber = function(balance) { return { toNumber: function() { return balance; } }; };
|
||||||
|
fixture.balance = fakeBignumber(fixture.balance);
|
||||||
controller.get('model.contributors').push(Contributor.create(fixture));
|
controller.get('model.contributors').push(Contributor.create(fixture));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { module, test } from 'ember-qunit';
|
import { module, test } from 'ember-qunit';
|
||||||
import schemas from 'npm:kosmos-schemas';
|
import schemas from 'npm:kosmos-schemas';
|
||||||
import tv4 from 'npm:tv4';
|
import tv4 from 'npm:tv4';
|
||||||
import { ContributionSerializer } from 'kredits-web/lib/kredits';
|
import ContributionSerializer from 'kredits-web/lib/kredits/serializers/contribution';
|
||||||
|
|
||||||
module('Serializers contribution');
|
module('Serializers contribution');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { module, test } from 'ember-qunit';
|
import { module, test } from 'ember-qunit';
|
||||||
import schemas from 'npm:kosmos-schemas';
|
import schemas from 'npm:kosmos-schemas';
|
||||||
import tv4 from 'npm:tv4';
|
import tv4 from 'npm:tv4';
|
||||||
import { ContributorSerializer } from 'kredits-web/lib/kredits';
|
import ContributorSerializer from 'kredits-web/lib/kredits/serializers/contributor';
|
||||||
|
|
||||||
module('Serializers contributor');
|
module('Serializers contributor');
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user