Skip to content
This repository has been archived by the owner on May 8, 2023. It is now read-only.

Commit

Permalink
Merge pull request #125 from JoinColony/feat/events
Browse files Browse the repository at this point in the history
Add ColonyEvents including example
  • Loading branch information
chmanie authored Apr 19, 2022
2 parents 5723407 + b1c191f commit 06a9697
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"rules": {
"no-unused-vars": "off",
"no-redeclare": "off",
"no-use-before-define": "off",
"no-dupe-class-members": "off",
"@typescript-eslint/no-unused-vars": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/src/basic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { providers, utils } from 'ethers';

import { ColonyNetwork, Tokens } from '../../../dist/esm';
import { ColonyNetwork, Tokens } from '../../../src';

const { formatEther, isAddress } = utils;

Expand Down
68 changes: 68 additions & 0 deletions examples/browser/src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { providers, utils } from 'ethers';

import { ColonyEvents } from '../../../src';
import type { ColonyEvent } from '../../../src';

const provider = new providers.JsonRpcProvider('https://xdai.colony.io/rpc2/');
const { isAddress } = utils;

// This event listener will only list for the `DomainAdded` event in the Colony of the user's choice. Run this and then create a Team in that Colony, to see it being picked up here
const setupEventListener = (
colonyAddress: string,
callback: (events: ColonyEvent[]) => void,
) => {
const colonyEvents = new ColonyEvents(provider);

const domainAdded = colonyEvents.createMultiFilter(
colonyEvents.eventSources.Colony,
'DomainAdded(address,uint256)',
colonyAddress,
);

let i = 0;

colonyEvents.provider.on('block', async (no) => {
i += 1;
// Only get events every 5 blocks to debounce this a little bit
if (i === 4) {
const events = await colonyEvents.getMultiEvents([domainAdded], {
fromBlock: no - i,
toBlock: no,
});
if (events.length) callback(events);
i = 0;
}
});
};

// Just some basic setup to display the UI
const addressInput: HTMLInputElement = document.querySelector('#address');
const button = document.querySelector('#button');
const errElm: HTMLParagraphElement = document.querySelector('#error');
const resultElm: HTMLParagraphElement = document.querySelector('#result');

const panik = (err: string) => {
errElm.innerText = err;
};
const kalm = () => {
errElm.innerText = '';
};
const speak = (msg: string) => {
resultElm.innerText = msg;
};

button.addEventListener('click', async () => {
kalm();
const colonyAddress = addressInput.value;
if (!isAddress(colonyAddress)) {
return panik('This is not a valid address');
}
addressInput.value = '';
setupEventListener(colonyAddress, (events) => {
speak(
`A domain with id ${events[0].data.domainId} was created on Colony ${events[0].address}`,
);
});
speak(`Set up event listener for Colony ${colonyAddress}`);
return null;
});
13 changes: 13 additions & 0 deletions examples/browser/web/events.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<body>
<h1>ColonyJS browser demo - events</h1>
</body>
<p>Listen to Colony Events as they unfold...</p>
<label for="address">Colony address:</label>
<input type="text" id="address" />
<button type="button" id="button">Listen</button>
<p style="color:red" id="error"></p>
<p style="color:blue" id="result"></p>
<script src="events.js"></script>
</html>
3 changes: 3 additions & 0 deletions examples/browser/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ <h1>ColonyJS browser demos</h1>
<li>
<a href="advanced.html">Advanced example (creating a domain within a Colony)</a>
</li>
<li>
<a href="events.html">Events example (listening to Colony events)</a>
</li>
</ul>
<script src="index.js"></script>
</html>
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"license": "GPL-3.0-only",
"dependencies": {
"@colony/colony-js": "^5.0.0"
"@colony/colony-js": "^5.0.2"
},
"devDependencies": {
"@colony/eslint-config-colony": "^9.0.2",
Expand All @@ -28,7 +28,7 @@
},
"scripts": {
"example:node": "npm run compile-cjs && ts-node src/index.ts",
"example:browser": "npm run compile-esm && esbuild --bundle examples/browser/src/*.ts --servedir=examples/browser/web",
"example:browser": "esbuild --bundle examples/browser/src/*.ts --servedir=examples/browser/web",
"build": "npm run clean && npm run compile-cjs && npm run compile-esm && npm run compile-types && npm run build-docs",
"build-docs": "typedoc --out docs --excludeInternal src/index.ts",
"clean": "rimraf ./dist",
Expand Down
2 changes: 1 addition & 1 deletion src/Colony.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { extractEvent } from './utils';

type SupportedColonyClient = ColonyClientV8;

export class Colony {
export default class Colony {
static SupportedVersion: 8 = 8;

private colonyClient: SupportedColonyClient;
Expand Down
177 changes: 177 additions & 0 deletions src/ColonyEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { constants, providers, EventFilter } from 'ethers';
import { Result } from 'ethers/lib/utils';
import {
IColonyEvents,
IColonyEventsFactory,
IColonyNetwork,
IColonyNetworkFactory,
} from '@colony/colony-js/extras';
import type { BlockTag, Filter } from '@ethersproject/abstract-provider';

import { addressesAreEqual, getLogs, nonNullable } from './utils';

type ValueOf<T> = T[keyof T];

interface EventSources {
Colony: IColonyEvents;
ColonyNetwork: IColonyNetwork;
}

type EventSource = ValueOf<EventSources>;

export interface ColonyFilter extends Filter {
eventSource: keyof EventSources;
eventName: string;
}

// TODO: consider allowing an address array
/** ColonyFilter with support for multi-events
* For the multi-event compatible filters the following assumptions prevail:
* - `address` is a mandatory field
* - it can only take a single `topic`
* - `fromBlock` and `toBlock` are not available
*/
export interface ColonyMultiFilter
extends Omit<ColonyFilter, 'address' | 'topics' | 'fromBlock' | 'toBlock'> {
address: string;
topic: string;
}

export interface ColonyEvent extends ColonyFilter {
data: Result;
}

export default class ColonyEvents {
eventSources: EventSources;

provider: providers.JsonRpcProvider;

constructor(provider: providers.JsonRpcProvider) {
this.provider = provider;
this.eventSources = {
Colony: IColonyEventsFactory.connect(constants.AddressZero, provider),
ColonyNetwork: IColonyNetworkFactory.connect(
constants.AddressZero,
provider,
),
};
}

private static extractSingleTopic(filter?: Filter) {
if (!filter || !filter.topics) return null;
const topic = filter.topics;
if (typeof topic[0] == 'string') return topic[0];
if (Array.isArray(topic[0]) && typeof topic[0][0] == 'string') {
return topic[0][0];
}
return null;
}

private getEventSourceName(contract: EventSource) {
// Find contract for filter in eventSources to store the name alongside it
const eventSource = Object.entries(this.eventSources).find(
([, c]) => c === contract,
);
return eventSource && (eventSource[0] as keyof EventSources);
}

/** Get all events for the defined filter list and a certain block number.
* All the filters are connected by a logical OR, i.e. it will find ALL given events for ALL the given contract addresses
* This is handy when you want to listen to a fixed set of events for a lot of different contracts
* @remarks: `fromBlock` and `toBlock` properties of the indivdual filters will be ignored
*/
async getMultiEvents(
filters: ColonyMultiFilter[],
options: { fromBlock?: BlockTag; toBlock?: BlockTag } = {},
): Promise<ColonyEvent[]> {
// Unique list of addresses
const addresses = Array.from(
new Set(filters.map(({ address }) => address)),
);
// Unique list of topics
const topics = Array.from(new Set(filters.map(({ topic }) => topic)));
const logs = await getLogs(
{
address: addresses,
fromBlock: options.fromBlock,
toBlock: options.toBlock,
topics: [topics],
},
this.provider,
);
return logs
.map((log) => {
const filter = filters.find(
({ topic, address }) =>
log.topics.includes(topic) &&
addressesAreEqual(address, log.address),
);
if (!filter) return null;
const { eventSource, topic, eventName, address } = filter;
const data = this.eventSources[eventSource].interface.decodeEventLog(
eventName,
log.data,
log.topics,
);
return {
address,
eventSource,
topic,
eventName,
data,
};
})
.filter(nonNullable);
}

createFilter<
T extends EventSource & {
filters: { [P in N]: (...args: never) => EventFilter };
},
N extends keyof T['filters'],
>(
contract: T,
eventName: N,
address?: string,
params?: Parameters<T['filters'][N]>,
options: { fromBlock?: BlockTag; toBlock?: BlockTag } = {},
): ColonyFilter {
// Create standard filter
const filter = contract.filters[eventName].apply(params);
const eventSource = this.getEventSourceName(contract);
if (!eventSource) {
throw new Error(`Could not find event contract for filter`);
}
return {
eventSource,
eventName: eventName as string,
topics: filter.topics,
address,
fromBlock: options.fromBlock,
toBlock: options.toBlock,
};
}

createMultiFilter<
T extends EventSource & {
filters: { [P in N]: (...args: never) => EventFilter };
},
N extends keyof T['filters'],
>(
contract: T,
eventName: N,
address: string,
params?: Parameters<T['filters'][N]>,
): ColonyMultiFilter {
const filter = this.createFilter(contract, eventName, address, params);
// Just use the first topic for now. Let's see how far we get with this. They will be connected with ORs
const topic = ColonyEvents.extractSingleTopic(filter);
if (!topic) {
throw new Error(`Filter does not have any topics: ${eventName}`);
}
delete filter.topics;
const colonyFilter = filter as ColonyMultiFilter;
colonyFilter.topic = topic;
return colonyFilter;
}
}
4 changes: 2 additions & 2 deletions src/ColonyNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
SignerOrProvider,
} from '@colony/colony-js';

import { Colony } from './Colony';
import Colony from './Colony';

export class ColonyNetwork {
export default class ColonyNetwork {
private networkClient: ColonyNetworkClient;

private signerOrProvider: SignerOrProvider;
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export { Tokens } from '@colony/colony-js';

export { ColonyNetwork } from './ColonyNetwork';
export { default as ColonyNetwork } from './ColonyNetwork';
export { default as ColonyEvents } from './ColonyEvents';

export type { ColonyEvent } from './ColonyEvents';
Loading

0 comments on commit 06a9697

Please sign in to comment.