Files
kredits-web/app/services/kredits.js
T
bumi 0587559939 Use getContributorById call to get contributor data
This new call also returns the balance of the contributor so we don't
have to make an extra call to the token contract.
2018-04-07 20:33:41 +02:00

363 lines
11 KiB
JavaScript

import ethers from 'npm:ethers';
import bs58 from 'npm:bs58';
import NpmBuffer from 'npm:buffer';
import RSVP from 'rsvp';
import Ember from 'ember';
import Service from 'ember-service';
import injectService from 'ember-service/inject';
import computed, { alias } from 'ember-computed';
import { isEmpty, isPresent } from 'ember-utils';
import config from 'kredits-web/config/environment';
import Proposal from 'kredits-web/models/proposal';
import abis from 'contracts/abis';
import addresses from 'contracts/addresses';
import { ContributorSerializer } from 'kredits-web/lib/kredits';
const {
getOwner,
Logger: {
debug,
warn,
error
}
} = Ember;
const Buffer = NpmBuffer.Buffer;
export default Service.extend({
ipfs: injectService(),
ethProvider: null,
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null,
currentUserIsContributor: computed('currentUser', function() {
return isPresent(this.get('currentUser'));
}),
currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: computed('currentUserAccounts', function() {
return !isEmpty(this.get('currentUserAccounts'));
}),
accountNeedsUnlock: computed('currentUserAccounts', function() {
return this.get('currentUserAccounts') && isEmpty(this.get('currentUserAccounts'));
}),
// 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
initEthProvider: function() {
return new Ember.RSVP.Promise((resolve) => {
let ethProvider;
let networkId;
if (typeof window.web3 !== 'undefined') {
debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
networkId = parseInt(window.web3.version.network);
ethProvider = new ethers.providers.Web3Provider(window.web3.currentProvider, {chainId: networkId});
ethProvider.listAccounts().then((accounts) => {
this.set('currentUserAccounts', accounts);
this.set('ethProvider', ethProvider);
if (accounts.length > 0) {
this.get('getCurrentUser').then((contributorData) => {
this.set('currentUser', contributorData);
resolve(ethProvider);
});
} else {
resolve(ethProvider);
}
});
} else {
debug('[kredits] Creating new instance from npm module class');
let providerUrl = localStorage.getItem('config:web3ProviderUrl') || config.web3ProviderUrl;
networkId = parseInt(config.contractMetadata.networkId);
ethProvider = new ethers.providers.JsonRpcProvider(providerUrl, {chainId: networkId});
this.set('ethProvider', ethProvider);
resolve(ethProvider);
}
window.ethProvider = ethProvider;
});
},
registryContract: computed('ethProvider', function() {
let networkId = this.get('ethProvider').chainId;
let registry = new ethers.Contract(addresses['Registry'][networkId], abis['Registry'], this.get('ethProvider'));
return registry;
}),
contributorsContract: computed('ethProvider', function() {
return this.contractFor('Contributors');
}),
kreditsContract: computed('ethProvider', function() {
return this.contractFor('Operator');
}),
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: Should be part of the service
buildContributor(attributes) {
debug('[kredits] buildContributor', attributes);
let contributor = getOwner(this).lookup('model:contributor');
contributor.setProperties(attributes);
return contributor;
},
getContributorById(id) {
return this.get('contributorsContract')
.then((contract) => contract.getContributorById(id))
// Set basic data
.then(({
account: address,
hashFunction,
hashSize,
isCore,
profileHash: digest,
balance
}) => {
let isCurrentUser = this.get('currentUserAccounts').includes(address);
let profileHash = this.getMultihashFromBytes32({
digest,
hashFunction,
size: hashSize
});
return {
id,
address,
isCore,
isCurrentUser,
profileHash,
balance: balance.toNumber()
};
})
// Fetch IPFS data if available
.then(this.loadContributorProfile.bind(this))
.then((attributes) => {
return this.buildContributor(attributes);
});
},
getContributors() {
return this.get('contributorsContract')
.then((contract) => contract.contributorsCount())
.then(contributorsCount => {
debug('[kredits] contributorsCount:', contributorsCount.toNumber());
let contributors = [];
for(var id = 1; id <= contributorsCount.toNumber(); id++) {
contributors.push(this.getContributorById(id));
}
return RSVP.all(contributors);
});
},
/**
* Loads the contributor's profile data from IPFS and returns the attributes
*
* @method
* @public
*/
loadContributorProfile(data) {
let profileHash = data.profileHash;
if (!profileHash) {
return data;
}
return this.get('ipfs')
.getFile(profileHash)
.then(ContributorSerializer.deserialize)
.then((attributes) => {
debug('[kredits] loaded contributor profile', attributes);
return Object.assign({}, data, attributes);
})
.catch((err) => {
error(
'[kredits] error trying to load contributor profile',
profileHash,
err
);
});
},
getProposalData(i) {
return this.get('kreditsContract')
.then((contract) => contract.proposals(i))
.then(p => {
let contributionIpfsHash = this.getMultihashFromBytes32({ digest: p.ipfsHash, hashFunction: p.hashFunction, size: p.hashSize });
let proposal = Proposal.create({
id : i,
creatorAddress : p.creator,
recipientId : p.recipientId.toNumber(),
votesCount : p.votesCount.toNumber(),
votesNeeded : p.votesNeeded.toNumber(),
amount : p.amount.toNumber(),
executed : p.executed,
ipfsHash : contributionIpfsHash
});
if (proposal.get('ipfsHash')) {
// TODO: move ipfs into model
return proposal
.loadContribution(this.get('ipfs'))
.then(() => { return proposal; });
} else {
warn('[kredits] proposal from blockchain is missing IPFS hash', proposal);
return proposal;
}
});
},
getProposals() {
return this.get('kreditsContract')
.then((contract) => contract.proposalsCount())
.then(proposalsCount => {
let proposals = [];
for(var i = 0; i < proposalsCount.toNumber(); i++) {
proposals.push(this.getProposalData(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: move into utils
getMultihashFromBytes32(multihash) {
const { digest, hashFunction, size } = multihash;
if (size === 0) {
return;
}
const hashBytes = Buffer.from(digest.slice(2), 'hex');
const multiHashBytes = new (hashBytes.constructor)(2 + hashBytes.length);
multiHashBytes[0] = hashFunction; //contributorData[2];
multiHashBytes[1] = size; //contributorData[3];
multiHashBytes.set(hashBytes, 2);
return bs58.encode(multiHashBytes);
},
getBytes32FromMultihash(multihash) {
const decoded = bs58.decode(multihash);
return {
digest: `0x${decoded.slice(2).toString('hex')}`,
hashFunction: decoded[0],
size: decoded[1],
};
},
// TODO: extract common logic to module
addContributor(attributes) {
debug('[kredits] add contributor', attributes);
let json = ContributorSerializer.serialize(attributes);
return this.get('ipfs')
.storeFile(json)
// Set profileHash
.then((profileHash) => {
attributes.profileHash = profileHash;
return attributes;
})
.then((attributes) => {
return this.get('kreditsContract')
.then((contract) => {
let { address, isCore, profileHash } = attributes;
let {
digest, hashFunction, size
} = this.getBytes32FromMultihash(profileHash);
let contributor = [
address,
digest,
hashFunction,
size,
isCore,
];
debug('[kredits] addContributor', ...contributor);
return contract.addContributor(...contributor);
});
})
.then((data) => {
debug('[kredits] add contributor response', data);
return this.buildContributor(attributes);
});
},
addProposal(proposal) {
const {
recipientAddress,
amount,
url
} = proposal.getProperties('recipientAddress', 'amount', 'url');
return this.get('ipfs')
.storeFile(proposal.serializeContribution())
.then(ipfsHash => {
return this.get('kreditsContract')
.then((contract) => {
return contract.addProposal(
recipientAddress,
amount,
url,
ipfsHash
);
})
.then((data) => {
debug('[kredits] add proposal response', data);
return data;
});
});
},
getCurrentUser: computed('ethProvider', function() {
if (isEmpty(this.get('currentUserAccounts'))) {
return RSVP.resolve();
}
return this.get('contributorsContract')
.then((contract) => {
return contract.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
.then((id) => {
id = id.toNumber();
// check if the user is a contributor or not
if (id === 0) {
return RSVP.resolve();
} else {
return this.getContributorById(id);
}
});
});
})
});