11f19c8344
Fixes contributions not being added when not already in memory (from creation). Simplifies the entire function.
734 lines
26 KiB
JavaScript
734 lines
26 KiB
JavaScript
import ethers from 'ethers';
|
|
import Kredits from '@kredits/contracts';
|
|
|
|
import Service from '@ember/service';
|
|
import EmberObject from '@ember/object';
|
|
import { computed } from '@ember/object';
|
|
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
|
|
import { isEmpty, isPresent } from '@ember/utils';
|
|
import { inject as service } from '@ember/service';
|
|
|
|
import { task, taskGroup } from 'ember-concurrency';
|
|
|
|
import groupBy from 'kredits-web/utils/group-by';
|
|
import processContributorData from 'kredits-web/utils/process-contributor-data';
|
|
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
|
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
|
|
import formatKredits from 'kredits-web/utils/format-kredits';
|
|
|
|
import config from 'kredits-web/config/environment';
|
|
import Contributor from 'kredits-web/models/contributor';
|
|
import Contribution from 'kredits-web/models/contribution';
|
|
import Reimbursement from 'kredits-web/models/reimbursement';
|
|
// Lets us access the model classes dynamically
|
|
const models = { Contributor, Contribution, Reimbursement };
|
|
|
|
export default Service.extend({
|
|
|
|
browserCache: service(),
|
|
|
|
currentBlock: null,
|
|
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
|
currentUser: null,
|
|
contributors: null,
|
|
contributions: null,
|
|
reimbursements: null,
|
|
githubAccessToken: null,
|
|
|
|
currentUserIsContributor: notEmpty('currentUser'),
|
|
currentUserIsCore: alias('currentUser.isCore'),
|
|
hasAccounts: notEmpty('currentUserAccounts'),
|
|
|
|
contributorsMined: filterBy('contributors', 'id'),
|
|
contributorsSorting: Object.freeze(['name:asc']),
|
|
contributorsSorted: sort('contributorsMined', 'contributorsSorting'),
|
|
|
|
// When data was loaded from cache, we need to fetch updates from the network
|
|
contributorsNeedSync: false,
|
|
contributionsNeedSync: false,
|
|
reimbursementsNeedSync: false,
|
|
|
|
init () {
|
|
this._super(...arguments);
|
|
this.set('contributors', []);
|
|
this.set('contributions', []);
|
|
this.set('reimbursements', []);
|
|
},
|
|
|
|
// This is called in the application route's beforeModel(). So it is
|
|
// initialized before everything else, and we can rely on the ethProvider and
|
|
// the potential currentUserAccounts to be available
|
|
getEthProvider () {
|
|
let ethProvider;
|
|
|
|
return new Promise(resolve => {
|
|
function instantiateWithoutAccount () {
|
|
console.debug('[kredits] Creating new instance from npm module class');
|
|
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
|
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
|
resolve({
|
|
ethProvider: ethProvider,
|
|
ethSigner: null
|
|
});
|
|
}
|
|
|
|
async function instantiateWithAccount (web3Provider, context) {
|
|
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
|
|
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
|
|
|
const network = await ethProvider.getNetwork();
|
|
if (isPresent(config.web3RequiredChainId) &&
|
|
network.chainId !== config.web3RequiredChainId) {
|
|
return instantiateWithoutAccount();
|
|
}
|
|
|
|
ethProvider.listAccounts().then(accounts => {
|
|
context.set('currentUserAccounts', accounts);
|
|
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
|
resolve({
|
|
ethProvider,
|
|
ethSigner
|
|
});
|
|
});
|
|
}
|
|
|
|
if (window.ethereum) {
|
|
if (window.ethereum.isConnected()) {
|
|
instantiateWithAccount(window.ethereum, this);
|
|
} else {
|
|
instantiateWithoutAccount();
|
|
}
|
|
}
|
|
// Legacy dapp browsers...
|
|
else if (window.web3) {
|
|
instantiateWithAccount(window.web3.currentProvider, this);
|
|
}
|
|
// Non-dapp browsers...
|
|
else {
|
|
instantiateWithoutAccount();
|
|
}
|
|
});
|
|
},
|
|
|
|
async setup () {
|
|
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
|
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
|
ipfsConfig: config.ipfs
|
|
});
|
|
});
|
|
|
|
await kredits.init();
|
|
|
|
this.set('kredits', kredits);
|
|
this.set('currentBlock', await kredits.provider.getBlockNumber());
|
|
|
|
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
|
this.getCurrentUser.then(contributorData => {
|
|
this.set('currentUser', contributorData);
|
|
});
|
|
}
|
|
|
|
return kredits;
|
|
},
|
|
|
|
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() {
|
|
if (isEmpty(this.currentUserAccounts)) {
|
|
return Promise.resolve();
|
|
}
|
|
return this.kredits.Contributor
|
|
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject)
|
|
.then((id) => {
|
|
// check if the user is a contributor or not
|
|
if (id === 0) {
|
|
return Promise.resolve();
|
|
} else {
|
|
return this.kredits.Contributor.getById(id);
|
|
}
|
|
});
|
|
}),
|
|
|
|
totalSupply: computed(function() {
|
|
return this.kredits.Token.functions.totalSupply().then(total => {
|
|
return formatKredits(total);
|
|
})
|
|
}),
|
|
|
|
totalKreditsEarned: computed(function() {
|
|
return this.kredits.Contribution.functions.totalKreditsEarned(true);
|
|
}),
|
|
|
|
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
|
|
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
|
|
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
|
|
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value);
|
|
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
|
|
|
|
const kreditsByContributor = contributionsGrouped.map(c => {
|
|
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
|
|
const contributor = this.contributors.findBy('id', c.value);
|
|
|
|
return EmberObject.create({
|
|
contributor: contributor,
|
|
amountUnconfirmed: amountUnconfirmed,
|
|
amountConfirmed: contributor.totalKreditsEarned,
|
|
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
|
|
})
|
|
});
|
|
|
|
contributorsWithOnlyConfirmed.forEach(c => {
|
|
kreditsByContributor.push(EmberObject.create({
|
|
contributor: c,
|
|
amountUnconfirmed: 0,
|
|
amountConfirmed: c.totalKreditsEarned,
|
|
amountTotal: c.totalKreditsEarned
|
|
}));
|
|
})
|
|
|
|
return kreditsByContributor;
|
|
}),
|
|
|
|
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
|
return this.contributions
|
|
.filter(c => c.confirmedAt > this.currentBlock);
|
|
}),
|
|
|
|
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
|
return this.contributions
|
|
.filterBy('vetoed', false)
|
|
.filter(c => c.confirmedAt <= this.currentBlock);
|
|
}),
|
|
|
|
reimbursementsUnconfirmed: computed('reimbursements.[]', 'currentBlock', function() {
|
|
return this.reimbursements
|
|
.filter(r => r.confirmedAt > this.currentBlock);
|
|
}),
|
|
|
|
reimbursementsConfirmed: computed('reimbursements.[]', 'currentBlock', function() {
|
|
return this.reimbursements
|
|
.filterBy('vetoed', false)
|
|
.filter(r => r.confirmedAt <= this.currentBlock);
|
|
}),
|
|
|
|
reimbursementsPending: computed('reimbursements.[]', 'pendingTx', function() {
|
|
return this.reimbursements.filter(r => !r.id);
|
|
}),
|
|
|
|
async loadInitialData () {
|
|
const numCachedContributors = await this.browserCache.contributors.length();
|
|
if (numCachedContributors > 0) {
|
|
await this.loadObjectsFromCache('Contributor');
|
|
this.set('contributorsNeedSync', true);
|
|
} else {
|
|
await this.fetchContributors();
|
|
}
|
|
|
|
const numCachedContributions = await this.browserCache.contributions.length();
|
|
if (numCachedContributions > 0) {
|
|
await this.loadObjectsFromCache('Contribution');
|
|
this.set('contributionsNeedSync', true);
|
|
} else {
|
|
await this.fetchContributions({ page: { size: 30 } });
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
|
|
addContributor (attributes) {
|
|
if (attributes.github_uid) {
|
|
const uidInt = parseInt(attributes.github_uid);
|
|
attributes.github_uid = uidInt;
|
|
}
|
|
|
|
console.debug('[kredits] add contributor', attributes);
|
|
|
|
return this.kredits.Contributor.add(attributes, { gasLimit: 350000 })
|
|
.then(data => {
|
|
console.debug('[kredits] add contributor response', data);
|
|
});
|
|
},
|
|
|
|
updateContributor (id, attributes) {
|
|
if (attributes.github_uid) {
|
|
const uidInt = parseInt(attributes.github_uid);
|
|
attributes.github_uid = uidInt;
|
|
}
|
|
|
|
console.debug('[kredits] update contributor', attributes);
|
|
|
|
return this.kredits.Contributor.updateProfile(id, attributes, { gasLimit: 350000 })
|
|
.then(data => {
|
|
console.debug('[kredits] updateProfile response', data);
|
|
});
|
|
},
|
|
|
|
fetchContributors () {
|
|
console.debug(`[kredits] Fetching all contributors from the network`);
|
|
return this.kredits.Contributor.all()
|
|
.then(contributors => {
|
|
return contributors.forEach(data => {
|
|
this.loadContributorFromData(data);
|
|
return;
|
|
});
|
|
})
|
|
.then(() => {
|
|
return this.cacheLoadedContributors();
|
|
});
|
|
},
|
|
|
|
loadContributorFromData (data) {
|
|
const contributor = Contributor.create(processContributorData(data));
|
|
const loadedContributor = this.contributors.findBy('id', contributor.id);
|
|
if (loadedContributor) { this.contributors.removeObject(loadedContributor); }
|
|
this.contributors.pushObject(contributor);
|
|
return contributor;
|
|
},
|
|
|
|
async cacheLoadedContributors () {
|
|
for (const c of this.contributors) {
|
|
await this.browserCache.contributors.setItem(c.id.toString(), c.serialize());
|
|
}
|
|
console.debug(`[kredits] Cached ${this.contributors.length} contributors in browser storage`);
|
|
return Promise.resolve();
|
|
},
|
|
|
|
async loadContributorsFromCache () {
|
|
return this.browserCache.contributors.iterate((value/*, key , iterationNumber */) => {
|
|
this.contributors.pushObject(Contributor.create(JSON.parse(value)));
|
|
}).then((/* result */) => {
|
|
console.debug(`[kredits] Loaded ${this.contributors.length} contributors from cache`);
|
|
});
|
|
},
|
|
|
|
syncContributors: task(function * () {
|
|
yield this.fetchContributors();
|
|
this.set('contributorsNeedSync', false);
|
|
}),
|
|
|
|
addContribution (attributes) {
|
|
console.debug('[kredits] Adding contribution', attributes);
|
|
|
|
return this.kredits.Contribution.add(attributes, { gasLimit: 300000 })
|
|
.then(data => {
|
|
console.debug('[kredits] Contribution.add response', data);
|
|
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
|
|
const contribution = Contribution.create(attributes);
|
|
contribution.set('pendingTx', data);
|
|
contribution.set('confirmedAtBlock', this.currentBlock + 40320);
|
|
this.contributions.pushObject(contribution);
|
|
return contribution;
|
|
});
|
|
},
|
|
|
|
fetchContributions (options = { page: { size: 200 } }) {
|
|
console.debug(`[kredits] Fetching contributions from the network`);
|
|
return this.kredits.Contribution.all(options)
|
|
.then(contributions => {
|
|
return contributions.map(data => {
|
|
const contribution = this.loadContributionFromData(data);
|
|
return contribution;
|
|
});
|
|
})
|
|
.then(contributions => {
|
|
const cacheWrites = contributions.map(c => {
|
|
return this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
|
});
|
|
return Promise.all(cacheWrites).then(() => {
|
|
console.debug(`[kredits] Cached ${contributions.length} contributions in browser storage`);
|
|
});
|
|
});
|
|
},
|
|
|
|
loadContributionFromData(data) {
|
|
const contribution = Contribution.create(processContributionData(data));
|
|
contribution.set('contributor', this.contributors.findBy('id', data.contributorId));
|
|
const loadedContribution = this.contributions.findBy('id', contribution.id);
|
|
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
|
|
this.contributions.pushObject(contribution);
|
|
return contribution;
|
|
},
|
|
|
|
async cacheLoadedContributions () {
|
|
for (const c of this.contributions) {
|
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
|
}
|
|
console.debug(`[kredits] Cached ${this.contributions.length} contributions in browser storage`);
|
|
return Promise.resolve();
|
|
},
|
|
|
|
async loadContributionsFromCache () {
|
|
return this.browserCache.contributions.iterate((value/*, key , iterationNumber */) => {
|
|
this.contributions.pushObject(Contribution.create(JSON.parse(value)));
|
|
}).then((/* result */) => {
|
|
console.debug(`[kredits] Loaded ${this.contributions.length} contributions from cache`);
|
|
});
|
|
},
|
|
|
|
contributionTasks: taskGroup().enqueue(),
|
|
|
|
syncContributions: task(function * () {
|
|
yield this.fetchNewContributions.perform();
|
|
yield this.syncUnconfirmedContributions.perform();
|
|
this.set('contributionsNeedSync', false);
|
|
}).group('contributionTasks'),
|
|
|
|
fetchNewContributions: task(function * () {
|
|
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
|
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
|
const toFetch = count - lastKnownContributionId;
|
|
|
|
if (toFetch > 0) {
|
|
console.debug(`[kredits] Fetching ${toFetch} new contributions`);
|
|
for (let id = lastKnownContributionId; id <= count; id++) {
|
|
const data = yield this.kredits.Contribution.getById(id);
|
|
const c = this.loadContributionFromData(data);
|
|
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
|
}
|
|
} else {
|
|
console.debug(`[kredits] No new contributions to fetch`);
|
|
}
|
|
}),
|
|
|
|
fetchMissingContributions: task(function * () {
|
|
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
|
const allIds = [...Array(count+1).keys()];
|
|
allIds.shift(); // remove first item, which is 0
|
|
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
|
const toFetch = allIds.filter(id => !loadedContributions.has(id));
|
|
if (toFetch.length === 0) {
|
|
console.debug(`[kredits] No contributions left to fetch`);
|
|
return;
|
|
}
|
|
console.debug(`[kredits] Fetching ${toFetch.length} past contributions`);
|
|
let countFetched = 0;
|
|
|
|
for (let id = count; id > 0; id--) {
|
|
if (loadedContributions.has(id)) {
|
|
continue;
|
|
} else {
|
|
const data = yield this.kredits.Contribution.getById(id);
|
|
const c = this.loadContributionFromData(data);
|
|
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
|
countFetched++;
|
|
if (countFetched % 20 === 0) {
|
|
console.debug(`[kredits] Fetched ${countFetched} more contributions`);
|
|
}
|
|
}
|
|
}
|
|
console.debug(`[kredits] Cached ${countFetched} past contributions`);
|
|
}).group('contributionTasks'),
|
|
|
|
syncUnconfirmedContributions: task(function * () {
|
|
if (this.contributionsUnconfirmed.length > 0) {
|
|
console.debug(`[kredits] Syncing unconfirmed contributions`);
|
|
for (const c of this.contributionsUnconfirmed) {
|
|
const data = yield this.kredits.Contribution.getById(c.id);
|
|
const contribution = this.loadContributionFromData(data);
|
|
yield this.browserCache.contributions.setItem(c.id.toString(), contribution.serialize());
|
|
}
|
|
} else {
|
|
console.debug(`[kredits] No unconfirmed contributions to sync`);
|
|
}
|
|
}),
|
|
|
|
veto (contributionId) {
|
|
console.debug('[kredits] veto against', contributionId);
|
|
const contribution = this.contributions.findBy('id', contributionId);
|
|
|
|
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 })
|
|
.then(data => {
|
|
console.debug('[kredits] veto response', data);
|
|
contribution.set('pendingTx', data);
|
|
return data;
|
|
});
|
|
},
|
|
|
|
//
|
|
// Generic data handling (for objects that can be vetoed)
|
|
//
|
|
|
|
fetchObjects(objectClass, options = { page: { size: 200 } }) {
|
|
const objectClassLowerCase = objectClass.toLowerCase();
|
|
console.debug(`[kredits] Fetching ${objectClassLowerCase}s from the network`);
|
|
return this.kredits[objectClass].all(options)
|
|
.then(objects => {
|
|
return objects.map(data => {
|
|
const classInstance = this[`load${objectClass}FromData`](data);
|
|
return classInstance;
|
|
});
|
|
})
|
|
.then(objects => {
|
|
const cacheWrites = objects.map(o => {
|
|
return this.browserCache[objectClassLowerCase+'s']
|
|
.setItem(o.id.toString(), o.serialize());
|
|
});
|
|
return Promise.all(cacheWrites).then(() => {
|
|
console.debug(`[kredits] Cached ${objects.length} ${objectClassLowerCase+'s'} in browser storage`);
|
|
});
|
|
});
|
|
},
|
|
|
|
removeObjectFromCollectionIfLoaded (collection, objectId) {
|
|
const loadedObj = this[collection].findBy('id', objectId);
|
|
if (loadedObj) { this[collection].removeObject(loadedObj); }
|
|
},
|
|
|
|
async cacheLoadedObjects (collection) {
|
|
for (const o of this[collection]) {
|
|
await this.browserCache[collection].setItem(o.id, o.serialize());
|
|
}
|
|
console.debug(`[kredits] Cached ${this[collection].length} ${collection} in browser storage`);
|
|
},
|
|
|
|
async loadObjectsFromCache (objectClass) {
|
|
const collection = objectClass.toLowerCase()+'s';
|
|
return this.browserCache[collection].iterate((value/*, key , iterationNumber */) => {
|
|
const obj = models[objectClass].create(JSON.parse(value));
|
|
this.removeObjectFromCollectionIfLoaded(collection, obj.id)
|
|
this[collection].pushObject(obj);
|
|
}).then((/* result */) => {
|
|
console.debug(`[kredits] Loaded ${this[collection].length} ${collection} from cache`);
|
|
});
|
|
},
|
|
|
|
syncTaskGroup: taskGroup().enqueue(),
|
|
|
|
fetchNewObjects: task(function * (objectClass) {
|
|
const collection = objectClass.toLowerCase()+'s';
|
|
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
|
|
const lastKnownObjectId = Math.max.apply(null, this[collection].mapBy('id'));
|
|
const toFetch = count - lastKnownObjectId;
|
|
|
|
if (toFetch > 0) {
|
|
console.debug(`[kredits] Fetching ${toFetch} new ${collection}`);
|
|
for (let id = lastKnownObjectId; id <= count; id++) {
|
|
const data = yield this.kredits[objectClass].getById(id);
|
|
const o = this[`load${objectClass}FromData`](data);
|
|
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
|
|
}
|
|
} else {
|
|
console.debug(`[kredits] No new ${collection} to fetch`);
|
|
}
|
|
}),
|
|
|
|
fetchMissingObjects: task(function * (objectClass) {
|
|
const collection = objectClass.toLowerCase()+'s';
|
|
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
|
|
const allIds = [...Array(count+1).keys()];
|
|
allIds.shift(); // remove first item, which is 0
|
|
const loadedObjects = new Set(this[collection].mapBy('id'));
|
|
const toFetch = allIds.filter(id => !loadedObjects.has(id));
|
|
if (toFetch.length === 0) {
|
|
console.debug(`[kredits] No ${collection} left to fetch`);
|
|
return;
|
|
}
|
|
console.debug(`[kredits] Fetching ${toFetch.length} past ${collection}`);
|
|
let countFetched = 0;
|
|
|
|
for (let id = count; id > 0; id--) {
|
|
if (loadedObjects.has(id)) {
|
|
continue;
|
|
} else {
|
|
const data = yield this.kredits[objectClass].getById(id);
|
|
const o = this[`load${objectClass}FromData`](data);
|
|
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
|
|
countFetched++;
|
|
if (countFetched % 20 === 0) {
|
|
console.debug(`[kredits] Fetched ${countFetched} more ${collection}`);
|
|
}
|
|
}
|
|
}
|
|
console.debug(`[kredits] Cached ${countFetched} past ${collection}`);
|
|
}),
|
|
|
|
syncUnconfirmedObjects: task(function * (objectClass) {
|
|
const collection = objectClass.toLowerCase()+'s';
|
|
if (this.get(`${collection}Unconfirmed`).length > 0) {
|
|
console.debug(`[kredits] Syncing unconfirmed ${collection}`);
|
|
for (const o of this[`${collection}Unconfirmed`]) {
|
|
if (isEmpty(o.id)) return;
|
|
const data = yield this.kredits[objectClass].getById(o.id);
|
|
const object = this[`load${objectClass}FromData`](data);
|
|
yield this.browserCache[collection]
|
|
.setItem(o.id.toString(), object.serialize());
|
|
}
|
|
} else {
|
|
console.debug(`[kredits] No unconfirmed ${collection} to sync`);
|
|
}
|
|
}),
|
|
|
|
vetoAgainstObject (objectClass, objectId) {
|
|
console.debug(`[kredits] veto against ${objectClass.toLowerCase()}`, objectId);
|
|
const collection = objectClass.toLowerCase()+'s';
|
|
const object = this[collection].findBy('id', objectId);
|
|
|
|
return this.kredits[objectClass].functions.veto(objectId, { gasLimit: 300000 })
|
|
.then(data => {
|
|
console.debug('[kredits] veto response', data);
|
|
object.set('pendingTx', data);
|
|
return data;
|
|
});
|
|
},
|
|
|
|
//
|
|
// Reimbursements
|
|
//
|
|
|
|
loadReimbursementFromData(data) {
|
|
const obj = Reimbursement.create(processReimbursementData(data));
|
|
obj.set('contributor', this.contributors.findBy('id', data.recipientId));
|
|
this.removeObjectFromCollectionIfLoaded('reimbursements', obj.id);
|
|
this.reimbursements.pushObject(obj);
|
|
return obj;
|
|
},
|
|
|
|
addReimbursement (attributes) {
|
|
console.debug('[kredits] add reimbursement', attributes);
|
|
|
|
return this.kredits.Reimbursement.add(attributes, { gasLimit: 300000 })
|
|
.then(data => {
|
|
console.debug('[kredits] add reimbursement response', data);
|
|
const reimbursement = Reimbursement.create(attributes);
|
|
reimbursement.setProperties({
|
|
contributor: this.contributors.findBy('id', attributes.recipientId),
|
|
pendingTx: data,
|
|
confirmedAt: this.currentBlock + 40320
|
|
});
|
|
this.reimbursements.pushObject(reimbursement);
|
|
|
|
// Listen to tx mining/execution status
|
|
data.wait()
|
|
.then(d => console.debug('[kredits] tx successful', d))
|
|
.catch(e => {
|
|
window.alert('The transaction failed to execute. Please check the browser console.');
|
|
console.log('[kredits] tx error', e);
|
|
});
|
|
|
|
return reimbursement;
|
|
});
|
|
},
|
|
|
|
syncReimbursements: task(function * () {
|
|
yield this.fetchNewObjects.perform('Reimbursement');
|
|
yield this.syncUnconfirmedObjects.perform('Reimbursement');
|
|
this.set('reimbursementsNeedSync', false);
|
|
}).group('syncTaskGroup'),
|
|
|
|
fetchMissingReimbursements: task(function * () {
|
|
yield this.fetchMissingObjects.perform('Reimbursement');
|
|
}).group('syncTaskGroup'),
|
|
|
|
//
|
|
// Contract events
|
|
//
|
|
|
|
addContractEventHandlers () {
|
|
this.kredits.Contributor
|
|
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
|
.on('ContributorAccountUpdated', this.handleContributorChange.bind(this))
|
|
.on('ContributorAdded', this.handleContributorChange.bind(this))
|
|
|
|
this.kredits.Contribution
|
|
.on('ContributionAdded', this.handleContributionAdded.bind(this))
|
|
.on('ContributionVetoed', this.handleContributionVetoed.bind(this))
|
|
|
|
this.kredits.Reimbursement
|
|
.on('ReimbursementAdded', this.handleReimbursementAdded.bind(this))
|
|
.on('ReimbursementVetoed', this.handleReimbursementVetoed.bind(this))
|
|
|
|
this.kredits.Token
|
|
.on('Transfer', this.handleTransfer.bind(this));
|
|
},
|
|
|
|
async handleContributorChange (contributorId, ...args) {
|
|
console.debug('[kredits] Contributor add/update event received for ID', contributorId);
|
|
console.debug('[kredits] Event data:', args);
|
|
const contributorData = await this.kredits.Contributor.getById(contributorId);
|
|
const newContributor = Contributor.create(contributorData);
|
|
|
|
// TODO check for actual differences in the contributor data first
|
|
|
|
const oldContributor = this.contributors.findBy('id', contributorId);
|
|
if (oldContributor) {
|
|
// console.debug('[kredits] cached contributor', oldContributor);
|
|
this.contributors.removeObject(oldContributor);
|
|
}
|
|
|
|
// console.debug('[kredits] incoming contributor data', newContributor);
|
|
this.contributors.pushObject(newContributor);
|
|
},
|
|
|
|
async handleContributionAdded (id, contributorId, amount) {
|
|
console.debug('[kredits] ContributionAdded event received', { id, contributorId, amount });
|
|
|
|
const pendingContribution = this.contributions.find(c => {
|
|
return (c.id === null) &&
|
|
(c.contributorId === contributorId) &&
|
|
(c.amount.toString() === amount.toString());
|
|
});
|
|
|
|
if (pendingContribution) {
|
|
this.contributions.removeObject(pendingContribution);
|
|
}
|
|
|
|
const data = await this.kredits.Contribution.getById(id);
|
|
const c = this.loadContributionFromData(data);
|
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
|
},
|
|
|
|
handleContributionVetoed (contributionId) {
|
|
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
|
const contribution = this.contributions.findBy('id', contributionId);
|
|
console.debug('[kredits] contribution', contribution);
|
|
|
|
if (contribution) {
|
|
contribution.set('vetoed', true);
|
|
contribution.set('pendingTx', null);
|
|
}
|
|
},
|
|
|
|
//
|
|
// TODO test when reimbursement txs are successful
|
|
//
|
|
async handleReimbursementAdded (id, addedByAccount, amount) {
|
|
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
|
|
|
const pendingReimbursement = this.reimbursementsPending.find(r => {
|
|
return r.amount.toString() === amount.toString();
|
|
});
|
|
|
|
if (pendingReimbursement) {
|
|
console.debug('[kredits] Found a pending reimbursement matching the event. Replacing it with the final record...');
|
|
this.reimbursements.removeObject(pendingReimbursement);
|
|
}
|
|
|
|
const data = await this.kredits.Reimbursement.getById(id);
|
|
this.loadReimbursementFromData(data);
|
|
},
|
|
|
|
//
|
|
// TODO test when reimbursement txs are successful and veto is implemented
|
|
//
|
|
handleReimbursementVetoed (id) {
|
|
console.debug('[kredits] ReimbursementVetoed received for ', id);
|
|
const reimbursement = this.reimbursements.findBy('id', id);
|
|
console.debug('[kredits] reimbursement', this.reimbursement);
|
|
|
|
if (reimbursement) {
|
|
reimbursement.set('vetoed', true);
|
|
reimbursement.set('pendingTx', null);
|
|
}
|
|
},
|
|
|
|
handleTransfer (from, to, value) {
|
|
value = value.toNumber();
|
|
|
|
this.contributors
|
|
.findBy('address', from)
|
|
.decrementProperty('balance', value);
|
|
|
|
this.contributors
|
|
.findBy('address', to)
|
|
.incrementProperty('balance', value);
|
|
},
|
|
});
|