Skip to content

Commit

Permalink
Emit user notifications from chain events. (#9)
Browse files Browse the repository at this point in the history
* Speccing out overall module structure for events.

* Attempt to fetch and parse blocks from Substrate.

* Make interface testable and add first unit test.

* Adding counter to subscribe test.

* Moving event handlers into shared.

* Move processor types to utils and add test.

* Fix node types import. Add standalone event subscriber.

* Wire up chain events to the database.

* Subscription modal and menu UI for chain events.

* Use association between Notification and ChainEvent.

* Augment ChainEventType with metadata.

* Update raw_name of ChainEventType to 'section.method'.

* Update header event display format.

* Add polling if chain or server goes offline.

* Always attempt default offline range algorithm.

* Add tentative migration.

* Stubbing out filter system.

* Add kinds for substrate event types.

* Remove balance formatter requiring chain from header.

* Add CWEvent abstraction layer and affectedAddresses to event.

* Fix affected address notification filter.

* Add tentative URLs to some chain events.

* Write poller unit tests.

* Add events README and update types.

* Increase test coverage.

* Revert forgotten local change to package.json

* Add new chain event types.

* Add event handler server tests.

* Add test-events command and remove db logging.

* Switch CircleCI tests to API only.

* Add exclude addresses to events. Remove author reliance in emitSubscription.

* Add event testing to circleci config.

* Add titler, remove modal, and update subscriptions page.

* Update readme for titler and subscription page.

* Add balance format, debug validator reward inconsistency.

* Fix down migration.

* Update for PR requests.

* Add version to events.

* Fix subscription page dropdown ordering.

* Fix merge issues.

* Fix dropdown sort for subscriptions, redux.

* Move version from event_data to chain_event model.

* Stub out new chain events.

* Implement new events fully.

* Fix unit tests and related bugs.

* Add processor fail tests.

* Update migration for new types.

* Add stubs for enricher tests.

* Move usage of chain version to type_parser and off db.

* Add TreasuryRewardMinted event.

* Add TreasuryRewardMintedV2 event.

* Add full enricher tests.

* Remove unused version from chain event creation.

* Fix errors from Kusama chain upgrade.

* Swap dispatch queue to new derive.

* Add subscribe all button for chain events.

* Fixes redirect on subscription page

* Fixes for creation and tx fees

* Fixes differences in client/server edgeware url setups

* Adds flag for event logging

Co-authored-by: Raymond Zhong <raykyri@gmail.com>
Co-authored-by: Drew Stone <drewstone329@gmail.com>
  • Loading branch information
3 people authored Apr 27, 2020
1 parent 28fb709 commit f77a2b6
Show file tree
Hide file tree
Showing 55 changed files with 4,228 additions and 188 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ jobs:
- run:
name: run api tests
command: yarn test-api
- run:
name: run event tests
command: yarn test-events
- store_artifacts:
path: coverage
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"lines-between-class-members": 0,
"max-classes-per-file": ["error", 5],
"max-len": ["error", {"code": 120, "tabWidth": 2}],
"dot-notation": 0
"no-trailing-spaces": ["error"],
"no-useless-constructor": 0,
"no-empty-function": 0,
"import/prefer-default-export": 0,
"dot-notation": 0,
"no-lonely-if": 0
}
}
3 changes: 2 additions & 1 deletion client/scripts/controllers/chain/edgeware/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Edgeware extends IChainAdapter<SubstrateCoin, SubstrateAccount> {

await super.init(async () => {
const edgTypes = Object.values(edgewareDefinitions)
.reduce((res, { types }): object => ({ ...res, ...types }), {});
.reduce((res, { default: { types } }): object => ({ ...res, ...types }), {});

await this.chain.resetApi(this.meta, {
types: {
Expand All @@ -65,6 +65,7 @@ class Edgeware extends IChainAdapter<SubstrateCoin, SubstrateAccount> {
StakingLedger: 'StakingLedgerTo223',
Votes: 'VotesTo230',
ReferendumInfo: 'ReferendumInfoTo239',
Weight: 'u32',
},
// override duplicate type name
typesAlias: { voting: { Tally: 'VotingTally' } },
Expand Down
13 changes: 10 additions & 3 deletions client/scripts/controllers/chain/substrate/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,16 @@ export class SubstrateAccount extends Account<SubstrateCoin> {
public get balanceTransferFee(): Observable<SubstrateCoin> {
if (this.chainClass === ChainClass.Edgeware) {
// grab const tx fee on edgeware
return this._Chain.api.pipe(
map((api: ApiRx) => this._Chain.coins(api.consts.balances.transferFee as Balance))
);
return from(this._Chain.api.pipe(map((api: ApiRx) => (api.consts.balances.transferFee as Balance)))
.toPromise()
.then((txFee) => {
if (txFee) {
return Promise.resolve(this._Chain.coins(txFee));
} else {
const dummyTxFunc = (api: ApiRx) => api.tx.balances.transfer(this.address, '0');
return this._Chain.computeFees(this.address, dummyTxFunc);
}
}));
} else {
// compute fee on Kusama
const dummyTxFunc = (api: ApiRx) => api.tx.balances.transfer(this.address, '0');
Expand Down
27 changes: 27 additions & 0 deletions client/scripts/models/ChainEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IChainEventData } from 'events/interfaces';
import ChainEventType from './ChainEventType';

class ChainEvent {
public readonly id: number;
public readonly blockNumber: number;
public readonly data: IChainEventData;
public readonly type: ChainEventType;

constructor(id, blockNumber, data, type) {
this.id = id;
this.blockNumber = blockNumber;
this.data = data;
this.type = type;
}

public static fromJSON(json) {
return new ChainEvent(
json.id,
json.block_number,
json.event_data,
ChainEventType.fromJSON(json.ChainEventType),
);
}
}

export default ChainEvent;
19 changes: 19 additions & 0 deletions client/scripts/models/ChainEventType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SubstrateEventKind } from 'shared/events/edgeware/types';

class ChainEventType {
public readonly id: string;
public readonly chain: string;
public readonly eventName: SubstrateEventKind;

constructor(id, chain, eventName) {
this.id = id;
this.chain = chain;
this.eventName = eventName;
}

public static fromJSON(json) {
return new ChainEventType(json.id, json.chain, json.event_name);
}
}

export default ChainEventType;
16 changes: 14 additions & 2 deletions client/scripts/models/Notification.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
import moment from 'moment-twitter';
import NotificationSubscription from './NotificationSubscription';
import ChainEvent from './ChainEvent';

class Notification {
public readonly id: number;
public readonly data: string;
public readonly createdAt: moment.Moment;
public readonly subscription: NotificationSubscription;
public readonly chainEvent?: ChainEvent;

private _isRead: boolean;
public get isRead(): boolean {
return this._isRead;
}

constructor(id, data, isRead, createdAt, subscription) {
constructor(id, data, isRead, createdAt, subscription, chainEvent?) {
this.id = id;
this.data = data;
this._isRead = isRead;
this.createdAt = moment(createdAt);
this.subscription = subscription;
this.chainEvent = chainEvent;
}

public markRead() {
if (this._isRead) {
throw new Error('notification already read!');
} else {
this._isRead = true;
}
}

public static fromJSON(json, subscription: NotificationSubscription) {
return new Notification(json.id, json.notification_data, json.is_read, json.created_at, subscription);
return new Notification(
json.id,
json.notification_data,
json.is_read,
json.created_at,
subscription,
json.ChainEvent ? ChainEvent.fromJSON(json.ChainEvent) : undefined,
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions client/scripts/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export { default as StorageModule } from './StorageModule';
export { default as ChainObject } from './ChainObject';
export { default as ChainObjectQuery } from './ChainObjectQuery';
export { default as ChainObjectVersion } from './ChainObjectVersion';
export { default as ChainEventType } from './ChainEventType';
export { default as ChainEvent } from './ChainEvent';

export { DepositVote, BinaryVote } from './votes';

Expand Down
87 changes: 54 additions & 33 deletions client/scripts/views/components/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
import mixpanel from 'mixpanel-browser';
import BN from 'bn.js';

import { initAppState } from 'app';
import app, { ApiStatus } from 'state';
import { ProposalType } from 'identifiers';
import { featherIcon, slugify } from 'helpers';
import { NotificationCategories } from 'types';

import { formatCoin } from 'adapters/currency';
import labelEdgewareEvent from 'events/edgeware/filters/labeler';

import Substrate from 'controllers/chain/substrate/main';
import Cosmos from 'controllers/chain/cosmos/main';
import Edgeware from 'controllers/chain/edgeware/main';
Expand Down Expand Up @@ -610,36 +615,52 @@ const HeaderNotificationRow: m.Component<IHeaderNotificationRow> = {
]);
};

const {
author,
createdAt,
notificationHeader,
notificationBody,
path,
pageJump
} = getNotificationFields(category, JSON.parse(notification.data));

return getHeaderNotificationRow(
author,
createdAt,
notificationHeader,
notificationBody,
path,
pageJump
);

// else if (category === NotificationCategories.NewCommunity) {
// //const { created_at, proposal_id } = JSON.parse(notification.data);
// //const thread = app.threads.store.getByIdentifier(proposal_id);
// const community = app.activeId();

// return getHeaderNotificationRow(
// moment.utc(created_at),
// null,
// `New community created`,
// '',
// `/${community}/`);
// }
if (category === NotificationCategories.ChainEvent) {
if (!notification.chainEvent) {
throw new Error('chain event notification does not have expected data');
}
// TODO: use different labelers depending on chain
const label = labelEdgewareEvent(
notification.chainEvent.blockNumber,
notification.chainEvent.type.chain,
notification.chainEvent.data,
);
return m('li.HeaderNotificationRow', {
class: notification.isRead ? '' : 'active',
onclick: async () => {
const notificationArray: Notification[] = [];
notificationArray.push(notification);
app.login.notifications.markAsRead(notificationArray).then(() => m.redraw());
if (!label.linkUrl) return;
await m.route.set(label.linkUrl);
m.redraw.sync();
},
}, [
m('.comment-body', [
m('.comment-body-top', label.heading),
m('.comment-body-bottom', `Block ${notification.chainEvent.blockNumber}`),
m('.comment-body-excerpt', label.label),
]),
]);
} else {
const {
author,
createdAt,
notificationHeader,
notificationBody,
path,
pageJump
} = getNotificationFields(category, JSON.parse(notification.data));

return getHeaderNotificationRow(
author,
createdAt,
notificationHeader,
notificationBody,
path,
pageJump
);
}
},
};

Expand Down Expand Up @@ -695,6 +716,7 @@ const NotificationButtons: m.Component<{ notifications }> = {
const { notifications } = vnode.attrs;
return m('.NotificationButtons', [
m('.button', {
class: notifications.length > 0 ? '' : 'disabled',
onclick: (e) => {
e.preventDefault();
if (notifications.length < 1) return;
Expand Down Expand Up @@ -726,9 +748,8 @@ const NotificationMenu : m.Component<{ menusOpen }> = {
class: (unreadCount > 0 || invites.length > 0) ? 'unread-notifications' : '',
}, unreadMessage),
}, [
notifications.length > 0 && m(NotificationButtons, { notifications }),
m(Notifications, { notifications }),
notifications.length > 0
&& m(NotificationButtons, { notifications }),
]);
}
};
Expand Down Expand Up @@ -763,7 +784,7 @@ const Header: m.Component<{}, IHeaderState> = {
e.preventDefault();
e.stopPropagation();
$(e.target).trigger('menuclose');
m.route.set(app.login.selectedNode ? `/${app.login.selectedNode.chain.id}/` : '/');
m.route.set(app.login.selectedNode ? `/${app.login.selectedNode.chain.id || app.login.selectedNode.chain}/` : '/');
},
}, [
m('.header-logo-image')
Expand Down
21 changes: 12 additions & 9 deletions client/scripts/views/components/settings/send_edg_well.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,19 @@ const getBalanceTransferChecks = (
`Transfer fee: ${formatCoin(txFee)}`
]);
}
if (canTransfer && recipientBalance.eqn(0) && (app.chain as Substrate).chain.creationfee.gtn(0)) {
checks.push([
featherIcon('info', 14, 2, '#444'),
`Account creation fee: ${formatCoin((app.chain as Substrate).chain.creationfee)}`
]);

const creationFee = (app.chain as Substrate).chain.creationfee;
if (canTransfer && recipientBalance.eqn(0) && creationFee) {
if (creationFee.gtn(0)) {
checks.push([
featherIcon('info', 14, 2, '#444'),
`Account creation fee: ${formatCoin((app.chain as Substrate).chain.creationfee)}`
]);
}
}
const resultingBalance = app.chain.chain.coins(recipientBalance.add(recipientBalance.gtn(0) ?
amount.sub(txFee) :
amount.sub(txFee)
.sub((app.chain as Substrate).chain.creationfee)));
const resultingBalance = app.chain.chain.coins(recipientBalance.add(recipientBalance.gtn(0)
? amount.sub(txFee)
: (creationFee) ? amount.sub(txFee).sub(creationFee) : amount.sub(txFee)));
if (recipientBalance.eqn(0) && resultingBalance.lt((app.chain as Substrate).chain.existentialdeposit)) {
checks.push([
featherIcon('slash', 14, 2, '#444'),
Expand Down
Loading

0 comments on commit f77a2b6

Please sign in to comment.