4
0
mirror of git://holbrook.no/eth-monitor.git synced 2025-04-08 21:01:02 +02:00

Compare commits

...

38 Commits

Author SHA1 Message Date
lash
ca80c0d75f
Bump version 2024-04-02 12:31:38 +01:00
lash
d7ee238bff
Fix pysha break 2024-04-02 12:21:55 +01:00
lash
2a54256821
Add state rundir with block height output 2023-08-19 11:04:52 +01:00
lash
a29ae35597
Skip cache rules filter when deactivated 2023-08-17 13:27:12 +01:00
lash
c99259b2ed
Add match-all criteria and flag 2023-08-17 12:48:33 +01:00
lash
fa694c957b
Start filter test writing 2023-08-17 10:28:27 +01:00
lash
e0b6e2c14b
Upgrade chainsyncer, remove state corruption bug on interrupt and partial filter list 2023-08-14 09:48:56 +01:00
lash
e176874c30
Handle crash on conrtact creation when recipient filter is active 2023-08-13 18:36:20 +01:00
lash
10e16dcb00
Implement and expose dialect filters for chain interface 2023-08-13 17:58:18 +01:00
lash
d36b3ed673
Update man page with rpc batch limit setting 2023-08-08 19:17:50 +01:00
lash
3f5d24a6c1
Update man pages 2023-08-08 09:10:59 +01:00
lash
3dee984eb9
Handle missing context key arg 2023-08-07 12:48:23 +01:00
lash
39ee38b0dc
Implement syncer context 2023-08-06 19:32:10 +01:00
lash
270a35bada
Implement RPC batch limit 2023-08-06 14:10:59 +01:00
lash
a66bb4e12d
Make rpc dialect work with chain interface 2023-06-03 08:32:47 +01:00
lash
f72b1740d6
Add readme 2023-05-13 21:46:49 +01:00
lash
705bdc9471
Add man in packaging, add missing config dir in manifest 2023-05-13 21:05:04 +01:00
lash
f2733b50f9
Upgrade deps 2023-03-22 10:38:58 +00:00
lash
5598740173
Correct tx index in default log sync cli output 2023-02-23 07:16:30 +00:00
lash
c6b3ef6707
Change license to AGPL3, waive copyright 2022-11-14 07:55:47 +00:00
lash
b65d63daa1
Add missing log module in cli lib 2022-11-12 09:20:46 +00:00
lash
b8fe525cbe
Reactivate cache rpc 2022-11-10 13:46:35 +00:00
lash
f59fd52d43
Bump version 2022-11-10 10:05:36 +00:00
lash
a175ea79ee
Update filter and callback signatures to chainsyncer 0.7.x 2022-11-10 08:07:59 +00:00
lash
243bc86b73
Handle missing filters from arg 2022-11-04 07:50:47 +00:00
lash
fe9b657b36
Handle filters and renderers from args in settings 2022-10-14 15:41:58 +00:00
lash
2f3e652ca3
Avoid having to specify session dir when using mem backend 2022-10-13 07:22:00 +00:00
lash
eedc886179
Avoid having to specify session dir when using mem backend 2022-10-13 07:13:31 +00:00
lash
a7814de98d
Update syncer settings code calls 2022-05-14 20:58:26 +00:00
lash
28584aec35
Bump minor version 2022-05-14 19:32:08 +00:00
lash
02d27ef167
Implement chainlib 0.3.0 structure 2022-05-13 07:11:54 +00:00
lash
d5b4a8d362
Upgrade chainsyncer 2022-05-10 18:33:46 +00:00
lash
177aa95abe
Move remaining setup to settings module 2022-05-10 18:28:45 +00:00
lash
39dc90fe9e
Move rules handling to settings module 2022-05-10 14:21:49 +00:00
lash
80eee2b779
WIP move rules compilation to settings module 2022-05-10 11:25:05 +00:00
lash
239e10ba5a
WIP implement chainlib cli and settings 2022-05-10 11:07:25 +00:00
lash
bc3f4ff8fb
Bump version, upgrade chainlib to 0.2.0 2022-05-10 10:10:47 +00:00
lash
b87aa875f0
Add block filter cmd option 2022-05-07 11:33:35 +00:00
42 changed files with 2477 additions and 526 deletions

View File

@ -1,3 +1,52 @@
- 0.8.8
* Skip rules filter processing for cache when deactivated
* Add match-all flag to rule processing
* Add match-all flag to CLI to toggle setting match_all flag to rule processing for include criteria
* Implement state dir (rundir), and last synced block output
- 0.8.7
* Upgrade chainsyncer (and shep) to avoid state deletion on partial filter list interrupts
- 0.8.6
* Handle crash on conrtact creation when recipient filter is active
- 0.8.5
* Instantiate constructor for chain interface superclass
* Remove unused settings transform method for sync interface
- 0.8.4
* Update man pages with rpc batch limit setting
- 0.8.3
* Update man pages
- 0.8.2
* Handle undefined content-key argument
- 0.8.1
* Implement syncer context
* Add key-value parameter for cli, environment, config to pass to syncer context
- 0.8.0
* Implement RPC batch limits for syncer
- 0.7.6
* Make rpc dialect work with chain interface
- 0.7.5
* Add readme (from man page)
* Add package descriptino (from man page)
- 0.7.4
* Add man pages to package
- 0.7.3
* Upgrade deps
- 0.7.2
* Fix tx index in default output for sync command
- 0.7.1
* Change license to AGPL3 and copyright waived to public domain
* Add missing log module in cli lib
- 0.7.0
* Reactivate cache rpc
* Upgrade dependencies
- 0.6.0
* Allow missing filters in arugments
- 0.5.1
* Remove useless need to add session dir for mem backend
* Reenable renderers and filters specified through args in new settings setup
- 0.5.0
* Implement on chainlib 0.3.0
- 0.4.8
* Upgrade chainlib to 0.2.0
- 0.4.5
* Upgrade chainsyncer
- 0.4.4

141
LICENSE
View File

