This repository has been archived by the owner on May 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #125 from JoinColony/feat/events
Add ColonyEvents including example
- Loading branch information
Showing
13 changed files
with
320 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Oops, something went wrong.