214 lines
7.5 KiB
Plaintext
214 lines
7.5 KiB
Plaintext
_section: Events @<events>
|
|
|
|
_subsection: Logs and Filtering
|
|
|
|
Logs and filtering are used quite often in blockchain applications,
|
|
since they allow for efficient queries of indexed data and provide
|
|
lower-cost data storage when the data is not required to be
|
|
accessed on-chain.
|
|
|
|
These can be used in conjunction with the [Provider Events API](Provider--event-methods)
|
|
and with the [Contract Events API](Contract--events).
|
|
|
|
The Contract Events API also provides [higher-level methods](Contract--filters)
|
|
to compute and query this data, which should be preferred over the lower-level filter.
|
|
|
|
_heading: Filters @<events--filters>
|
|
|
|
When a Contract creates a log, it can include up to 4 pieces of
|
|
data to be indexed by. The indexed data is hashed and included in
|
|
a [[link-wiki-bloomfilter]], which is a data structure that allows
|
|
for efficient filtering.
|
|
|
|
So, a filter may correspondingly have up to 4 topic-sets, where each
|
|
topic-set refers to a condition that must match the indexed log topic
|
|
in that position (i.e. each condition is ``AND``-ed together).
|
|
|
|
If a topic-set is ``null``, a log topic in that position is **not filtered**
|
|
at all and **any value** matches.
|
|
|
|
If a topic-set is a single topic, a log topic in that position **must** match
|
|
**that topic**.
|
|
|
|
If a topic-set is an array of topics, a log topic in that position must
|
|
match **any one** of the topics (i.e. the topic in this position are ``OR``-ed).
|
|
|
|
This may sound complicated at first, but is more easily understood with
|
|
some examples.
|
|
|
|
_table: Example Log Matching @style<full>
|
|
|
|
$TopicABaCD: **[** (topic[0] = A) **OR** (topic[0] = B) **]** **AND**
|
|
**[** (topic[1] = C) **OR** (topic[1] = D) **]**
|
|
|
|
| **Topic-Sets** | **Matching Logs** <<|
|
|
| [ A ] | topic[0] = A <<|
|
|
| [ A, null ] | ^ |
|
|
| [ null, B ] | topic[1] = B <<|
|
|
| [ null, [ B ] ] | ^ |
|
|
| [ null, [ B ], null ] | ^ |
|
|
| [ A, B ] | (topic[0] = A) **AND** (topic[1] = B) <<|
|
|
| [ A, [ B ] ] | ^ |
|
|
| [ A, [ B ], null ] | ^ |
|
|
| [ [ A, B ] ] | (topic[0] = A) **OR** (topic[0] = B) <<|
|
|
| [ [ A, B ], null ] | ^ |
|
|
| [ [ A, B ], [ C, D ] ] | $TopicABaCD <<|
|
|
|
|
|
|
_code: ERC-20 Transfer Filter Examples @lang<javascript>
|
|
|
|
//_hide: const tokenAddress = ethers.constants.AddressZero;
|
|
//_hide: const myAddress = ethers.constants.AddressZero;
|
|
//_hide: const myOtherAddress = ethers.constants.AddressZero;
|
|
//_hide: const id = ethers.utils.id;
|
|
//_hide: const hexZeroPad = ethers.utils.hexZeroPad;
|
|
|
|
// Short example of manually creating filters for an ERC-20
|
|
// Transfer event.
|
|
//
|
|
// Most users should generally use the Contract API to
|
|
// compute filters, as it is much simpler, but this is
|
|
// provided as an illustration for those curious. See
|
|
// below for examples of the equivalent Contract API.
|
|
|
|
// ERC-20:
|
|
// Transfer(address indexed src, address indexed dst, uint val)
|
|
//
|
|
// -------------------^
|
|
// ----------------------------------------^
|
|
//
|
|
// Notice that only *src* and *dst* are *indexed*, so ONLY they
|
|
// qualify for filtering.
|
|
//
|
|
// Also, note that in Solidity an Event uses the first topic to
|
|
// identify the Event name; for Transfer this will be:
|
|
// id("Transfer(address,address,uint256)")
|
|
//
|
|
// Other Notes:
|
|
// - A topic must be 32 bytes; so shorter types must be padded
|
|
|
|
// List all token transfers *from* myAddress
|
|
filter = {
|
|
address: tokenAddress,
|
|
topics: [
|
|
id("Transfer(address,address,uint256)"),
|
|
hexZeroPad(myAddress, 32)
|
|
]
|
|
};
|
|
|
|
// List all token transfers *to* myAddress:
|
|
filter = {
|
|
address: tokenAddress,
|
|
topics: [
|
|
id("Transfer(address,address,uint256)"),
|
|
null,
|
|
hexZeroPad(myAddress, 32)
|
|
]
|
|
};
|
|
|
|
// List all token transfers *to* myAddress or myOtherAddress:
|
|
filter = {
|
|
address: tokenAddress,
|
|
topics: [
|
|
id("Transfer(address,address,uint256)"),
|
|
null,
|
|
[
|
|
hexZeroPad(myAddress, 32),
|
|
hexZeroPad(myOtherAddress, 32),
|
|
]
|
|
]
|
|
};
|
|
|
|
_null:
|
|
|
|
To simplify life, ..., explain here, the contract API
|
|
|
|
|
|
_code: ERC-20 Contract Filter Examples @lang<javascript>
|
|
|
|
//_hide: const tokenAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; /* DAI */
|
|
//_hide: const myAddress = "0x8ba1f109551bD432803012645Ac136ddd64DBA72";
|
|
//_hide: const otherAddress = "0xEA517D5a070e6705Cc5467858681Ed953d285Eb9";
|
|
//_hide: const provider = ethers.getDefaultProvider();
|
|
//_hide: const Contract = ethers.Contract;
|
|
|
|
abi = [
|
|
"event Transfer(address indexed src, address indexed dst, uint val)"
|
|
];
|
|
|
|
contract = new Contract(tokenAddress, abi, provider);
|
|
|
|
// List all token transfers *from* myAddress
|
|
//_result:
|
|
contract.filters.Transfer(myAddress)
|
|
//_log:
|
|
|
|
// List all token transfers *to* myAddress:
|
|
//_result:
|
|
contract.filters.Transfer(null, myAddress)
|
|
//_log:
|
|
|
|
// List all token transfers *from* myAddress *to* otherAddress:
|
|
//_result:
|
|
contract.filters.Transfer(myAddress, otherAddress)
|
|
//_log:
|
|
|
|
// List all token transfers *to* myAddress OR otherAddress:
|
|
//_result:
|
|
contract.filters.Transfer(null, [ myAddress, otherAddress ])
|
|
//_log:
|
|
|
|
|
|
_subsection: Solidity Topics @<events-solidity>
|
|
|
|
This is a quick (and non-comprehensive) overview of how events are computed
|
|
in Solidity.
|
|
|
|
This is likely out of the scope for most developers, but may be interesting
|
|
to those who want to learn a bit more about the underlying technology.
|
|
|
|
Solidity provides two types of events, anonymous and non-anonymous. The
|
|
default is non-anonymous, and most developers will not need to worry about
|
|
anonymous events.
|
|
|
|
For non-anonymous events, up to 3 topics may be indexed (instead of 4), since
|
|
the first topic is reserved to specify the event signature. This allows
|
|
non-anonymous events to always be filtered by their event signature.
|
|
|
|
This topic hash is always in the first slot of the indexed data, and is
|
|
computed by normalizing the Event signature and taking the keccak256 hash
|
|
of it.
|
|
|
|
For anonymous events, up to 4 topics may be indexed, and there is no
|
|
signature topic hash, so the events cannot be filtered by the event
|
|
signature.
|
|
|
|
Each additional indexed property is processed depending on whether its
|
|
length is fixed or dynamic.
|
|
|
|
For fixed length types (e.g. ``uint``, ``bytes5``), all of which are
|
|
internally exactly 32 bytes (shorter types are padded with zeros;
|
|
numeric values are padded on the left, data values padded on the right),
|
|
these are included directly by their actual value, 32 bytes of data.
|
|
|
|
For dynamic types (e.g. ``string``, ``uint256[]``) , the value is hashed
|
|
using keccak256 and this hash is used.
|
|
|
|
Because dynamic types are hashed, there are important consequences in
|
|
parsing events that should be kept in mind. Mainly that the original
|
|
value is lost in the event. So, it is possible to tell is a topic is
|
|
equal to a given string, but if they do not match, there is no way
|
|
to determine what the value was.
|
|
|
|
If a developer requires that a string value is required to be both
|
|
able to be filtered and also able to be read, the value must be included
|
|
in the signature twice, once indexed and once non-indexed (e.g.
|
|
``someEvent(string indexed searchBy, string clearText)``).
|
|
|
|
For a more detailed description, please refer to the
|
|
[Solidity Event Documentation](link-solidity-events).
|
|
|
|
_heading: Other Things? TODO
|
|
|
|
Explain what happens to strings and bytes, how to filter and retain the value
|