@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@ -7,17 +7,15 @@
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -1 +1 @@
include *requirements.txt eth_monitor/data/config/*
include *requirements.txt LICENSE CHANGELOG WAIVER WAIVER.asc eth_monitor/data/config/* man/build/*.1 README*

View File

@ -1,10 +1,12 @@
PREFIX ?= /usr/local
BUILD_DIR = build/$(PREFIX)/share/man
#BUILD_DIR = build/$(PREFIX)/share/man
MAN_DIR = man
man:
mkdir -vp $(BUILD_DIR)
chainlib-man.py -b 0xbf -v -n eth-monitor -d $(BUILD_DIR)/ man
chainlib-man.py -b 0xbf -v -n eth-monitor-list -d $(BUILD_DIR)/ man
chainlib-man.py -b 0xbf -v -n eth-monitor-import -d $(BUILD_DIR)/ man
mkdir -vp $(MAN_DIR)/build
chainlib-man.py -b 0xbf -v -n eth-monitor -d $(MAN_DIR)/build $(MAN_DIR)
cp -v $(MAN_DIR)/build/eth-monitor.1 $(MAN_DIR)/build/eth-monitor-sync.1
chainlib-man.py -b 0xbf -v -n eth-monitor-list -d $(MAN_DIR)/build $(MAN_DIR)
chainlib-man.py -b 0xbf -v -n eth-monitor-import -d $(MAN_DIR)/build $(MAN_DIR)
.PHONY: man

284
README.md Normal file
View File

@ -0,0 +1,284 @@
# NAME
eth-monitor - Cache, index and monitor transactions with an EVM node rpc
# SYNOPSIS
**eth-monitor** \[ --skip-history \] \[ --single \] \[ p *eth_provider*
\] \[ --includes-file *file* \] \[ -i chain_spec \] **eth-monitor** \[
--skip-history \] \[ --single \] \[ p *eth_provider* \] \[
--excludes-file *file* \] \[ --include-default \] \[ -i chain_spec \] 
# DESCRIPTION
The **eth-monitor** has fulfills three distinct but related functions:
> 1\. A customizable view of on transactions of interest.
>
> 2\. A block and transaction cache.
>
> 3\. Arbitrary code executions using a transaction (and its block) as
> input.
Using an EVM RPC endpoint, the **eth-monitor** tool will retrieve blocks
within a given range and provides arbitrary processing of each
transaction.
A collection of options is provided to control the behavior of which
block ranges to sync, which criteria to use for display and cache, and
what code to execute for matching transactions. Details on each topic
can be found in the **SYNCING**, **MATCHING ADDRESSES** and **DEFINING
FILTERS** sections below, respectively.
Example executions of the tool can be found in the **EXAMPLES** section.
## OPTIONS
**-0**
Omit newline to output
**--address ***address*
Add an address of interest to match any role. Complements
**--address-file***.*
**-c ***config_dir, ***--config ***config_dir*
Load configuration files from given directory. All files with an .ini
extension will be loaded, of which all must contain valid ini file data.
**--dumpconfig ***format*
Output configuration settings rendered from environment and inputs.
Valid arguments are *ini for ini file output, and env for environment
variable output. See ***CONFIGURATION***.*
**--env-prefix**
Environment prefix for variables to overwrite configuration. Example: If
**--env-prefix*** is set to ***FOO*** then configuration variable
***BAR_BAZ*** would be set by environment variable ***FOO_BAZ_BAR***.
Also see ***ENVIRONMENT***.*
**--excludes-file ***file*
Load address exclude matching rules from file. See **MATCHING
ADDRESSES***.*
**--exec ***address*
Add an address of interest to executable address array. Complements
**--address-file***.*
**--filter ***module*
Add code execution filter to all matched transactions. The argument must
be a python module path. Several filters may be added by supplying the
option multiple times. Filters will be executed in the order the options
are given. See **DEFINING FILTERS*** section of ***eth-monitor (1)***
for more details.*
**--height**
Block height at which to query state for. Does not apply to
transactions.
**-i ***chain_spec, ***--chain-spec ***chain_spec*
Chain specification string, in the format
\<engine\>:\<fork\>:\<chain_id\>:\<common_name\>. Example:
"evm:london:1:ethereum". Overrides the *RPC_CREDENTIALS configuration
setting.*
**--include-default **
Match all addresses by default. Addresses may be excluded using
--excludes-file. If this is set, --input, --output, --exec and
--includes-file will have no effect.
**--includes-file ***file*
Load address include matching rules from file. See **MATCHING
ADDRESSES***.*
**--input ***address*
Add an address of interest to inputs (recipients) array. Complements
**--address-file***.*
**-n ***namespace, ***--namespace ***namespace*
Load given configuration namespace. Configuration will be loaded from
the immediate configuration subdirectory with the same name.
**--no-logs**
Turn of logging completely. Negates **-v*** and ***-vv**
**--output ***address*
Add an address of interest to outputs (sender) array. Complements
**--address-file***.*
**-p***, ***--rpc-provider**
Fully-qualified URL of RPC provider. Overrides the *RPC_PROVIDER
configuration setting.*
**--raw**
Produce output most optimized for machines.
**--renderer ***module*
Add output renderer filter to all matched transactions. The argument
must be a python module path. Several renderers may be added by
supplying the option multiple times. See **RENDERERS*** section of
***eth-monitor (1)*** for more details.*
**--rpc-dialect**
RPC backend dialect. If specified it *may help with encoding and
decoding issues. Overrides the RPC_DIALECT configuration setting.*
**--store-block-data **
Store block data in cache for matching transactions. Requires
**--cache-dir***.*
**--store-tx-data **
Store transaction data in cache for matching transactions. Requires
**--cache-dir***.*
**-u***, ***--unsafe**
Allow addresses that do not pass checksum.
**-v**
Verbose. Show logs for important state changes.
**-vv**
Very verbose. Show logs with debugging information.
**--x-address ***address*
Add an address of interest to match any role.
**--x-exec ***address*
Add an address of disinterest to executable address array.
**--x-input ***address*
Add an address of disinterest to inputs (recipients) array.
**--x-output ***address*
Add an address of disinterest to outputs (sender) array.
# CONFIGURATION
All configuration settings may be overriden both by environment
variables, or by overriding settings with the contents of ini-files in
the directory defined by the **-c*** option.*
The active configuration, with values assigned from environment and
arguments, can be output using the **--dumpconfig*** format option. Note
that entries having keys prefixed with underscore (e.g. \_SEQ) are not
actual configuration settings, and thus cannot be overridden with
environment variables.*
To refer to a configuration setting by environment variables, the
*section and key are concatenated together with an underscore, and
transformed to upper-case. For example, the configuration variable
FOO_BAZ_BAR refers to an ini-file entry as follows:*
[foo]
bar_baz = xyzzy
In the **ENVIRONMENT*** section below, the relevant configuration
settings for this tool is listed along with a short description of its
meaning.*
Some configuration settings may also be overriden by command line
options. Also note that the use of the **-n*** and ***--env-prefix***
options affect how environment and configuration is read. The effects of
options on how configuration settings are affective is described in the
respective ***OPTIONS*** section.*
# MATCHING ADDRESSES
By default, addresses to match against transactions need to be
explicitly specified. This behavior can be reversed with the
**--include-default*** option. Addresses to match are defined using the
***--input***, ***--output*** and ***--exec*** options. Addresses
specified multiple times will be deduplicated.*
Inclusion rules may also be loaded from file by specifying the
**--includes-file*** and ***--excludes-file*** options. Each file must
specify the outputs, inputs and exec addresses as comma separated lists
respectively, separated by tabs.*
In the current state of this tool, address matching will affect all
parts of the processing; cache, code execution and rendering.
# SYNCING
When a sync is initiated, the state of this sync is persisted. This way,
previous syncs that did not complete for some reason will be resumed
where they left off.
A special sync type **--head*** starts syncing at the current head of
the chain, and continue to sync until interrupted. When resuming sync, a
new sync range between the current block head and the block height at
which the previous ***--head*** sync left off will automatically be
created.*
Syncs can be forced to (re)run for ranges regardless of previous state
by using the **--single*** option. However, there is no protection in
place from preventing code filters from being executed again on the same
transaction when this is done. See ***DEFINING FILTERS*** below.*
# CACHE
When syncing, the hash of a block and transaction matching the address
criteria will be stored in the cache. The hashes can be used for future
data lookups.
If **--store-block-data*** and/or ***--store-tx-data*** is set, a copy
of the block and/or transaction data will also be stored, respectively.*
# RENDERING
Rendering in the context of **eth-monitor*** refers to a formatted
output stream that occurs independently of caching and code execution.*
Filters for rendering may be specified by specifying python modules to
the **--renderer*** option. This option may be specified multiple
times.*
Rendering filters will be executed in order, and the first filter to
return *False*
# DEFINING FILTERS
A python module used for filter must fulfill two conditions:
> 1\. It must provide a class named *Filter in the package base
> namespace.*
>
> 2\. The *Filter class must include a method named filter with the
> signature def filter(self, conn, block, tx, db_session=None). *
Filters will strictly be executed in the order which they are defined on
the command line.
# FURTHER READING
Refer to the **chainsyncer*** chapter n info chaintool for in-depth
information on the subjects of syncing and filtering.*
# ENVIRONMENT
*CHAIN_SPEC*
String specifying the type of chain connected to, in the format
*\<engine\>:\<fork\>:\<network_id\>:\<common_name\>. For EVM nodes the
engine value will always be evm.*
*RPC_DIALECT*
Enables translations of EVM node specific formatting and response codes.
*RPC_PROVIDER*
Fully-qualified URL to the RPC endpoint of the blockchain node.
# LICENSE
This documentation and its source is licensed under the Creative Commons
Attribution-Sharealike 4.0 International license.
The source code of the tool this documentation describes is licensed
under the GNU General Public License 3.0.
# COPYRIGHT
Louis Holbrook \<dev@holbrook.no\> (https://holbrook.no) PGP:
59A844A484AC11253D3A3E9DCDCBD24DD1D0E001
# SOURCE CODE
https://git.defalsify.org

17
WAIVER Normal file
View File

@ -0,0 +1,17 @@
# Copyright waiver for the python package "eth-monitor"
I dedicate any and all copyright interest in this software to the
public domain. I make this dedication for the benefit of the public at
large and to the detriment of my heirs and successors. I intend this
dedication to be an overt act of relinquishment in perpetuity of all
present and future rights to this software under copyright law.
To the best of my knowledge and belief, my contributions are either
originally authored by me or are derived from prior works which I have
verified are also in the public domain and are not subject to claims
of copyright by other parties.
To the best of my knowledge and belief, no individual, business,
organization, government, or other entity has any copyright interest
in my contributions, and I affirm that I will not make contributions
that are otherwise encumbered.

29
WAIVER.asc Normal file
View File

@ -0,0 +1,29 @@
-----BEGIN PGP MESSAGE-----
owGVU39QFFUc58fFwHYYYvwq0cfPoO6umsoEJgMJ4wQVGn7IiMje3ru7B7e7d/vj
rmMwS7iwMPImp8LUIGdATWgsHamjyYCQPGZkYEIkQ7QcEobRVIYGTXu7B2r1V3/c
zO2+7/fz8+2uYH+fQN+YwQn3jq7WNb5t/rwuoDhDW5T1GmW96YwDmazFwSGjSQB2
EtkgBwwsBwQTBBaHYGIZYCGpStIIQSwUTGqaZZDAcrEEoQV6qEcUKUBAMg780wPS
bAbUPTTECJCDvPQHwyEe8KxBsJMcBAIr4RMWUWdGFNCzNIkYDdACmqyE3tF5aITp
F9ToIAMNSACswSvOu0wKhJnkjFDm9+LiZYFDNGTkWdoBTBBxvDzAixQFeZ7leIlO
EihtYULiAUKMopPwAIvDEABJyTgcNCPGKiLeJCNjTxbIWaAgIsEhnWPvhAXblQ4l
KoMoiNiqHAXvVfZgBCKjx0nfD8tM2jUEUcDOW+UXtFcyrN0M9fMGdVgENKikA4pl
sEudKEnG5jAkRHiXI1gMiBgsB3ci4gI5iPccgIYABynNYWJcM1bIsTSwcAi/trNc
JQ/sJkSZcC4m0gYJ7B0ZEB6TVkgzz3prvBe8tzVv7XiCYQWcrq4C4rSwWcpMIpon
sIf7FrEIVlKILxQnIMj/D7+MxK5HNqQXSbMK6EQeMbhHFTZrJBlUJRenAkapMUbq
RyWZ9bLhJ6kiE8nL9/S/95PANv4dqEqm1wLSYEAcjUWSAn6yI3zBJafyRf3HAiGP
SEnIrHbE40IYSqR1mEOvId7xe07h4xvoEx0eowhMnG62qJfV9V6qy1/4LB/ykz5G
HyIoZOFNd90jtxQHErIvnUw+crf7TkR7+uXvTo3EF1TtnJoq72mEuYl96unSuZCI
8zDj9Ppjm3unPFlP3bg9Xp7KbOskM5OuG/6ccl17V/vsiLt4Q8qXnujZjceO1Mwe
3fHm1pOhqc6WU+uvDpS8Yu+mF/kPLEkqVuZuGdwY3DFdv51uSPg0Mj8kq93xbQfV
8Jerv+tVpXbvocLG1LSKQ+1Lgt8W7dd+y94VUNZ6Tlml73um+pfhbfqBmR/O1nKj
YQH7xjTllr7C1V2LfdTDdfFXnwbWE4teyEmfW5wUalwXPe23O/r7nJXvryrpWD4G
n3j8p6HkweaalIuPOXO+2fTh5pSgioo97tKZccVeNTFX+YHxRubnEVHK30d2Hld0
Dm1tWXewv35Prtv3eJNr7mNdUUZV650y99qm/ZdbHK9PehJXwpnJ0WXh8KP86pzc
ULfTj7bF6iOjFNn7mtq+GA8vS9mdN7G6wDX86IraTav+CANKD3K+cdD/4X5Tz+TR
puaCHyeH2p8nn7yotqTF5sc3xrV+0tOpGdgf1G1962bRr4eXj4vp9FiYx2rz+FWt
Ma14ObL5q3Mp57dUO/NqPjszd/tKEGcLnTibfCu9ovGMUFLa67owU852LVXWD96d
bLii2B7cdjhG8+KGn8vFAy+9N+tKczgzHa64wrxGd28XuzQ1avpEWu7Xa1NrL4Se
TrDqRq//DQ==
=yHWM
-----END PGP MESSAGE-----

39
eth_monitor/callback.py Normal file
View File

@ -0,0 +1,39 @@
# standard imports
import logging
logg = logging.getLogger(__name__)
class BlockCallbackFilter:
def __init__(self):
self.filters = []
def register(self, fltr):
self.filters.append(fltr)
def filter(self, conn, block):
for fltr in self.filters:
fltr.filter(conn, block)
def state_change_callback(k, old_state, new_state):
logg.log(logging.STATETRACE, 'state change: {} {} -> {}'.format(k, old_state, new_state))
def filter_change_callback(k, old_state, new_state):
logg.log(logging.STATETRACE, 'filter change: {} {} -> {}'.format(k, old_state, new_state))
def pre_callback(conn):
logg.debug('starting sync loop iteration')
def post_callback(conn):
logg.debug('ending sync loop iteration')
def block_callback(conn, block):
logg.info('processing {} {}'.format(block, datetime.datetime.fromtimestamp(block.timestamp)))

View File

@ -1,18 +1,26 @@
# external imports
from chainlib.interface import ChainInterface
from chainlib.eth.block import (
block_latest,
block_by_number,
Block,
)
from chainlib.eth.tx import (
receipt,
Tx,
transaction,
)
class EthChainInterface(ChainInterface):
def __init__(self):
def __init__(self, dialect_filter=None, batch_limit=1):
super(EthChainInterface, self).__init__(dialect_filter=dialect_filter, batch_limit=batch_limit)
self.batch_limit = batch_limit
self._block_latest = block_latest
self._block_by_number = block_by_number
self._block_from_src = Block.from_src
self._tx_from_src = Tx.from_src
self._tx_receipt = receipt
self._src_normalize = Tx.src_normalize
self._dialect_filter = dialect_filter
self._tx_by_hash = transaction

View File

@ -0,0 +1,2 @@
from .arg import process_args
from .config import process_config

37
eth_monitor/cli/arg.py Normal file
View File

@ -0,0 +1,37 @@
def process_args(argparser, args, flags):
# session flags
argparser.add_argument('--state-dir', dest='state_dir', type=str, help='Directory to store sync state')
argparser.add_argument('--session-id', dest='session_id', type=str, help='Use state from specified session id')
argparser.add_argument('--cache-dir', dest='cache_dir', type=str, help='Directory to store tx data')
# address rules flags
argparser.add_argument('--input', action='append', type=str, help='Add input (recipient) addresses to includes list')
argparser.add_argument('--output', action='append', type=str, help='Add output (sender) addresses to includes list')
argparser.add_argument('--exec', action='append', type=str, help='Add exec (contract) addresses to includes list')
argparser.add_argument('--data', action='append', type=str, help='Add data prefix strings to include list')
argparser.add_argument('--data-in', action='append', dest='data_in', type=str, help='Add data contain strings to include list')
argparser.add_argument('--x-data', action='append', dest='x_data', type=str, help='Add data prefix string to exclude list')
argparser.add_argument('--x-data-in', action='append', dest='x_data_in', type=str, help='Add data contain string to exclude list')
argparser.add_argument('--address', action='append', type=str, help='Add addresses as input, output and exec to includes list')
argparser.add_argument('--x-input', action='append', type=str, dest='x_input', help='Add input (recipient) addresses to excludes list')
argparser.add_argument('--x-output', action='append', type=str, dest='x_output', help='Add output (sender) addresses to excludes list')
argparser.add_argument('--x-exec', action='append', type=str, dest='x_exec', help='Add exec (contract) addresses to excludes list')
argparser.add_argument('--x-address', action='append', type=str, dest='x_address', help='Add addresses as input, output and exec to excludes list')
argparser.add_argument('--includes-file', type=str, dest='includes_file', help='Load include rules from file')
argparser.add_argument('--excludes-file', type=str, dest='excludes_file', help='Load exclude rules from file')
argparser.add_argument('--include-default', dest='include_default', action='store_true', help='Include all transactions by default')
# filter flags
argparser.add_argument('--renderer', type=str, action='append', default=[], help='Python modules to dynamically load for rendering of transaction output')
argparser.add_argument('--filter', type=str, action='append', help='Add python module to tx filter path')
argparser.add_argument('--block-filter', type=str, dest='block_filter', action='append', help='Add python module to block filter path')
# cache flags
argparser.add_argument('--store-tx-data', action='store_true', dest='store_tx_data', help='Store tx data in cache store')
argparser.add_argument('--store-block-data', action='store_true', dest='store_block_data', help='Store block data in cache store')
argparser.add_argument('--fresh', action='store_true', help='Do not read block and tx data from cache, even if available')
argparser.add_argument('--match-all', action='store_true', dest='match_all', help='Match all include filter criteria')
# misc flags
argparser.add_argument('-k', '--context-key', dest='context_key', action='append', type=str, help='Add a key-value pair to be added to the context')
argparser.add_argument('--run-dir', type=str, dest='run_dir', help='Output key sync and processing state properties to given diretory')

62
eth_monitor/cli/config.py Normal file
View File

@ -0,0 +1,62 @@
# local imports
from .rules import (
rules_address_args,
rules_data_args,
to_config_names,
)
def process_config(config, arg, args, flags):
arg_override = {}
rules_args = rules_address_args + rules_data_args
for rules_arg in rules_args:
(vy, vn) = to_config_names(rules_arg)
arg = getattr(args, rules_arg)
if arg == None:
v = config.get(vy)
if bool(v):
arg_override[vy] = v.split(',')
else:
arg_override[vy] = arg
arg = getattr(args, 'x_' + rules_arg)
if arg == None:
v = config.get(vn)
if bool(v):
arg_override[vn] = v.split(',')
else:
arg_override[vn] = arg
arg_override['ETHMONITOR_INCLUDES_FILE'] = getattr(args, 'includes_file')
arg_override['ETHMONITOR_EXCLUDES_FILE'] = getattr(args, 'excludes_file')
arg_override['ETHMONITOR_INCLUDE_DEFAULT'] = getattr(args, 'include_default')
arg_override['ETHMONITOR_RENDERER'] = getattr(args, 'renderer')
arg_override['ETHMONITOR_FILTER'] = getattr(args, 'filter')
arg_override['ETHMONITOR_BLOCK_FILTER'] = getattr(args, 'block_filter')
arg_override['ETHMONITOR_STATE_DIR'] = getattr(args, 'state_dir')
arg_override['ETHMONITOR_CONTEXT_KEY'] = getattr(args, 'context_key')
arg_override['ETHMONITOR_MATCH_ALL'] = getattr(args, 'match_all')
arg_override['ETHCACHE_STORE_BLOCK'] = getattr(args, 'store_block_data')
arg_override['ETHCACHE_STORE_TX'] = getattr(args, 'store_tx_data')
config.dict_override(arg_override, 'local cli args')
for rules_arg in rules_args:
(vy, vn) = to_config_names(rules_arg)
if config.get(vy) == None:
config.add([], vy, True)
if config.get(vn) == None:
config.add([], vn, True)
config.add(getattr(args, 'session_id'), '_SESSION_ID', False)
config.add(getattr(args, 'cache_dir'), '_CACHE_DIR', False)
config.add(getattr(args, 'run_dir'), '_RUN_DIR', False)
config.add(getattr(args, 'fresh'), '_FRESH', False)
return config

21
eth_monitor/cli/log.py Normal file
View File

@ -0,0 +1,21 @@
# standard imports
import logging
# external imports
from chainlib.cli.log import process_log as base_process_log
logging.STATETRACE = 5
def process_log(args, logger):
if args.vvv:
logger.setLevel(logging.STATETRACE)
else:
logger = base_process_log(args, logger)
logging.getLogger('chainlib.connection').setLevel(logging.WARNING)
logging.getLogger('chainlib.eth.tx').setLevel(logging.WARNING)
logging.getLogger('chainlib.eth.src').setLevel(logging.WARNING)
return logger

16
eth_monitor/cli/rules.py Normal file
View File

@ -0,0 +1,16 @@
rules_address_args = [
'input',
'output',
'exec',
'address',
]
rules_data_args = [
'data',
'data_in',
]
def to_config_names(v):
v = v.upper()
return ('ETHMONITOR_' + v, 'ETHMONITOR_X_' + v)

View File

@ -0,0 +1,3 @@
[ethcache]
store_tx = 0
store_block = 0

View File

@ -1,2 +0,0 @@
[chain]
spec = evm:berlin:1:ethereum

View File

@ -0,0 +1,22 @@
[ethmonitor]
input =
output =
exec =
x_input =
x_output =
x_exec =
address =
x_address =
data =
x_data =
data_in =
x_data_in =
includes_file =
excludes_file =
renderer =
filter =
block_filter =
include_default = 0
state_dir = ./.eth-monitor
context_key =
match_all = 0

View File

@ -1,3 +0,0 @@
[syncer]
loop_interval = 5
backend = mem

2
eth_monitor/error.py Normal file
View File

@ -0,0 +1,2 @@
class RuleFail(Exception):
pass

View File

@ -12,26 +12,11 @@ logg = logging.getLogger(__name__)
class RuledFilter(SyncFilter):
def __init__(self, rules_filter=None):
if self.store.chain_dir == None:
raise RuntimeError('store must be initialized. call RuledFilter.init() first')
def __init__(self, rules_filter=None, store=None):
self.rules_filter = rules_filter
@staticmethod
def init(store, include_block_data=False, include_tx_data=False):
RuledFilter.store = store
RuledFilter.include_block_data = include_block_data
RuledFilter.include_tx_data = include_tx_data
@classmethod
def block_callback(cls, block, extra=None):
logg.info('processing {}'.format(block))
cls.store.put_block(block, include_data=cls.include_block_data)
def filter(self, conn, block, tx, db_session=None):
def filter(self, conn, block, tx, **kwargs):
if self.rules_filter != None:
if not self.rules_filter.apply_rules(tx):
logg.debug('rule match failed for tx {}'.format(tx.hash))

View File

@ -0,0 +1,13 @@
# standard imports
import os
class Filter:
def __init__(self, store, include_block_data=False):
self.store = store
self.include_block_data = include_block_data
def filter(self, conn, block, **kwargs):
self.store.put_block(block, include_data=self.include_block_data)

View File

@ -9,14 +9,20 @@ logg = logging.getLogger(__name__)
class Filter(RuledFilter):
def ruled_filter(self, conn, block, tx, db_session=None):
def __init__(self, store, rules_filter=None, include_tx_data=False):
super(Filter, self).__init__(rules_filter=rules_filter)
self.store = store
self.include_tx_data = include_tx_data
def ruled_filter(self, conn, block, tx, **kwargs):
self.store.put_tx(tx, include_data=self.include_tx_data)
def filter(self, conn, block, tx, db_session=None):
r = super(Filter, self).filter(conn, block, tx, db_session=db_session)
def filter(self, conn, block, tx, **kwargs):
r = super(Filter, self).filter(conn, block, tx, **kwargs)
if r == True:
return True
self.ruled_filter(conn, block, tx, db_session=db_session)
self.ruled_filter(conn, block, tx, **kwargs)
return False

View File

@ -16,7 +16,7 @@ logg = logging.getLogger(__name__)
# Interface defining the signature for renderer in OutFilter
# return string after local transformation
def apply_interface(c, s, chain_str, conn, block, tx, db_session=None):
def apply_interface(c, s, chain_str, conn, block, tx, **kwargs):
pass
@ -49,8 +49,8 @@ class OutFilter(RuledFilter):
self.result = OutResult()
def filter(self, conn, block, tx, db_session=None):
r = super(OutFilter, self).filter(conn, block, tx, db_session=db_session)
def filter(self, conn, block, tx, **kwargs):
r = super(OutFilter, self).filter(conn, block, tx, **kwargs)
if r == True:
return True
@ -74,7 +74,7 @@ class OutFilter(RuledFilter):
datetime.datetime.fromtimestamp(block.timestamp),
block.number,
strip_0x(block.hash),
tx.index,
tx.index + 1,
tx_count,
strip_0x(tx.hash),
tx.status.name,

View File

@ -0,0 +1,16 @@
# standard imports
import os
class Filter:
def __init__(self, run_dir):
self.run_dir = run_dir
self.fp = os.path.join(run_dir, 'block')
def filter(self, conn, block):
f = open(self.fp, 'w')
f.write(str(block.number))
f.close()
return False

View File

@ -35,7 +35,7 @@ class Importer:
block = Block(r)
if self.block_callback != None:
self.block_callback(block)
self.block_callback(None, block)
tx_src = block.txs[int(v['transactionIndex'])]

View File

@ -4,6 +4,7 @@ import uuid
# external imports
from chainlib.eth.address import is_same_address
from .error import RuleFail
logg = logging.getLogger()
@ -11,14 +12,17 @@ logg = logging.getLogger()
class RuleData:
def __init__(self, fragments, description=None):
def __init__(self, fragments, description=None, match_all=False):
self.fragments = fragments
self.description = description
if self.description == None:
self.description = str(uuid.uuid4())
self.match_all = match_all
def check(self, sender, recipient, data, tx_hash):
have_fail = False
have_match = False
if len(self.fragments) == 0:
return False
@ -28,9 +32,16 @@ class RuleData:
continue
if fragment in data:
logg.debug('tx {} rule {} match in DATA FRAGMENT {}'.format(tx_hash, self.description, fragment))
return True
if not self.match_all:
return True
have_match = True
else:
logg.debug('data match all {}'.format(self.match_all))
if self.match_all:
return False
have_fail = True
return False
return have_match
def __str__(self):
@ -41,11 +52,13 @@ class RuleData:
class RuleMethod:
def __init__(self, methods, description=None):
def __init__(self, methods, description=None, match_all=False):
self.methods = methods
self.description = description
if self.description == None:
self.description = str(uuid.uuid4())
if match_all:
logg.warning('match_all ignord for RuleMethod rule')
def check(self, sender, recipient, data, tx_hash):
@ -71,28 +84,62 @@ class RuleMethod:
class RuleSimple:
def __init__(self, outputs, inputs, executables, description=None):
def __init__(self, outputs, inputs, executables, description=None, match_all=False):
self.description = description
if self.description == None:
self.description = str(uuid.uuid4())
self.outputs = outputs
self.inputs = inputs
self.executables = executables
self.match_all = match_all
def check(self, sender, recipient, data, tx_hash):
r = None
try:
r = self.__check(sender, recipient, data, tx_hash)
except RuleFail:
return False
return r
def __check(self, sender, recipient, data, tx_hash):
have_fail = False
have_match = False
for rule in self.outputs:
if rule != None and is_same_address(sender, rule):
logg.debug('tx {} rule {} match in SENDER {}'.format(tx_hash, self.description, sender))
return True
if not self.match_all:
return True
have_match = True
else:
if self.match_all:
raise RuleFail(rule)
have_fail = True
if recipient == None:
return False
for rule in self.inputs:
if rule != None and is_same_address(recipient, rule):
logg.debug('tx {} rule {} match in RECIPIENT {}'.format(tx_hash, self.description, recipient))
return True
if not self.match_all:
return True
have_match = True
else:
if self.match_all:
raise RuleFail(rule)
have_fail = True
for rule in self.executables:
if rule != None and is_same_address(recipient, rule):
logg.debug('tx {} rule {} match in EXECUTABLE {}'.format(tx_hash, self.description, recipient))
return True
if not self.match_all:
return True
have_match = True
else:
if self.match_all:
raise RuleFail(rule)
have_fail = True
return have_match
def __str__(self):
@ -105,10 +152,11 @@ class RuleSimple:
class AddressRules:
def __init__(self, include_by_default=False):
def __init__(self, include_by_default=False, match_all=False):
self.excludes = []
self.includes = []
self.include_by_default = include_by_default
self.match_all = match_all
def exclude(self, rule):
@ -125,20 +173,29 @@ class AddressRules:
return self.apply_rules_addresses(tx.outputs[0], tx.inputs[0], tx.payload, tx.hash)
# TODO: rename
def apply_rules_addresses(self, sender, recipient, data, tx_hash):
v = self.include_by_default
have_fail = False
have_match = False
for rule in self.includes:
if rule.check(sender, recipient, data, tx_hash):
v = True
logg.info('match in includes rule: {}'.format(rule))
if not self.match_all:
break
elif self.match_all:
v = False
break
if not v:
return v
for rule in self.excludes:
if rule.check(sender, recipient, data, tx_hash):
v = False
logg.info('match in excludes rule: {}'.format(rule))
break
if not self.match_all:
break
return v

17
eth_monitor/run.py Normal file
View File

@ -0,0 +1,17 @@
# standard imports
import os
import logging
logg = logging.getLogger(__name__)
def cleanup_run(settings):
if not settings.get('RUN_OUT'):
return
lockfile = os.path.join(settings.get('RUN_DIR'), '.lock')
os.unlink(lockfile)
logg.debug('freed rundir {}'.format(settings.get('RUN_DIR')))
def cleanup(settings):
cleanup_run(settings)

View File

@ -10,6 +10,8 @@ import uuid
import datetime
# external imports
import chainlib.cli
import chainsyncer.cli
from chainlib.chain import ChainSpec
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.block import block_latest
@ -17,78 +19,54 @@ from hexathon import (
strip_0x,
add_0x,
)
#from chainsyncer.store.fs import SyncFsStore
from chainsyncer.cli.arg import (
apply_arg as apply_arg_sync,
apply_flag as apply_flag_sync,
)
from chainsyncer.cli.config import process_config as process_config_sync
from chainsyncer.driver.chain_interface import ChainInterfaceDriver
from chainsyncer.error import SyncDone
from eth_cache.rpc import CacheRPC
from eth_cache.store.file import FileStore
from chainsyncer.data import config_dir as chainsyncer_config_dir
from chainlib.settings import ChainSettings
from chainlib.eth.settings import process_settings
from chainlib.eth.cli.arg import (
Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
# local imports
from eth_monitor.chain import EthChainInterface
from eth_monitor.filters.cache import Filter as CacheFilter
from eth_monitor.rules import (
AddressRules,
RuleSimple,
RuleMethod,
RuleData,
from eth_monitor.callback import (
pre_callback,
post_callback,
)
from eth_monitor.filters import RuledFilter
from eth_monitor.filters.out import OutFilter
from eth_monitor.config import override, list_from_prefix
import eth_monitor.cli
from eth_monitor.cli.log import process_log
from eth_monitor.settings import process_settings as process_settings_local
from eth_monitor.run import cleanup
logging.STATETRACE = 5
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.realpath(os.path.dirname(__file__))
exec_dir = os.path.realpath(os.getcwd())
#default_config_dir = os.environ.get('CONFINI_DIR', os.path.join(exec_dir, 'config'))
base_config_dir = os.path.join(script_dir, '..', 'data', 'config')
config_dir = os.path.join(script_dir, '..', 'data', 'config')
argparser = argparse.ArgumentParser('master eth events monitor')
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-c', type=str, help='config file')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, help='Chain specification string')
argparser.add_argument('--offset', type=int, default=0, help='Start sync on this block')
argparser.add_argument('--until', type=int, default=0, help='Terminate sync on this block')
argparser.add_argument('--head', action='store_true', help='Start at current block height (overrides --offset, assumes --keep-alive)')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('--skip-history', action='store_true', dest='skip_history', help='Skip history sync')
argparser.add_argument('--keep-alive', action='store_true', dest='keep_alive', help='Continue to sync head after history sync complete')
argparser.add_argument('--input', default=[], action='append', type=str, help='Add input (recipient) addresses to includes list')
argparser.add_argument('--output', default=[], action='append', type=str, help='Add output (sender) addresses to includes list')
argparser.add_argument('--exec', default=[], action='append', type=str, help='Add exec (contract) addresses to includes list')
argparser.add_argument('--data', default=[], action='append', type=str, help='Add data prefix strings to include list')
argparser.add_argument('--data-in', default=[], action='append', dest='data_in', type=str, help='Add data contain strings to include list')
argparser.add_argument('--x-data', default=[], action='append', dest='xdata', type=str, help='Add data prefix string to exclude list')
argparser.add_argument('--x-data-in', default=[], action='append', dest='xdata_in', type=str, help='Add data contain string to exclude list')
argparser.add_argument('--address', default=[], action='append', type=str, help='Add addresses as input, output and exec to includes list')
argparser.add_argument('--x-input', default=[], action='append', type=str, dest='xinput', help='Add input (recipient) addresses to excludes list')
argparser.add_argument('--x-output', default=[], action='append', type=str, dest='xoutput', help='Add output (sender) addresses to excludes list')
argparser.add_argument('--x-exec', default=[], action='append', type=str, dest='xexec', help='Add exec (contract) addresses to excludes list')
argparser.add_argument('--x-address', default=[], action='append', type=str, dest='xaddress', help='Add addresses as input, output and exec to excludes list')
argparser.add_argument('--includes-file', type=str, dest='includes_file', help='Load include rules from file')
argparser.add_argument('--include-default', dest='include_default', action='store_true', help='Include all transactions by default')
argparser.add_argument('--store-tx-data', dest='store_tx_data', action='store_true', help='Include all transaction data objects by default')
argparser.add_argument('--store-block-data', dest='store_block_data', action='store_true', help='Include all block data objects by default')
argparser.add_argument('--address-file', type=str, dest='excludes_file', help='Load exclude rules from file')
argparser.add_argument('--renderer', type=str, action='append', default=[], help='Python modules to dynamically load for rendering of transaction output')
argparser.add_argument('--filter', type=str, action='append', help='Add python module filter path')
argparser.add_argument('--cache-dir', dest='cache_dir', type=str, help='Directory to store tx data')
argparser.add_argument('--state-dir', dest='state_dir', default=exec_dir, type=str, help='Directory to store sync state')
argparser.add_argument('--fresh', action='store_true', help='Do not read block and tx data from cache, even if available')
argparser.add_argument('--single', action='store_true', help='Execute a single sync, regardless of previous states')
argparser.add_argument('--session-id', dest='session_id', type=str, help='Use state from specified session id')
argparser.add_argument('--backend', type=str, help='State store backend')
arg_flags = ArgFlag()
arg_flags = apply_flag_sync(arg_flags)
arg = Arg(arg_flags)
arg = apply_arg_sync(arg)
flags = arg_flags.STD_BASE_READ | arg_flags.PROVIDER | arg_flags.CHAIN_SPEC | arg_flags.VERYVERBOSE | arg_flags.SYNC_RANGE_EXT | arg_flags.STATE
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
argparser.add_argument('--list-backends', dest='list_backends', action='store_true', help='List built-in store backends')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('-vvv', action='store_true', help='Be incredibly verbose')
eth_monitor.cli.process_args(argparser, arg, flags)
args = argparser.parse_args(sys.argv[1:])
if args.list_backends:
@ -100,343 +78,41 @@ if args.list_backends:
print(v)
sys.exit(0)
if args.vvv:
logg.setLevel(logging.STATETRACE)
else:
logging.getLogger('chainlib.connection').setLevel(logging.WARNING)
logging.getLogger('chainlib.eth.tx').setLevel(logging.WARNING)
logging.getLogger('chainsyncer.driver.history').setLevel(logging.WARNING)
logging.getLogger('chainsyncer.driver.head').setLevel(logging.WARNING)
logging.getLogger('chainsyncer.backend.file').setLevel(logging.WARNING)
logging.getLogger('chainsyncer.backend.sql').setLevel(logging.WARNING)
logging.getLogger('chainsyncer.filter').setLevel(logging.WARNING)
logg = process_log(args, logg)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
config_dir = args.c
config = confini.Config(base_config_dir, os.environ.get('CONFINI_ENV_PREFIX'), override_dirs=args.c)
config.process()
args_override = {
'CHAIN_SPEC': getattr(args, 'i'),
'SYNCER_BACKEND': getattr(args, 'backend'),
}
config.dict_override(args_override, 'cli')
config.add(args.offset, '_SYNC_OFFSET', True)
config.add(args.skip_history, '_NO_HISTORY', True)
config.add(args.single, '_SINGLE', True)
config.add(args.head, '_HEAD', True)
config.add(args.keep_alive, '_KEEP_ALIVE', True)
config.add(os.path.realpath(args.state_dir), '_STATE_DIR', True)
config.add(args.cache_dir, '_CACHE_DIR', True)
config.add(args.session_id, '_SESSION_ID', True)
override(config, 'renderer', env=os.environ, args=args)
override(config, 'filter', env=os.environ, args=args)
if config.get('_SESSION_ID') == None:
if config.get('_SINGLE'):
config.add(str(uuid.uuid4()), '_SESSION_ID', True)
else:
config.add('default', '_SESSION_ID', True)
config = Config()
config.add_schema_dir(config_dir)
config.add_schema_dir(chainsyncer_config_dir)
config = process_config(config, arg, args, flags)
config = process_config_sync(config, arg, args, flags)
config = eth_monitor.cli.process_config(config, arg, args, flags)
logg.debug('loaded config:\n{}'.format(config))
chain_spec = ChainSpec.from_chain_str(args.i)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
rpc = EthHTTPConnection(args.p)
def setup_address_arg_rules(rules, args):
include_inputs = args.input
include_outputs = args.output
include_exec = args.exec
exclude_inputs = args.xinput
exclude_outputs = args.xoutput
exclude_exec = args.xexec
for address in args.address:
include_inputs.append(address)
include_outputs.append(address)
include_exec.append(address)
for address in args.xaddress:
exclude_inputs.append(address)
exclude_outputs.append(address)
exclude_exec.append(address)
includes = RuleSimple(include_outputs, include_inputs, include_exec, description='INCLUDE')
rules.include(includes)
excludes = RuleSimple(exclude_outputs, exclude_inputs, exclude_exec, description='EXCLUDE')
rules.exclude(excludes)
return rules
def setup_data_arg_rules(rules, args):
include_data = []
for v in args.data:
include_data.append(v.lower())
exclude_data = []
for v in args.xdata:
exclude_data.append(v.lower())
includes = RuleMethod(include_data, description='INCLUDE')
rules.include(includes)
excludes = RuleMethod(exclude_data, description='EXCLUDE')
rules.exclude(excludes)
include_data = []
for v in args.data_in:
include_data.append(v.lower())
exclude_data = []
for v in args.xdata_in:
exclude_data.append(v.lower())
includes = RuleData(include_data, description='INCLUDE')
rules.include(includes)
excludes = RuleData(exclude_data, description='EXCLUDE')
rules.exclude(excludes)
return rules
def setup_address_file_rules(rules, includes_file=None, excludes_file=None, include_default=False, include_block_default=False):
if includes_file != None:
f = open(includes_file, 'r')
logg.debug('reading includes rules from {}'.format(os.path.realpath(includes_file)))
while True:
r = f.readline()
if r == '':
break
r = r.rstrip()
v = r.split("\t")
sender = []
recipient = []
executable = []
try:
if v[0] != '':
sender = v[0].split(',')
except IndexError:
pass
try:
if v[1] != '':
recipient = v[1].split(',')
except IndexError:
pass
try:
if v[2] != '':
executable = v[2].split(',')
except IndexError:
pass
rule = RuleSimple(sender, recipient, executable)
rules.include(rule)
if excludes_file != None:
f = open(includes_file, 'r')
logg.debug('reading excludes rules from {}'.format(os.path.realpath(excludes_file)))
while True:
r = f.readline()
if r == '':
break
r = r.rstrip()
v = r.split("\t")
sender = None
recipient = None
executable = None
if v[0] != '':
sender = v[0].strip(',')
if v[1] != '':
recipient = v[1].strip(',')
if v[2] != '':
executable = v[2].strip(',')
rule = RuleSimple(sender, recipient, executable)
rules.exclude(rule)
return rules
def setup_filter(chain_spec, cache_dir, include_tx_data, include_block_data):
store = None
if cache_dir == None:
logg.warning('no cache dir specified, will discard everything!!')
from eth_cache.store.null import NullStore
store = NullStore()
else:
store = FileStore(chain_spec, cache_dir)
cache_dir = os.path.realpath(cache_dir)
if cache_dir == None:
import tempfile
cache_dir = tempfile.mkdtemp()
logg.info('using chain spec {} and store {}'.format(chain_spec, store))
RuledFilter.init(store, include_tx_data=include_tx_data, include_block_data=include_block_data)
return store
def setup_cache_filter(rules_filter=None):
return CacheFilter(rules_filter=rules_filter)
def pre_callback():
logg.debug('starting sync loop iteration')
def post_callback():
logg.debug('ending sync loop iteration')
def block_callback(block, tx):
logg.info('processing {} {}'.format(block, datetime.datetime.fromtimestamp(block.timestamp)))
def state_change_callback(k, old_state, new_state):
logg.log(logging.STATETRACE, 'state change: {} {} -> {}'.format(k, old_state, new_state))
def filter_change_callback(k, old_state, new_state):
logg.log(logging.STATETRACE, 'filter change: {} {} -> {}'.format(k, old_state, new_state))
settings = ChainSettings()
settings = process_settings(settings, config)
settings = process_settings_local(settings, config)
logg.debug('loaded settings:\n{}'.format(settings))
def main():
o = block_latest()
r = rpc.do(o)
block_offset = int(strip_0x(r), 16) + 1
logg.info('network block height is {}'.format(block_offset))
logg.info('session is {}'.format(settings.get('SESSION_ID')))
keep_alive = False
session_block_offset = 0
block_limit = 0
if args.head:
session_block_offset = block_offset
block_limit = -1
keep_alive = True
else:
session_block_offset = args.offset
if args.until > 0:
if not args.head and args.until <= session_block_offset:
raise ValueError('sync termination block number must be later than offset ({} >= {})'.format(session_block_offset, args.until))
block_limit = args.until
elif config.true('_KEEP_ALIVE'):
keep_alive=True
block_limit = -1
if session_block_offset == -1:
session_block_offset = block_offset
elif not config.true('_KEEP_ALIVE'):
if block_limit == 0:
block_limit = block_offset
address_rules = AddressRules(include_by_default=args.include_default)
address_rules = setup_data_arg_rules(
address_rules,
args,
drv = ChainInterfaceDriver(
settings.get('SYNC_STORE'),
settings.get('SYNCER_INTERFACE'),
offset=settings.get('SYNCER_OFFSET'),
target=settings.get('SYNCER_LIMIT'),
pre_callback=pre_callback,
post_callback=post_callback,
block_callback=settings.get('BLOCK_HANDLER').filter,
)
address_rules = setup_address_file_rules(
address_rules,
includes_file=args.includes_file,
excludes_file=args.excludes_file,
)
address_rules = setup_address_arg_rules(
address_rules,
args,
)
store = setup_filter(
chain_spec,
config.get('_CACHE_DIR'),
bool(args.store_tx_data),
bool(args.store_block_data),
)
cache_filter = setup_cache_filter(
rules_filter=address_rules,
)
filters = [
cache_filter,
]
for fltr in list_from_prefix(config, 'filter'):
m = importlib.import_module(fltr)
fltr_object = m.Filter(rules_filter=address_rules)
filters.append(fltr_object)
logg.info('using filter module {}'.format(fltr))
renderers_mods = []
for renderer in list_from_prefix(config, 'renderer'):
m = importlib.import_module(renderer)
renderers_mods.append(m)
logg.info('using renderer module {}'.format(renderer))
chain_interface = EthChainInterface()
out_filter = OutFilter(chain_spec, rules_filter=address_rules, renderers=renderers_mods)
filters.append(out_filter)
syncer_store_module = None
syncer_store_class = None
state_dir = None
if config.get('SYNCER_BACKEND') == 'mem':
syncer_store_module = importlib.import_module('chainsyncer.store.mem')
syncer_store_class = getattr(syncer_store_module, 'SyncMemStore')
else:
if config.get('SYNCER_BACKEND') == 'fs':
syncer_store_module = importlib.import_module('chainsyncer.store.fs')
syncer_store_class = getattr(syncer_store_module, 'SyncFsStore')
elif config.get('SYNCER_BACKEND') == 'rocksdb':
syncer_store_module = importlib.import_module('chainsyncer.store.rocksdb')
syncer_store_class = getattr(syncer_store_module, 'SyncRocksDbStore')
else:
syncer_store_module = importlib.import_module(config.get('SYNCER_BACKEND'))
syncer_store_class = getattr(syncer_store_module, 'SyncStore')
state_dir = os.path.join(config.get('_STATE_DIR'), config.get('SYNCER_BACKEND'))
logg.info('using engine {} module {}.{}'.format(config.get('SYNCER_BACKEND'), syncer_store_module.__file__, syncer_store_class.__name__))
state_dir = os.path.join(state_dir, config.get('_SESSION_ID'))
if state_dir == None:
sync_store = syncer_store_class(session_id=config.get('_SESSION_ID'), state_event_callback=state_change_callback, filter_state_event_callback=filter_change_callback)
else:
sync_store = syncer_store_class(state_dir, session_id=config.get('_SESSION_ID'), state_event_callback=state_change_callback, filter_state_event_callback=filter_change_callback)
logg.info('session is {}'.format(sync_store.session_id))
for fltr in filters:
sync_store.register(fltr)
drv = ChainInterfaceDriver(sync_store, chain_interface, offset=session_block_offset, target=block_limit, pre_callback=pre_callback, post_callback=post_callback, block_callback=block_callback)
use_rpc = rpc
if not args.fresh:
use_rpc = CacheRPC(rpc, store)
i = 0
try:
r = drv.run(use_rpc)
r = drv.run(settings.get('CONN'), ctx=settings.get('SYNCER_CONTEXT'))
except SyncDone as e:
sys.stderr.write("sync {} done at block {}\n".format(drv, e))
i += 1
cleanup(settings)
if __name__ == '__main__':

438
eth_monitor/settings.py Normal file
View File

@ -0,0 +1,438 @@
# standard imports
import logging
import os
import uuid
import importlib
import tempfile
# external imports
from chainlib.settings import ChainSettings
from chainlib.eth.connection import EthHTTPConnection
from chainsyncer.settings import *
from eth_monitor.chain import EthChainInterface
from chainlib.eth.address import is_address
from eth_cache.rpc import CacheRPC
from eth_cache.store.file import FileStore
from chainsyncer.settings import process_sync_range
# local imports
from eth_monitor.rules import (
AddressRules,
RuleSimple,
RuleMethod,
RuleData,
)
from eth_monitor.cli.rules import to_config_names
from eth_monitor.callback import (
state_change_callback,
filter_change_callback,
BlockCallbackFilter,
)
from eth_monitor.filters import RuledFilter
from eth_monitor.filters.cache import Filter as CacheFilter
from eth_monitor.config import override, list_from_prefix
from eth_monitor.filters.out import OutFilter
from eth_monitor.filters.block import Filter as BlockFilter
from eth_monitor.filters.run import Filter as RunFilter
logg = logging.getLogger(__name__)
def process_monitor_session(settings, config):
session_id = config.get('_SESSION_ID')
if session_id == None:
if config.get('_SINGLE'):
session_id = str(uuid.uuid4())
else:
session_id = 'default'
settings.set('SESSION_ID', session_id)
settings.set('SESSION_OK', True)
return settings
def process_monitor_rundir(settings, config):
settings.set('RUN_OUT', False)
if config.get('_RUN_DIR') == None:
return settings
run_dir = config.get('_RUN_DIR')
try:
os.makedirs(run_dir, exist_ok=True)
except Exception as e:
logg.error('could not create run dir, deactivating run output: ' + str(e))
return settings
lockfile = os.path.join(run_dir, '.lock')
try:
f = open(lockfile, 'x')
f.close()
except FileExistsError:
logg.error('run dir {} is already in use, deactivating run output'.format(run_dir))
return settings
settings.set('RUN_OUT', True)
settings.set('RUN_DIR', run_dir)
return settings
def process_monitor_session_dir(settings, config):
syncer_store_module = None
syncer_store_class = None
sync_store = None
session_id = settings.get('SESSION_ID')
state_dir = None
if config.get('SYNCER_BACKEND') == 'mem':
syncer_store_module = importlib.import_module('chainsyncer.store.mem')
syncer_store_class = getattr(syncer_store_module, 'SyncMemStore')
sync_store = syncer_store_class(
session_id=session_id,
state_event_callback=state_change_callback,
filter_state_event_callback=filter_change_callback,
)
else:
if config.get('SYNCER_BACKEND') == 'fs':
syncer_store_module = importlib.import_module('chainsyncer.store.fs')
syncer_store_class = getattr(syncer_store_module, 'SyncFsStore')
elif config.get('SYNCER_BACKEND') == 'rocksdb':
syncer_store_module = importlib.import_module('chainsyncer.store.rocksdb')
syncer_store_class = getattr(syncer_store_module, 'SyncRocksDbStore')
else:
syncer_store_module = importlib.import_module(config.get('SYNCER_BACKEND'))
syncer_store_class = getattr(syncer_store_module, 'SyncStore')
state_dir = os.path.join(config.get('ETHMONITOR_STATE_DIR'), config.get('SYNCER_BACKEND'))
os.makedirs(state_dir, exist_ok=True)
session_dir = os.path.join(state_dir, session_id)
sync_store = syncer_store_class(
session_dir,
session_id=session_id,
state_event_callback=state_change_callback,
filter_state_event_callback=filter_change_callback,
)
settings.set('SESSION_DIR', session_dir)
logg.info('using engine {} module {}.{}'.format(config.get('SYNCER_BACKEND'), syncer_store_module.__file__, syncer_store_class.__name__))
settings.set('STATE_DIR', state_dir)
settings.set('SYNC_STORE', sync_store)
return settings
def process_address_arg_rules(settings, config):
rules = settings.get('RULES')
category = {
'input': {
'i': [],
'x': [],
},
'output': {
'i': [],
'x': [],
},
'exec': {
'i': [],
'x': [],
},
}
for rules_arg in [
'input',
'output',
'exec',
]:
(vy, vn) = to_config_names(rules_arg)
for address in config.get(vy):
if not is_address(address):
raise ValueError('invalid address in config {}: {}'.format(vy, address))
category[rules_arg]['i'].append(address)
for address in config.get(vn):
if not is_address(address):
raise ValueError('invalid address in config {}: {}'.format(vn, address))
category[rules_arg]['x'].append(address)
includes = RuleSimple(
category['output']['i'],
category['input']['i'],
category['exec']['i'],
description='INCLUDE',
match_all=settings.get('MATCH_ALL'),
)
rules.include(includes)
excludes = RuleSimple(
category['output']['x'],
category['input']['x'],
category['exec']['x'],
description='EXCLUDE',
)
rules.exclude(excludes)
return settings
def process_data_arg_rules(settings, config):
rules = settings.get('RULES')
include_data = []
for v in config.get('ETHMONITOR_DATA'):
include_data.append(v.lower())
exclude_data = []
for v in config.get('ETHMONITOR_X_DATA'):
exclude_data.append(v.lower())
includes = RuleMethod(include_data, description='INCLUDE')
rules.include(includes)
excludes = RuleMethod(exclude_data, description='EXCLUDE')
rules.exclude(excludes)
include_data = []
for v in config.get('ETHMONITOR_DATA_IN'):
include_data.append(v.lower())
exclude_data = []
for v in config.get('ETHMONITOR_X_DATA_IN'):
exclude_data.append(v.lower())
includes = RuleData(include_data, description='INCLUDE', match_all=settings.get('MATCH_ALL'))
rules.include(includes)
excludes = RuleData(exclude_data, description='EXCLUDE')
rules.exclude(excludes)
return settings
def process_address_file_rules(settings, config): #rules, includes_file=None, excludes_file=None, include_default=False, include_block_default=False):
rules = settings.get('RULES')
includes_file = config.get('ETHMONITOR_INCLUDES_FILE')
if includes_file != None:
f = open(includes_file, 'r')
logg.debug('reading includes rules from {}'.format(os.path.realpath(includes_file)))
while True:
r = f.readline()
if r == '':
break
r = r.rstrip()
v = r.split("\t")
sender = []
recipient = []
executable = []
try:
if v[0] != '':
sender = v[0].split(',')
except IndexError:
pass
try:
if v[1] != '':
recipient = v[1].split(',')
except IndexError:
pass
try:
if v[2] != '':
executable = v[2].split(',')
except IndexError:
pass
rule = RuleSimple(sender, recipient, executable, match_all=settings.get('MATCH_ALL'))
rules.include(rule)
excludes_file = config.get('ETHMONITOR_EXCLUDES_FILE')
if excludes_file != None:
f = open(includes_file, 'r')
logg.debug('reading excludes rules from {}'.format(os.path.realpath(excludes_file)))
while True:
r = f.readline()
if r == '':
break
r = r.rstrip()
v = r.split("\t")
sender = None
recipient = None
executable = None
if v[0] != '':
sender = v[0].strip(',')
if v[1] != '':
recipient = v[1].strip(',')
if v[2] != '':
executable = v[2].strip(',')
rule = RuleSimple(sender, recipient, executable)
rules.exclude(rule)
return settings
def process_arg_rules(settings, config):
address_rules = AddressRules(include_by_default=config.get('ETHMONITOR_INCLUDE_DEFAULT'))
settings.set('MATCH_ALL', config.true('ETHMONITOR_MATCH_ALL'))
settings.set('RULES', address_rules)
settings = process_address_arg_rules(settings, config)
settings = process_data_arg_rules(settings, config)
settings = process_address_file_rules(settings, config)
return settings
def process_cache_store(settings, config):
cache_dir = config.get('_CACHE_DIR')
store = None
if cache_dir == None:
logg.warning('no cache dir specified, will discard everything!!')
from eth_cache.store.null import NullStore
store = NullStore()
else:
store = FileStore(settings.get('CHAIN_SPEC'), cache_dir)
cache_dir = os.path.realpath(cache_dir)
if cache_dir == None:
import tempfile
cache_dir = tempfile.mkdtemp()
logg.info('using cache store {}'.format(store))
settings.set('CACHE_STORE', store)
return settings
def process_cache_filter(settings, config):
cache_store = settings.get('CACHE_STORE')
cache_rules = AddressRules(include_by_default=True)
if str(cache_store) != 'Nullstore':
cache_rules = settings.o['RULES']
fltr = CacheFilter(cache_store, rules_filter=cache_rules, include_tx_data=config.true('ETHCACHE_STORE_TX'))
sync_store = settings.get('SYNC_STORE')
sync_store.register(fltr)
fltr = BlockFilter(cache_store, include_block_data=config.true('ETHCACHE_STORE_BLOCK'))
hndlr = settings.get('BLOCK_HANDLER')
hndlr.register(fltr)
return settings
def process_run_filter(settings, config):
if not settings.get('RUN_OUT'):
return settings
fltr = RunFilter(settings.get('RUN_DIR'))
hndlr = settings.get('BLOCK_HANDLER')
hndlr.register(fltr)
return settings
def process_tx_filter(settings, config):
for fltr in list_from_prefix(config, 'filter'):
m = importlib.import_module(fltr)
fltr_object = m.Filter(rules_filter=settings.get('RULES'))
store = settings.get('SYNC_STORE')
store.register(fltr_object)
logg.info('using filter module {}'.format(fltr))
return settings
def process_block_filter(settings, config):
block_filter_handler = BlockCallbackFilter()
for block_filter in list_from_prefix(config, 'block_filter'):
m = importlib.import_module(block_filter)
block_filter_handler.register(m)
logg.info('using block filter module {}'.format(block_filter))
settings.set('BLOCK_HANDLER', block_filter_handler)
return settings
def process_out_filter(settings, config):
out_filter = OutFilter(
settings.o['CHAIN_SPEC'],
rules_filter=settings.o['RULES'],
renderers=settings.o['RENDERER'],
)
store = settings.get('SYNC_STORE')
store.register(out_filter)
return settings
def process_arg_filter(settings, config):
store = settings.get('SYNC_STORE')
if config.get('ETHMONITOR_FILTER') != None:
for k in config.get('ETHMONITOR_FILTER'):
m = importlib.import_module(k)
fltr = m.Filter()
store.register(fltr)
return settings
def process_filter(settings, config):
settings.set('FILTER', [])
settings = process_renderer(settings, config)
settings = process_block_filter(settings, config)
settings = process_cache_filter(settings, config)
settings = process_run_filter(settings, config)
settings = process_tx_filter(settings, config)
settings = process_out_filter(settings, config)
settings = process_arg_filter(settings, config)
return settings
def process_renderer(settings, config):
renderers_mods = []
for renderer in config.get('ETHMONITOR_RENDERER'):
m = importlib.import_module(renderer)
renderers_mods.append(m)
logg.info('using renderer module {}'.format(renderer))
settings.set('RENDERER', renderers_mods)
return settings
def process_cache_rpc(settings, config):
if str(settings.get('CACHE_STORE')) == 'Nullstore':
logg.debug('cache store is null, cache rpc proxy will be deactivated')
return settings
if not config.true('_FRESH'):
rpc = CacheRPC(settings.get('CONN'), settings.get('CACHE_STORE'))
settings.set('CONN', rpc)
return settings
def process_sync(settings, config):
dialect_filter = settings.get('RPC_DIALECT_FILTER')
settings.set('SYNCER_INTERFACE', EthChainInterface(dialect_filter=dialect_filter, batch_limit=settings.get('RPC_BATCH_LIMIT')))
settings = process_sync_range(settings, config)
return settings
def process_cache(settings, config):
settings = process_cache_store(settings, config)
settings = process_cache_rpc(settings, config)
return settings
def process_user_context(settings, config):
ctx_usr = {}
ks = config.get('ETHMONITOR_CONTEXT_KEY')
if ks != None:
for kv in ks:
(k, v) = kv.split('=', 1)
ctx_usr[k] = v
ctx = {
'driver': 'eth-monitor',
'rundir': settings.get('RUN_DIR'),
'usr': ctx_usr,
}
settings.set('SYNCER_CONTEXT', ctx)
return settings
def process_settings(settings, config):
settings = process_monitor_session(settings, config)
settings = process_monitor_session_dir(settings, config)
settings = process_monitor_rundir(settings, config)
settings = process_arg_rules(settings, config)
settings = process_sync(settings, config)
settings = process_cache(settings, config)
settings = process_user_context(settings, config)
settings = process_filter(settings, config)
return settings

View File

@ -0,0 +1,170 @@
.TH eth-monitor-import 1
.SH NAME
eth-monitor-import \- Import transaction data from an indexing service
.SH SYNOPSIS
.SY eth-monitor-import
[ -i \fIchain_spec\fP] [ --api-key-file \fIfile\fp ] [ --address-file \fIfile\fP ] [ -a \fIaddress\fP ... ] [ --cache-dir \fIdirectory\fP ] \fIservice\fP
.SH DESCRIPTION
Use an indexing service to retrieve transaction hashes for one or more addresses. Supported services may be listed using the \fB--list-services\fP option.
.P
Which addresses to retrieve data for may be defined by the \fB-a\fP \fIaddress\fP option. Alternatively, the \fB--address-file\fP \fIfile\fP option may be used, where addresses are supplied from the given file as a comma-separated list. The address matching mechanism used in transaction processing is the same as for \fBeth-monitor(1)\fP.
.P
Only block and transaction hashes are used from the indexing service. The RPC endpoint will be used to retrieve the block and transaction data.
.P
If \fB--cache-dir\fP \fIdirectory\fP is defined, data will be cached to the given path using the same caching filter as \fBeth-monitor(1)\fP. \fB--store-tx-data\fP and \fB--store-block-data-\fP define whether also transaction and block data is stored to cache, respectively.
.SS OPTIONS
.TP
\fB-0\fP
Omit newline to output
.TP
\fB--address-file \fI\fIfile
\fP\fP
Load address include matching rules from file. Addresses must be given as a comma-separated list.
.TP
\fB-c \fI\fIconfig_dir\fP\fP, \fB--config \fI\fIconfig_dir\fP\fP
Load configuration files from given directory. All files with an .ini extension will be loaded, of which all must contain valid ini file data.
.TP
\fB--dumpconfig \fI\fIformat\fP\fP
Output configuration settings rendered from environment and inputs. Valid arguments are \fIini\fP for ini file output, and \fIenv\fP for environment variable output. See \fBCONFIGURATION\fP.
.TP
\fB--env-prefix\fP
Environment prefix for variables to overwrite configuration. Example: If \fB--env-prefix\fP is set to \fBFOO\fP then configuration variable \fBBAR_BAZ\fP would be set by environment variable \fBFOO_BAZ_BAR\fP. Also see \fBENVIRONMENT\fP.
.TP
\fB--height\fP
Block height at which to query state for. Does not apply to transactions.
.TP
\fB-i \fI\fIchain_spec\fP\fP, \fB--chain-spec \fI\fIchain_spec\fP\fP
Chain specification string, in the format <engine>:<fork>:<chain_id>:<common_name>. Example: "evm:london:1:ethereum". Overrides the \fIRPC_CREDENTIALS\fP configuration setting.
.TP
\fB--list-services \fI\fI
\fP\fP
List all supported services.
.TP
\fB-n \fI\fInamespace\fP\fP, \fB--namespace \fI\fInamespace\fP\fP
Load given configuration namespace. Configuration will be loaded from the immediate configuration subdirectory with the same name.
.TP
\fB--no-logs\fP
Turn of logging completely. Negates \fB-v\fP and \fB-vv\fP
.TP
\fB-p\fP, \fB--rpc-provider\fP
Fully-qualified URL of RPC provider. Overrides the \fIRPC_PROVIDER\fP configuration setting.
.TP
\fB--raw\fP
Produce output most optimized for machines.
.TP
\fB--rpc-batch-limit\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together. Overrides the \fIRPC_BATCH_LIMIT\fP configuration setting.
.TP
\fB--rpc-dialect\fP
RPC backend dialect. If specified it \fImay\fP help with encoding and decoding issues. Overrides the \fIRPC_DIALECT\fP configuration setting.
.TP
\fB--socks-host \fI\fIhost
\fP\fP
Connect through the specified socks4a host (e.g. tor)
.TP
\fB--socks-port \fI\fIport
\fP\fP
Connect through the specified socks4a host port (e.g. tor)
.TP
\fB--store-block-data \fI\fI
\fP\fP
Store block data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB--store-tx-data \fI\fI
\fP\fP
Store transaction data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB-u\fP, \fB--unsafe\fP
Allow addresses that do not pass checksum.
.TP
\fB-v\fP
Verbose. Show logs for important state changes.
.TP
\fB-vv\fP
Very verbose. Show logs with debugging information.
.SH CONFIGURATION
All configuration settings may be overriden both by environment variables, or by overriding settings with the contents of ini-files in the directory defined by the \fB-c\fP option.
The active configuration, with values assigned from environment and arguments, can be output using the \fB--dumpconfig\fP \fIformat\fP option. Note that entries having keys prefixed with underscore (e.g. _SEQ) are not actual configuration settings, and thus cannot be overridden with environment variables.
To refer to a configuration setting by environment variables, the \fIsection\fP and \fIkey\fP are concatenated together with an underscore, and transformed to upper-case. For example, the configuration variable \fIFOO_BAZ_BAR\fP refers to an ini-file entry as follows:
.EX
[foo]
bar_baz = xyzzy
.EE
In the \fBENVIRONMENT\fP section below, the relevant configuration settings for this tool is listed along with a short description of its meaning.
Some configuration settings may also be overriden by command line options. Also note that the use of the \fB-n\fP and \fB--env-prefix\fP options affect how environment and configuration is read. The effects of options on how configuration settings are affective is described in the respective \fBOPTIONS\fP section.
.SH ENVIRONMENT
.TP
\fICHAIN_SPEC\fP
String specifying the type of chain connected to, in the format \fI<engine>:<fork>:<network_id>:<common_name>\fP. For EVM nodes the \fIengine\fP value will always be \fIevm\fP.
.TP
\fIRPC_BATCH_LIMIT\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together.
.TP
\fIRPC_DIALECT\fP
Enables translations of EVM node specific formatting and response codes.
.TP
\fIRPC_PROVIDER\fP
Fully-qualified URL to the RPC endpoint of the blockchain node.
.SH LICENSE
This documentation and its source is licensed under the Creative Commons Attribution-Sharealike 4.0 International license.
The source code of the tool this documentation describes is licensed under the GNU General Public License 3.0.
.SH COPYRIGHT
Louis Holbrook <dev@holbrook.no> (https://holbrook.no)
PGP: 59A844A484AC11253D3A3E9DCDCBD24DD1D0E001
.SH SOURCE CODE
https://git.defalsify.org
.SH SEE ALSO
eth-monitor (1)

View File

@ -0,0 +1,159 @@
.TH eth-monitor-list 1
.SH NAME
eth-monitor-list \- Query transactions cache
.SH SYNOPSIS
.SY eth-monitor-list
[ -i \fIchain_spec\fP ] [ p \fIeth_provider\fP ] [ -a \fIaddress\fP ... ] \fIcache_dir\fP
.YS
.SH DESCRIPTION
List transactions stored in cache matching the given address.
.P
Any block data and/or transaction data matchin the relevant hashes returned by the query will be used to create the output. The \fB--fresh\fP option may be defined to force all block and transaction data from the RPC provider endpoint instead.
.P
For details on rendering and filtering, please refer to to \fBeth-monitor (1)\fP man page.
.SS OPTIONS
.TP
\fB-0\fP
Omit newline to output
.TP
\fB--address \fI\fIaddress
\fP\fP
Add an address of interest to match any role. Complements \fB--address-file\fP.
.TP
\fB-c \fI\fIconfig_dir\fP\fP, \fB--config \fI\fIconfig_dir\fP\fP
Load configuration files from given directory. All files with an .ini extension will be loaded, of which all must contain valid ini file data.
.TP
\fB--dumpconfig \fI\fIformat\fP\fP
Output configuration settings rendered from environment and inputs. Valid arguments are \fIini\fP for ini file output, and \fIenv\fP for environment variable output. See \fBCONFIGURATION\fP.
.TP
\fB--env-prefix\fP
Environment prefix for variables to overwrite configuration. Example: If \fB--env-prefix\fP is set to \fBFOO\fP then configuration variable \fBBAR_BAZ\fP would be set by environment variable \fBFOO_BAZ_BAR\fP. Also see \fBENVIRONMENT\fP.
.TP
\fB--filter \fI\fImodule
\fP\fP
Add code execution filter to all matching transactions. The argument must be a python module path. Several filters may be added by supplying the option multiple times. Filters will be executed in the order the options are given. See \fBDEFINING FILTERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--fresh \fI\fI
\fP\fP
Only use hashes from cache, and retrieve all block and transaction data from RPC endpoint.
.TP
\fB--height\fP
Block height at which to query state for. Does not apply to transactions.
.TP
\fB-i \fI\fIchain_spec\fP\fP, \fB--chain-spec \fI\fIchain_spec\fP\fP
Chain specification string, in the format <engine>:<fork>:<chain_id>:<common_name>. Example: "evm:london:1:ethereum". Overrides the \fIRPC_CREDENTIALS\fP configuration setting.
.TP
\fB-n \fI\fInamespace\fP\fP, \fB--namespace \fI\fInamespace\fP\fP
Load given configuration namespace. Configuration will be loaded from the immediate configuration subdirectory with the same name.
.TP
\fB--no-logs\fP
Turn of logging completely. Negates \fB-v\fP and \fB-vv\fP
.TP
\fB-p\fP, \fB--rpc-provider\fP
Fully-qualified URL of RPC provider. Overrides the \fIRPC_PROVIDER\fP configuration setting.
.TP
\fB--raw\fP
Produce output most optimized for machines.
.TP
\fB--renderer \fI\fImodule
\fP\fP
Add output renderer filter to all matching transactions. The argument must be a python module path. Several renderers may be added by supplying the option multiple times. See \fBRENDERERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--rpc-batch-limit\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together. Overrides the \fIRPC_BATCH_LIMIT\fP configuration setting.
.TP
\fB--rpc-dialect\fP
RPC backend dialect. If specified it \fImay\fP help with encoding and decoding issues. Overrides the \fIRPC_DIALECT\fP configuration setting.
.TP
\fB-u\fP, \fB--unsafe\fP
Allow addresses that do not pass checksum.
.TP
\fB-v\fP
Verbose. Show logs for important state changes.
.TP
\fB-vv\fP
Very verbose. Show logs with debugging information.
.SH CONFIGURATION
All configuration settings may be overriden both by environment variables, or by overriding settings with the contents of ini-files in the directory defined by the \fB-c\fP option.
The active configuration, with values assigned from environment and arguments, can be output using the \fB--dumpconfig\fP \fIformat\fP option. Note that entries having keys prefixed with underscore (e.g. _SEQ) are not actual configuration settings, and thus cannot be overridden with environment variables.
To refer to a configuration setting by environment variables, the \fIsection\fP and \fIkey\fP are concatenated together with an underscore, and transformed to upper-case. For example, the configuration variable \fIFOO_BAZ_BAR\fP refers to an ini-file entry as follows:
.EX
[foo]
bar_baz = xyzzy
.EE
In the \fBENVIRONMENT\fP section below, the relevant configuration settings for this tool is listed along with a short description of its meaning.
Some configuration settings may also be overriden by command line options. Also note that the use of the \fB-n\fP and \fB--env-prefix\fP options affect how environment and configuration is read. The effects of options on how configuration settings are affective is described in the respective \fBOPTIONS\fP section.
.SH ENVIRONMENT
.TP
\fICHAIN_SPEC\fP
String specifying the type of chain connected to, in the format \fI<engine>:<fork>:<network_id>:<common_name>\fP. For EVM nodes the \fIengine\fP value will always be \fIevm\fP.
.TP
\fIRPC_BATCH_LIMIT\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together.
.TP
\fIRPC_DIALECT\fP
Enables translations of EVM node specific formatting and response codes.
.TP
\fIRPC_PROVIDER\fP
Fully-qualified URL to the RPC endpoint of the blockchain node.
.SH LICENSE
This documentation and its source is licensed under the Creative Commons Attribution-Sharealike 4.0 International license.
The source code of the tool this documentation describes is licensed under the GNU General Public License 3.0.
.SH COPYRIGHT
Louis Holbrook <dev@holbrook.no> (https://holbrook.no)
PGP: 59A844A484AC11253D3A3E9DCDCBD24DD1D0E001
.SH SOURCE CODE
https://git.defalsify.org
.SH SEE ALSO
eth-monitor (1)

View File

@ -0,0 +1,276 @@
.TH eth-monitor 1
.SH NAME
eth-monitor \- Cache, index and monitor transactions with an EVM node rpc
.SH SYNOPSIS
.SY eth-monitor
[ --skip-history ] [ --single ] [ p \fIeth_provider\fP ] [ --includes-file \fIfile\fP ] [ -i chain_spec ]
.YS
.SY eth-monitor
[ --skip-history ] [ --single ] [ p \fIeth_provider\fP ] [ --excludes-file \fIfile\fP ] [ --include-default ] [ -i chain_spec ] 
.YS
.SH DESCRIPTION
The \fBeth-monitor\fP has fulfills three distinct but related functions:
.IP
1. A customizable view of on transactions of interest.
.IP
2. A block and transaction cache.
.IP
3. Arbitrary code executions using a transaction (and its block) as input.
.P
Using an EVM RPC endpoint, the \fBeth-monitor\fP tool will retrieve blocks within a given range and provides arbitrary processing of each transaction.
.P
A collection of options is provided to control the behavior of which block ranges to sync, which criteria to use for display and cache, and what code to execute for matching transactions. Details on each topic can be found in the \fBSYNCING\fP, \fBMATCHING ADDRESSES\fP and \fBDEFINING FILTERS\fP sections below, respectively.
.P
Example executions of the tool can be found in the \fBEXAMPLES\fP section.
.P
.SS OPTIONS
.TP
\fB-0\fP
Omit newline to output
.TP
\fB--address \fI\fIaddress
\fP\fP
Add an address of interest to match any role. Complements \fB--address-file\fP.
.TP
\fB-c \fI\fIconfig_dir\fP\fP, \fB--config \fI\fIconfig_dir\fP\fP
Load configuration files from given directory. All files with an .ini extension will be loaded, of which all must contain valid ini file data.
.TP
\fB--context-key \fI\fIkey=value
\fP\fP
Add a key-value pair that gets passed to the syncer context. May be specified several times.
.TP
\fB--dumpconfig \fI\fIformat\fP\fP
Output configuration settings rendered from environment and inputs. Valid arguments are \fIini\fP for ini file output, and \fIenv\fP for environment variable output. See \fBCONFIGURATION\fP.
.TP
\fB--env-prefix\fP
Environment prefix for variables to overwrite configuration. Example: If \fB--env-prefix\fP is set to \fBFOO\fP then configuration variable \fBBAR_BAZ\fP would be set by environment variable \fBFOO_BAZ_BAR\fP. Also see \fBENVIRONMENT\fP.
.TP
\fB--excludes-file \fI\fIfile
\fP\fP
Load address exclude matching rules from file. See \fBMATCHING ADDRESSES\fP.
.TP
\fB--exec \fI\fIaddress
\fP\fP
Add an address of interest to executable address array. Complements \fB--address-file\fP.
.TP
\fB--filter \fI\fImodule
\fP\fP
Add code execution filter to all matched transactions. The argument must be a python module path. Several filters may be added by supplying the option multiple times. Filters will be executed in the order the options are given. See \fBDEFINING FILTERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--height\fP
Block height at which to query state for. Does not apply to transactions.
.TP
\fB-i \fI\fIchain_spec\fP\fP, \fB--chain-spec \fI\fIchain_spec\fP\fP
Chain specification string, in the format <engine>:<fork>:<chain_id>:<common_name>. Example: "evm:london:1:ethereum". Overrides the \fIRPC_CREDENTIALS\fP configuration setting.
.TP
\fB--include-default \fI\fI
\fP\fP
Match all addresses by default. Addresses may be excluded using --excludes-file. If this is set, --input, --output, --exec and --includes-file will have no effect.
.TP
\fB--includes-file \fI\fIfile
\fP\fP
Load address include matching rules from file. See \fBMATCHING ADDRESSES\fP.
.TP
\fB--input \fI\fIaddress
\fP\fP
Add an address of interest to inputs (recipients) array. Complements \fB--address-file\fP.
.TP
\fB-n \fI\fInamespace\fP\fP, \fB--namespace \fI\fInamespace\fP\fP
Load given configuration namespace. Configuration will be loaded from the immediate configuration subdirectory with the same name.
.TP
\fB--no-logs\fP
Turn of logging completely. Negates \fB-v\fP and \fB-vv\fP
.TP
\fB--output \fI\fIaddress
\fP\fP
Add an address of interest to outputs (sender) array. Complements \fB--address-file\fP.
.TP
\fB-p\fP, \fB--rpc-provider\fP
Fully-qualified URL of RPC provider. Overrides the \fIRPC_PROVIDER\fP configuration setting.
.TP
\fB--raw\fP
Produce output most optimized for machines.
.TP
\fB--renderer \fI\fImodule
\fP\fP
Add output renderer filter to all matched transactions. The argument must be a python module path. Several renderers may be added by supplying the option multiple times. See \fBRENDERERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--rpc-batch-limit\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together. Overrides the \fIRPC_BATCH_LIMIT\fP configuration setting.
.TP
\fB--rpc-dialect\fP
RPC backend dialect. If specified it \fImay\fP help with encoding and decoding issues. Overrides the \fIRPC_DIALECT\fP configuration setting.
.TP
\fB--store-block-data \fI\fI
\fP\fP
Store block data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB--store-tx-data \fI\fI
\fP\fP
Store transaction data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB-u\fP, \fB--unsafe\fP
Allow addresses that do not pass checksum.
.TP
\fB-v\fP
Verbose. Show logs for important state changes.
.TP
\fB-vv\fP
Very verbose. Show logs with debugging information.
.TP
\fB--x-address \fI\fIaddress
\fP\fP
Add an address of interest to match any role.
.TP
\fB--x-exec \fI\fIaddress
\fP\fP
Add an address of disinterest to executable address array.
.TP
\fB--x-input \fI\fIaddress
\fP\fP
Add an address of disinterest to inputs (recipients) array.
.TP
\fB--x-output \fI\fIaddress
\fP\fP
Add an address of disinterest to outputs (sender) array.
.SH CONFIGURATION
All configuration settings may be overriden both by environment variables, or by overriding settings with the contents of ini-files in the directory defined by the \fB-c\fP option.
The active configuration, with values assigned from environment and arguments, can be output using the \fB--dumpconfig\fP \fIformat\fP option. Note that entries having keys prefixed with underscore (e.g. _SEQ) are not actual configuration settings, and thus cannot be overridden with environment variables.
To refer to a configuration setting by environment variables, the \fIsection\fP and \fIkey\fP are concatenated together with an underscore, and transformed to upper-case. For example, the configuration variable \fIFOO_BAZ_BAR\fP refers to an ini-file entry as follows:
.EX
[foo]
bar_baz = xyzzy
.EE
In the \fBENVIRONMENT\fP section below, the relevant configuration settings for this tool is listed along with a short description of its meaning.
Some configuration settings may also be overriden by command line options. Also note that the use of the \fB-n\fP and \fB--env-prefix\fP options affect how environment and configuration is read. The effects of options on how configuration settings are affective is described in the respective \fBOPTIONS\fP section.
.SH MATCHING ADDRESSES
By default, addresses to match against transactions need to be explicitly specified. This behavior can be reversed with the \fB--include-default\fP option. Addresses to match are defined using the \fB--input\fP, \fB--output\fP and \fB--exec\fP options. Addresses specified multiple times will be deduplicated.
.P
Inclusion rules may also be loaded from file by specifying the \fB--includes-file\fP and \fB--excludes-file\fP options. Each file must specify the outputs, inputs and exec addresses as comma separated lists respectively, separated by tabs.
.P
In the current state of this tool, address matching will affect all parts of the processing; cache, code execution and rendering.
.SH SYNCING
When a sync is initiated, the state of this sync is persisted. This way, previous syncs that did not complete for some reason will be resumed where they left off.
.P
A special sync type \fB--head\fP starts syncing at the current head of the chain, and continue to sync until interrupted. When resuming sync, a new sync range between the current block head and the block height at which the previous \fB--head\fP sync left off will automatically be created.
.P
Syncs can be forced to (re)run for ranges regardless of previous state by using the \fB--single\fP option. However, there is no protection in place from preventing code filters from being executed again on the same transaction when this is done. See \fBDEFINING FILTERS\fP below.
.SH CACHE
When syncing, the hash of a block and transaction matching the address criteria will be stored in the cache. The hashes can be used for future data lookups.
.P
If \fB--store-block-data\fP and/or \fB--store-tx-data\fP is set, a copy of the block and/or transaction data will also be stored, respectively.
.SH RENDERING
Rendering in the context of \fBeth-monitor\fP refers to a formatted output stream that occurs independently of caching and code execution.
.P
Filters for rendering may be specified by specifying python modules to the \fB--renderer\fP option. This option may be specified multiple times.
.P
Rendering filters will be executed in order, and the first filter to return \fIFalse\fP
.SH DEFINING FILTERS
Filters will strictly be executed in the order which they are defined on the command line.
A python module used for filter must fulfill two conditions:
.IP
1. It must provide a class named \fIFilter\fP in the package base namespace.
.IP
2. The \fIFilter\fP class must extend the \fIchainsyncer.filter.SyncFilter\fP interface, and at least override the \fIfilter\fP method.
.SS SYNCER AND FILTER CONTEXT
Key-value pairs specified with `--context-key` will be passed to the filter's \fIprepare\fP method, aswell as the \fIctx\fP parameter of the \fIfilter\fP method.
.SH FURTHER READING
Refer to the \fBchainsyncer\fP chapter n \fIinfo chaintool\fP for in-depth information on the subjects of syncing and filtering.
.SH ENVIRONMENT
.TP
\fICHAIN_SPEC\fP
String specifying the type of chain connected to, in the format \fI<engine>:<fork>:<network_id>:<common_name>\fP. For EVM nodes the \fIengine\fP value will always be \fIevm\fP.
.TP
\fIRPC_BATCH_LIMIT\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together.
.TP
\fIRPC_DIALECT\fP
Enables translations of EVM node specific formatting and response codes.
.TP
\fIRPC_PROVIDER\fP
Fully-qualified URL to the RPC endpoint of the blockchain node.
.SH LICENSE
This documentation and its source is licensed under the Creative Commons Attribution-Sharealike 4.0 International license.
The source code of the tool this documentation describes is licensed under the GNU General Public License 3.0.
.SH COPYRIGHT
Louis Holbrook <dev@holbrook.no> (https://holbrook.no)
PGP: 59A844A484AC11253D3A3E9DCDCBD24DD1D0E001
.SH SOURCE CODE
https://git.defalsify.org

276
man/build/eth-monitor.1 Normal file
View File

@ -0,0 +1,276 @@
.TH eth-monitor 1
.SH NAME
eth-monitor \- Cache, index and monitor transactions with an EVM node rpc
.SH SYNOPSIS
.SY eth-monitor
[ --skip-history ] [ --single ] [ p \fIeth_provider\fP ] [ --includes-file \fIfile\fP ] [ -i chain_spec ]
.YS
.SY eth-monitor
[ --skip-history ] [ --single ] [ p \fIeth_provider\fP ] [ --excludes-file \fIfile\fP ] [ --include-default ] [ -i chain_spec ] 
.YS
.SH DESCRIPTION
The \fBeth-monitor\fP has fulfills three distinct but related functions:
.IP
1. A customizable view of on transactions of interest.
.IP
2. A block and transaction cache.
.IP
3. Arbitrary code executions using a transaction (and its block) as input.
.P
Using an EVM RPC endpoint, the \fBeth-monitor\fP tool will retrieve blocks within a given range and provides arbitrary processing of each transaction.
.P
A collection of options is provided to control the behavior of which block ranges to sync, which criteria to use for display and cache, and what code to execute for matching transactions. Details on each topic can be found in the \fBSYNCING\fP, \fBMATCHING ADDRESSES\fP and \fBDEFINING FILTERS\fP sections below, respectively.
.P
Example executions of the tool can be found in the \fBEXAMPLES\fP section.
.P
.SS OPTIONS
.TP
\fB-0\fP
Omit newline to output
.TP
\fB--address \fI\fIaddress
\fP\fP
Add an address of interest to match any role. Complements \fB--address-file\fP.
.TP
\fB-c \fI\fIconfig_dir\fP\fP, \fB--config \fI\fIconfig_dir\fP\fP
Load configuration files from given directory. All files with an .ini extension will be loaded, of which all must contain valid ini file data.
.TP
\fB--context-key \fI\fIkey=value
\fP\fP
Add a key-value pair that gets passed to the syncer context. May be specified several times.
.TP
\fB--dumpconfig \fI\fIformat\fP\fP
Output configuration settings rendered from environment and inputs. Valid arguments are \fIini\fP for ini file output, and \fIenv\fP for environment variable output. See \fBCONFIGURATION\fP.
.TP
\fB--env-prefix\fP
Environment prefix for variables to overwrite configuration. Example: If \fB--env-prefix\fP is set to \fBFOO\fP then configuration variable \fBBAR_BAZ\fP would be set by environment variable \fBFOO_BAZ_BAR\fP. Also see \fBENVIRONMENT\fP.
.TP
\fB--excludes-file \fI\fIfile
\fP\fP
Load address exclude matching rules from file. See \fBMATCHING ADDRESSES\fP.
.TP
\fB--exec \fI\fIaddress
\fP\fP
Add an address of interest to executable address array. Complements \fB--address-file\fP.
.TP
\fB--filter \fI\fImodule
\fP\fP
Add code execution filter to all matched transactions. The argument must be a python module path. Several filters may be added by supplying the option multiple times. Filters will be executed in the order the options are given. See \fBDEFINING FILTERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--height\fP
Block height at which to query state for. Does not apply to transactions.
.TP
\fB-i \fI\fIchain_spec\fP\fP, \fB--chain-spec \fI\fIchain_spec\fP\fP
Chain specification string, in the format <engine>:<fork>:<chain_id>:<common_name>. Example: "evm:london:1:ethereum". Overrides the \fIRPC_CREDENTIALS\fP configuration setting.
.TP
\fB--include-default \fI\fI
\fP\fP
Match all addresses by default. Addresses may be excluded using --excludes-file. If this is set, --input, --output, --exec and --includes-file will have no effect.
.TP
\fB--includes-file \fI\fIfile
\fP\fP
Load address include matching rules from file. See \fBMATCHING ADDRESSES\fP.
.TP
\fB--input \fI\fIaddress
\fP\fP
Add an address of interest to inputs (recipients) array. Complements \fB--address-file\fP.
.TP
\fB-n \fI\fInamespace\fP\fP, \fB--namespace \fI\fInamespace\fP\fP
Load given configuration namespace. Configuration will be loaded from the immediate configuration subdirectory with the same name.
.TP
\fB--no-logs\fP
Turn of logging completely. Negates \fB-v\fP and \fB-vv\fP
.TP
\fB--output \fI\fIaddress
\fP\fP
Add an address of interest to outputs (sender) array. Complements \fB--address-file\fP.
.TP
\fB-p\fP, \fB--rpc-provider\fP
Fully-qualified URL of RPC provider. Overrides the \fIRPC_PROVIDER\fP configuration setting.
.TP
\fB--raw\fP
Produce output most optimized for machines.
.TP
\fB--renderer \fI\fImodule
\fP\fP
Add output renderer filter to all matched transactions. The argument must be a python module path. Several renderers may be added by supplying the option multiple times. See \fBRENDERERS\fP section of \fBeth-monitor (1)\fP for more details.
.TP
\fB--rpc-batch-limit\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together. Overrides the \fIRPC_BATCH_LIMIT\fP configuration setting.
.TP
\fB--rpc-dialect\fP
RPC backend dialect. If specified it \fImay\fP help with encoding and decoding issues. Overrides the \fIRPC_DIALECT\fP configuration setting.
.TP
\fB--store-block-data \fI\fI
\fP\fP
Store block data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB--store-tx-data \fI\fI
\fP\fP
Store transaction data in cache for matching transactions. Requires \fB--cache-dir\fP.
.TP
\fB-u\fP, \fB--unsafe\fP
Allow addresses that do not pass checksum.
.TP
\fB-v\fP
Verbose. Show logs for important state changes.
.TP
\fB-vv\fP
Very verbose. Show logs with debugging information.
.TP
\fB--x-address \fI\fIaddress
\fP\fP
Add an address of interest to match any role.
.TP
\fB--x-exec \fI\fIaddress
\fP\fP
Add an address of disinterest to executable address array.
.TP
\fB--x-input \fI\fIaddress
\fP\fP
Add an address of disinterest to inputs (recipients) array.
.TP
\fB--x-output \fI\fIaddress
\fP\fP
Add an address of disinterest to outputs (sender) array.
.SH CONFIGURATION
All configuration settings may be overriden both by environment variables, or by overriding settings with the contents of ini-files in the directory defined by the \fB-c\fP option.
The active configuration, with values assigned from environment and arguments, can be output using the \fB--dumpconfig\fP \fIformat\fP option. Note that entries having keys prefixed with underscore (e.g. _SEQ) are not actual configuration settings, and thus cannot be overridden with environment variables.
To refer to a configuration setting by environment variables, the \fIsection\fP and \fIkey\fP are concatenated together with an underscore, and transformed to upper-case. For example, the configuration variable \fIFOO_BAZ_BAR\fP refers to an ini-file entry as follows:
.EX
[foo]
bar_baz = xyzzy
.EE
In the \fBENVIRONMENT\fP section below, the relevant configuration settings for this tool is listed along with a short description of its meaning.
Some configuration settings may also be overriden by command line options. Also note that the use of the \fB-n\fP and \fB--env-prefix\fP options affect how environment and configuration is read. The effects of options on how configuration settings are affective is described in the respective \fBOPTIONS\fP section.
.SH MATCHING ADDRESSES
By default, addresses to match against transactions need to be explicitly specified. This behavior can be reversed with the \fB--include-default\fP option. Addresses to match are defined using the \fB--input\fP, \fB--output\fP and \fB--exec\fP options. Addresses specified multiple times will be deduplicated.
.P
Inclusion rules may also be loaded from file by specifying the \fB--includes-file\fP and \fB--excludes-file\fP options. Each file must specify the outputs, inputs and exec addresses as comma separated lists respectively, separated by tabs.
.P
In the current state of this tool, address matching will affect all parts of the processing; cache, code execution and rendering.
.SH SYNCING
When a sync is initiated, the state of this sync is persisted. This way, previous syncs that did not complete for some reason will be resumed where they left off.
.P
A special sync type \fB--head\fP starts syncing at the current head of the chain, and continue to sync until interrupted. When resuming sync, a new sync range between the current block head and the block height at which the previous \fB--head\fP sync left off will automatically be created.
.P
Syncs can be forced to (re)run for ranges regardless of previous state by using the \fB--single\fP option. However, there is no protection in place from preventing code filters from being executed again on the same transaction when this is done. See \fBDEFINING FILTERS\fP below.
.SH CACHE
When syncing, the hash of a block and transaction matching the address criteria will be stored in the cache. The hashes can be used for future data lookups.
.P
If \fB--store-block-data\fP and/or \fB--store-tx-data\fP is set, a copy of the block and/or transaction data will also be stored, respectively.
.SH RENDERING
Rendering in the context of \fBeth-monitor\fP refers to a formatted output stream that occurs independently of caching and code execution.
.P
Filters for rendering may be specified by specifying python modules to the \fB--renderer\fP option. This option may be specified multiple times.
.P
Rendering filters will be executed in order, and the first filter to return \fIFalse\fP
.SH DEFINING FILTERS
Filters will strictly be executed in the order which they are defined on the command line.
A python module used for filter must fulfill two conditions:
.IP
1. It must provide a class named \fIFilter\fP in the package base namespace.
.IP
2. The \fIFilter\fP class must extend the \fIchainsyncer.filter.SyncFilter\fP interface, and at least override the \fIfilter\fP method.
.SS SYNCER AND FILTER CONTEXT
Key-value pairs specified with `--context-key` will be passed to the filter's \fIprepare\fP method, aswell as the \fIctx\fP parameter of the \fIfilter\fP method.
.SH FURTHER READING
Refer to the \fBchainsyncer\fP chapter n \fIinfo chaintool\fP for in-depth information on the subjects of syncing and filtering.
.SH ENVIRONMENT
.TP
\fICHAIN_SPEC\fP
String specifying the type of chain connected to, in the format \fI<engine>:<fork>:<network_id>:<common_name>\fP. For EVM nodes the \fIengine\fP value will always be \fIevm\fP.
.TP
\fIRPC_BATCH_LIMIT\fP
Set number of RPC requests that can be set to the RPC provider as a batch request. This is made available through settings to any request builder implementing batch requests. A value of 1 means no batch will be used. A value of 0 indicates that the limit is not relevant. Any other positive value signals the maximum number of requests to be batched together.
.TP
\fIRPC_DIALECT\fP
Enables translations of EVM node specific formatting and response codes.
.TP
\fIRPC_PROVIDER\fP
Fully-qualified URL to the RPC endpoint of the blockchain node.
.SH LICENSE
This documentation and its source is licensed under the Creative Commons Attribution-Sharealike 4.0 International license.
The source code of the tool this documentation describes is licensed under the GNU General Public License 3.0.
.SH COPYRIGHT
Louis Holbrook <dev@holbrook.no> (https://holbrook.no)
PGP: 59A844A484AC11253D3A3E9DCDCBD24DD1D0E001
.SH SOURCE CODE
https://git.defalsify.org

View File

@ -29,14 +29,19 @@ Rendering filters will be executed in order, and the first filter to return \fIF
.SH DEFINING FILTERS
Filters will strictly be executed in the order which they are defined on the command line.
A python module used for filter must fulfill two conditions:
.IP
1. It must provide a class named \fIFilter\fP in the package base namespace.
.IP
2. The \fIFilter\fP class must include a method named \fIfilter\fP with the signature \fIdef filter(self, conn, block, tx, db_session=None)\fP.
2. The \fIFilter\fP class must extend the \fIchainsyncer.filter.SyncFilter\fP interface, and at least override the \fIfilter\fP method.
Filters will strictly be executed in the order which they are defined on the command line.
.SS SYNCER AND FILTER CONTEXT
Key-value pairs specified with `--context-key` will be passed to the filter's \fIprepare\fP method, aswell as the \fIctx\fP parameter of the \fIfilter\fP method.
.SH FURTHER READING

View File

@ -13,3 +13,4 @@ storetx Store transaction data in cache for matching transactions. Requires \fB-
storeblock Store block data in cache for matching transactions. Requires \fB--cache-dir\fP. --store-block-data
renderer Add output renderer filter to all matched transactions. The argument must be a python module path. Several renderers may be added by supplying the option multiple times. See \fBRENDERERS\fP section of \fBeth-monitor (1)\fP for more details. --renderer module
filter Add code execution filter to all matched transactions. The argument must be a python module path. Several filters may be added by supplying the option multiple times. Filters will be executed in the order the options are given. See \fBDEFINING FILTERS\fP section of \fBeth-monitor (1)\fP for more details. --filter module
context_key Add a key-value pair that gets passed to the syncer context. May be specified several times. --context-key key=value

View File

@ -1,5 +1,6 @@
chainlib-eth~=0.1.2
chainlib~=0.1.2
chainsyncer~=0.4.4
chainlib-eth~=0.6.0
chainlib~=0.5.2
chainsyncer~=0.8.5
leveldir~=0.3.0
eth-cache~=0.1.2
eth-cache~=0.4.0
confini~=0.6.3

17
run_tests.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
set -a
set -e
set -x
default_pythonpath=$PYTHONPATH:.
export PYTHONPATH=${default_pythonpath:-.}
>&2 echo using pythonpath $PYTHONPATH
for f in `ls tests/*.py`; do
python $f
done
for f in `ls tests/rules/*.py`; do
python $f
done
set +x
set +e
set +a

View File

@ -1,6 +1,6 @@
[metadata]
name = eth-monitor
version = 0.4.6
version = 0.8.9
description = Monitor and cache transactions using match filters
author = Louis Holbrook
author_email = dev@holbrook.no
@ -17,23 +17,25 @@ classifiers =
Topic :: Software Development :: Libraries
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Topic :: Internet
# Topic :: Blockchain :: EVM
license = GPL3
license = AGPLv3+
licence_files =
LICENSE
[options]
include_package_data = True
python_requires = >=3.7
python_requires = >=3.8
packages =
eth_monitor
eth_monitor.importers
eth_monitor.filters
eth_monitor.runnable
eth_monitor.mock
eth_monitor.cli
[options.entry_points]
console_scripts =
eth-monitor = eth_monitor.runnable.sync:main
eth-monitor-sync = eth_monitor.runnable.sync:main

View File

@ -21,8 +21,19 @@ while True:
test_requirements.append(l.rstrip())
f.close()
f = open('README.md', 'r')
description = f.read()
f.close()
man_dir = 'man/build'
setup(
install_requires=requirements,
tests_require=test_requirements,
data_files=[("man/man1", [
os.path.join(man_dir, 'eth-monitor.1'),
os.path.join(man_dir, 'eth-monitor-sync.1'),
]
)],
long_description=description,
long_description_content_type='text/markdown',
)

View File

@ -1,5 +1,5 @@
eth_tester==0.5.0b3
py-evm==0.3.0a20
rlp==2.0.1
eth_tester==0.10.0b4
py-evm==0.10.0b4
rlp==3.0.0
pytest==6.0.1
coverage==5.5

160
tests/rules/test_base.py Normal file
View File

@ -0,0 +1,160 @@
# standard imports
import logging
import unittest
import os
# local imports
from eth_monitor.rules import *
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
class TestRule(unittest.TestCase):
def setUp(self):
self.alice = os.urandom(20).hex()
self.bob = os.urandom(20).hex()
self.carol = os.urandom(20).hex()
self.dave = os.urandom(20).hex()
self.x = os.urandom(20).hex()
self.y = os.urandom(20).hex()
self.hsh = os.urandom(32).hex()
def test_address_include(self):
data = b''
outs = [self.alice]
ins = []
execs = []
rule = RuleSimple(outs, ins, execs)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
outs = []
ins = [self.alice]
execs = []
rule = RuleSimple(outs, ins, execs)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertTrue(r)
outs = []
ins = []
execs = [self.x]
rule = RuleSimple(outs, ins, execs)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
data = b'deadbeef0123456789'
data_match = [data[:8]]
rule = RuleMethod(data_match)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, b'abcd' + data, self.hsh)
self.assertFalse(r)
rule = RuleData(data_match)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, b'abcd' + data, self.hsh)
self.assertTrue(r)
def test_address_exclude(self):
data = b''
outs = [self.alice]
ins = []
execs = []
rule = RuleSimple(outs, ins, execs)
c = AddressRules()
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
c = AddressRules(include_by_default=True)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertTrue(r)
outs = []
ins = [self.alice]
execs = []
rule = RuleSimple(outs, ins, execs)
c = AddressRules(include_by_default=True)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
outs = []
ins = []
execs = [self.x]
rule = RuleSimple(outs, ins, execs)
c = AddressRules(include_by_default=True)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertTrue(r)
data = b'deadbeef0123456789'
data_match = [data[:8]]
rule = RuleMethod(data_match)
c = AddressRules(include_by_default=True)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, b'abcd' + data, self.hsh)
self.assertTrue(r)
rule = RuleData(data_match)
c = AddressRules(include_by_default=True)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.x, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, b'abcd' + data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, b'abcd', self.hsh)
self.assertTrue(r)
def test_address_include_exclude(self):
data = b''
outs = [self.alice]
ins = []
execs = []
rule = RuleSimple(outs, ins, execs)
c = AddressRules()
c.include(rule)
c.exclude(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,90 @@
import logging
import unittest
import os
# local imports
from eth_monitor.rules import *
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
class TestRule(unittest.TestCase):
def setUp(self):
self.alice = os.urandom(20).hex()
self.bob = os.urandom(20).hex()
self.carol = os.urandom(20).hex()
self.dave = os.urandom(20).hex()
self.x = os.urandom(20).hex()
self.y = os.urandom(20).hex()
self.hsh = os.urandom(32).hex()
def test_greedy_includes(self):
data = b''
outs = [self.alice]
ins = [self.carol]
execs = []
rule = RuleSimple(outs, ins, execs, match_all=True)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.carol, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.alice, self.carol, data, self.hsh)
self.assertTrue(r)
rule = RuleSimple(outs, ins, execs)
c = AddressRules(match_all=True)
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.bob, self.alice, data, self.hsh)
self.assertFalse(r)
r = c.apply_rules_addresses(self.bob, self.carol, data, self.hsh)
self.assertTrue(r)
r = c.apply_rules_addresses(self.alice, self.carol, data, self.hsh)
self.assertTrue(r)
def test_greedy_data(self):
data = os.urandom(128).hex()
data_match_one = data[4:8]
data_match_two = data[32:42]
data_match_fail = os.urandom(64).hex()
data_match = [data_match_one]
rule = RuleData(data_match, match_all=True)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
data_match = [data_match_two]
rule = RuleData(data_match, match_all=True)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
data_match = [data_match_two, data_match_one]
rule = RuleData(data_match, match_all=True)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertTrue(r)
data_match = [data_match_two, data_match_fail, data_match_one]
rule = RuleData(data_match, match_all=True)
c = AddressRules()
c.include(rule)
r = c.apply_rules_addresses(self.alice, self.bob, data, self.hsh)
self.assertFalse(r)
if __name__ == '__main__':
unittest.main()