Repository: makerdao/pymaker
Branch: master
Commit: 479506b356a0
Files: 168
Total size: 1.1 MB
Directory structure:
gitextract_8lkti2po/
├── .github/
│ └── workflows/
│ └── tests.yaml
├── .gitignore
├── .python-version
├── COPYING
├── Makefile
├── README.md
├── config/
│ ├── kovan-addresses.json
│ ├── mainnet-addresses.json
│ └── testnet-addresses.json
├── docker-compose.yml
├── docs/
│ ├── conf.py
│ └── index.rst
├── pymaker/
│ ├── __init__.py
│ ├── abi/
│ │ ├── Cat.abi
│ │ ├── Clipper.abi
│ │ ├── ClipperCallee.abi
│ │ ├── DSAuth.abi
│ │ ├── DSChief.abi
│ │ ├── DSEthToken.abi
│ │ ├── DSGuard.abi
│ │ ├── DSPause.abi
│ │ ├── DSProxy.abi
│ │ ├── DSProxyCache.abi
│ │ ├── DSProxyFactory.abi
│ │ ├── DSRoles.abi
│ │ ├── DSToken.abi
│ │ ├── DSValue.abi
│ │ ├── DSVault.abi
│ │ ├── DaiJoin.abi
│ │ ├── Dog.abi
│ │ ├── DsrManager.abi
│ │ ├── DssCdpManager.abi
│ │ ├── DssProxyActionsDsr.abi
│ │ ├── ERC20Token.abi
│ │ ├── ESM.abi
│ │ ├── End.abi
│ │ ├── EtherDelta.abi
│ │ ├── EtherToken.abi
│ │ ├── Exchange.abi
│ │ ├── ExchangeV2-ERC20Proxy.abi
│ │ ├── ExchangeV2.abi
│ │ ├── Flapper.abi
│ │ ├── Flipper.abi
│ │ ├── Flopper.abi
│ │ ├── GemJoin.abi
│ │ ├── GemJoin5.abi
│ │ ├── Jug.abi
│ │ ├── MakerOtcSupportMethods.abi
│ │ ├── MatchingMarket.abi
│ │ ├── OSM.abi
│ │ ├── Pit.abi
│ │ ├── Pot.abi
│ │ ├── ProxyRegistry.abi
│ │ ├── SaiTap.abi
│ │ ├── SaiTop.abi
│ │ ├── SaiTub.abi
│ │ ├── SaiVox.abi
│ │ ├── SimpleMarket.abi
│ │ ├── Spotter.abi
│ │ ├── TokenFaucet.abi
│ │ ├── TokenTransferProxy.abi
│ │ ├── TxManager.abi
│ │ ├── Vat.abi
│ │ ├── Vow.abi
│ │ ├── ZRXToken.abi
│ │ └── diff-abi.sh
│ ├── approval.py
│ ├── auctions.py
│ ├── auth.py
│ ├── cdpmanager.py
│ ├── collateral.py
│ ├── deployment.py
│ ├── dsr.py
│ ├── dsrmanager.py
│ ├── dss.py
│ ├── etherdelta.py
│ ├── feed.py
│ ├── gas.py
│ ├── governance.py
│ ├── ilk.py
│ ├── join.py
│ ├── keys.py
│ ├── lifecycle.py
│ ├── logging.py
│ ├── model.py
│ ├── numeric.py
│ ├── oasis.py
│ ├── oracles.py
│ ├── proxy.py
│ ├── reloadable_config.py
│ ├── sai.py
│ ├── shutdown.py
│ ├── sign.py
│ ├── tightly_packed.py
│ ├── token.py
│ ├── transactional.py
│ ├── util.py
│ ├── vault.py
│ ├── zrx.py
│ └── zrxv2.py
├── requirements-dev.txt
├── requirements.txt
├── setup.py
├── test-dss.sh
├── test.sh
├── tests/
│ ├── __init__.py
│ ├── abi/
│ │ ├── DaiMock.abi
│ │ ├── DaiMock.sol
│ │ ├── GemMock.abi
│ │ ├── GemMock.sol
│ │ ├── OasisMockPriceOracle.abi
│ │ └── OasisMockPriceOracle.sol
│ ├── accounts/
│ │ ├── 0_0x9596c16d7bf9323265c2f2e22f43e6c80eb3d943.json
│ │ ├── 1_0xe415482ca06eeb684ad3f758c2129fca4b1eb1f4.json
│ │ ├── 2_0x270b0e8d873e858abd698a000b0da0b94e21d84c.json
│ │ ├── 3_0x812e87be5d4198fca55cb52fa60cb46620617474.json
│ │ ├── 4_0x13314e21cd6d343ceb857073f3f6d9368919d1ef.json
│ │ ├── 5_0x176087fea5c41fc370fabbd850521bc4451690ca.json
│ │ └── pass
│ ├── config/
│ │ ├── keys/
│ │ │ └── UnlimitedChain/
│ │ │ ├── key.json
│ │ │ ├── key1.json
│ │ │ ├── key2.json
│ │ │ ├── key3.json
│ │ │ └── key4.json
│ │ └── parity-dev-constantinopole.json
│ ├── conftest.py
│ ├── dss_token.py
│ ├── helpers.py
│ ├── manual_test_async_tx.py
│ ├── manual_test_cdpmanager.py
│ ├── manual_test_dsr.py
│ ├── manual_test_goerli.py
│ ├── manual_test_mcd.py
│ ├── manual_test_node.py
│ ├── manual_test_tx_recovery.py
│ ├── manual_test_zrxv2.py
│ ├── test_approval.py
│ ├── test_auctions.py
│ ├── test_auth.py
│ ├── test_cdpmanager.py
│ ├── test_dsrmanager.py
│ ├── test_dss.py
│ ├── test_etherdelta.py
│ ├── test_feed.py
│ ├── test_gas.py
│ ├── test_general.py
│ ├── test_general2.py
│ ├── test_governance.py
│ ├── test_keys.py
│ ├── test_lifecycle.py
│ ├── test_model.py
│ ├── test_numeric.py
│ ├── test_oasis.py
│ ├── test_proxy.py
│ ├── test_reloadable_config.py
│ ├── test_sai.py
│ ├── test_savings.py
│ ├── test_shutdown.py
│ ├── test_sign.py
│ ├── test_token.py
│ ├── test_transactional.py
│ ├── test_util.py
│ ├── test_vault.py
│ ├── test_zrx.py
│ └── test_zrxv2.py
└── utils/
└── etherdelta-client/
├── .gitignore
├── main.js
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/tests.yaml
================================================
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository and submodules
uses: actions/checkout@v3
with:
submodules: recursive
- name: setup python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: install python packages
run: |
python -m pip install --upgrade pip
pip install virtualenv --upgrade
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: execute tests
run: ./test.sh
================================================
FILE: .gitignore
================================================
.idea
*.iml
__pycache__
.cache
.coverage
.pytest_cache
.DS_Store
logs/*.json.log
tests/config/keys/UnlimitedChain/address_book.json
docs/_doc
_virtualenv
================================================
FILE: .python-version
================================================
3.6.6
================================================
FILE: COPYING
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
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,
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.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
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.
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.
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.
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.
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.
TERMS AND CONDITIONS
0. Definitions.
"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.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
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. 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 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 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 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 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 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 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.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
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 AGPL, see
.
================================================
FILE: Makefile
================================================
help: ## Show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
doc-clean: ## Clean the documentation output directory
cd docs; rm -rf _doc
doc-build: doc-clean ## Build the documentation
cd docs; sphinx-build . _doc
doc-open: doc-build ## Open the documentation
cd docs; open _doc/index.html
doc-deploy: doc-build ## Deploy the documentation at http://maker-keeper-docs.surge.sh
cd docs; surge --project _doc --domain maker-keeper-docs.surge.sh
================================================
FILE: README.md
================================================
# pymaker
Python API for Maker contracts.

## Introduction
The _DAI Stablecoin System_ incentivizes external agents, called _keepers_,
to automate certain operations around the Ethereum blockchain. In order to ease their
development, an API around most of the Maker contracts has been created. It can be used
not only by keepers, but may also be found useful by authors of some other, unrelated
utilities aiming to interact with these contracts.
Based on this API, a set of reference Maker keepers is being developed. They all used to reside
in this repository, but now each of them has an individual one:
[bite-keeper](https://github.com/makerdao/bite-keeper) (SCD only),
[arbitrage-keeper](https://github.com/makerdao/arbitrage-keeper),
[auction-keeper](https://github.com/makerdao/auction-keeper) (MCD only),
[cdp-keeper](https://github.com/makerdao/cdp-keeper) (SCD only),
[market-maker-keeper](https://github.com/makerdao/market-maker-keeper).
You only need to install this project directly if you want to build your own keepers,
or if you want to play with this API library itself. If you just want to install
one of reference keepers, go to one of the repositories linked above and start from there.
Each of these keepers references some version of `pymaker` via a Git submodule.
## Installation
This project uses *Python 3.6.6*.
In order to clone the project and install required third-party packages please execute:
```
git clone https://github.com/makerdao/pymaker.git
cd pymaker
pip3 install -r requirements.txt
```
### Known Ubuntu issues
In order for the `secp256k` Python dependency to compile properly, following packages will need to be installed:
```
sudo apt-get install build-essential automake libtool pkg-config libffi-dev python-dev python-pip libsecp256k1-dev
```
(for Ubuntu 18.04 Server)
### Known macOS issues
In order for the Python requirements to install correctly on _macOS_, please install
`openssl`, `libtool`, `pkg-config` and `automake` using [Homebrew](https://brew.sh/):
```
brew install openssl libtool pkg-config automake
```
and set the `LDFLAGS` environment variable before you run `pip3 install -r requirements.txt`:
```
export LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include"
```
### Known node issues
* `pymaker` has been tested against **Parity/OpenEthereum** and **Geth**. It has not been tested against **Hyperledger Besu**.
* Many Ethereum node providers do not support the full [JSON-RPC API](https://eth.wiki/json-rpc/API#json-rpc-methods).
As such, certain JSON-RPC calls in `__init__.py` may not function properly.
* Some node providers only support certain calls using websocket endpoints. Unfortunately, Web3.py's
`WebsocketProvider` [does not support](https://github.com/ethereum/web3.py/issues/1413) multiple threads awaiting a
response from the websocket, breaking some core `pymaker` functionality in `Lifecycle` and `Transact` classes.
* When using an **Infura** node to pull event logs, ensure your requests are batched into a small enough chunks such
that no more than 10,000 results will be returned for each request.
* Asynchronous submission of simultaneous transactions often doesn't work on third-party node providers because RPC
calls to `parity_nextNonce` and `getTransactionCount` are inappropriately proxied, cached, or just plain not
supported. To remedy this, a serial-incrementing nonce is used for these providers' URLs. The downside to a serial-
incrementing nonce is that transactions submitted for the same account from another wallet or keeper will bring the
next nonce out-of-alignment, causing transaction failures or unexpected replacements. To work around this, stop the
application, wait for pending transactions for the account to be mined, and then restart the application.
* Recovery of pending transactions does not work on certain third-party node providers.
## Available APIs
The current version provides APIs around:
* `ERC20Token`,
* `Tub`, `Tap`,`Top` and `Vox` (),
* `Vat`, `Cat`, `Vow`, `Jug`, `Flipper`, `Flapper`, `Flopper` ()
* `SimpleMarket`, `ExpiringMarket` and `MatchingMarket` (),
* `TxManager` (),
* `DSGuard` (),
* `DSToken` (),
* `DSEthToken` (),
* `DSValue` (),
* `DSVault` (),
* `EtherDelta` (),
* `0x v1` (, ),
* `0x v2`.
APIs around the following functionality have not been implemented:
* Governance (`DSAuth`, `DSGuard`, `DSSpell`, `Mom`)
Contributions from the community are appreciated.
## Code samples
Below you can find some code snippets demonstrating how the API can be used both for developing
your own keepers and for creating some other utilities interacting with the _DAI Stablecoin_
ecosystem contracts.
### Token transfer
This snippet demonstrates how to transfer some SAI from our default address. The SAI token address
is discovered by querying the `Tub`, so all we need as a `Tub` address:
```python
from web3 import HTTPProvider, Web3
from pymaker import Address
from pymaker.token import ERC20Token
from pymaker.numeric import Wad
from pymaker.sai import Tub
web3 = Web3(HTTPProvider(endpoint_uri="http://localhost:8545"))
tub = Tub(web3=web3, address=Address('0xb7ae5ccabd002b5eebafe6a8fad5499394f67980'))
sai = ERC20Token(web3=web3, address=tub.sai())
sai.transfer(address=Address('0x0000000000111111111100000000001111111111'),
value=Wad.from_number(10)).transact()
```
### Updating a DSValue
This snippet demonstrates how to update a `DSValue` with the ETH/USD rate pulled from _CryptoCompare_:
```python
import json
import urllib.request
from web3 import HTTPProvider, Web3
from pymaker import Address
from pymaker.feed import DSValue
from pymaker.numeric import Wad
def cryptocompare_rate() -> Wad:
with urllib.request.urlopen("https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD") as url:
data = json.loads(url.read().decode())
return Wad.from_number(data['USD'])
web3 = Web3(HTTPProvider(endpoint_uri="http://localhost:8545"))
dsvalue = DSValue(web3=web3, address=Address('0x038b3d8288df582d57db9be2106a27be796b0daf'))
dsvalue.poke_with_int(cryptocompare_rate().value).transact()
```
### SAI introspection
This snippet demonstrates how to fetch data from `Tub` and `Tap` contracts:
```python
from web3 import HTTPProvider, Web3
from pymaker import Address
from pymaker.token import ERC20Token
from pymaker.numeric import Ray
from pymaker.sai import Tub, Tap
web3 = Web3(HTTPProvider(endpoint_uri="http://localhost:8545"))
tub = Tub(web3=web3, address=Address('0x448a5065aebb8e423f0896e6c5d525c040f59af3'))
tap = Tap(web3=web3, address=Address('0xbda109309f9fafa6dd6a9cb9f1df4085b27ee8ef'))
sai = ERC20Token(web3=web3, address=tub.sai())
skr = ERC20Token(web3=web3, address=tub.skr())
gem = ERC20Token(web3=web3, address=tub.gem())
print(f"")
print(f"Token summary")
print(f"-------------")
print(f"SAI total supply : {sai.total_supply()} SAI")
print(f"SKR total supply : {skr.total_supply()} SKR")
print(f"GEM total supply : {gem.total_supply()} GEM")
print(f"")
print(f"Collateral summary")
print(f"------------------")
print(f"GEM collateral : {tub.pie()} GEM")
print(f"SKR collateral : {tub.air()} SKR")
print(f"SKR pending liquidation: {tap.fog()} SKR")
print(f"")
print(f"Debt summary")
print(f"------------")
print(f"Debt ceiling : {tub.cap()} SAI")
print(f"Good debt : {tub.din()} SAI")
print(f"Bad debt : {tap.woe()} SAI")
print(f"Surplus : {tap.joy()} SAI")
print(f"")
print(f"Feed summary")
print(f"------------")
print(f"REF per GEM feed : {tub.pip()}")
print(f"REF per SKR price : {tub.tag()}")
print(f"GEM per SKR price : {tub.per()}")
print(f"")
print(f"Tub parameters")
print(f"--------------")
print(f"Liquidation ratio : {tub.mat()*100} %")
print(f"Liquidation penalty : {tub.axe()*100 - Ray.from_number(100)} %")
print(f"Stability fee : {tub.tax()} %")
print(f"")
print(f"All cups")
print(f"--------")
for cup_id in range(1, tub.cupi()+1):
cup = tub.cups(cup_id)
print(f"Cup #{cup_id}, lad={cup.lad}, ink={cup.ink} SKR, tab={tub.tab(cup_id)} SAI, safe={tub.safe(cup_id)}")
```
### Multi-collateral Dai
This snippet demonstrates how to create a CDP and draw Dai.
```python
import sys
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.numeric import Wad
web3 = Web3(HTTPProvider(endpoint_uri="https://localhost:8545",
request_kwargs={"timeout": 10}))
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
mcd = DssDeployment.from_json(web3=web3, conf=open("tests/config/kovan-addresses.json", "r").read())
our_address = Address(web3.eth.defaultAccount)
# Choose the desired collateral; in this case we'll wrap some Eth
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
collateral.gem.deposit(Wad.from_number(3)).transact()
# Add collateral and allocate the desired amount of Dai
collateral.approve(our_address)
collateral.adapter.join(our_address, Wad.from_number(3)).transact()
mcd.vat.frob(ilk, our_address, dink=Wad.from_number(3), dart=Wad.from_number(153)).transact()
print(f"CDP Dai balance before withdrawal: {mcd.vat.dai(our_address)}")
# Mint and withdraw our Dai
mcd.approve_dai(our_address)
mcd.dai_adapter.exit(our_address, Wad.from_number(153)).transact()
print(f"CDP Dai balance after withdrawal: {mcd.vat.dai(our_address)}")
# Repay (and burn) our Dai
assert mcd.dai_adapter.join(our_address, Wad.from_number(153)).transact()
print(f"CDP Dai balance after repayment: {mcd.vat.dai(our_address)}")
# Withdraw our collateral
mcd.vat.frob(ilk, our_address, dink=Wad(0), dart=Wad.from_number(-153)).transact()
mcd.vat.frob(ilk, our_address, dink=Wad.from_number(-3), dart=Wad(0)).transact()
collateral.adapter.exit(our_address, Wad.from_number(3)).transact()
print(f"CDP Dai balance w/o collateral: {mcd.vat.dai(our_address)}")
```
### Asynchronous invocation of Ethereum transactions
This snippet demonstrates how multiple token transfers can be executed asynchronously:
```python
from web3 import HTTPProvider
from web3 import Web3
from pymaker import Address, synchronize
from pymaker.numeric import Wad
from pymaker.sai import Tub
from pymaker.token import ERC20Token
web3 = Web3(HTTPProvider(endpoint_uri="http://localhost:8545"))
tub = Tub(web3=web3, address=Address('0x448a5065aebb8e423f0896e6c5d525c040f59af3'))
sai = ERC20Token(web3=web3, address=tub.sai())
skr = ERC20Token(web3=web3, address=tub.skr())
synchronize([sai.transfer(Address('0x0101010101020202020203030303030404040404'), Wad.from_number(1.5)).transact_async(),
skr.transfer(Address('0x0303030303040404040405050505050606060606'), Wad.from_number(2.5)).transact_async()])
```
### Multiple invocations in one Ethereum transaction
This snippet demonstrates how multiple token transfers can be executed in one Ethereum transaction.
A `TxManager` instance has to be deployed and owned by the caller.
```python
from web3 import HTTPProvider
from web3 import Web3
from pymaker import Address
from pymaker.approval import directly
from pymaker.numeric import Wad
from pymaker.sai import Tub
from pymaker.token import ERC20Token
from pymaker.transactional import TxManager
web3 = Web3(HTTPProvider(endpoint_uri="http://localhost:8545"))
tub = Tub(web3=web3, address=Address('0x448a5065aebb8e423f0896e6c5d525c040f59af3'))
sai = ERC20Token(web3=web3, address=tub.sai())
skr = ERC20Token(web3=web3, address=tub.skr())
tx = TxManager(web3=web3, address=Address('0x57bFE16ae8fcDbD46eDa9786B2eC1067cd7A8f48'))
tx.approve([sai, skr], directly())
tx.execute([sai.address, skr.address],
[sai.transfer(Address('0x0101010101020202020203030303030404040404'), Wad.from_number(1.5)).invocation(),
skr.transfer(Address('0x0303030303040404040405050505050606060606'), Wad.from_number(2.5)).invocation()]).transact()
```
### Ad-hoc increasing of gas price for asynchronous transactions
```python
import asyncio
from random import randint
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.gas import FixedGasPrice
from pymaker.oasis import SimpleMarket
web3 = Web3(HTTPProvider(endpoint_uri=f"http://localhost:8545"))
otc = SimpleMarket(web3=web3, address=Address('0x375d52588c3f39ee7710290237a95C691d8432E7'))
async def bump_with_increasing_gas_price(order_id):
gas_price = FixedGasPrice(gas_price=1000000000)
task = asyncio.ensure_future(otc.bump(order_id).transact_async(gas_price=gas_price))
while not task.done():
await asyncio.sleep(1)
gas_price.update_gas_price(gas_price.gas_price + randint(0, gas_price.gas_price))
return task.result()
bump_task = asyncio.ensure_future(bump_with_increasing_gas_price(otc.get_orders()[-1].order_id))
event_loop = asyncio.get_event_loop()
bump_result = event_loop.run_until_complete(bump_task)
print(bump_result)
print(bump_result.transaction_hash)
```
## Testing
Prerequisites:
* [docker and docker-compose](https://www.docker.com/get-started) - for containerized deployments of Ganache and Parity
* [seth](https://dapp.tools/seth/) - to enable the token faucet
This project uses [pytest](https://docs.pytest.org/en/latest/) for unit testing. Testing of Multi-collateral Dai is
performed on a Dockerized local testchain included in `tests\config`.
In order to be able to run tests, please install development dependencies first by executing:
```
pip3 install -r requirements-dev.txt
```
You can then run all tests with:
```
./test.sh
```
By default, `pymaker` will not send a transaction to the chain if gas estimation fails, because this means the
transaction would revert. For testing purposes, it is sometimes useful to send bad transactions to the chain. To
accomplish this, set class variable `gas_estimate_for_bad_txs` in your application. For example:
```
from pymaker import Transact
Transact.gas_estimate_for_bad_txs = 200000
```
## License
See [COPYING](https://github.com/makerdao/pymaker/blob/master/COPYING) file.
================================================
FILE: config/kovan-addresses.json
================================================
{
"CHANGELOG": "0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F",
"MULTICALL": "0xC6D81A2e375Eee15a20E6464b51c5FC6Bb949fdA",
"FAUCET": "0x57aAeAE905376a4B1899bA81364b4cE2519CBfB3",
"MCD_DEPLOY": "0x13141b8a5E4A82Ebc6b636849dd6A515185d6236",
"FLIP_FAB": "0x7c890e1e492FDDA9096353D155eE1B26C1656a62",
"CLIP_FAB": "0x54659eebDFB1Dd0b53ed2252ed344a8dCbCC96CD",
"CALC_FAB": "0xA81598667AC561986b70ae11bBE2dd5348ed4327",
"LERP_FAB": "0xa6766Ed3574bAFc6114618E74035C7bb5e9a6aa9",
"MCD_GOV": "0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD",
"GOV_GUARD": "0xE50303C6B67a2d869684EFb09a62F6aaDD06387B",
"MCD_ADM": "0x27E0c9567729Ea6e3241DE74B3dE499b7ddd3fe6",
"VOTE_PROXY_FACTORY": "0x1400798AA746457E467A1eb9b3F3f72C25314429",
"MCD_VAT": "0xbA987bDB501d131f766fEe8180Da5d81b34b69d9",
"MCD_JUG": "0xcbB7718c9F39d05aEEDE1c472ca8Bf804b2f1EaD",
"MCD_CAT": "0xdDb5F7A3A5558b9a6a1f3382BD75E2268d1c6958",
"MCD_DOG": "0x121D0953683F74e9a338D40d9b4659C0EBb539a0",
"MCD_VOW": "0x0F4Cbe6CBA918b7488C26E29d9ECd7368F38EA3b",
"MCD_JOIN_DAI": "0x5AA71a3ae1C0bd6ac27A1f28e1415fFFB6F15B8c",
"MCD_FLAP": "0xc6d3C83A080e2Ef16E4d7d4450A869d0891024F5",
"MCD_FLOP": "0x52482a3100F79FC568eb2f38C4a45ba457FBf5fA",
"MCD_PAUSE": "0x8754E6ecb4fe68DaA5132c2886aB39297a5c7189",
"MCD_PAUSE_PROXY": "0x0e4725db88Bb038bBa4C4723e91Ba183BE11eDf3",
"MCD_GOV_ACTIONS": "0x0Ca17E81073669741714354f16D800af64e95C75",
"MCD_DAI": "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa",
"MCD_SPOT": "0x3a042de6413eDB15F2784f2f97cC68C7E9750b2D",
"MCD_POT": "0xEA190DBDC7adF265260ec4dA6e9675Fd4f5A78bb",
"MCD_END": "0x3d9603037FF096af03B83725dFdB1CDA9EA02CE4",
"MCD_ESM": "0xD5D728446275B0A12E4a4038527974b92353B4a9",
"PROXY_ACTIONS": "0xd1D24637b9109B7f61459176EdcfF9Be56283a7B",
"PROXY_ACTIONS_END": "0x7c3f28f174F2b0539C202a5307Ff48efa61De982",
"PROXY_ACTIONS_DSR": "0xc5CC1Dfb64A62B9C7Bb6Cbf53C2A579E2856bf92",
"CDP_MANAGER": "0x1476483dD8C35F25e568113C5f70249D3976ba21",
"DSR_MANAGER": "0x7f5d60432DE4840a3E7AE7218f7D6b7A2412683a",
"GET_CDPS": "0x592301a23d37c591C5856f28726AF820AF8e7014",
"ILK_REGISTRY": "0xc3F42deABc0C506e8Ae9356F2d4fc1505196DCDB",
"OSM_MOM": "0x5dA9D1C3d4f1197E5c52Ff963916Fe84D2F5d8f3",
"FLIPPER_MOM": "0x50dC6120c67E456AdA2059cfADFF0601499cf681",
"CLIPPER_MOM": "0x96E9a19Be6EA91d1C0908e5E207f944dc2E7B878",
"MCD_IAM_AUTO_LINE": "0xe7D7d61c0ed9306B6c93E7C65F6C9DDF38b9320b",
"PROXY_FACTORY": "0xe11E3b391F7E8bC47247866aF32AF67Dd58Dc800",
"PROXY_REGISTRY": "0x64A436ae831C1672AE81F674CAb8B6775df3475C",
"ETH": "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"PIP_ETH": "0x75dD74e8afE8110C8320eD397CcCff3B8134d981",
"MCD_JOIN_ETH_A": "0x775787933e92b709f2a3C70aa87999696e74A9F8",
"MCD_CLIP_ETH_A": "0x7dD1Fb6b9aFdBA9F28DB89c81723b8c6B27A2Fbe",
"MCD_CLIP_CALC_ETH_A": "0x46bE29C1993d64f0C93e81D69FfAFDF4881806f2",
"MCD_JOIN_ETH_B": "0xd19A770F00F89e6Dd1F12E6D6E6839b95C084D85",
"MCD_CLIP_ETH_B": "0x004676c737FC75A2799dFe745d23F5597620Ad43",
"MCD_CLIP_CALC_ETH_B": "0x4672215ADF0556Af60261e97E221c875ce9F0863",
"MCD_JOIN_ETH_C": "0xD166b57355BaCE25e5dEa5995009E68584f60767",
"MCD_CLIP_ETH_C": "0x86D5eA244cf6c79227CA73004C963b72431f23ac",
"MCD_CLIP_CALC_ETH_C": "0xa8AfB2680cced6de0E1dfe5C35F0FEdFB8E95720",
"BAT": "0x9f8cFB61D3B2aF62864408DD703F9C3BEB55dff7",
"PIP_BAT": "0x5C40C9Eb35c76069fA4C3A00EA59fAc6fFA9c113",
"MCD_JOIN_BAT_A": "0x2a4C485B1B8dFb46acCfbeCaF75b6188A59dBd0a",
"MCD_CLIP_BAT_A": "0x332B44A24e2CF8A258E8A1932b13296b9316a74c",
"MCD_CLIP_CALC_BAT_A": "0x4AB9058A9cAB0B18B4b40621Fa44B2131836Ad32",
"USDC": "0xBD84be3C303f6821ab297b840a99Bd0d4c4da6b5",
"PIP_USDC": "0x4c51c2584309b7BF328F89609FDd03B3b95fC677",
"MCD_JOIN_USDC_A": "0x4c514656E7dB7B859E994322D2b511d99105C1Eb",
"MCD_CLIP_USDC_A": "0x09D45087c035DbcD8d6fB5e9d4c5341b9101E626",
"MCD_CLIP_CALC_USDC_A": "0xF8D26c26Ac481794E4Aebf4F35B10d8E9748086a",
"MCD_JOIN_USDC_B": "0xaca10483e7248453BB6C5afc3e403e8b7EeDF314",
"MCD_CLIP_USDC_B": "0xedFc36f75faafa80e39cd4623def15da6CF2B5C0",
"MCD_CLIP_CALC_USDC_B": "0x275076c9c101AF880BD944991258d564FA31D61B",
"WBTC": "0x7419f744bBF35956020C1687fF68911cD777f865",
"PIP_WBTC": "0x2f38a1bD385A9B395D01f2Cbf767b4527663edDB",
"MCD_JOIN_WBTC_A": "0xB879c7d51439F8e7AC6b2f82583746A0d336e63F",
"MCD_CLIP_WBTC_A": "0x5518C2f409Bed4bD5FF3542d9D5002251EEDA892",
"MCD_CLIP_CALC_WBTC_A": "0x2c39F8C9aE16B84076D7fEA15CE5855925a09DA6",
"TUSD": "0xD6CE59F06Ff2070Dd5DcAd0866A7D8cd9270041a",
"PIP_TUSD": "0xE4bAECdba7A8Ff791E14c6BF7e8089Dfdf75C7E7",
"MCD_JOIN_TUSD_A": "0xe53f6755A031708c87d80f5B1B43c43892551c17",
"MCD_CLIP_TUSD_A": "0x9D547d599489B3950485cBa119FC37Bba9c15c13",
"MCD_CLIP_CALC_TUSD_A": "0x4AE93701287b8C86f17E5a0Cb4D0732b5ae6EFBD",
"ZRX": "0xC2C08A566aD44129E69f8FC98684EAA28B01a6e7",
"PIP_ZRX": "0x218037a42947E634191A231fcBAEAE8b16a39b3f",
"MCD_JOIN_ZRX_A": "0x85D38fF6a6FCf98bD034FB5F9D72cF15e38543f2",
"MCD_CLIP_ZRX_A": "0x9072C477FEb67eEFd8865737206e87570444885E",
"MCD_CLIP_CALC_ZRX_A": "0xCd8Aa54176A333C3B668f65Ff8F11ee909f9A698",
"KNC": "0x9800a0a3c7e9682e1AEb7CAA3200854eFD4E9327",
"PIP_KNC": "0x10799280EF9d7e2d037614F5165eFF2cB8522651",
"MCD_JOIN_KNC_A": "0xE42427325A0e4c8e194692FfbcACD92C2C381598",
"MCD_CLIP_KNC_A": "0x09EA13E49885C29dD270B5c3F557D71A30479333",
"MCD_CLIP_CALC_KNC_A": "0x8D11DC42F5Cc6fE19FeE799e3e24b506cEadAB4b",
"MANA": "0x221F4D62636b7B51b99e36444ea47Dc7831c2B2f",
"PIP_MANA": "0xE97D2b077Fe19c80929718d377981d9F754BF36e",
"MCD_JOIN_MANA_A": "0xdC9Fe394B27525e0D9C827EE356303b49F607aaF",
"MCD_CLIP_MANA_A": "0xFd79e5881CC59F4637ddb3799D302BF089dEE832",
"MCD_CLIP_CALC_MANA_A": "0x14cd62bB700d3cDe2bC45Db2875b58200DDD2503",
"USDT": "0x9245BD36FA20fcD292F4765c4b5dF83Dc3fD5e86",
"PIP_USDT": "0x3588A7973D41AaeA7B203549553C991C4311951e",
"MCD_JOIN_USDT_A": "0x9B011a74a690dFd9a1e4996168d3EcBDE73c2226",
"MCD_CLIP_USDT_A": "0xBDd2d10dAF8D86dA1f02bB7c7C7841bC9A4F62D4",
"MCD_CLIP_CALC_USDT_A": "0xa3a5163Fa4d46D799fE4B036349f0289D69A4445",
"PAXUSD": "0xa6383AF46c36219a472b9549d70E4768dfA8894c",
"PIP_PAXUSD": "0xD01fefed46eb21cd057bAa14Ff466842C31a0Cd9",
"MCD_JOIN_PAXUSD_A": "0x3d6a14C9542B429a4e3d255F6687754d4898D897",
"MCD_CLIP_PAXUSD_A": "0x3939B686a0A7265512D38Ea3fe700812A703BF31",
"MCD_CLIP_CALC_PAXUSD_A": "0x784863edC4C28D73192bf56944D8803c0b5E0CbF",
"COMP": "0x1dDe24ACE93F9F638Bfd6fCE1B38b842703Ea1Aa",
"PIP_COMP": "0xcc10b1C53f4BFFEE19d0Ad00C40D7E36a454D5c4",
"MCD_JOIN_COMP_A": "0x16D567c1F6824ffFC460A11d48F61E010ae43766",
"MCD_CLIP_COMP_A": "0xCDe79465D0B98775c1831957b88BFa12b8A3f020",
"MCD_CLIP_CALC_COMP_A": "0x3e41fCB2DC5370F8612884CB2928E74FED77Cb4B",
"LRC": "0xF070662e48843934b5415f150a18C250d4D7B8aB",
"PIP_LRC": "0xcEE47Bb8989f625b5005bC8b9f9A0B0892339721",
"MCD_JOIN_LRC_A": "0x436286788C5dB198d632F14A20890b0C4D236800",
"MCD_CLIP_LRC_A": "0xaF94A206A3f3948c0BDB6a195a119862F26F5e92",
"MCD_CLIP_CALC_LRC_A": "0xD47DF2Cae1a86fC22e8A8b9B06b22f27860Cb333",
"LINK": "0xa36085F69e2889c224210F603D836748e7dC0088",
"PIP_LINK": "0x20D5A457e49D05fac9729983d9701E0C3079Efac",
"MCD_JOIN_LINK_A": "0xF4Df626aE4fb446e2Dcce461338dEA54d2b9e09b",
"MCD_CLIP_LINK_A": "0x1eB71cC879960606F8ab0E02b3668EEf92CE6D98",
"MCD_CLIP_CALC_LINK_A": "0xbd586d6352Fcf0C45f77FC9348F4Ee7539F6e2bD",
"BAL": "0x630D82Cbf82089B09F71f8d3aAaff2EBA6f47B15",
"PIP_BAL": "0x4fd34872F3AbC07ea6C45c7907f87041C0801DdE",
"MCD_JOIN_BAL_A": "0x8De5EA9251E0576e3726c8766C56E27fAb2B6597",
"MCD_CLIP_BAL_A": "0x8F6C48A26ebf4006Ab542d030D4090DfeC39652E",
"MCD_CLIP_CALC_BAL_A": "0xd041ED45EC5e4539BbbCd91B97D36C76F9d678C9",
"YFI": "0x251F1c3077FEd1770cB248fB897100aaE1269FFC",
"PIP_YFI": "0x9D8255dc4e25bB85e49c65B21D8e749F2293862a",
"MCD_JOIN_YFI_A": "0x5b683137481F2FE683E2f2385792B1DeB018050F",
"MCD_CLIP_YFI_A": "0x9020C96B06d2ac59e98A0F35f131D491EEcAa2C2",
"MCD_CLIP_CALC_YFI_A": "0x54A18C6ceEBDf42D8532EBf5e0a67C430a51b2f6",
"GUSD": "0x31D8EdbF6F33ef858c80d68D06Ec83f33c2aA150",
"PIP_GUSD": "0xb6630DE6Eda0f3f3d96Db4639914565d6b82CfEF",
"MCD_JOIN_GUSD_A": "0x0c6B26e6AB583D2e4528034037F74842ea988909",
"MCD_CLIP_GUSD_A": "0x448eD0ff4e154C1cBefE2c8057906Dd3dA194dA5",
"MCD_CLIP_CALC_GUSD_A": "0x4DD8AaB74a710E7a95937ef1b2618ee76F829Ba6",
"UNI": "0x0C527850e5D6B2B406F1d65895d5b17c5A29Ce51",
"PIP_UNI": "0xe573a75BF4827658F6D600FD26C205a3fe34ee28",
"MCD_JOIN_UNI_A": "0xb6E6EE050B4a74C8cc1DfdE62cAC8C6d9D8F4CAa",
"MCD_CLIP_UNI_A": "0xed3D15e390750f0808E64e0Af1F791e6c5b47c2e",
"MCD_CLIP_CALC_UNI_A": "0x1ee2ecD5149F4b46257a37195994337F4a35E5e8",
"RENBTC": "0xe3dD56821f8C422849AF4816fE9B3c53c6a2F0Bd",
"PIP_RENBTC": "0x2f38a1bD385A9B395D01f2Cbf767b4527663edDB",
"MCD_JOIN_RENBTC_A": "0x12F1F6c7E5fDF1B671CebFBDE974341847d0Caa4",
"MCD_CLIP_RENBTC_A": "0xEf9EEb37CDB15eaD336440BebC30C4CD37Da1891",
"MCD_CLIP_CALC_RENBTC_A": "0xF47749299BCCe427cFd9d015D543aEF83D3BD4Da",
"AAVE": "0x7B339a530Eed72683F56868deDa87BbC64fD9a12",
"PIP_AAVE": "0xd2d9B1355Ea96567E7D6C7A6945f5c7ec8150Cc9",
"MCD_JOIN_AAVE_A": "0x9f1Ed3219035e6bDb19E0D95d316c7c39ad302EC",
"MCD_CLIP_AAVE_A": "0xC8D2d6692981abc7DC5Bf4E345ce3Ce462FA90c9",
"MCD_CLIP_CALC_AAVE_A": "0x0FdF9CecFF267a49f4e9f67014AFEc873143677D",
"MATIC": "0x688E1A8830Ea8dd8fe389FA2228997C663b3807A",
"PIP_MATIC": "0x13594bF4E0C61946936674217c415c6d555Fec50",
"MCD_JOIN_MATIC_A": "0x4Af8801fbDD5ae4FDe2cbC9F844b09c6777525CE",
"MCD_CLIP_MATIC_A": "0x75FE5CD0c23894C8424ac835C054aCA92B994445",
"MCD_CLIP_CALC_MATIC_A": "0x0AB67AA706F1cECD3df457016E822a09bFf18f23",
"UNIV2DAIETH": "0xB10cf58E08b94480fCb81d341A63295eBb2062C2",
"PIP_UNIV2DAIETH": "0xED9201cd545F1d2457D2D48981E7832C754959e9",
"MCD_JOIN_UNIV2DAIETH_A": "0x03f18d97D25c13FecB15aBee143276D3bD2742De",
"MCD_CLIP_UNIV2DAIETH_A": "0xfcFd4255F67C70Cf5fB534535eBe8152Ba6DC5Cd",
"MCD_CLIP_CALC_UNIV2DAIETH_A": "0x0Aa53A82182dd60a630A49eCc286b295fEC5Ba98",
"MIP21_LIQUIDATION_ORACLE": "0x2881c5dF65A8D81e38f7636122aFb456514804CC",
"RWA001": "0x8F9A8cbBdfb93b72d646c8DEd6B4Fe4D86B315cB",
"PIP_RWA001": "0x09710C9440e5FF5c473efe61d5a2f14cA05A6752",
"MCD_JOIN_RWA001_A": "0x029A554f252373e146f76Fa1a7455f73aBF4d38e",
"RWA001_A_URN": "0x3Ba90D86f7E3218C48b7E0FCa959EcF43d9A30F4",
"RWA001_A_INPUT_CONDUIT": "0xB944B07EC3B680b2cEA753125667F7663d424DC3",
"RWA001_A_OUTPUT_CONDUIT": "0xc54fEee07421EAB8000AC8c921c0De9DbfbE780B",
"RWA002": "0xea8a2f6DC9236edb3f53744f5019a444e24F4379",
"PIP_RWA002": "0xaD6495E5918C5F66650EDf291C97b31aBaf5Cd7B",
"MCD_JOIN_RWA002_A": "0x3B3fAD77D6977a19cc7B156143056a3E9C6Ca329",
"RWA002_A_URN": "0xc615F4188C255445290fB9E6dB5E021fe4CA8ECf",
"RWA002_A_INPUT_CONDUIT": "0x2CfADbd094a4D650049C53832B15842a3c59Db34",
"RWA002_A_OUTPUT_CONDUIT": "0x2CfADbd094a4D650049C53832B15842a3c59Db34",
"RWA003": "0xDBC559F5058E593981C48f4f09fA34323df42d51",
"PIP_RWA003": "0xA6f7FBeCef878a8B0Fa9AcB214040e962840f209",
"MCD_JOIN_RWA003_A": "0x4CCc7fED3912A32B6Cf7Db2FdA1554a9FF574099",
"RWA003_A_URN": "0x993c239179D6858769996bcAb5989ab2DF75913F",
"RWA003_A_INPUT_CONDUIT": "0x45e17E350279a2f28243983053B634897BA03b64",
"RWA003_A_OUTPUT_CONDUIT": "0x45e17E350279a2f28243983053B634897BA03b64",
"RWA004": "0x146b0abaB80a60Bfa3b4fDDb5056bBcFa4f1fec1",
"PIP_RWA004": "0xF8E535B9C1c230342EAdD1fe2636a872BdC3d8b4",
"MCD_JOIN_RWA004_A": "0xa92D4082BabF785Ba02f9C419509B7d08f2ef271",
"RWA004_A_URN": "0xf22C7F5A2AecE1E85263e3cec522BDCD3e392B59",
"RWA004_A_INPUT_CONDUIT": "0x303dFE04Be5731207c5213FbB54488B3aD9B9FE3",
"RWA004_A_OUTPUT_CONDUIT": "0x303dFE04Be5731207c5213FbB54488B3aD9B9FE3",
"RWA005": "0xcB2A48D26970eE7193d66BAc6F1b3090f2E8f82B",
"PIP_RWA005": "0x0dA25CE01BA1eCBD907694D5237Df0A2740Ce9E7",
"MCD_JOIN_RWA005_A": "0x1233d0DBb55A4Bb41D711d4B584f8DDB15A2Ff88",
"RWA005_A_URN": "0xdB9f0700EbBac596CCeF5b14D5e23664Db2A184f",
"RWA005_A_INPUT_CONDUIT": "0x17E5954Cdd3611Dd84e444F0ed555CC3a06cB319",
"RWA005_A_OUTPUT_CONDUIT": "0x17E5954Cdd3611Dd84e444F0ed555CC3a06cB319",
"RWA006": "0x4E65F06574F1630B4fF756C898Fe02f276D53E86",
"PIP_RWA006": "0x8A4B5c3fDe49486CDEda4D7Fbb4dec6CC6Af8258",
"MCD_JOIN_RWA006_A": "0x039B74bD0Adc35046B67E88509900D41b9D95430",
"RWA006_A_URN": "0x6fa6F9C11f5F129f6ECA4B391D9d32038A9666cD",
"RWA006_A_INPUT_CONDUIT": "0x652A3B3b91459504A8D1d785B0c923A34D638218",
"RWA006_A_OUTPUT_CONDUIT": "0x652A3B3b91459504A8D1d785B0c923A34D638218",
"PROXY_PAUSE_ACTIONS": "0x7c52826c1efEAE3199BDBe68e3916CC3eA222E29",
"PROXY_DEPLOYER": "0xA9fCcB07DD3f774d5b9d02e99DE1a27f47F91189",
"MCD_FLASH": "0x5aA1323f61D679E52a90120DFDA2ed1A76E4475A",
"VOTE_DELEGATE_PROXY_FACTORY": "0x1740F3bD55b1900C816A0071F8972C201566e3a3"
}
================================================
FILE: config/mainnet-addresses.json
================================================
{
"CHANGELOG": "0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F",
"MULTICALL": "0x5e227AD1969Ea493B43F840cfF78d08a6fc17796",
"FAUCET": "0x0000000000000000000000000000000000000000",
"MCD_DEPLOY": "0xbaa65281c2FA2baAcb2cb550BA051525A480D3F4",
"FLIP_FAB": "0x4ACdbe9dd0d00b36eC2050E805012b8Fc9974f2b",
"CLIP_FAB": "0x0716F25fBaAae9b63803917b6125c10c313dF663",
"CALC_FAB": "0xE1820A2780193d74939CcA104087CADd6c1aA13A",
"LERP_FAB": "0x9175561733D138326FDeA86CdFdF53e92b588276",
"JOIN_FAB": "0xf1738d22140783707Ca71CB3746e0dc7Bf2b0264",
"MCD_GOV": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2",
"GOV_GUARD": "0x6eEB68B2C7A918f36B78E2DB80dcF279236DDFb8",
"MCD_ADM": "0x0a3f6849f78076aefaDf113F5BED87720274dDC0",
"VOTE_PROXY_FACTORY": "0x6FCD258af181B3221073A96dD90D1f7AE7eEc408",
"VOTE_DELEGATE_PROXY_FACTORY": "0xD897F108670903D1d6070fcf818f9db3615AF272",
"MCD_VAT": "0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B",
"MCD_JUG": "0x19c0976f590D67707E62397C87829d896Dc0f1F1",
"MCD_CAT": "0xa5679C04fc3d9d8b0AaB1F0ab83555b301cA70Ea",
"MCD_DOG": "0x135954d155898D42C90D2a57824C690e0c7BEf1B",
"MCD_VOW": "0xA950524441892A31ebddF91d3cEEFa04Bf454466",
"MCD_JOIN_DAI": "0x9759A6Ac90977b93B58547b4A71c78317f391A28",
"MCD_FLAP": "0xC4269cC7acDEdC3794b221aA4D9205F564e27f0d",
"MCD_FLOP": "0xA41B6EF151E06da0e34B009B86E828308986736D",
"MCD_PAUSE": "0xbE286431454714F511008713973d3B053A2d38f3",
"MCD_PAUSE_PROXY": "0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB",
"MCD_GOV_ACTIONS": "0x4F5f0933158569c026d617337614d00Ee6589B6E",
"MCD_DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"MCD_SPOT": "0x65C79fcB50Ca1594B025960e539eD7A9a6D434A3",
"MCD_POT": "0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7",
"MCD_END": "0xBB856d1742fD182a90239D7AE85706C2FE4e5922",
"MCD_ESM": "0x29CfBd381043D00a98fD9904a431015Fef07af2f",
"PROXY_ACTIONS": "0x82ecD135Dce65Fbc6DbdD0e4237E0AF93FFD5038",
"PROXY_ACTIONS_END": "0x7AfF9FC9faD225e3c88cDA06BC56d8Aca774bC57",
"PROXY_ACTIONS_DSR": "0x07ee93aEEa0a36FfF2A9B95dd22Bd6049EE54f26",
"CDP_MANAGER": "0x5ef30b9986345249bc32d8928B7ee64DE9435E39",
"DSR_MANAGER": "0x373238337Bfe1146fb49989fc222523f83081dDb",
"GET_CDPS": "0x36a724Bd100c39f0Ea4D3A20F7097eE01A8Ff573",
"ILK_REGISTRY": "0x5a464C28D19848f44199D003BeF5ecc87d090F87",
"OSM_MOM": "0x76416A4d5190d071bfed309861527431304aA14f",
"FLIPPER_MOM": "0xc4bE7F74Ee3743bDEd8E0fA218ee5cf06397f472",
"CLIPPER_MOM": "0x79FBDF16b366DFb14F66cE4Ac2815Ca7296405A0",
"MCD_IAM_AUTO_LINE": "0xC7Bdd1F2B16447dcf3dE045C4a039A60EC2f0ba3",
"MCD_FLASH": "0x1EB4CF3A948E7D72A198fe073cCb8C7a948cD853",
"PROXY_FACTORY": "0xA26e15C895EFc0616177B7c1e7270A4C7D51C997",
"PROXY_REGISTRY": "0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4",
"MCD_VEST_DAI": "0x2Cc583c0AaCDaC9e23CB601fDA8F1A0c56Cdcb71",
"MCD_VEST_MKR": "0x0fC8D4f2151453ca0cA56f07359049c8f07997Bd",
"MCD_VEST_MKR_TREASURY": "0x6D635c8d08a1eA2F1687a5E46b666949c977B7dd",
"ETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"PIP_ETH": "0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763",
"MCD_JOIN_ETH_A": "0x2F0b23f53734252Bda2277357e97e1517d6B042A",
"MCD_CLIP_ETH_A": "0xc67963a226eddd77B91aD8c421630A1b0AdFF270",
"MCD_CLIP_CALC_ETH_A": "0x7d9f92DAa9254Bbd1f479DBE5058f74C2381A898",
"MCD_JOIN_ETH_B": "0x08638eF1A205bE6762A8b935F5da9b700Cf7322c",
"MCD_CLIP_ETH_B": "0x71eb894330e8a4b96b8d6056962e7F116F50e06F",
"MCD_CLIP_CALC_ETH_B": "0x19E26067c4a69B9534adf97ED8f986c49179dE18",
"MCD_JOIN_ETH_C": "0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E",
"MCD_CLIP_ETH_C": "0xc2b12567523e3f3CBd9931492b91fe65b240bc47",
"MCD_CLIP_CALC_ETH_C": "0x1c4fC274D12b2e1BBDF97795193D3148fCDa6108",
"BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF",
"PIP_BAT": "0xB4eb54AF9Cc7882DF0121d26c5b97E802915ABe6",
"MCD_JOIN_BAT_A": "0x3D0B1912B66114d4096F48A8CEe3A56C231772cA",
"MCD_CLIP_BAT_A": "0x3D22e6f643e2F4c563fD9db22b229Cbb0Cd570fb",
"MCD_CLIP_CALC_BAT_A": "0x2e118153D304a0d9C5838D5FCb70CEfCbEc81DC2",
"USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"PIP_USDC": "0x77b68899b99b686F415d074278a9a16b336085A0",
"MCD_JOIN_USDC_A": "0xA191e578a6736167326d05c119CE0c90849E84B7",
"MCD_CLIP_USDC_A": "0x046b1A5718da6A226D912cFd306BA19980772908",
"MCD_CLIP_CALC_USDC_A": "0x0FCa4ba0B80123b5d22dD3C8BF595F3E561d594D",
"MCD_JOIN_USDC_B": "0x2600004fd1585f7270756DDc88aD9cfA10dD0428",
"MCD_CLIP_USDC_B": "0x5590F23358Fe17361d7E4E4f91219145D8cCfCb3",
"MCD_CLIP_CALC_USDC_B": "0xD6FE411284b92d309F79e502Dd905D7A3b02F561",
"WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"PIP_WBTC": "0xf185d0682d50819263941e5f4EacC763CC5C6C42",
"MCD_JOIN_WBTC_A": "0xBF72Da2Bd84c5170618Fbe5914B0ECA9638d5eb5",
"MCD_CLIP_WBTC_A": "0x0227b54AdbFAEec5f1eD1dFa11f54dcff9076e2C",
"MCD_CLIP_CALC_WBTC_A": "0x5f4CEa97ca1030C6Bd38429c8a0De7Cd4981C70A",
"MCD_JOIN_WBTC_B": "0xfA8c996e158B80D77FbD0082BB437556A65B96E0",
"MCD_CLIP_WBTC_B": "0xe30663C6f83A06eDeE6273d72274AE24f1084a22",
"MCD_CLIP_CALC_WBTC_B": "0xeb911E99D7ADD1350DC39d84D60835BA9B287D96",
"MCD_JOIN_WBTC_C": "0x7f62f9592b823331E012D3c5DdF2A7714CfB9de2",
"MCD_CLIP_WBTC_C": "0x39F29773Dcb94A32529d0612C6706C49622161D1",
"MCD_CLIP_CALC_WBTC_C": "0x4fa2A328E7f69D023fE83454133c273bF5ACD435",
"TUSD": "0x0000000000085d4780B73119b644AE5ecd22b376",
"PIP_TUSD": "0xeE13831ca96d191B688A670D47173694ba98f1e5",
"MCD_JOIN_TUSD_A": "0x4454aF7C8bb9463203b66C816220D41ED7837f44",
"MCD_CLIP_TUSD_A": "0x0F6f88f8A4b918584E3539182793a0C276097f44",
"MCD_CLIP_CALC_TUSD_A": "0x059acdf311E38aAF77139638228d393Ff27639bF",
"ZRX": "0xE41d2489571d322189246DaFA5ebDe1F4699F498",
"PIP_ZRX": "0x7382c066801E7Acb2299aC8562847B9883f5CD3c",
"MCD_JOIN_ZRX_A": "0xc7e8Cd72BDEe38865b4F5615956eF47ce1a7e5D0",
"MCD_CLIP_ZRX_A": "0xdc90d461E148552387f3aB3EBEE0Bdc58Aa16375",
"MCD_CLIP_CALC_ZRX_A": "0xebe5e9D77b9DBBA8907A197f4c2aB00A81fb0C4e",
"KNC": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200",
"PIP_KNC": "0xf36B79BD4C0904A5F350F1e4f776B81208c13069",
"MCD_JOIN_KNC_A": "0x475F1a89C1ED844A08E8f6C50A00228b5E59E4A9",
"MCD_CLIP_KNC_A": "0x006Aa3eB5E666D8E006aa647D4afAB212555Ddea",
"MCD_CLIP_CALC_KNC_A": "0x82c41e2ADE28C066a5D3A1E3f5B444a4075C1584",
"MANA": "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942",
"PIP_MANA": "0x8067259EA630601f319FccE477977E55C6078C13",
"MCD_JOIN_MANA_A": "0xA6EA3b9C04b8a38Ff5e224E7c3D6937ca44C0ef9",
"MCD_CLIP_MANA_A": "0xF5C8176E1eB0915359E46DEd16E52C071Bb435c0",
"MCD_CLIP_CALC_MANA_A": "0xABbCd14FeDbb2D39038327055D9e615e178Fd64D",
"USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"PIP_USDT": "0x7a5918670B0C390aD25f7beE908c1ACc2d314A3C",
"MCD_JOIN_USDT_A": "0x0Ac6A1D74E84C2dF9063bDDc31699FF2a2BB22A2",
"MCD_CLIP_USDT_A": "0xFC9D6Dd08BEE324A5A8B557d2854B9c36c2AeC5d",
"MCD_CLIP_CALC_USDT_A": "0x1Cf3DE6D570291CDB88229E70037d1705d5be748",
"PAXUSD": "0x8E870D67F660D95d5be530380D0eC0bd388289E1",
"PAX": "0x8E870D67F660D95d5be530380D0eC0bd388289E1",
"PIP_PAXUSD": "0x043B963E1B2214eC90046167Ea29C2c8bDD7c0eC",
"PIP_PAX": "0x043B963E1B2214eC90046167Ea29C2c8bDD7c0eC",
"MCD_JOIN_PAXUSD_A": "0x7e62B7E279DFC78DEB656E34D6a435cC08a44666",
"MCD_CLIP_PAXUSD_A": "0xBCb396Cd139D1116BD89562B49b9D1d6c25378B0",
"MCD_CLIP_CALC_PAXUSD_A": "0xAB98De83840b8367046383D2Adef9959E130923e",
"COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888",
"PIP_COMP": "0xBED0879953E633135a48a157718Aa791AC0108E4",
"MCD_JOIN_COMP_A": "0xBEa7cDfB4b49EC154Ae1c0D731E4DC773A3265aA",
"MCD_CLIP_COMP_A": "0x2Bb690931407DCA7ecE84753EA931ffd304f0F38",
"MCD_CLIP_CALC_COMP_A": "0x1f546560EAa70985d962f1562B65D4B182341a63",
"LRC": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD",
"PIP_LRC": "0x9eb923339c24c40Bef2f4AF4961742AA7C23EF3a",
"MCD_JOIN_LRC_A": "0x6C186404A7A238D3d6027C0299D1822c1cf5d8f1",
"MCD_CLIP_LRC_A": "0x81C5CDf4817DBf75C7F08B8A1cdaB05c9B3f70F7",
"MCD_CLIP_CALC_LRC_A": "0x6856CCA4c881CAf29B6563bA046C7Bb73121fb9d",
"LINK": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
"PIP_LINK": "0x9B0C694C6939b5EA9584e9b61C7815E8d97D9cC7",
"MCD_JOIN_LINK_A": "0xdFccAf8fDbD2F4805C174f856a317765B49E4a50",
"MCD_CLIP_LINK_A": "0x832Dd5f17B30078a5E46Fdb8130A68cBc4a74dC0",
"MCD_CLIP_CALC_LINK_A": "0x7B1696677107E48B152e9Bf400293e98B7D86Eb1",
"BAL": "0xba100000625a3754423978a60c9317c58a424e3D",
"PIP_BAL": "0x3ff860c0F28D69F392543A16A397D0dAe85D16dE",
"MCD_JOIN_BAL_A": "0x4a03Aa7fb3973d8f0221B466EefB53D0aC195f55",
"MCD_CLIP_BAL_A": "0x6AAc067bb903E633A422dE7BE9355E62B3CE0378",
"MCD_CLIP_CALC_BAL_A": "0x79564a41508DA86721eDaDac07A590b5A51B2c01",
"YFI": "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e",
"PIP_YFI": "0x5F122465bCf86F45922036970Be6DD7F58820214",
"MCD_JOIN_YFI_A": "0x3ff33d9162aD47660083D7DC4bC02Fb231c81677",
"MCD_CLIP_YFI_A": "0x9daCc11dcD0aa13386D295eAeeBBd38130897E6f",
"MCD_CLIP_CALC_YFI_A": "0x1f206d7916Fd3B1b5B0Ce53d5Cab11FCebc124DA",
"GUSD": "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd",
"PIP_GUSD": "0xf45Ae69CcA1b9B043dAE2C83A5B65Bc605BEc5F5",
"MCD_JOIN_GUSD_A": "0xe29A14bcDeA40d83675aa43B72dF07f649738C8b",
"MCD_CLIP_GUSD_A": "0xa47D68b9dB0A0361284fA04BA40623fcBd1a263E",
"MCD_CLIP_CALC_GUSD_A": "0xF7e80359Cb9C4E6D178E6689eD8A6A6f91060747",
"UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
"PIP_UNI": "0xf363c7e351C96b910b92b45d34190650df4aE8e7",
"MCD_JOIN_UNI_A": "0x3BC3A58b4FC1CbE7e98bB4aB7c99535e8bA9b8F1",
"MCD_CLIP_UNI_A": "0x3713F83Ee6D138Ce191294C131148176015bC29a",
"MCD_CLIP_CALC_UNI_A": "0xeA7FE6610e6708E2AFFA202948cA19ace3F580AE",
"RENBTC": "0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D",
"PIP_RENBTC": "0xf185d0682d50819263941e5f4EacC763CC5C6C42",
"MCD_JOIN_RENBTC_A": "0xFD5608515A47C37afbA68960c1916b79af9491D0",
"MCD_CLIP_RENBTC_A": "0x834719BEa8da68c46484E001143bDDe29370a6A3",
"MCD_CLIP_CALC_RENBTC_A": "0xcC89F368aad8D424d3e759c1525065e56019a0F4",
"AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
"PIP_AAVE": "0x8Df8f06DC2dE0434db40dcBb32a82A104218754c",
"MCD_JOIN_AAVE_A": "0x24e459F61cEAa7b1cE70Dbaea938940A7c5aD46e",
"MCD_CLIP_AAVE_A": "0x8723b74F598DE2ea49747de5896f9034CC09349e",
"MCD_CLIP_CALC_AAVE_A": "0x76024a8EfFCFE270e089964a562Ece6ea5f3a14C",
"MATIC": "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
"PIP_MATIC": "0x8874964279302e6d4e523Fb1789981C39a1034Ba",
"MCD_JOIN_MATIC_A": "0x885f16e177d45fC9e7C87e1DA9fd47A9cfcE8E13",
"MCD_CLIP_MATIC_A": "0x29342F530ed6120BDB219D602DaFD584676293d1",
"MCD_CLIP_CALC_MATIC_A": "0xdF8C347B06a31c6ED11f8213C2366348BFea68dB",
"STETH": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84",
"WSTETH": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"PIP_WSTETH": "0xFe7a2aC0B945f12089aEEB6eCebf4F384D9f043F",
"MCD_JOIN_WSTETH_A": "0x10CD5fbe1b404B7E19Ef964B63939907bdaf42E2",
"MCD_CLIP_WSTETH_A": "0x49A33A28C4C7D9576ab28898F4C9ac7e52EA457A",
"MCD_CLIP_CALC_WSTETH_A": "0x15282b886675cc1Ce04590148f456428E87eaf13",
"RETH":"0xae78736Cd615f374D3085123A210448E74Fc6393",
"PIP_RETH":"0xeE7F0b350aA119b3d05DC733a4621a81972f7D47",
"MCD_JOIN_RETH_A":"0xC6424e862f1462281B0a5FAc078e4b63006bDEBF",
"MCD_CLIP_CALC_RETH_A":"0xc59B62AFC96cf9737F717B5e5815070C0f154396",
"MCD_CLIP_RETH_A":"0x27CA5E525ea473eD52Ea9423CD08cCc081d96a98",
"GNO":"0x6810e776880C02933D47DB1b9fc05908e5386b96",
"PIP_GNO":"0xd800ca44fFABecd159c7889c3bf64a217361AEc8",
"MCD_JOIN_GNO_A":"0x7bD3f01e24E0f0838788bC8f573CEA43A80CaBB5",
"MCD_CLIP_CALC_GNO_A":"0x17b6D0e4237ea7F880aF5F58257cd232a04171D9",
"MCD_CLIP_GNO_A":"0xd9e758bd239e5d568f44D0A748633f6a8d52CBbb",
"UNIV2DAIETH": "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11",
"PIP_UNIV2DAIETH": "0xFc8137E1a45BAF0030563EC4F0F851bd36a85b7D",
"MCD_JOIN_UNIV2DAIETH_A": "0x2502F65D77cA13f183850b5f9272270454094A08",
"MCD_CLIP_UNIV2DAIETH_A": "0x9F6981bA5c77211A34B76c6385c0f6FA10414035",
"MCD_CLIP_CALC_UNIV2DAIETH_A": "0xf738C272D648Cc4565EaFb43c0C5B35BbA3bf29d",
"UNIV2WBTCETH": "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940",
"PIP_UNIV2WBTCETH": "0x8400D2EDb8B97f780356Ef602b1BdBc082c2aD07",
"MCD_JOIN_UNIV2WBTCETH_A": "0xDc26C9b7a8fe4F5dF648E314eC3E6Dc3694e6Dd2",
"MCD_CLIP_UNIV2WBTCETH_A": "0xb15afaB996904170f87a64Fe42db0b64a6F75d24",
"MCD_CLIP_CALC_UNIV2WBTCETH_A": "0xC94ee71e909DbE08d63aA9e6EFbc9976751601B4",
"UNIV2USDCETH": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"PIP_UNIV2USDCETH": "0xf751f24DD9cfAd885984D1bA68860F558D21E52A",
"MCD_JOIN_UNIV2USDCETH_A": "0x03Ae53B33FeeAc1222C3f372f32D37Ba95f0F099",
"MCD_CLIP_UNIV2USDCETH_A": "0x93AE03815BAF1F19d7F18D9116E4b637cc32A131",
"MCD_CLIP_CALC_UNIV2USDCETH_A": "0x022ff40643e8b94C43f0a1E54f51EF6D070AcbC4",
"UNIV2DAIUSDC": "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5",
"PIP_UNIV2DAIUSDC": "0x25D03C2C928ADE19ff9f4FFECc07d991d0df054B",
"MCD_JOIN_UNIV2DAIUSDC_A": "0xA81598667AC561986b70ae11bBE2dd5348ed4327",
"MCD_CLIP_UNIV2DAIUSDC_A": "0x9B3310708af333f6F379FA42a5d09CBAA10ab309",
"MCD_CLIP_CALC_UNIV2DAIUSDC_A": "0xbEF2ab2aA5CC780A03bccf22AD3320c8CF35af6A",
"UNIV2ETHUSDT": "0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852",
"PIP_UNIV2ETHUSDT": "0x5f6dD5B421B8d92c59dC6D907C9271b1DBFE3016",
"MCD_JOIN_UNIV2ETHUSDT_A": "0x4aAD139a88D2dd5e7410b408593208523a3a891d",
"MCD_CLIP_UNIV2ETHUSDT_A": "0x2aC4C9b49051275AcB4C43Ec973082388D015D48",
"MCD_CLIP_CALC_UNIV2ETHUSDT_A": "0xA475582E3D6Ec35091EaE81da3b423C1B27fa029",
"UNIV2LINKETH": "0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974",
"PIP_UNIV2LINKETH": "0xd7d31e62AE5bfC3bfaa24Eda33e8c32D31a1746F",
"MCD_JOIN_UNIV2LINKETH_A": "0xDae88bDe1FB38cF39B6A02b595930A3449e593A6",
"MCD_CLIP_UNIV2LINKETH_A": "0x6aa0520354d1b84e1C6ABFE64a708939529b619e",
"MCD_CLIP_CALC_UNIV2LINKETH_A": "0x8aCeC2d937a4A4cAF42565aFbbb05ac242134F14",
"UNIV2UNIETH": "0xd3d2E2692501A5c9Ca623199D38826e513033a17",
"PIP_UNIV2UNIETH": "0x8462A88f50122782Cc96108F476deDB12248f931",
"MCD_JOIN_UNIV2UNIETH_A": "0xf11a98339FE1CdE648e8D1463310CE3ccC3d7cC1",
"MCD_CLIP_UNIV2UNIETH_A": "0xb0ece6F5542A4577E2f1Be491A937Ccbbec8479e",
"MCD_CLIP_CALC_UNIV2UNIETH_A": "0xad609Ed16157014EF955C94553E40e94A09049f0",
"UNIV2WBTCDAI": "0x231B7589426Ffe1b75405526fC32aC09D44364c4",
"PIP_UNIV2WBTCDAI": "0x5bB72127a196392cf4aC00Cf57aB278394d24e55",
"MCD_JOIN_UNIV2WBTCDAI_A": "0xD40798267795Cbf3aeEA8E9F8DCbdBA9b5281fcC",
"MCD_CLIP_UNIV2WBTCDAI_A": "0x4fC53a57262B87ABDa61d6d0DB2bE7E9BE68F6b8",
"MCD_CLIP_CALC_UNIV2WBTCDAI_A": "0x863AEa7D2c4BF2B5Aa191B057240b6Dc29F532eB",
"UNIV2AAVEETH": "0xDFC14d2Af169B0D36C4EFF567Ada9b2E0CAE044f",
"PIP_UNIV2AAVEETH": "0x32d8416e8538Ac36272c44b0cd962cD7E0198489",
"MCD_JOIN_UNIV2AAVEETH_A": "0x42AFd448Df7d96291551f1eFE1A590101afB1DfF",
"MCD_CLIP_UNIV2AAVEETH_A": "0x854b252BA15eaFA4d1609D3B98e00cc10084Ec55",
"MCD_CLIP_CALC_UNIV2AAVEETH_A": "0x5396e541E1F648EC03faf338389045F1D7691960",
"UNIV2DAIUSDT": "0xB20bd5D04BE54f870D5C0d3cA85d82b34B836405",
"PIP_UNIV2DAIUSDT": "0x9A1CD705dc7ac64B50777BcEcA3529E58B1292F1",
"MCD_JOIN_UNIV2DAIUSDT_A": "0xAf034D882169328CAf43b823a4083dABC7EEE0F4",
"MCD_CLIP_UNIV2DAIUSDT_A": "0xe4B82Be84391b9e7c56a1fC821f47569B364dd4a",
"MCD_CLIP_CALC_UNIV2DAIUSDT_A": "0x4E88cE740F6bEa31C2b14134F6C5eB2a63104fcF",
"GUNIV3DAIUSDC1": "0xAbDDAfB225e10B90D798bB8A886238Fb835e2053",
"PIP_GUNIV3DAIUSDC1": "0x7F6d78CC0040c87943a0e0c140De3F77a273bd58",
"MCD_JOIN_GUNIV3DAIUSDC1_A": "0xbFD445A97e7459b0eBb34cfbd3245750Dba4d7a4",
"MCD_CLIP_GUNIV3DAIUSDC1_A": "0x5048c5Cd3102026472f8914557A1FD35c8Dc6c9e",
"MCD_CLIP_CALC_GUNIV3DAIUSDC1_A": "0x25B17065b94e3fDcD97d94A2DA29E7F77105aDd7",
"GUNIV3DAIUSDC2": "0x50379f632ca68D36E50cfBC8F78fe16bd1499d1e",
"PIP_GUNIV3DAIUSDC2": "0xcCBa43231aC6eceBd1278B90c3a44711a00F4e93",
"MCD_JOIN_GUNIV3DAIUSDC2_A": "0xA7e4dDde3cBcEf122851A7C8F7A55f23c0Daf335",
"MCD_CLIP_GUNIV3DAIUSDC2_A": "0xB55da3d3100C4eBF9De755b6DdC24BF209f6cc06",
"MCD_CLIP_CALC_GUNIV3DAIUSDC2_A": "0xef051Ca2A2d809ba47ee0FC8caaEd06E3D832225",
"CRVV1ETHSTETH": "0x06325440D014e39736583c165C2963BA99fAf14E",
"PIP_CRVV1ETHSTETH": "0xEa508F82728927454bd3ce853171b0e2705880D4",
"MCD_JOIN_CRVV1ETHSTETH_A": "0x82d8bfdb61404c796385f251654f6d7e92092b5d",
"MCD_CLIP_CRVV1ETHSTETH_A": "0x1926862f899410bfc19fefb8a3c69c7aed22463a",
"MCD_CLIP_CALC_CRVV1ETHSTETH_A": "0x8a4780acabadcae1a297b2eae5deebd7d50deeb8",
"MIP21_LIQUIDATION_ORACLE": "0x88f88Bb9E66241B73B84f3A6E197FbBa487b1E30",
"RWA001": "0x10b2aA5D77Aa6484886d8e244f0686aB319a270d",
"PIP_RWA001": "0x76A9f30B45F4ebFD60Ce8a1c6e963b1605f7cB6d",
"MCD_JOIN_RWA001_A": "0x476b81c12Dc71EDfad1F64B9E07CaA60F4b156E2",
"RWA001_A_URN": "0xa3342059BcDcFA57a13b12a35eD4BBE59B873005",
"RWA001_A_INPUT_CONDUIT": "0x486C85e2bb9801d14f6A8fdb78F5108a0fd932f2",
"RWA001_A_OUTPUT_CONDUIT": "0xb3eFb912e1cbC0B26FC17388Dd433Cecd2206C3d",
"RWA002": "0xAAA760c2027817169D7C8DB0DC61A2fb4c19AC23",
"PIP_RWA002": "0xd2473237E20Bd52F8E7cE0FD79403A6a82fbAEC8",
"MCD_JOIN_RWA002_A": "0xe72C7e90bc26c11d45dBeE736F0acf57fC5B7152",
"RWA002_A_URN": "0x225B3da5BE762Ee52B182157E67BeA0b31968163",
"RWA002_A_INPUT_CONDUIT": "0x2474F297214E5d96Ba4C81986A9F0e5C260f445D",
"RWA002_A_OUTPUT_CONDUIT": "0x2474F297214E5d96Ba4C81986A9F0e5C260f445D",
"RWA003": "0x07F0A80aD7AeB7BfB7f139EA71B3C8f7E17156B9",
"PIP_RWA003": "0xDeF7E88447F7D129420FC881B2a854ABB52B73B8",
"MCD_JOIN_RWA003_A": "0x1Fe789BBac5b141bdD795A3Bc5E12Af29dDB4b86",
"RWA003_A_URN": "0x7bF825718e7C388c3be16CFe9982539A7455540F",
"RWA003_A_INPUT_CONDUIT": "0x2A9798c6F165B6D60Cfb923Fe5BFD6f338695D9B",
"RWA003_A_OUTPUT_CONDUIT": "0x2A9798c6F165B6D60Cfb923Fe5BFD6f338695D9B",
"RWA004": "0x873F2101047A62F84456E3B2B13df2287925D3F9",
"PIP_RWA004": "0x5eEE1F3d14850332A75324514CcbD2DBC8Bbc566",
"MCD_JOIN_RWA004_A": "0xD50a8e9369140539D1c2D113c4dC1e659c6242eB",
"RWA004_A_URN": "0xeF1699548717aa4Cf47aD738316280b56814C821",
"RWA004_A_INPUT_CONDUIT": "0xe1ed3F588A98bF8a3744f4BF74Fd8540e81AdE3f",
"RWA004_A_OUTPUT_CONDUIT": "0xe1ed3F588A98bF8a3744f4BF74Fd8540e81AdE3f",
"RWA005": "0x6DB236515E90fC831D146f5829407746EDdc5296",
"PIP_RWA005": "0x8E6039C558738eb136833aB50271ae065c700d2B",
"MCD_JOIN_RWA005_A": "0xA4fD373b93aD8e054970A3d6cd4Fd4C31D08192e",
"RWA005_A_URN": "0xc40907545C57dB30F01a1c2acB242C7c7ACB2B90",
"RWA005_A_INPUT_CONDUIT": "0x5b702e1fEF3F556cbe219eE697D7f170A236cc66",
"RWA005_A_OUTPUT_CONDUIT": "0x5b702e1fEF3F556cbe219eE697D7f170A236cc66",
"RWA006": "0x4EE03cfBF6E784c462839f5954d60f7C2B60b113",
"PIP_RWA006": "0xB8AeCF04Fdf22Ef6C0c6b6536896e1F2870C41D3",
"MCD_JOIN_RWA006_A": "0x5E11E34b6745FeBa9449Ae53c185413d6EdC66BE",
"RWA006_A_URN": "0x0C185bf5388DdfDB288F4D875265d456D18FD9Cb",
"RWA006_A_INPUT_CONDUIT": "0x8Fe38D1E4293181273E2e323e4c16e0D1d4861e3",
"RWA006_A_OUTPUT_CONDUIT": "0x8Fe38D1E4293181273E2e323e4c16e0D1d4861e3",
"PROXY_PAUSE_ACTIONS": "0x6bda13D43B7EDd6CAfE1f70fB98b5d40f61A1370",
"PROXY_DEPLOYER": "0x1b93556AB8dcCEF01Cd7823C617a6d340f53Fb58",
"OPTIMISM_DAI_BRIDGE": "0x10E6593CDda8c58a1d0f14C5164B376352a55f2F",
"OPTIMISM_ESCROW": "0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65",
"OPTIMISM_GOV_RELAY": "0x09B354CDA89203BB7B3131CC728dFa06ab09Ae2F",
"ARBITRUM_DAI_BRIDGE": "0xD3B5b60020504bc3489D6949d545893982BA3011",
"ARBITRUM_ESCROW": "0xA10c7CE4b876998858b1a9E12b10092229539400",
"ARBITRUM_GOV_RELAY": "0x9ba25c289e351779E0D481Ba37489317c34A899d"
}
================================================
FILE: config/testnet-addresses.json
================================================
{
"DEPLOYER": "0x00a329c0648769A73afAc7F9381E08FB43dBEA72",
"MULTICALL": "0x492934308E98b590A626666B703A6dDf2120e85e",
"FAUCET": "0x0A64DF94bc0E039474DB42bb52FEca0c1d540402",
"MCD_DEPLOY": "0xd29915F1A3fF9846fE5D8d9d2C954de21932AF7F",
"MCD_GOV": "0x1FD8397e8108ada12eC07976D92F773364ba46e7",
"GOV_GUARD": "0x39a812a6aA4C475b6562B73Bf0584eb3655e8D6C",
"MCD_IOU": "0xDfBc5fbEaa41bD1cd15F9d6b77265DBc3CB2A677",
"MCD_ADM": "0x8c39d4833812A7516BaCD455dA7F97f0a8C11B05",
"VOTE_PROXY_FACTORY": "0x2dC383E93ec3DB735777a3E9ae69E2aD81edaF03",
"MCD_VAT": "0x3D72e5B28FbA05Bd4090A2A587Bb3eCC899f33b2",
"MCD_JUG": "0x77a371Ed06fbA2D93D05C5bDE6d8eC58b3a35fbd",
"MCD_CAT": "0x31865076D1E28ad4eA06D5Db7aAa4AAF225f1Fb5",
"MCD_DOG": "0xd9b3B2429F1b301156Bf3103419ef6B78E888386",
"MCD_VOW": "0xA2F9C8C13118c88f14501cDCB2b52Af3751622ae",
"MCD_JOIN_DAI": "0x7f8241b7250c5C5368788543E4dA2F9A919E9F02",
"MCD_FLAP": "0xB5054202380d093A02916e0137d75b54D6182A23",
"MCD_FLOP": "0xd57D9931b305f1bc1622B97c8Cc6747E4A9254a0",
"MCD_PAUSE": "0x967aE1FB90aA36C7d3B16B5328504F542495D952",
"MCD_PAUSE_PROXY": "0x3689de8F568e4A59254eE7eaB1A37d87044f52Da",
"MCD_GOV_ACTIONS": "0x2287909BB95FA078C73CC2d5a5AF6fE1244b0911",
"MCD_DAI": "0x8A1567046e610Fec30F120BB70Df94B50561C1d3",
"MCD_SPOT": "0x5422Ee4e22603E336905CDC9D59aE3F0012fe4c8",
"MCD_POT": "0xe51e9A4D22b7451c0232508455195E7c0a6e0f19",
"MCD_END": "0x27547dE5f11122283825Adf97FB98eF301f5F73c",
"MCD_ESM": "0x8a606fD18B9f0CA3Cf480e70639c58EAa98d7389",
"PROXY_ACTIONS": "0x84617303947304444Ceb641582c024f277BBF4Ff",
"PROXY_ACTIONS_END": "0x78c362A5690447EA2BBC3E8008502efD13936F79",
"PROXY_ACTIONS_DSR": "0x277aD07109FE52a742B808a3E6765Ee1Ad0e7Ad2",
"CDP_MANAGER": "0x79a8FC3D98Fc84c9BC2B3a737EA992321a1b86A3",
"DSR_MANAGER": "0x0Faf2F31Ab165B55F42E55c8065c0EC7170A0d45",
"GET_CDPS": "0x4f05AfbC371854D027263e756487BDefD099178f",
"ILK_REGISTRY": "0x8e23974b151827f0E8151aC526C4c4c974c06A90",
"OSM_MOM": "0x96724aa934979936aE5c3Afda1599b5ed61252ce",
"FLIPPER_MOM": "0xcbfD09D76140D01E573b452bB984d82589571fC2",
"CLIPPER_MOM": "0x9BB69befBAA567a7EaEE33b671756596517338F4",
"MCD_IAM_AUTO_LINE": "0x01E354A7eF79962DbB690705e46bd54c1C855E80",
"PROXY_FACTORY": "0x3DD0864668C36D27B53a98137764c99F9FD5B7B2",
"PROXY_REGISTRY": "0x26C8d09E5C0B423E2827844c770F61c9af2870E7",
"ETH": "0xEddA486ddB7eaa8f9FEce8c682EFD40f535b3Ad5",
"VAL_ETH": "0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84",
"MCD_JOIN_ETH_A": "0x9119B5d8b735E4cEbaE7386AF6cD2B863c7d35A8",
"MCD_FLIP_ETH_A": "0x0BD7632aF5F7020575e59E80ABbca739035Ac0EC",
"MCD_JOIN_ETH_B": "0xe2dD18a6000030F30ecB1237B15605533f814c59",
"MCD_CLIP_ETH_B": "0x3f2603979a4A185acE9B9c941193704FfBD24F4A",
"MCD_CLIP_CALC_ETH_B": "0x8cabea65F0140962A7D7Fe9f31a265a2B19Dc305",
"MCD_JOIN_ETH_C": "0x9FdC3bBD89ae1fB19054241644EF3dfbdcA85544",
"MCD_FLIP_ETH_C": "0xB0b7Db244994E9B922998f49b7ae61956314CA35",
"BAT": "0x39b4C0A63c4c16DD1816D104F2C18a296Dbd4e70",
"VAL_BAT": "0x62d69f6867A0A084C6d313943dC22023Bc263691",
"MCD_JOIN_BAT_A": "0xA616aD7D4562dCD9208425Af4038defD0a9057B0",
"MCD_FLIP_BAT_A": "0xB36901dB56E2fb44862a7D0eAE9F5Cf9a7E449bD",
"USDC": "0x32Ee2bF1267253f76298D4199095B9C6b5A389c0",
"VAL_USDC": "0xee35211C4D9126D520bBfeaf3cFee5FE7B86F221",
"MCD_JOIN_USDC_A": "0x59ea98A4b40B72140b7dc93c29c098AE607Ce20D",
"MCD_FLIP_USDC_A": "0xE8a124764cCcb7Ee3E8e320aAaA841Ea249197D4",
"MCD_JOIN_USDC_B": "0xaD1Bf7D34Fa48f7Cf7CA1CE3c7408f9151DF2745",
"MCD_FLIP_USDC_B": "0xC8055bC4415Ac354fA6EFbC3bcf57d5cBcc072ed",
"TUSD": "0xB014e899ddb9a55af72fE09E8570E700A5167b6d",
"VAL_TUSD": "0x7C276DcAab99BD16163c1bcce671CaD6A1ec0945",
"MCD_JOIN_TUSD_A": "0xAFB95880bc835B6Eeb041ce57D570D013360beC6",
"MCD_FLIP_TUSD_A": "0x6b0f809E52218192AbAf32C9C74F31229E07B626",
"WBTC": "0x123010c0Fe7D4d7420f309431bb95060393fe3B7",
"VAL_WBTC": "0x3f85D0b6119B38b7E6B119F7550290fec4BE0e3c",
"MCD_JOIN_WBTC_A": "0xf4C33a989bD0C9e9268c5bfCbCE2C8501B9dBa25",
"MCD_FLIP_WBTC_A": "0x53781B6DC81F5b90421371172c1b06e384858e44",
"GUSD": "0x0363Ef677c78bd8C8302DB33be2F6629E33E72Fe",
"VAL_GUSD": "0xd5F051401ca478B34C80D0B5A119e437Dc6D9df5",
"MCD_JOIN_GUSD_A": "0x01C957395029E9aCCbCb25a6Ab72C618252CACf9",
"MCD_FLIP_GUSD_A": "0x334490acE32D96808B104A1f8723cFAB5881DE46",
"PROXY_PAUSE_ACTIONS": "0x23263d4ebB1190A483A84e90B9a6Dd8720979284",
"PROXY_DEPLOYER": "0x30c8860f6a38819B59E1255A499A10bCBF4Ee747"
}
================================================
FILE: docker-compose.yml
================================================
version: "3.2"
services:
parity:
image: makerdao/testchain-pymaker:unit-testing-2.0.0
container_name: parity-pymaker-test
ports:
- "8545:8545"
- "8546:8546"
expose:
- "8545"
- "8546"
user: root
working_dir: /home/parity
ganache:
image: trufflesuite/ganache-cli:v6.9.1
container_name: ganache
ports:
- "8555:8555"
expose:
- "8555"
command: "--gasLimit 10000000
-p 8555
--account=\"0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead,1000000000000000000000000\"
--account=\"0xc0a550404067ce46a51283e0cc99ec3ba832940064587147a8db9a7ba355ef27,1000000000000000000000000\",
--account=\"0x6ca1cfaba9715aa485504cb8a3d3fe54191e0991b5f47eb982e8fb40d1b8e8d8,1000000000000000000000000\",
--account=\"0x1a9e422172e3d84487f7c833e3895f2f65c35eff7e68783adaa0c5bbe741ca8a,1000000000000000000000000\""
================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'pymaker'
copyright = '2017, MakerDAO'
author = 'MakerDAO'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = ''
# The full version, including alpha/beta/rc tags.
release = ''
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'classic'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'pymaker'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pymaker.py.tex', 'pymaker Documentation',
'MakerDAO', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pymaker', 'pymaker Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pymaker', 'pymaker Documentation',
author, 'pymaker', 'One line description of project.',
'Miscellaneous'),
]
================================================
FILE: docs/index.rst
================================================
pymaker API
===========
The `pymaker` API exists to provide a simple way of interacting with Maker smart contracts.
It was designed to simplify and facilitate creation of external profit-seeking agents, usually called keepers,
that operate around the stablecoin set of smart contracts. The API can also be used to automate certain tasks for
other entities involved in the platform, like DAI issuers or traders.
General
-------
Address
~~~~~~~
.. autoclass:: pymaker.Address
:members:
Transact
~~~~~~~~
.. autoclass:: pymaker.Transact
:members:
Calldata
~~~~~~~~
.. autoclass:: pymaker.Calldata
:members:
Invocation
~~~~~~~~~~
.. autoclass:: pymaker.Invocation
:members:
Receipt
~~~~~~~
.. autoclass:: pymaker.Receipt
:members:
Transfer
~~~~~~~~
.. autoclass:: pymaker.Transfer
:members:
Numeric types
-------------
Most of the numeric data throughout the entire platform is kept as either `Wad` (18-digit precision type)
or `Ray` (27-digit precision type).
Wad
~~~
.. autoclass:: pymaker.numeric.Wad
:members:
Ray
~~~
.. autoclass:: pymaker.numeric.Ray
:members:
Gas price
---------
.. autoclass:: pymaker.gas.GasPrice
:members:
The following implementations of `GasPrice` are available:
DefaultGasPrice
~~~~~~~~~~~~~~~
.. autoclass:: pymaker.gas.DefaultGasPrice
:members:
FixedGasPrice
~~~~~~~~~~~~~
.. autoclass:: pymaker.gas.FixedGasPrice
:members:
IncreasingGasPrice
~~~~~~~~~~~~~~~~~~
.. autoclass:: pymaker.gas.IncreasingGasPrice
:members:
Approvals
---------
.. automodule:: pymaker.approval
:members:
Contracts
---------
DAI Stablecoin
~~~~~~~~~~~~~~
Tub
"""
.. autoclass:: pymaker.sai.Tub
:members:
Tap
"""
.. autoclass:: pymaker.sai.Tap
:members:
Top
"""
.. autoclass:: pymaker.sai.Top
:members:
Vox
"""
.. autoclass:: pymaker.sai.Vox
:members:
ERC20
~~~~~
ERC20Token
""""""""""
.. autoclass:: pymaker.token.ERC20Token
:members:
DSToken
"""""""
.. autoclass:: pymaker.token.DSToken
:members:
DSEthToken
""""""""""
.. autoclass:: pymaker.token.DSEthToken
:members:
Exchanges
~~~~~~~~~
`OaaisDEX`, `EtherDelta` and `0x` are decentralized exchanges which also provide some arbitrage opportunities
for profit-seeking agents. Because of that an API has been created around them as well. Also an API for
the `Bibox` centralized exchange is present.
OasisDEX
""""""""
.. automodule:: pymaker.oasis
:members:
EtherDelta
""""""""""
.. automodule:: pymaker.etherdelta
:members:
0x
""
.. automodule:: pymaker.zrx
:members:
Bibox
"""""
.. automodule:: pymaker.bibox
:members:
Authentication
~~~~~~~~~~~~~~
DSGuard
"""""""
.. autoclass:: pymaker.auth.DSGuard
:members:
DSValue
~~~~~~~
.. autoclass:: pymaker.feed.DSValue
:members:
DSVault
~~~~~~~
.. autoclass:: pymaker.vault.DSVault
:members:
Atomic transactions
-------------------
TxManager
~~~~~~~~~
.. autoclass:: pymaker.transactional.TxManager
:members:
================================================
FILE: pymaker/__init__.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import json
import logging
import re
import requests
import sys
import time
from enum import Enum, auto
from functools import total_ordering, wraps
from threading import Lock
from typing import Optional
from weakref import WeakKeyDictionary
import eth_utils
import pkg_resources
from hexbytes import HexBytes
from web3 import HTTPProvider, Web3
from web3._utils.contracts import get_function_info, encode_abi
from web3._utils.events import get_event_data
from web3.exceptions import TransactionNotFound
from web3.middleware import geth_poa_middleware
from web3.exceptions import LogTopicError, TransactionNotFound
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker.gas import DefaultGasPrice, GasPrice
from pymaker.numeric import Wad
from pymaker.util import synchronize, bytes_to_hexstring, is_contract_at
filter_threads = []
nonce_calc = WeakKeyDictionary()
next_nonce = {}
transaction_lock = Lock()
logger = logging.getLogger()
def web3_via_http(endpoint_uri: str, timeout=60, http_pool_size=20):
assert isinstance(endpoint_uri, str)
adapter = requests.adapters.HTTPAdapter(pool_connections=http_pool_size, pool_maxsize=http_pool_size)
session = requests.Session()
if endpoint_uri.startswith("http"):
# Mount over both existing adaptors created by default (rather than just the one which applies to our URI)
session.mount('http://', adapter)
session.mount('https://', adapter)
else:
raise ValueError("Unsupported protocol")
web3 = Web3(HTTPProvider(endpoint_uri=endpoint_uri, request_kwargs={"timeout": timeout}, session=session))
if web3.net.version == "5": # goerli
web3.middleware_onion.inject(geth_poa_middleware, layer=0)
return web3
class NonceCalculation(Enum):
TX_COUNT = auto()
PARITY_NEXTNONCE = auto()
SERIAL = auto()
PARITY_SERIAL = auto()
def _get_nonce_calc(web3: Web3) -> NonceCalculation:
assert isinstance(web3, Web3)
global nonce_calc
if web3 not in nonce_calc:
providers_without_nonce_calculation = ['infura', 'quiknode']
requires_serial_nonce = any(provider in web3.manager.provider.endpoint_uri for provider in
providers_without_nonce_calculation)
is_parity = "parity" in web3.clientVersion.lower() or "openethereum" in web3.clientVersion.lower()
if is_parity and requires_serial_nonce:
nonce_calc[web3] = NonceCalculation.PARITY_SERIAL
elif requires_serial_nonce:
nonce_calc[web3] = NonceCalculation.SERIAL
elif is_parity:
nonce_calc[web3] = NonceCalculation.PARITY_NEXTNONCE
else:
nonce_calc[web3] = NonceCalculation.TX_COUNT
logger.debug(f"node clientVersion={web3.clientVersion}, will use {nonce_calc[web3]}")
return nonce_calc[web3]
def register_filter_thread(filter_thread):
filter_threads.append(filter_thread)
def any_filter_thread_present() -> bool:
return len(filter_threads) > 0
def all_filter_threads_alive() -> bool:
return all(filter_thread_alive(filter_thread) for filter_thread in filter_threads)
def filter_thread_alive(filter_thread) -> bool:
# it's a wicked way of detecting whether a web3.py filter is still working
# but unfortunately I wasn't able to find any other one
return hasattr(filter_thread, '_args') and hasattr(filter_thread, '_kwargs') or not filter_thread.is_alive()
def stop_all_filter_threads():
for filter_thread in filter_threads:
try:
filter_thread.stop_watching(timeout=60)
except:
pass
def _track_status(f):
@wraps(f)
async def wrapper(*args, **kwds):
# Check for multiple execution
if args[0].status != TransactStatus.NEW:
raise Exception("Each `Transact` can only be executed once")
# Set current status to in progress
args[0].status = TransactStatus.IN_PROGRESS
try:
return await f(*args, **kwds)
finally:
args[0].status = TransactStatus.FINISHED
return wrapper
@total_ordering
class Address:
"""Represents an Ethereum address.
Addresses get normalized automatically, so instances of this class can be safely compared to each other.
Args:
address: Can be any address representation allowed by web3.py
or another instance of the Address class.
Attributes:
address: Normalized hexadecimal representation of the Ethereum address.
"""
def __init__(self, address):
if isinstance(address, Address):
self.address = address.address
else:
self.address = eth_utils.to_checksum_address(address)
@staticmethod
def zero():
return Address("0x0000000000000000000000000000000000000000")
def as_bytes(self) -> bytes:
"""Return the address as a 20-byte bytes array."""
return bytes.fromhex(self.address.replace('0x', ''))
def __str__(self):
return f"{self.address}"
def __repr__(self):
return f"Address('{self.address}')"
def __hash__(self):
return self.address.__hash__()
def __eq__(self, other):
assert(isinstance(other, Address))
return self.address == other.address
def __lt__(self, other):
assert(isinstance(other, Address))
return self.address < other.address
class Contract:
logger = logging.getLogger()
@staticmethod
def _deploy(web3: Web3, abi: list, bytecode: str, args: list) -> Address:
assert(isinstance(web3, Web3))
assert(isinstance(abi, list))
assert(isinstance(bytecode, str))
assert(isinstance(args, list))
contract = web3.eth.contract(abi=abi, bytecode=bytecode)
tx_hash = contract.constructor(*args).transact(
transaction={'from': eth_utils.to_checksum_address(web3.eth.defaultAccount)})
receipt = web3.eth.getTransactionReceipt(tx_hash)
return Address(receipt['contractAddress'])
@staticmethod
def _get_contract(web3: Web3, abi: list, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(abi, list))
assert(isinstance(address, Address))
if not is_contract_at(web3, address):
raise Exception(f"No contract found at {address}")
return web3.eth.contract(abi=abi)(address=address.address)
def _past_events(self, contract, event, cls, number_of_past_blocks, event_filter) -> list:
block_number = contract.web3.eth.blockNumber
return self._past_events_in_block_range(contract, event, cls, max(block_number-number_of_past_blocks, 0),
block_number, event_filter)
def _past_events_in_block_range(self, contract, event, cls, from_block, to_block, event_filter) -> list:
assert(isinstance(from_block, int))
assert(isinstance(to_block, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
def _event_callback(cls, past):
def callback(log):
if past:
self.logger.debug(f"Past event {log['event']} discovered, block_number={log['blockNumber']},"
f" tx_hash={bytes_to_hexstring(log['transactionHash'])}")
else:
self.logger.debug(f"Event {log['event']} discovered, block_number={log['blockNumber']},"
f" tx_hash={bytes_to_hexstring(log['transactionHash'])}")
return cls(log)
return callback
result = contract.events[event].createFilter(fromBlock=from_block, toBlock=to_block,
argument_filters=event_filter).get_all_entries()
return list(map(_event_callback(cls, True), result))
@staticmethod
def _load_abi(package, resource) -> list:
return json.loads(pkg_resources.resource_string(package, resource))
@staticmethod
def _load_bin(package, resource) -> str:
return str(pkg_resources.resource_string(package, resource), "utf-8")
class Calldata:
"""Represents Ethereum calldata.
Attributes:
value: Calldata as either a string starting with `0x`, or as bytes.
"""
def __init__(self, value):
if isinstance(value, str):
assert(value.startswith('0x'))
self.value = value
elif isinstance(value, bytes):
self.value = bytes_to_hexstring(value)
else:
raise Exception(f"Unable to create calldata from '{value}'")
@classmethod
def from_signature(cls, web3: Web3, fn_sign: str, fn_args: list):
""" Allow to create a `Calldata` from a function signature and a list of arguments.
:param fn_sign: the function signature ie. "function(uint256,address)"
:param fn_args: arguments to the function ie. [123, "0x00...00"]
"""
assert isinstance(fn_sign, str)
assert isinstance(fn_args, list)
fn_split = re.split('[(),]', fn_sign)
fn_name = fn_split[0]
fn_args_type = [{"type": type} for type in fn_split[1:] if type]
fn_abi = {"type": "function", "name": fn_name, "inputs": fn_args_type}
fn_abi, fn_selector, fn_arguments = get_function_info("test", abi_codec=web3.codec, fn_abi=fn_abi, args=fn_args)
calldata = encode_abi(web3, fn_abi, fn_arguments, fn_selector)
return cls(calldata)
@classmethod
def from_contract_abi(cls, web3: Web3, fn_sign: str, fn_args: list, contract_abi):
""" Create a `Calldata` according to the given contract abi """
assert isinstance(web3, Web3)
assert isinstance(fn_sign, str)
assert isinstance(fn_args, list)
fn_split = re.split('[(),]', fn_sign)
fn_name = fn_split[0]
fn_abi, fn_selector, fn_arguments = get_function_info(fn_name, abi_codec=web3.codec, contract_abi=contract_abi, args=fn_args)
calldata = encode_abi(web3, fn_abi, fn_arguments, fn_selector)
return cls(calldata)
def as_bytes(self) -> bytes:
"""Return the calldata as a byte array."""
return bytes.fromhex(self.value.replace('0x', ''))
def __str__(self):
return f"{self.value}"
def __repr__(self):
return f"Calldata('{self.value}')"
def __hash__(self):
return self.value.__hash__()
def __eq__(self, other):
assert(isinstance(other, Calldata))
return self.value == other.value
class Invocation(object):
"""Single contract method invocation, to be used together with `TxManager`.
Attributes:
address: Contract address.
calldata: The calldata of the invocation.
"""
def __init__(self, address: Address, calldata: Calldata):
assert(isinstance(address, Address))
assert(isinstance(calldata, Calldata))
self.address = address
self.calldata = calldata
class Receipt:
"""Represents a receipt for an Ethereum transaction.
Attributes:
raw_receipt: Raw receipt received from the Ethereum node.
transaction_hash: Hash of the Ethereum transaction.
gas_used: Amount of gas used by the Ethereum transaction.
transfers: A list of ERC20 token transfers resulting from the execution
of this Ethereum transaction. Each transfer is an instance of the
:py:class:`pymaker.Transfer` class.
result: Transaction-specific return value (i.e. new order id for Oasis
order creation transaction).
successful: Boolean flag which is `True` if the Ethereum transaction
was successful. We consider transaction successful if the contract
method has been executed without throwing.
"""
def __init__(self, receipt):
self.raw_receipt = receipt
self.transaction_hash = receipt['transactionHash']
self.gas_used = receipt['gasUsed']
self.transfers = []
self.result = None
receipt_logs = receipt['logs']
if (receipt_logs is not None) and (len(receipt_logs) > 0):
self.successful = True
for receipt_log in receipt_logs:
if len(receipt_log['topics']) > 0:
# $ seth keccak $(seth --from-ascii "Transfer(address,address,uint256)")
# 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
if receipt_log['topics'][0] == HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'):
from pymaker.token import ERC20Token
transfer_abi = [abi for abi in ERC20Token.abi if abi.get('name') == 'Transfer'][0]
codec = ABICodec(default_registry)
try:
event_data = get_event_data(codec, transfer_abi, receipt_log)
self.transfers.append(Transfer(token_address=Address(event_data['address']),
from_address=Address(event_data['args']['from']),
to_address=Address(event_data['args']['to']),
value=Wad(event_data['args']['value'])))
# UniV3 Mint logIndex: 3 has an NFT mint of 1, from null, to a given address, but only 2 types (address, address)
except LogTopicError:
continue
# $ seth keccak $(seth --from-ascii "Mint(address,uint256)")
# 0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885
if receipt_log['topics'][0] == HexBytes('0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885'):
from pymaker.token import DSToken
transfer_abi = [abi for abi in DSToken.abi if abi.get('name') == 'Mint'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, transfer_abi, receipt_log)
self.transfers.append(Transfer(token_address=Address(event_data['address']),
from_address=Address('0x0000000000000000000000000000000000000000'),
to_address=Address(event_data['args']['guy']),
value=Wad(event_data['args']['wad'])))
# $ seth keccak $(seth --from-ascii "Burn(address,uint256)")
# 0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5
if receipt_log['topics'][0] == HexBytes('0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5'):
from pymaker.token import DSToken
transfer_abi = [abi for abi in DSToken.abi if abi.get('name') == 'Burn'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, transfer_abi, receipt_log)
self.transfers.append(Transfer(token_address=Address(event_data['address']),
from_address=Address(event_data['args']['guy']),
to_address=Address('0x0000000000000000000000000000000000000000'),
value=Wad(event_data['args']['wad'])))
else:
self.successful = False
@property
def logs(self):
return self.raw_receipt['logs']
class TransactStatus(Enum):
NEW = auto()
IN_PROGRESS = auto()
FINISHED = auto()
def get_pending_transactions(web3: Web3, address: Address = None) -> list:
"""Retrieves a list of pending transactions from the mempool."""
assert isinstance(web3, Web3)
assert isinstance(address, Address) or address is None
if address is None:
address = Address(web3.eth.defaultAccount)
# Get the list of pending transactions and their details from specified sources
if _get_nonce_calc(web3) in (NonceCalculation.PARITY_NEXTNONCE, NonceCalculation.PARITY_SERIAL):
items = web3.manager.request_blocking("parity_pendingTransactions", [])
items = filter(lambda item: item['from'].lower() == address.address.lower(), items)
items = filter(lambda item: item['blockNumber'] is None, items)
txes = map(lambda item: RecoveredTransact(web3=web3, address=address, nonce=int(item['nonce'], 16),
latest_tx_hash=item['hash'], current_gas=int(item['gasPrice'], 16)),
items)
else:
items = web3.manager.request_blocking("eth_getBlockByNumber", ["pending", True])['transactions']
items = filter(lambda item: item['from'].lower() == address.address.lower(), items)
list(items) # Unsure why this is required
txes = map(lambda item: RecoveredTransact(web3=web3, address=address, nonce=item['nonce'],
latest_tx_hash=item['hash'], current_gas=item['gasPrice']),
items)
return list(txes)
class Transact:
"""Represents an Ethereum transaction before it gets executed."""
logger = logging.getLogger()
gas_estimate_for_bad_txs = None
def __init__(self,
origin: Optional[object],
web3: Web3,
abi: Optional[list],
address: Address,
contract: Optional[object],
function_name: Optional[str],
parameters: Optional[list],
extra: Optional[dict] = None,
result_function=None):
assert(isinstance(origin, object) or (origin is None))
assert(isinstance(web3, Web3))
assert(isinstance(abi, list) or (abi is None))
assert(isinstance(address, Address))
assert(isinstance(contract, object) or (contract is None))
assert(isinstance(function_name, str) or (function_name is None))
assert(isinstance(parameters, list) or (parameters is None))
assert(isinstance(extra, dict) or (extra is None))
assert(callable(result_function) or (result_function is None))
self.origin = origin
self.web3 = web3
self.abi = abi
self.address = address
self.contract = contract
self.function_name = function_name
self.parameters = parameters
self.extra = extra
self.result_function = result_function
self.initial_time = None
self.status = TransactStatus.NEW
self.nonce = None
self.replaced = False
self.gas_price = None
self.gas_price_last = 0
self.tx_hashes = []
def _get_receipt(self, transaction_hash: str) -> Optional[Receipt]:
try:
raw_receipt = self.web3.eth.getTransactionReceipt(transaction_hash)
if raw_receipt is not None and raw_receipt['blockNumber'] is not None:
receipt = Receipt(raw_receipt)
receipt.result = self.result_function(receipt) if self.result_function is not None else None
return receipt
except (TransactionNotFound, ValueError):
self.logger.debug(f"Transaction {transaction_hash} not found (may have been dropped/replaced)")
return None
def _as_dict(self, dict_or_none) -> dict:
if dict_or_none is None:
return {}
else:
return dict(**dict_or_none)
def _gas(self, gas_estimate: int, **kwargs) -> int:
if 'gas' in kwargs and 'gas_buffer' in kwargs:
raise Exception('"gas" and "gas_buffer" keyword arguments may not be specified at the same time')
if 'gas' in kwargs:
return kwargs['gas']
elif 'gas_buffer' in kwargs:
return gas_estimate + kwargs['gas_buffer']
else:
return gas_estimate + 100000
def _func(self, from_account: str, gas: int, gas_price: Optional[int], nonce: Optional[int]):
gas_price_dict = {'gasPrice': gas_price} if gas_price is not None else {}
nonce_dict = {'nonce': nonce} if nonce is not None else {}
transaction_params = {**{'from': from_account, 'gas': gas},
**gas_price_dict,
**nonce_dict,
**self._as_dict(self.extra)}
if self.contract is not None:
if self.function_name is None:
return bytes_to_hexstring(self.web3.eth.sendTransaction({**transaction_params,
**{'to': self.address.address,
'data': self.parameters[0]}}))
else:
return bytes_to_hexstring(self._contract_function().transact(transaction_params))
else:
return bytes_to_hexstring(self.web3.eth.sendTransaction({**transaction_params,
**{'to': self.address.address}}))
def _contract_function(self):
if '(' in self.function_name:
function_factory = self.contract.get_function_by_signature(self.function_name)
else:
function_factory = self.contract.get_function_by_name(self.function_name)
return function_factory(*self.parameters)
def name(self) -> str:
"""Returns the nicely formatted name of this pending Ethereum transaction.
Returns:
Nicely formatted name of this pending Ethereum transaction.
"""
if self.origin:
def format_parameter(parameter):
if isinstance(parameter, bytes):
return bytes_to_hexstring(parameter)
else:
return parameter
formatted_parameters = str(list(map(format_parameter, self.parameters))).lstrip("[").rstrip("]")
name = f"{repr(self.origin)}.{self.function_name}({formatted_parameters})"
else:
name = f"Regular transfer to {self.address}"
return name if self.extra is None else name + f" with {self.extra}"
def estimated_gas(self, from_address: Address) -> int:
"""Return an estimated amount of gas which will get consumed by this Ethereum transaction.
May throw an exception if the actual transaction will fail as well.
Args:
from_address: Address to simulate sending the transaction from.
Returns:
Amount of gas as an integer.
"""
assert(isinstance(from_address, Address))
if self.contract is not None:
if self.function_name is None:
return self.web3.eth.estimateGas({**self._as_dict(self.extra), **{'from': from_address.address,
'to': self.address.address,
'data': self.parameters[0]}})
else:
estimate = self._contract_function() \
.estimateGas({**self._as_dict(self.extra), **{'from': from_address.address}})
else:
estimate = 21000
return estimate
def transact(self, **kwargs) -> Optional[Receipt]:
"""Executes the Ethereum transaction synchronously.
Executes the Ethereum transaction synchronously. The method will block until the
transaction gets mined i.e. it will return when either the transaction execution
succeeded or failed. In case of the former, a :py:class:`pymaker.Receipt`
object will be returned.
Out-of-gas exceptions are automatically recognized as transaction failures.
Allowed keyword arguments are: `from_address`, `replace`, `gas`, `gas_buffer`, `gas_price`.
`gas_price` needs to be an instance of a class inheriting from :py:class:`pymaker.gas.GasPrice`.
`from_address` needs to be an instance of :py:class:`pymaker.Address`.
The `gas` keyword argument is the gas limit for the transaction, whereas `gas_buffer`
specifies how much gas should be added to the estimate. They can not be present
at the same time. If none of them are present, a default buffer is added to the estimate.
Returns:
A :py:class:`pymaker.Receipt` object if the transaction invocation was successful.
`None` otherwise.
"""
return synchronize([self.transact_async(**kwargs)])[0]
@_track_status
async def transact_async(self, **kwargs) -> Optional[Receipt]:
"""Executes the Ethereum transaction asynchronously.
Executes the Ethereum transaction asynchronously. The method will return immediately.
Ultimately, its future value will become either a :py:class:`pymaker.Receipt` or `None`,
depending on whether the transaction execution was successful or not.
Out-of-gas exceptions are automatically recognized as transaction failures.
Allowed keyword arguments are: `from_address`, `replace`, `gas`, `gas_buffer`, `gas_price`.
`gas_price` needs to be an instance of a class inheriting from :py:class:`pymaker.gas.GasPrice`.
The `gas` keyword argument is the gas limit for the transaction, whereas `gas_buffer`
specifies how much gas should be added to the estimate. They can not be present
at the same time. If none of them are present, a default buffer is added to the estimate.
Returns:
A future value of either a :py:class:`pymaker.Receipt` object if the transaction
invocation was successful, or `None` if it failed.
"""
global next_nonce
self.initial_time = time.time()
unknown_kwargs = set(kwargs.keys()) - {'from_address', 'replace', 'gas', 'gas_buffer', 'gas_price'}
if len(unknown_kwargs) > 0:
raise ValueError(f"Unknown kwargs: {unknown_kwargs}")
# Get the from account; initialize the first nonce for the account.
from_account = kwargs['from_address'].address if ('from_address' in kwargs) else self.web3.eth.defaultAccount
if not next_nonce or from_account not in next_nonce:
next_nonce[from_account] = self.web3.eth.getTransactionCount(from_account, block_identifier='pending')
# First we try to estimate the gas usage of the transaction. If gas estimation fails
# it means there is no point in sending the transaction, thus we fail instantly and
# do not increment the nonce. If the estimation is successful, we pass the calculated
# gas value (plus some `gas_buffer`) to the subsequent `transact` calls so it does not
# try to estimate it again.
try:
gas_estimate = self.estimated_gas(Address(from_account))
except:
if Transact.gas_estimate_for_bad_txs:
self.logger.warning(f"Transaction {self.name()} will fail, submitting anyway")
gas_estimate = Transact.gas_estimate_for_bad_txs
else:
self.logger.warning(f"Transaction {self.name()} will fail, refusing to send ({sys.exc_info()[1]})")
return None
# Get or calculate `gas`. Get `gas_price`, which in fact refers to a gas pricing algorithm.
gas = self._gas(gas_estimate, **kwargs)
self.gas_price = kwargs['gas_price'] if ('gas_price' in kwargs) else DefaultGasPrice()
assert(isinstance(self.gas_price, GasPrice))
# Get the transaction this one is supposed to replace.
# If there is one, try to borrow the nonce from it as long as that transaction isn't finished.
replaced_tx = kwargs['replace'] if ('replace' in kwargs) else None
if replaced_tx is not None:
while replaced_tx.nonce is None and replaced_tx.status != TransactStatus.FINISHED:
await asyncio.sleep(0.25)
replaced_tx.replaced = True
self.nonce = replaced_tx.nonce
# Gas should be calculated from the original time of submission
self.initial_time = replaced_tx.initial_time if replaced_tx.initial_time else time.time()
# Use gas strategy from the original transaction if one was not provided
if 'gas_price' not in kwargs:
self.gas_price = replaced_tx.gas_price if replaced_tx.gas_price else DefaultGasPrice()
self.gas_price_last = replaced_tx.gas_price_last
# Detain replacement until gas strategy produces a price acceptable to the node
if replaced_tx.tx_hashes:
most_recent_tx = replaced_tx.tx_hashes[-1]
self.tx_hashes = [most_recent_tx]
while True:
seconds_elapsed = int(time.time() - self.initial_time)
# CAUTION: if transact_async is called rapidly, we will hammer the node with these JSON-RPC requests
if self.nonce is not None and self.web3.eth.getTransactionCount(from_account) > self.nonce:
# Check if any transaction sent so far has been mined (has a receipt).
# If it has, we return either the receipt (if if was successful) or `None`.
for attempt in range(1, 11):
if self.replaced:
self.logger.info(f"Transaction with nonce={self.nonce} was replaced with a newer transaction")
return None
for tx_hash in self.tx_hashes:
receipt = self._get_receipt(tx_hash)
if receipt:
if receipt.successful:
self.logger.info(f"Transaction {self.name()} was successful (tx_hash={tx_hash})")
return receipt
else:
self.logger.warning(f"Transaction {self.name()} mined successfully but generated no single"
f" log entry, assuming it has failed (tx_hash={tx_hash})")
return None
self.logger.debug(f"No receipt found in attempt #{attempt}/10 (nonce={self.nonce},"
f" getTransactionCount={self.web3.eth.getTransactionCount(from_account)})")
await asyncio.sleep(0.5)
# If we can not find a mined receipt but at the same time we know last used nonce
# has increased, then it means that the transaction we tried to send failed.
self.logger.warning(f"Transaction {self.name()} has been overridden by another transaction"
f" with the same nonce, which means it has failed")
return None
# Trap replacement after the tx has entered the mempool and before it has been mined
if self.replaced:
self.logger.info(f"Transaction {self.name()} with nonce={self.nonce} is being replaced")
return None
# Send a transaction if:
# - no transaction has been sent yet, or
# - the requested gas price has changed enough since the last transaction has been sent
# - the gas price on a replacement has sufficiently exceeded that of the original transaction
gas_price_value = self.gas_price.get_gas_price(seconds_elapsed)
transaction_was_sent = len(self.tx_hashes) > 0 or (replaced_tx is not None and len(replaced_tx.tx_hashes) > 0)
# Uncomment this to debug state during transaction submission
# self.logger.debug(f"Transaction {self.name()} is churning: was_sent={transaction_was_sent}, gas_price_value={gas_price_value} gas_price_last={self.gas_price_last}")
if not transaction_was_sent or (gas_price_value is not None and gas_price_value > self.gas_price_last * 1.125):
self.gas_price_last = gas_price_value
try:
# We need the lock in order to not try to send two transactions with the same nonce.
with transaction_lock:
if self.nonce is None:
nonce_calculation = _get_nonce_calc(self.web3)
if nonce_calculation == NonceCalculation.PARITY_NEXTNONCE:
self.nonce = int(self.web3.manager.request_blocking("parity_nextNonce", [from_account]), 16)
elif nonce_calculation == NonceCalculation.TX_COUNT:
self.nonce = self.web3.eth.getTransactionCount(from_account, block_identifier='pending')
elif nonce_calculation == NonceCalculation.SERIAL:
tx_count = self.web3.eth.getTransactionCount(from_account, block_identifier='pending')
next_serial = next_nonce[from_account]
self.nonce = max(tx_count, next_serial)
elif nonce_calculation == NonceCalculation.PARITY_SERIAL:
tx_count = int(self.web3.manager.request_blocking("parity_nextNonce", [from_account]), 16)
next_serial = next_nonce[from_account]
self.nonce = max(tx_count, next_serial)
next_nonce[from_account] = self.nonce + 1
# Trap replacement while original is holding the lock awaiting nonce assignment
if self.replaced:
self.logger.info(f"Transaction {self.name()} with nonce={self.nonce} was replaced")
return None
tx_hash = self._func(from_account, gas, gas_price_value, self.nonce)
self.tx_hashes.append(tx_hash)
self.logger.info(f"Sent transaction {self.name()} with nonce={self.nonce}, gas={gas},"
f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
f" (tx_hash={tx_hash})")
except Exception as e:
self.logger.warning(f"Failed to send transaction {self.name()} with nonce={self.nonce}, gas={gas},"
f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
f" ({e})")
if len(self.tx_hashes) == 0:
raise
await asyncio.sleep(0.25)
def invocation(self) -> Invocation:
"""Returns the `Invocation` object for this pending Ethereum transaction.
The :py:class:`pymaker.Invocation` object may be used with :py:class:`pymaker.transactional.TxManager`
to invoke multiple contract calls in one Ethereum transaction.
Please see :py:class:`pymaker.transactional.TxManager` documentation for more details.
Returns:
:py:class:`pymaker.Invocation` object for this pending Ethereum transaction.
"""
return Invocation(self.address, Calldata(self._contract_function()._encode_transaction_data()))
class RecoveredTransact(Transact):
""" Models a pending transaction retrieved from the mempool.
These can be created by a call to `get_pending_transactions`, enabling the consumer to implement logic which
cancels pending transactions upon keeper/bot startup.
"""
def __init__(self, web3: Web3,
address: Address,
nonce: int,
latest_tx_hash: str,
current_gas: int):
assert isinstance(current_gas, int)
super().__init__(origin=None,
web3=web3,
abi=None,
address=address,
contract=None,
function_name=None,
parameters=None)
self.nonce = nonce
self.tx_hashes.append(latest_tx_hash)
self.current_gas = current_gas
def name(self):
return f"Recovered tx with nonce {self.nonce}"
@_track_status
async def transact_async(self, **kwargs) -> Optional[Receipt]:
# TODO: Read transaction data from chain, create a new state machine to manage gas for the transaction.
raise NotImplementedError()
def cancel(self, gas_price: GasPrice):
return synchronize([self.cancel_async(gas_price)])[0]
async def cancel_async(self, gas_price: GasPrice):
assert isinstance(gas_price, GasPrice)
initial_time = time.time()
self.gas_price_last = self.current_gas
self.tx_hashes.clear()
if gas_price.get_gas_price(0) <= self.current_gas * 1.125:
self.logger.warning(f"Recovery gas price is less than current gas price {self.current_gas}; "
"cancellation will be deferred until the strategy produces an acceptable price.")
while True:
seconds_elapsed = int(time.time() - initial_time)
gas_price_value = gas_price.get_gas_price(seconds_elapsed)
if gas_price_value > self.gas_price_last * 1.125:
self.gas_price_last = gas_price_value
# Transaction lock isn't needed here, as we are replacing an existing nonce
tx_hash = bytes_to_hexstring(self.web3.eth.sendTransaction({'from': self.address.address,
'to': self.address.address,
'gasPrice': gas_price_value,
'nonce': self.nonce,
'value': 0}))
self.tx_hashes.append(tx_hash)
self.logger.info(f"Attempting to cancel recovered tx with nonce={self.nonce}, "
f"gas_price={gas_price_value} (tx_hash={tx_hash})")
for tx_hash in self.tx_hashes:
receipt = self._get_receipt(tx_hash)
if receipt:
self.logger.info(f"{self.name()} was cancelled (tx_hash={tx_hash})")
return
await asyncio.sleep(0.75)
class Transfer:
"""Represents an ERC20 token transfer.
Represents an ERC20 token transfer resulting from contract method execution.
A list of transfers can be found in the :py:class:`pymaker.Receipt` class.
Attributes:
token_address: Address of the ERC20 token that has been transferred.
from_address: Source address of the transfer.
to_address: Destination address of the transfer.
value: Value transferred.
"""
def __init__(self, token_address: Address, from_address: Address, to_address: Address, value: Wad):
assert(isinstance(token_address, Address))
assert(isinstance(from_address, Address))
assert(isinstance(to_address, Address))
assert(isinstance(value, Wad))
self.token_address = token_address
self.from_address = from_address
self.to_address = to_address
self.value = value
def __eq__(self, other):
assert(isinstance(other, Transfer))
return self.token_address == other.token_address and \
self.from_address == other.from_address and \
self.to_address == other.to_address and \
self.value == other.value
def __hash__(self):
return hash((self.token_address, self.from_address, self.token_address, self.value))
def eth_transfer(web3: Web3, to: Address, amount: Wad) -> Transact:
return Transact(None, web3, None, to, None, None, None, {'value': amount.value})
================================================
FILE: pymaker/abi/Cat.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"address","name":"urn","type":"address"},{"indexed":false,"internalType":"uint256","name":"ink","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"art","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"address","name":"flip","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Bite","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"urn","type":"address"}],"name":"bite","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"box","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"claw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"flip","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"address","name":"flip","type":"address"},{"internalType":"uint256","name":"chop","type":"uint256"},{"internalType":"uint256","name":"dunk","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"litter","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vow","outputs":[{"internalType":"contract VowLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/Clipper.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"spotter_","type":"address"},{"internalType":"address","name":"dog_","type":"address"},{"internalType":"bytes32","name":"ilk_","type":"bytes32"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Deny","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"data","type":"uint256"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"address","name":"data","type":"address"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"top","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"address","name":"kpr","type":"address"},{"indexed":false,"internalType":"uint256","name":"coin","type":"uint256"}],"name":"Kick","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"top","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"address","name":"kpr","type":"address"},{"indexed":false,"internalType":"uint256","name":"coin","type":"uint256"}],"name":"Redo","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Rely","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"max","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"price","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"owe","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Take","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Yank","type":"event"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"active","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"buf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"calc","outputs":[{"internalType":"contract AbacusLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"chip","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"chost","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"count","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cusp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"dog","outputs":[{"internalType":"contract DogLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getStatus","outputs":[{"internalType":"bool","name":"needsRedo","type":"bool"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"tab","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ilk","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tab","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"address","name":"kpr","type":"address"}],"name":"kick","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"kicks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"list","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"kpr","type":"address"}],"name":"redo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"sales","outputs":[{"internalType":"uint256","name":"pos","type":"uint256"},{"internalType":"uint256","name":"tab","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint96","name":"tic","type":"uint96"},{"internalType":"uint256","name":"top","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"spotter","outputs":[{"internalType":"contract SpotterLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stopped","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"tail","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amt","type":"uint256"},{"internalType":"uint256","name":"max","type":"uint256"},{"internalType":"address","name":"who","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"take","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tip","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"upchost","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vow","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"yank","outputs":[],"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/ClipperCallee.abi
================================================
[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"clipperCall","outputs":[],"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/DSAuth.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSChief.abi
================================================
[{"inputs":[{"internalType":"contract DSToken","name":"GOV","type":"address"},{"internalType":"contract DSToken","name":"IOU","type":"address"},{"internalType":"uint256","name":"MAX_YAYS","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"slate","type":"bytes32"}],"name":"Etch","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"guy","type":"address"},{"indexed":true,"internalType":"bytes32","name":"foo","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"bar","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"},{"constant":true,"inputs":[],"name":"GOV","outputs":[{"internalType":"contract DSToken","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"IOU","outputs":[{"internalType":"contract DSToken","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_YAYS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"approvals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"internalType":"contract DSAuthority","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"caller","type":"address"},{"internalType":"address","name":"code","type":"address"},{"internalType":"bytes4","name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"deposits","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address[]","name":"yays","type":"address[]"}],"name":"etch","outputs":[{"internalType":"bytes32","name":"slate","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"free","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"code","type":"address"},{"internalType":"bytes4","name":"sig","type":"bytes4"}],"name":"getCapabilityRoles","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"who","type":"address"}],"name":"getUserRoles","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"uint8","name":"role","type":"uint8"}],"name":"hasUserRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"hat","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"code","type":"address"},{"internalType":"bytes4","name":"sig","type":"bytes4"}],"name":"isCapabilityPublic","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"who","type":"address"}],"name":"isUserRoot","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"last","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"launch","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"whom","type":"address"}],"name":"lift","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"lock","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"contract DSAuthority","name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"code","type":"address"},{"internalType":"bytes4","name":"sig","type":"bytes4"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setPublicCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint8","name":"role","type":"uint8"},{"internalType":"address","name":"code","type":"address"},{"internalType":"bytes4","name":"sig","type":"bytes4"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setRoleCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setRootUser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"who","type":"address"},{"internalType":"uint8","name":"role","type":"uint8"},{"internalType":"bool","name":"enabled","type":"bool"}],"name":"setUserRole","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"slates","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"slate","type":"bytes32"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address[]","name":"yays","type":"address[]"}],"name":"vote","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"votes","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/DSEthToken.abi
================================================
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[],"name":"wrap","outputs":[],"payable":true,"type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"unwrap","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"tryWithdraw","outputs":[{"name":"ok","type":"bool"}],"payable":false,"type":"function"},{"payable":true,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Withdrawal","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}]
================================================
FILE: pymaker/abi/DSGuard.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"sig","type":"bytes32"}],"name":"forbid","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"bytes32"},{"name":"dst","type":"bytes32"},{"name":"sig","type":"bytes32"}],"name":"forbid","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"ANY","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"src_","type":"address"},{"name":"dst_","type":"address"},{"name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"sig","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"bytes32"},{"name":"dst","type":"bytes32"},{"name":"sig","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogPermit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogForbid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSPause.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"usr","type":"address"},{"name":"fax","type":"bytes"},{"name":"era","type":"uint256"}],"name":"drop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"delay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"usr","type":"address"},{"name":"fax","type":"bytes"},{"name":"era","type":"uint256"}],"name":"plan","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"plans","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"usr","type":"address"},{"name":"fax","type":"bytes"},{"name":"era","type":"uint256"}],"name":"exec","outputs":[{"name":"response","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"delay_","type":"uint256"},{"name":"owner_","type":"address"},{"name":"authority_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"usr","type":"address"},{"indexed":false,"name":"fax","type":"bytes"},{"indexed":false,"name":"era","type":"uint256"}],"name":"Plan","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"usr","type":"address"},{"indexed":false,"name":"fax","type":"bytes"},{"indexed":false,"name":"era","type":"uint256"}],"name":"Drop","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"usr","type":"address"},{"indexed":false,"name":"fax","type":"bytes"},{"indexed":false,"name":"era","type":"uint256"}],"name":"Exec","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSProxy.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_target","type":"address"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"response","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"target","type":"address"},{"name":"response","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"cache","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_cacheAddr","type":"address"}],"name":"setCache","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_cacheAddr","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSProxyCache.abi
================================================
[{"constant":false,"inputs":[{"name":"_code","type":"bytes"}],"name":"write","outputs":[{"name":"target","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_code","type":"bytes"}],"name":"read","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/DSProxyFactory.abi
================================================
[{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isProxy","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"cache","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"address"}],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":true,"name":"owner","type":"address"},{"indexed":false,"name":"proxy","type":"address"},{"indexed":false,"name":"cache","type":"address"}],"name":"Created","type":"event"}]
================================================
FILE: pymaker/abi/DSRoles.abi
================================================
[{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"getUserRoles","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"getCapabilityRoles","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"isCapabilityPublic","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"who","type":"address"},{"name":"role","type":"uint8"},{"name":"enabled","type":"bool"}],"name":"setUserRole","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"role","type":"uint8"},{"name":"code","type":"address"},{"name":"sig","type":"bytes4"},{"name":"enabled","type":"bool"}],"name":"setRoleCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"},{"name":"role","type":"uint8"}],"name":"hasUserRole","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"caller","type":"address"},{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"},{"name":"enabled","type":"bool"}],"name":"setPublicCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"who","type":"address"},{"name":"enabled","type":"bool"}],"name":"setRootUser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"isUserRoot","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSToken.abi
================================================
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"mint","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"name_","type":"bytes32"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"stopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"mint","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"push","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"move","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"start","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"wad","type":"uint256"}],"name":"pull","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"symbol_","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}]
================================================
FILE: pymaker/abi/DSValue.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"wut","type":"bytes32"}],"name":"poke","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"read","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"peek","outputs":[{"name":"","type":"bytes32"},{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"void","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DSVault.abi
================================================
[{"constant":false,"inputs":[{"name":"token_","type":"address"}],"name":"swap","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint128"}],"name":"push","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint128"}],"name":"push","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"burn","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"}],"name":"pull","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"src","type":"address"}],"name":"pull","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint128"}],"name":"mint","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"src","type":"address"},{"name":"wad","type":"uint128"}],"name":"pull","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"wad","type":"uint128"}],"name":"burn","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"wad","type":"uint128"}],"name":"pull","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"}],"name":"burn","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"}],"name":"push","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint128"}],"name":"burn","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"dst","type":"address"}],"name":"push","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"wad","type":"uint128"}],"name":"mint","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"token","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/DaiJoin.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"dai_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dai","outputs":[{"internalType":"contract DSTokenLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/Dog.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"address","name":"urn","type":"address"},{"indexed":false,"internalType":"uint256","name":"ink","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"art","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"due","type":"uint256"},{"indexed":false,"internalType":"address","name":"clip","type":"address"},{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"}],"name":"Bark","type":"event"},{"anonymous":false,"inputs":[],"name":"Cage","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Deny","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"rad","type":"uint256"}],"name":"Digs","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"data","type":"uint256"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"address","name":"data","type":"address"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"data","type":"uint256"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"address","name":"clip","type":"address"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Rely","type":"event"},{"inputs":[],"name":"Dirt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"Hole","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"urn","type":"address"},{"internalType":"address","name":"kpr","type":"address"}],"name":"bark","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"chop","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"digs","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"clip","type":"address"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"address","name":"clip","type":"address"},{"internalType":"uint256","name":"chop","type":"uint256"},{"internalType":"uint256","name":"hole","type":"uint256"},{"internalType":"uint256","name":"dirt","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vow","outputs":[{"internalType":"contract VowLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/DsrManager.abi
================================================
[{"inputs":[{"internalType":"address","name":"pot_","type":"address"},{"internalType":"address","name":"daiJoin_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Exit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Join","type":"event"},{"constant":true,"inputs":[],"name":"dai","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"daiBalance","outputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"daiJoin","outputs":[{"internalType":"contract JoinLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"dst","type":"address"}],"name":"exitAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"pieOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"pot","outputs":[{"internalType":"contract PotLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"supply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/DssCdpManager.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"address","name":"own","type":"address"},{"indexed":true,"internalType":"uint256","name":"cdp","type":"uint256"}],"name":"NewCdp","type":"event"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"ok","type":"uint256"}],"name":"cdpAllow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"cdpCan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"cdpi","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"count","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"uint256","name":"cdp","type":"uint256"}],"name":"enter","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"first","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"flux","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"flux","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"frob","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"dst","type":"address"}],"name":"give","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"ilks","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"last","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"list","outputs":[{"internalType":"uint256","name":"prev","type":"uint256"},{"internalType":"uint256","name":"next","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"move","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"usr","type":"address"}],"name":"open","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"owns","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdp","type":"uint256"},{"internalType":"address","name":"dst","type":"address"}],"name":"quit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"cdpSrc","type":"uint256"},{"internalType":"uint256","name":"cdpDst","type":"uint256"}],"name":"shift","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"ok","type":"uint256"}],"name":"urnAllow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"urnCan","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"urns","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/DssProxyActionsDsr.abi
================================================
[{"constant":false,"inputs":[{"internalType":"address","name":"apt","type":"address"},{"internalType":"address","name":"urn","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"daiJoin_join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"daiJoin","type":"address"},{"internalType":"address","name":"pot","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"daiJoin","type":"address"},{"internalType":"address","name":"pot","type":"address"}],"name":"exitAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"daiJoin","type":"address"},{"internalType":"address","name":"pot","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/ERC20Token.abi
================================================
[{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"supply","type":"uint256"}],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}]
================================================
FILE: pymaker/abi/ESM.abi
================================================
[{"inputs":[{"internalType":"address","name":"gem_","type":"address"},{"internalType":"address","name":"end_","type":"address"},{"internalType":"address","name":"proxy_","type":"address"},{"internalType":"uint256","name":"min_","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[],"name":"Fire","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Join","type":"event"},{"inputs":[],"name":"Sum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"name":"deny","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"end","outputs":[{"internalType":"contract EndLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fire","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"gem","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"min","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxy","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"revokesGovernanceAccess","outputs":[{"internalType":"bool","name":"ret","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"sum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/End.abi
================================================
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[],"name":"Cage","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"Cage","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Cash","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Deny","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"data","type":"uint256"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"what","type":"bytes32"},{"indexed":false,"internalType":"address","name":"data","type":"address"}],"name":"File","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"Flow","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"ink","type":"uint256"}],"name":"Free","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"}],"name":"Pack","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"usr","type":"address"}],"name":"Rely","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"address","name":"urn","type":"address"},{"indexed":false,"internalType":"uint256","name":"wad","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"art","type":"uint256"}],"name":"Skim","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"art","type":"uint256"}],"name":"Skip","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"art","type":"uint256"}],"name":"Snip","type":"event"},{"anonymous":false,"inputs":[],"name":"Thaw","type":"event"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"Art","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"bag","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"cage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"cash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cat","outputs":[{"internalType":"contract CatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"debt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"dog","outputs":[{"internalType":"contract DogLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"fix","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"flow","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"free","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"gap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"out","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"pack","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pot","outputs":[{"internalType":"contract PotLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"urn","type":"address"}],"name":"skim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"uint256","name":"id","type":"uint256"}],"name":"skip","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"uint256","name":"id","type":"uint256"}],"name":"snip","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"spot","outputs":[{"internalType":"contract SpotLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"tag","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"thaw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"vow","outputs":[{"internalType":"contract VowLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"wait","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"when","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/EtherDelta.abi
================================================
[{"constant":false,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"user","type":"address"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"trade","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"}],"name":"order","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"bytes32"}],"name":"orderFills","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"cancelOrder","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"amount","type":"uint256"}],"name":"depositToken","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"user","type":"address"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"amountFilled","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"tokens","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"feeMake_","type":"uint256"}],"name":"changeFeeMake","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeMake","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"feeRebate_","type":"uint256"}],"name":"changeFeeRebate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeAccount","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"user","type":"address"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"},{"name":"amount","type":"uint256"},{"name":"sender","type":"address"}],"name":"testTrade","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"feeAccount_","type":"address"}],"name":"changeFeeAccount","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeRebate","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"feeTake_","type":"uint256"}],"name":"changeFeeTake","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"admin_","type":"address"}],"name":"changeAdmin","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"amount","type":"uint256"}],"name":"withdrawToken","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"bytes32"}],"name":"orders","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeTake","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"accountLevelsAddr_","type":"address"}],"name":"changeAccountLevelsAddr","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"accountLevelsAddr","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"},{"name":"user","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"admin","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"tokenGet","type":"address"},{"name":"amountGet","type":"uint256"},{"name":"tokenGive","type":"address"},{"name":"amountGive","type":"uint256"},{"name":"expires","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"user","type":"address"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"availableVolume","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"admin_","type":"address"},{"name":"feeAccount_","type":"address"},{"name":"accountLevelsAddr_","type":"address"},{"name":"feeMake_","type":"uint256"},{"name":"feeTake_","type":"uint256"},{"name":"feeRebate_","type":"uint256"}],"payable":false,"type":"constructor"},{"payable":false,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":false,"name":"tokenGet","type":"address"},{"indexed":false,"name":"amountGet","type":"uint256"},{"indexed":false,"name":"tokenGive","type":"address"},{"indexed":false,"name":"amountGive","type":"uint256"},{"indexed":false,"name":"expires","type":"uint256"},{"indexed":false,"name":"nonce","type":"uint256"},{"indexed":false,"name":"user","type":"address"}],"name":"Order","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"tokenGet","type":"address"},{"indexed":false,"name":"amountGet","type":"uint256"},{"indexed":false,"name":"tokenGive","type":"address"},{"indexed":false,"name":"amountGive","type":"uint256"},{"indexed":false,"name":"expires","type":"uint256"},{"indexed":false,"name":"nonce","type":"uint256"},{"indexed":false,"name":"user","type":"address"},{"indexed":false,"name":"v","type":"uint8"},{"indexed":false,"name":"r","type":"bytes32"},{"indexed":false,"name":"s","type":"bytes32"}],"name":"Cancel","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"tokenGet","type":"address"},{"indexed":false,"name":"amountGet","type":"uint256"},{"indexed":false,"name":"tokenGive","type":"address"},{"indexed":false,"name":"amountGive","type":"uint256"},{"indexed":false,"name":"get","type":"address"},{"indexed":false,"name":"give","type":"address"}],"name":"Trade","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"token","type":"address"},{"indexed":false,"name":"user","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"balance","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"token","type":"address"},{"indexed":false,"name":"user","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"balance","type":"uint256"}],"name":"Withdraw","type":"event"}]
================================================
FILE: pymaker/abi/EtherToken.abi
================================================
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"payable":true,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]
================================================
FILE: pymaker/abi/Exchange.abi
================================================
[{"constant":true,"inputs":[{"name":"numerator","type":"uint256"},{"name":"denominator","type":"uint256"},{"name":"target","type":"uint256"}],"name":"isRoundingError","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"filled","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"cancelled","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5][]"},{"name":"orderValues","type":"uint256[6][]"},{"name":"fillTakerTokenAmount","type":"uint256"},{"name":"shouldThrowOnInsufficientBalanceOrAllowance","type":"bool"},{"name":"v","type":"uint8[]"},{"name":"r","type":"bytes32[]"},{"name":"s","type":"bytes32[]"}],"name":"fillOrdersUpTo","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5]"},{"name":"orderValues","type":"uint256[6]"},{"name":"cancelTakerTokenAmount","type":"uint256"}],"name":"cancelOrder","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"ZRX_TOKEN_CONTRACT","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5][]"},{"name":"orderValues","type":"uint256[6][]"},{"name":"fillTakerTokenAmounts","type":"uint256[]"},{"name":"v","type":"uint8[]"},{"name":"r","type":"bytes32[]"},{"name":"s","type":"bytes32[]"}],"name":"batchFillOrKillOrders","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5]"},{"name":"orderValues","type":"uint256[6]"},{"name":"fillTakerTokenAmount","type":"uint256"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"fillOrKillOrder","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"orderHash","type":"bytes32"}],"name":"getUnavailableTakerTokenAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"signer","type":"address"},{"name":"hash","type":"bytes32"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"isValidSignature","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"numerator","type":"uint256"},{"name":"denominator","type":"uint256"},{"name":"target","type":"uint256"}],"name":"getPartialAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"TOKEN_TRANSFER_PROXY_CONTRACT","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5][]"},{"name":"orderValues","type":"uint256[6][]"},{"name":"fillTakerTokenAmounts","type":"uint256[]"},{"name":"shouldThrowOnInsufficientBalanceOrAllowance","type":"bool"},{"name":"v","type":"uint8[]"},{"name":"r","type":"bytes32[]"},{"name":"s","type":"bytes32[]"}],"name":"batchFillOrders","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5][]"},{"name":"orderValues","type":"uint256[6][]"},{"name":"cancelTakerTokenAmounts","type":"uint256[]"}],"name":"batchCancelOrders","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"orderAddresses","type":"address[5]"},{"name":"orderValues","type":"uint256[6]"},{"name":"fillTakerTokenAmount","type":"uint256"},{"name":"shouldThrowOnInsufficientBalanceOrAllowance","type":"bool"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"fillOrder","outputs":[{"name":"filledTakerTokenAmount","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"orderAddresses","type":"address[5]"},{"name":"orderValues","type":"uint256[6]"}],"name":"getOrderHash","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"EXTERNAL_QUERY_GAS_LIMIT","outputs":[{"name":"","type":"uint16"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"VERSION","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"inputs":[{"name":"_zrxToken","type":"address"},{"name":"_tokenTransferProxy","type":"address"}],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"maker","type":"address"},{"indexed":false,"name":"taker","type":"address"},{"indexed":true,"name":"feeRecipient","type":"address"},{"indexed":false,"name":"makerToken","type":"address"},{"indexed":false,"name":"takerToken","type":"address"},{"indexed":false,"name":"filledMakerTokenAmount","type":"uint256"},{"indexed":false,"name":"filledTakerTokenAmount","type":"uint256"},{"indexed":false,"name":"paidMakerFee","type":"uint256"},{"indexed":false,"name":"paidTakerFee","type":"uint256"},{"indexed":true,"name":"tokens","type":"bytes32"},{"indexed":false,"name":"orderHash","type":"bytes32"}],"name":"LogFill","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"maker","type":"address"},{"indexed":true,"name":"feeRecipient","type":"address"},{"indexed":false,"name":"makerToken","type":"address"},{"indexed":false,"name":"takerToken","type":"address"},{"indexed":false,"name":"cancelledMakerTokenAmount","type":"uint256"},{"indexed":false,"name":"cancelledTakerTokenAmount","type":"uint256"},{"indexed":true,"name":"tokens","type":"bytes32"},{"indexed":false,"name":"orderHash","type":"bytes32"}],"name":"LogCancel","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"errorId","type":"uint8"},{"indexed":true,"name":"orderHash","type":"bytes32"}],"name":"LogError","type":"event"}]
================================================
FILE: pymaker/abi/ExchangeV2-ERC20Proxy.abi
================================================
[
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "addAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "authorities",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "removeAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
},
{
"name": "index",
"type": "uint256"
}
],
"name": "removeAuthorizedAddressAtIndex",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getProxyId",
"outputs": [
{
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "authorized",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAuthorizedAddresses",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"payable": false,
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "target",
"type": "address"
},
{
"indexed": true,
"name": "caller",
"type": "address"
}
],
"name": "AuthorizedAddressAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "target",
"type": "address"
},
{
"indexed": true,
"name": "caller",
"type": "address"
}
],
"name": "AuthorizedAddressRemoved",
"type": "event"
}
]
================================================
FILE: pymaker/abi/ExchangeV2.abi
================================================
[
{
"constant": true,
"inputs": [
{
"name": "",
"type": "bytes32"
}
],
"name": "filled",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "takerAssetFillAmounts",
"type": "uint256[]"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "batchFillOrders",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "bytes32"
}
],
"name": "cancelled",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "hash",
"type": "bytes32"
},
{
"name": "signerAddress",
"type": "address"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "preSign",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "leftOrder",
"type": "tuple"
},
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "rightOrder",
"type": "tuple"
},
{
"name": "leftSignature",
"type": "bytes"
},
{
"name": "rightSignature",
"type": "bytes"
}
],
"name": "matchOrders",
"outputs": [
{
"components": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "left",
"type": "tuple"
},
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "right",
"type": "tuple"
},
{
"name": "leftMakerAssetSpreadAmount",
"type": "uint256"
}
],
"name": "matchedFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "order",
"type": "tuple"
},
{
"name": "takerAssetFillAmount",
"type": "uint256"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "fillOrderNoThrow",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "fillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "bytes4"
}
],
"name": "assetProxies",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
}
],
"name": "batchCancelOrders",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "takerAssetFillAmounts",
"type": "uint256[]"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "batchFillOrKillOrders",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "targetOrderEpoch",
"type": "uint256"
}
],
"name": "cancelOrdersUpTo",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "takerAssetFillAmounts",
"type": "uint256[]"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "batchFillOrdersNoThrow",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "assetProxyId",
"type": "bytes4"
}
],
"name": "getAssetProxy",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "bytes32"
}
],
"name": "transactions",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "order",
"type": "tuple"
},
{
"name": "takerAssetFillAmount",
"type": "uint256"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "fillOrKillOrder",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "fillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "validatorAddress",
"type": "address"
},
{
"name": "approval",
"type": "bool"
}
],
"name": "setSignatureValidatorApproval",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "allowedValidators",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "takerAssetFillAmount",
"type": "uint256"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "marketSellOrders",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
}
],
"name": "getOrdersInfo",
"outputs": [
{
"components": [
{
"name": "orderStatus",
"type": "uint8"
},
{
"name": "orderHash",
"type": "bytes32"
},
{
"name": "orderTakerAssetFilledAmount",
"type": "uint256"
}
],
"name": "",
"type": "tuple[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "bytes32"
},
{
"name": "",
"type": "address"
}
],
"name": "preSigned",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "hash",
"type": "bytes32"
},
{
"name": "signerAddress",
"type": "address"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "isValidSignature",
"outputs": [
{
"name": "isValid",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "makerAssetFillAmount",
"type": "uint256"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "marketBuyOrdersNoThrow",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "order",
"type": "tuple"
},
{
"name": "takerAssetFillAmount",
"type": "uint256"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "fillOrder",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "fillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "salt",
"type": "uint256"
},
{
"name": "signerAddress",
"type": "address"
},
{
"name": "data",
"type": "bytes"
},
{
"name": "signature",
"type": "bytes"
}
],
"name": "executeTransaction",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "assetProxy",
"type": "address"
}
],
"name": "registerAssetProxy",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "order",
"type": "tuple"
}
],
"name": "getOrderInfo",
"outputs": [
{
"components": [
{
"name": "orderStatus",
"type": "uint8"
},
{
"name": "orderHash",
"type": "bytes32"
},
{
"name": "orderTakerAssetFilledAmount",
"type": "uint256"
}
],
"name": "orderInfo",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "order",
"type": "tuple"
}
],
"name": "cancelOrder",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "orderEpoch",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ZRX_ASSET_DATA",
"outputs": [
{
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "takerAssetFillAmount",
"type": "uint256"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "marketSellOrdersNoThrow",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "EIP712_DOMAIN_HASH",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"components": [
{
"name": "makerAddress",
"type": "address"
},
{
"name": "takerAddress",
"type": "address"
},
{
"name": "feeRecipientAddress",
"type": "address"
},
{
"name": "senderAddress",
"type": "address"
},
{
"name": "makerAssetAmount",
"type": "uint256"
},
{
"name": "takerAssetAmount",
"type": "uint256"
},
{
"name": "makerFee",
"type": "uint256"
},
{
"name": "takerFee",
"type": "uint256"
},
{
"name": "expirationTimeSeconds",
"type": "uint256"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "makerAssetData",
"type": "bytes"
},
{
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "orders",
"type": "tuple[]"
},
{
"name": "makerAssetFillAmount",
"type": "uint256"
},
{
"name": "signatures",
"type": "bytes[]"
}
],
"name": "marketBuyOrders",
"outputs": [
{
"components": [
{
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"name": "makerFeePaid",
"type": "uint256"
},
{
"name": "takerFeePaid",
"type": "uint256"
}
],
"name": "totalFillResults",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "currentContextAddress",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "VERSION",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "signerAddress",
"type": "address"
},
{
"indexed": true,
"name": "validatorAddress",
"type": "address"
},
{
"indexed": false,
"name": "approved",
"type": "bool"
}
],
"name": "SignatureValidatorApproval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "makerAddress",
"type": "address"
},
{
"indexed": true,
"name": "feeRecipientAddress",
"type": "address"
},
{
"indexed": false,
"name": "takerAddress",
"type": "address"
},
{
"indexed": false,
"name": "senderAddress",
"type": "address"
},
{
"indexed": false,
"name": "makerAssetFilledAmount",
"type": "uint256"
},
{
"indexed": false,
"name": "takerAssetFilledAmount",
"type": "uint256"
},
{
"indexed": false,
"name": "makerFeePaid",
"type": "uint256"
},
{
"indexed": false,
"name": "takerFeePaid",
"type": "uint256"
},
{
"indexed": true,
"name": "orderHash",
"type": "bytes32"
},
{
"indexed": false,
"name": "makerAssetData",
"type": "bytes"
},
{
"indexed": false,
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "Fill",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "makerAddress",
"type": "address"
},
{
"indexed": true,
"name": "feeRecipientAddress",
"type": "address"
},
{
"indexed": false,
"name": "senderAddress",
"type": "address"
},
{
"indexed": true,
"name": "orderHash",
"type": "bytes32"
},
{
"indexed": false,
"name": "makerAssetData",
"type": "bytes"
},
{
"indexed": false,
"name": "takerAssetData",
"type": "bytes"
}
],
"name": "Cancel",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "makerAddress",
"type": "address"
},
{
"indexed": true,
"name": "senderAddress",
"type": "address"
},
{
"indexed": false,
"name": "orderEpoch",
"type": "uint256"
}
],
"name": "CancelUpTo",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "id",
"type": "bytes4"
},
{
"indexed": false,
"name": "assetProxy",
"type": "address"
}
],
"name": "AssetProxyRegistered",
"type": "event"
}
]
================================================
FILE: pymaker/abi/Flapper.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"gem_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"bid","type":"uint256"}],"name":"Kick","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"beg","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"bids","outputs":[{"internalType":"uint256","name":"bid","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"address","name":"guy","type":"address"},{"internalType":"uint48","name":"tic","type":"uint48"},{"internalType":"uint48","name":"end","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"deal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"kick","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"kicks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tau","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"tend","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"tick","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ttl","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"yank","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/Flipper.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"cat_","type":"address"},{"internalType":"bytes32","name":"ilk_","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"bid","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tab","type":"uint256"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"address","name":"gal","type":"address"}],"name":"Kick","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"beg","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"bids","outputs":[{"internalType":"uint256","name":"bid","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"address","name":"guy","type":"address"},{"internalType":"uint48","name":"tic","type":"uint48"},{"internalType":"uint48","name":"end","type":"uint48"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"address","name":"gal","type":"address"},{"internalType":"uint256","name":"tab","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"cat","outputs":[{"internalType":"contract CatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"deal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"dent","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ilk","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"address","name":"gal","type":"address"},{"internalType":"uint256","name":"tab","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"kick","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"kicks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tau","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"tend","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"tick","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ttl","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"yank","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/Flopper.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"gem_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"lot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"bid","type":"uint256"},{"indexed":true,"internalType":"address","name":"gal","type":"address"}],"name":"Kick","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"beg","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"bids","outputs":[{"internalType":"uint256","name":"bid","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"address","name":"guy","type":"address"},{"internalType":"uint48","name":"tic","type":"uint48"},{"internalType":"uint48","name":"end","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"deal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"dent","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"gal","type":"address"},{"internalType":"uint256","name":"lot","type":"uint256"},{"internalType":"uint256","name":"bid","type":"uint256"}],"name":"kick","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"kicks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"pad","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tau","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"tick","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ttl","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vow","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"yank","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/GemJoin.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"bytes32","name":"ilk_","type":"bytes32"},{"internalType":"address","name":"gem_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dec","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ilk","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/GemJoin5.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"bytes32","name":"ilk_","type":"bytes32"},{"internalType":"address","name":"gem_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dec","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"},{"internalType":"uint256","name":"amt","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"internalType":"contract GemLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ilk","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"urn","type":"address"},{"internalType":"uint256","name":"amt","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/Jug.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"base","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"drip","outputs":[{"internalType":"uint256","name":"rate","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"uint256","name":"duty","type":"uint256"},{"internalType":"uint256","name":"rho","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"init","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vow","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/MakerOtcSupportMethods.abi
================================================
[{"constant":true,"inputs":[{"name":"otc","type":"address"},{"name":"payToken","type":"address"},{"name":"buyToken","type":"address"}],"name":"getOffers","outputs":[{"name":"ids","type":"uint256[100]"},{"name":"payAmts","type":"uint256[100]"},{"name":"buyAmts","type":"uint256[100]"},{"name":"owners","type":"address[100]"},{"name":"timestamps","type":"uint256[100]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"otc","type":"address"},{"name":"buyToken","type":"address"},{"name":"buyAmt","type":"uint256"},{"name":"payToken","type":"address"}],"name":"getOffersAmountToBuyAll","outputs":[{"name":"ordersToTake","type":"uint256"},{"name":"takesPartialOrder","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"otc","type":"address"},{"name":"offerId","type":"uint256"}],"name":"getOffers","outputs":[{"name":"ids","type":"uint256[100]"},{"name":"payAmts","type":"uint256[100]"},{"name":"buyAmts","type":"uint256[100]"},{"name":"owners","type":"address[100]"},{"name":"timestamps","type":"uint256[100]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"otc","type":"address"},{"name":"payToken","type":"address"},{"name":"payAmt","type":"uint256"},{"name":"buyToken","type":"address"}],"name":"getOffersAmountToSellAll","outputs":[{"name":"ordersToTake","type":"uint256"},{"name":"takesPartialOrder","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/MatchingMarket.abi
================================================
[{"inputs":[{"internalType":"address","name":"_dustToken","type":"address"},{"internalType":"uint256","name":"_dustLimit","type":"uint256"},{"internalType":"address","name":"_priceOracle","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogBump","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"keeper","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogDelete","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"keeper","type":"address"},{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogInsert","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogItemUpdate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogKill","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogMake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"uint256","name":"min_amount","type":"uint256"}],"name":"LogMinSell","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogSortedOffer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":true,"internalType":"address","name":"taker","type":"address"},{"indexed":false,"internalType":"uint128","name":"take_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"give_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogTake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"pay_amt","type":"uint256"},{"indexed":true,"internalType":"address","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"uint256","name":"buy_amt","type":"uint256"},{"indexed":true,"internalType":"address","name":"buy_gem","type":"address"}],"name":"LogTrade","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogUnsortedOffer","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"_best","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"_dust","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"_near","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"_rank","outputs":[{"internalType":"uint256","name":"next","type":"uint256"},{"internalType":"uint256","name":"prev","type":"uint256"},{"internalType":"uint256","name":"delb","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"_span","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id_","type":"bytes32"}],"name":"bump","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"buy","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"max_fill_amount","type":"uint256"}],"name":"buyAllAmount","outputs":[{"internalType":"uint256","name":"fill_amt","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"cancel","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"del_rank","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dustLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"dustToken","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract ERC20","name":"sell_gem","type":"address"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"}],"name":"getBestOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getBetterOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"pay_amt","type":"uint256"}],"name":"getBuyAmount","outputs":[{"internalType":"uint256","name":"fill_amt","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFirstUnsortedOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"}],"name":"getMinSell","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getNextUnsortedOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"contract ERC20","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"contract ERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract ERC20","name":"sell_gem","type":"address"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"}],"name":"getOfferCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getOwner","outputs":[{"internalType":"address","name":"owner","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"}],"name":"getPayAmount","outputs":[{"internalType":"uint256","name":"fill_amt","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getWorseOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"pos","type":"uint256"}],"name":"insert","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"isActive","outputs":[{"internalType":"bool","name":"active","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"isOfferSorted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"last_offer_id","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint128","name":"pay_amt","type":"uint128"},{"internalType":"uint128","name":"buy_amt","type":"uint128"}],"name":"make","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint256","name":"pos","type":"uint256"}],"name":"offer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint256","name":"pos","type":"uint256"},{"internalType":"bool","name":"rounding","type":"bool"}],"name":"offer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"}],"name":"offer","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"offers","outputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint64","name":"timestamp","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"priceOracle","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint256","name":"min_fill_amount","type":"uint256"}],"name":"sellAllAmount","outputs":[{"internalType":"uint256","name":"fill_amt","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"}],"name":"setMinSell","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"},{"internalType":"uint128","name":"maxTakeAmount","type":"uint128"}],"name":"take","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/OSM.abi
================================================
[{"inputs":[{"internalType":"address","name":"src_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"val","type":"bytes32"}],"name":"LogValue","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"bud","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"src_","type":"address"}],"name":"change","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address[]","name":"a","type":"address[]"}],"name":"diss","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"a","type":"address"}],"name":"diss","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hop","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address[]","name":"a","type":"address[]"}],"name":"kiss","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"a","type":"address"}],"name":"kiss","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"pass","outputs":[{"internalType":"bool","name":"ok","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"peek","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"peep","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"poke","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"read","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"src","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"start","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint16","name":"ts","type":"uint16"}],"name":"step","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"stopped","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"void","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"zzz","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/Pit.abi
================================================
[{"constant":false,"inputs":[{"name":"ilk","type":"bytes32"},{"name":"what","type":"bytes32"},{"name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"what","type":"bytes32"},{"name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"ilk","type":"bytes32"},{"name":"dink","type":"int256"},{"name":"dart","type":"int256"}],"name":"frob","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"Line","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"wards","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"ilks","outputs":[{"name":"spot","type":"uint256"},{"name":"line","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"ilk","type":"bytes32"},{"indexed":true,"name":"urn","type":"bytes32"},{"indexed":false,"name":"ink","type":"uint256"},{"indexed":false,"name":"art","type":"uint256"},{"indexed":false,"name":"dink","type":"int256"},{"indexed":false,"name":"dart","type":"int256"},{"indexed":false,"name":"iInk","type":"uint256"},{"indexed":false,"name":"iArt","type":"uint256"}],"name":"Frob","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"}]
================================================
FILE: pymaker/abi/Pot.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"Pie","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"chi","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"drip","outputs":[{"internalType":"uint256","name":"tmp","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dsr","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"addr","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"pie","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"rho","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vow","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/ProxyRegistry.abi
================================================
[{"inputs":[{"internalType":"address","name":"factory_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":false,"inputs":[],"name":"build","outputs":[{"internalType":"address payable","name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"build","outputs":[{"internalType":"address payable","name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"proxies","outputs":[{"internalType":"contract DSProxy","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/SaiTap.abi
================================================
[{"constant":false,"inputs":[],"name":"heal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"sin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"skr","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"vent","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"cash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"woe","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tub","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"mock","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"bid","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"joy","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"s2s","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"off","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vox","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"gap","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fog","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"sai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"param","type":"bytes32"},{"name":"val","type":"uint256"}],"name":"mold","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"fix_","type":"uint256"}],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"fix","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"bust","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"boom","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"ask","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"tub_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/SaiTop.abi
================================================
[{"constant":true,"inputs":[],"name":"sin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"skr","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"era","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"flow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tub","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cooldown_","type":"uint256"}],"name":"setCooldown","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vox","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"cooldown","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"sai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fix","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fit","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"caged","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"tub_","type":"address"},{"name":"tap_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/SaiTub.abi
================================================
[{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"sin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"skr","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"gov","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"era","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"cup","type":"bytes32"}],"name":"ink","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"rho","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"air","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"rhi","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"flow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"cap","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"}],"name":"bite","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"draw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"wad","type":"uint256"}],"name":"bid","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"cupi","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"axe","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tag","outputs":[{"name":"wad","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"off","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vox","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"gap","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"}],"name":"rap","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"wipe","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"tap_","type":"address"}],"name":"turn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"per","outputs":[{"name":"ray","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"pip_","type":"address"}],"name":"setPip","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"pie","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"fit_","type":"uint256"},{"name":"jam","type":"uint256"}],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"rum","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"sai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"param","type":"bytes32"},{"name":"val","type":"uint256"}],"name":"mold","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tax","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"drip","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"free","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"mat","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"pep","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"out","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"lock","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"}],"name":"shut","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"},{"name":"guy","type":"address"}],"name":"give","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fit","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"chi","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"vox_","type":"address"}],"name":"setVox","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"pip","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"pep_","type":"address"}],"name":"setPep","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"cup","type":"bytes32"}],"name":"lad","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"din","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"wad","type":"uint256"}],"name":"ask","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"}],"name":"safe","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"pit","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cup","type":"bytes32"}],"name":"tab","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"open","outputs":[{"name":"cup","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"cups","outputs":[{"name":"lad","type":"address"},{"name":"ink","type":"uint256"},{"name":"art","type":"uint256"},{"name":"ire","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"sai_","type":"address"},{"name":"sin_","type":"address"},{"name":"skr_","type":"address"},{"name":"gem_","type":"address"},{"name":"gov_","type":"address"},{"name":"pip_","type":"address"},{"name":"pep_","type":"address"},{"name":"vox_","type":"address"},{"name":"pit_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"lad","type":"address"},{"indexed":false,"name":"cup","type":"bytes32"}],"name":"LogNewCup","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/SaiVox.abi
================================================
[{"constant":false,"inputs":[],"name":"prod","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"era","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"how","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"par","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"tell","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"way","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"param","type":"bytes32"},{"name":"val","type":"uint256"}],"name":"mold","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"fix","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"tune","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tau","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"par_","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/SimpleMarket.abi
================================================
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogBump","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"id","type":"uint256"}],"name":"LogItemUpdate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogKill","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":false,"internalType":"uint128","name":"pay_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"buy_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogMake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"pair","type":"bytes32"},{"indexed":true,"internalType":"address","name":"maker","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"indexed":true,"internalType":"address","name":"taker","type":"address"},{"indexed":false,"internalType":"uint128","name":"take_amt","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"give_amt","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"LogTake","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"pay_amt","type":"uint256"},{"indexed":true,"internalType":"address","name":"pay_gem","type":"address"},{"indexed":false,"internalType":"uint256","name":"buy_amt","type":"uint256"},{"indexed":true,"internalType":"address","name":"buy_gem","type":"address"}],"name":"LogTrade","type":"event"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id_","type":"bytes32"}],"name":"bump","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"buy","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"cancel","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getOffer","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"contract ERC20","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"contract ERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getOwner","outputs":[{"internalType":"address","name":"owner","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"isActive","outputs":[{"internalType":"bool","name":"active","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"last_offer_id","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"uint128","name":"pay_amt","type":"uint128"},{"internalType":"uint128","name":"buy_amt","type":"uint128"}],"name":"make","outputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"}],"name":"offer","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"offers","outputs":[{"internalType":"uint256","name":"pay_amt","type":"uint256"},{"internalType":"contract ERC20","name":"pay_gem","type":"address"},{"internalType":"uint256","name":"buy_amt","type":"uint256"},{"internalType":"contract ERC20","name":"buy_gem","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint64","name":"timestamp","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"},{"internalType":"uint128","name":"maxTakeAmount","type":"uint128"}],"name":"take","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: pymaker/abi/Spotter.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"ilk","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"val","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"spot","type":"uint256"}],"name":"Poke","type":"event"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"pip_","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"contract PipLike","name":"pip","type":"address"},{"internalType":"uint256","name":"mat","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"par","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"poke","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/TokenFaucet.abi
================================================
[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"amt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"done","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"gem","type":"address"},{"internalType":"address[]","name":"addrs","type":"address[]"}],"name":"gulp","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"gem","type":"address"}],"name":"gulp","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"guy","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"gem","type":"address"},{"internalType":"uint256","name":"amt_","type":"uint256"}],"name":"setAmt","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"contract ERC20Like","name":"gem","type":"address"}],"name":"shut","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/TokenTransferProxy.abi
================================================
[{"constant":false,"inputs":[{"name":"token","type":"address"},{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"target","type":"address"}],"name":"addAuthorizedAddress","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"authorities","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"target","type":"address"}],"name":"removeAuthorizedAddress","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"authorized","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getAuthorizedAddresses","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"target","type":"address"},{"indexed":true,"name":"caller","type":"address"}],"name":"LogAuthorizedAddressAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"target","type":"address"},{"indexed":true,"name":"caller","type":"address"}],"name":"LogAuthorizedAddressRemoved","type":"event"}]
================================================
FILE: pymaker/abi/TxManager.abi
================================================
[{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tokens","type":"address[]"},{"name":"script","type":"bytes"}],"name":"execute","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}]
================================================
FILE: pymaker/abi/Vat.abi
================================================
[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg3","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"Line","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"can","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"dai","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"debt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"flux","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"int256","name":"rate","type":"int256"}],"name":"fold","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"fork","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"frob","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"gem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"grab","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"heal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"hope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"uint256","name":"Art","type":"uint256"},{"internalType":"uint256","name":"rate","type":"uint256"},{"internalType":"uint256","name":"spot","type":"uint256"},{"internalType":"uint256","name":"line","type":"uint256"},{"internalType":"uint256","name":"dust","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"init","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"move","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"nope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"sin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"int256","name":"wad","type":"int256"}],"name":"slip","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"suck","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"urns","outputs":[{"internalType":"uint256","name":"ink","type":"uint256"},{"internalType":"uint256","name":"art","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/Vow.abi
================================================
[{"inputs":[{"internalType":"address","name":"vat_","type":"address"},{"internalType":"address","name":"flapper_","type":"address"},{"internalType":"address","name":"flopper_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":true,"internalType":"address","name":"usr","type":"address"},{"indexed":true,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":true,"inputs":[],"name":"Ash","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"Sin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"bump","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dump","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"tab","type":"uint256"}],"name":"fess","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"address","name":"data","type":"address"}],"name":"file","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"flap","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"flapper","outputs":[{"internalType":"contract FlapLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"era","type":"uint256"}],"name":"flog","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"flop","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"flopper","outputs":[{"internalType":"contract FlopLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"heal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hump","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"kiss","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"sin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"sump","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"wait","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
================================================
FILE: pymaker/abi/ZRXToken.abi
================================================
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]
================================================
FILE: pymaker/abi/diff-abi.sh
================================================
#!/bin/bash
# This script is useful when updating ABIs as newer contracts are released.
vimdiff <(git show HEAD:pymaker/abi/$@ | jq '.') <(jq '.' < $@)
================================================
FILE: pymaker/approval.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from pymaker import Address, Contract
from pymaker import Transact
from pymaker.numeric import Wad
from pymaker.token import ERC20Token
from pymaker.transactional import TxManager
def directly(**kwargs):
"""Approval function: Approves the caller to access tokens directly.
This function is meant to be passed as a parameter to the `approve(...)` method
of `Tub`, `SimpleMarket`, 'EtherDelta', 'TxManager', `ZrxExchange` and possibly
others in the future.
"""
def approval_function(token: ERC20Token, spender_address: Address, spender_name: str):
address_to_check = kwargs['from_address'] if 'from_address' in kwargs else Address(token.web3.eth.defaultAccount)
if token.allowance_of(address_to_check, spender_address) < Wad(2 ** 128 - 1):
logger = logging.getLogger()
logger.info(f"Approving {spender_name} ({spender_address}) to access our {token.address} directly")
if not token.approve(spender_address).transact(**kwargs):
raise RuntimeError("Approval failed!")
return approval_function
def via_tx_manager(tx_manager: TxManager, **kwargs):
"""Approval function: Approves the caller to access tokens via the `TxManager`.
This function is meant to be passed as a parameter to the `approve(...)` method
of `Tub`, `SimpleMarket`, 'EtherDelta', 'TxManager', `ZrxExchange` and possibly
others in the future.
"""
assert(isinstance(tx_manager, TxManager))
def approval_function(token: ERC20Token, spender_address: Address, spender_name: str):
if token.allowance_of(tx_manager.address, spender_address) < Wad(2 ** 128 - 1):
logger = logging.getLogger()
logger.info(f"Approving {spender_name} ({spender_address}) to access our {token.address}"
f" via TxManager {tx_manager.address}")
if not tx_manager.execute([], [(token.approve(spender_address).invocation())]).transact(**kwargs):
raise RuntimeError("Approval failed!")
return approval_function
def hope_directly(**kwargs):
"""Approval function: Approves the caller to access tokens directly.
This function is meant to be passed as a parameter to the `approve(...)` method
of `Flipper` and `Flopper` and possibly others in the future.
"""
move_abi = [{'constant': False, 'inputs': [{'name': 'guy', 'type': 'address'}], 'name': 'hope', 'outputs': [],
'payable': False, 'stateMutability': 'nonpayable', 'type': 'function'},
{'constant': True, 'inputs': [{'name': '', 'type': 'address'}, {'name': '', 'type': 'address'}],
'name': 'can', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': False, 'stateMutability': 'view',
'type': 'function'}]
def approval_function(token: ERC20Token, spender_address: Address, spender_name: str):
address_to_check = kwargs['from_address'] if 'from_address' in kwargs else Address(
token.web3.eth.defaultAccount)
move_contract = Contract._get_contract(web3=token.web3, abi=move_abi, address=token.address)
if move_contract.functions.can(address_to_check.address, spender_address.address).call() is False:
logger = logging.getLogger()
logger.info(f"Approving {spender_name} ({spender_address}) to move our {token.address} directly")
hope = Transact(move_contract, move_contract.web3, move_contract.abi, Address(move_contract.address),
move_contract, 'hope', [spender_address.address])
if not hope.transact(**kwargs):
raise RuntimeError("Approval failed!")
return approval_function
================================================
FILE: pymaker/auctions.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018-2019 reverendus, bargst, EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from datetime import datetime
import logging
from pprint import pformat
from typing import List
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker import Contract, Address, Transact
from pymaker.dss import Dog, Vat
from pymaker.logging import LogNote
from pymaker.numeric import Wad, Rad, Ray
from pymaker.token import ERC20Token
def toBytes(string: str):
assert(isinstance(string, str))
return string.encode('utf-8').ljust(32, bytes(1))
logger = logging.getLogger()
class AuctionContract(Contract):
"""Abstract baseclass shared across all auction contracts."""
def __init__(self, web3: Web3, address: Address, abi: list):
if self.__class__ == AuctionContract:
raise NotImplemented('Abstract class; please call Clipper, Flapper, Flipper, or Flopper ctor')
assert isinstance(web3, Web3)
assert isinstance(address, Address)
assert isinstance(abi, list)
self.web3 = web3
self.address = address
self.abi = abi
self._contract = self._get_contract(web3, abi, address)
self.log_note_abi = None
self.kick_abi = None
for member in abi:
if not self.log_note_abi and member.get('name') == 'LogNote':
self.log_note_abi = member
elif not self.kick_abi and member.get('name') == 'Kick':
self.kick_abi = member
def approve(self, source: Address, approval_function):
"""Approve the auction to access our collateral, Dai, or MKR so we can participate in auctions.
For available approval functions (i.e. approval modes) see `directly` and `hope_directly`
in `pymaker.approval`.
Args:
source: Address of the contract or token relevant to the auction (for Flipper and Flopper pass Vat address,
for Flapper pass MKR token address)
approval_function: Approval function (i.e. approval mode)
"""
assert isinstance(source, Address)
assert(callable(approval_function))
approval_function(token=ERC20Token(web3=self.web3, address=source),
spender_address=self.address, spender_name=self.__class__.__name__)
def wards(self, address: Address) -> bool:
assert isinstance(address, Address)
return bool(self._contract.functions.wards(address.address).call())
def vat(self) -> Address:
"""Returns the `vat` address.
Returns:
The address of the `vat` contract.
"""
return Address(self._contract.functions.vat().call())
def get_past_lognotes(self, abi: list, from_block: int, to_block: int = None, chunk_size=20000) -> List[LogNote]:
current_block = self._contract.web3.eth.blockNumber
assert isinstance(from_block, int)
assert from_block < current_block
if to_block is None:
to_block = current_block
else:
assert isinstance(to_block, int)
assert to_block >= from_block
assert to_block <= current_block
assert chunk_size > 0
assert isinstance(abi, list)
logger.debug(f"Consumer requested auction data from block {from_block} to {to_block}")
start = from_block
end = None
chunks_queried = 0
events = []
while end is None or start <= to_block:
chunks_queried += 1
end = min(to_block, start + chunk_size)
filter_params = {
'address': self.address.address,
'fromBlock': start,
'toBlock': end
}
logger.debug(f"Querying logs from block {start} to {end} ({end-start} blocks); "
f"accumulated {len(events)} events in {chunks_queried-1} requests")
logs = self.web3.eth.getLogs(filter_params)
events.extend(list(map(lambda l: self.parse_event(l), logs)))
start += chunk_size
return list(filter(lambda l: l is not None, events))
def parse_event(self, event):
raise NotImplemented()
class DealableAuctionContract(AuctionContract):
"""Abstract baseclass shared across original auction contracts."""
class DealLog:
def __init__(self, lognote: LogNote):
# This is whoever called `deal`, which could differ from the `guy` who won the auction
self.usr = Address(lognote.usr)
self.id = Web3.toInt(lognote.arg1)
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"AuctionContract.DealLog({pformat(vars(self))})"
def __init__(self, web3: Web3, address: Address, abi: list, bids: callable):
if self.__class__ == DealableAuctionContract:
raise NotImplemented('Abstract class; please call Flipper, Flapper, or Flopper ctor')
super(DealableAuctionContract, self).__init__(web3, address, abi)
self._bids = bids
def active_auctions(self) -> list:
active_auctions = []
auction_count = self.kicks()+1
for index in range(1, auction_count):
bid = self._bids(index)
if bid.guy != Address("0x0000000000000000000000000000000000000000"):
now = datetime.now().timestamp()
if (bid.tic == 0 or now < bid.tic) and now < bid.end:
active_auctions.append(bid)
index += 1
return active_auctions
def beg(self) -> Wad:
"""Returns the percentage minimum bid increase.
Returns:
The percentage minimum bid increase.
"""
return Wad(self._contract.functions.beg().call())
def ttl(self) -> int:
"""Returns the bid lifetime.
Returns:
The bid lifetime (in seconds).
"""
return int(self._contract.functions.ttl().call())
def tau(self) -> int:
"""Returns the total auction length.
Returns:
The total auction length (in seconds).
"""
return int(self._contract.functions.tau().call())
def kicks(self) -> int:
"""Returns the number of auctions started so far.
Returns:
The number of auctions started so far.
"""
return int(self._contract.functions.kicks().call())
def deal(self, id: int) -> Transact:
assert(isinstance(id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'deal', [id])
def tick(self, id: int) -> Transact:
"""Resurrect an auction which expired without any bids."""
assert(isinstance(id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'tick', [id])
class Flipper(DealableAuctionContract):
"""A client for the `Flipper` contract, used to interact with collateral auctions.
You can find the source code of the `Flipper` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Flipper` contract.
Event signatures:
0x65fae35e: (deployment-related)
0x9c52a7f1: (deployment-related)
0x29ae8114: file
0xc84ce3a1172f0dec3173f04caaa6005151a4bfe40d4c9f3ea28dba5f719b2a7a: kick
0x4b43ed12: tend
0x5ff3a382: dent
0xc959c42b: deal
"""
abi = Contract._load_abi(__name__, 'abi/Flipper.abi')
bin = Contract._load_bin(__name__, 'abi/Flipper.bin')
class Bid:
def __init__(self, id: int, bid: Rad, lot: Wad, guy: Address, tic: int, end: int,
usr: Address, gal: Address, tab: Rad):
assert(isinstance(id, int))
assert(isinstance(bid, Rad))
assert(isinstance(lot, Wad))
assert(isinstance(guy, Address))
assert(isinstance(tic, int))
assert(isinstance(end, int))
assert(isinstance(usr, Address))
assert(isinstance(gal, Address))
assert(isinstance(tab, Rad))
self.id = id
self.bid = bid
self.lot = lot
self.guy = guy
self.tic = tic
self.end = end
self.usr = usr
self.gal = gal
self.tab = tab
def __repr__(self):
return f"Flipper.Bid({pformat(vars(self))})"
class KickLog:
def __init__(self, log):
args = log['args']
self.id = args['id']
self.lot = Wad(args['lot'])
self.bid = Rad(args['bid'])
self.tab = Rad(args['tab'])
self.usr = Address(args['usr'])
self.gal = Address(args['gal'])
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
def __repr__(self):
return f"Flipper.KickLog({pformat(vars(self))})"
class TendLog:
def __init__(self, lognote: LogNote):
self.guy = Address(lognote.usr)
self.id = Web3.toInt(lognote.arg1)
self.lot = Wad(Web3.toInt(lognote.arg2))
self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2)))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"Flipper.TendLog({pformat(vars(self))})"
class DentLog:
def __init__(self, lognote: LogNote):
self.guy = Address(lognote.usr)
self.id = Web3.toInt(lognote.arg1)
self.lot = Wad(Web3.toInt(lognote.arg2))
self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2)))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"Flipper.DentLog({pformat(vars(self))})"
def __init__(self, web3: Web3, address: Address):
super(Flipper, self).__init__(web3, address, Flipper.abi, self.bids)
def bids(self, id: int) -> Bid:
"""Returns the auction details.
Args:
id: Auction identifier.
Returns:
The auction details.
"""
assert(isinstance(id, int))
array = self._contract.functions.bids(id).call()
return Flipper.Bid(id=id,
bid=Rad(array[0]),
lot=Wad(array[1]),
guy=Address(array[2]),
tic=int(array[3]),
end=int(array[4]),
usr=Address(array[5]),
gal=Address(array[6]),
tab=Rad(array[7]))
def tend(self, id: int, lot: Wad, bid: Rad) -> Transact:
assert(isinstance(id, int))
assert(isinstance(lot, Wad))
assert(isinstance(bid, Rad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'tend', [id, lot.value, bid.value])
def dent(self, id: int, lot: Wad, bid: Rad) -> Transact:
assert(isinstance(id, int))
assert(isinstance(lot, Wad))
assert(isinstance(bid, Rad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'dent', [id, lot.value, bid.value])
def past_logs(self, from_block: int, to_block: int = None, chunk_size=20000):
logs = super().get_past_lognotes(Flipper.abi, from_block, to_block, chunk_size)
history = []
for log in logs:
if log is None:
continue
elif isinstance(log, Flipper.KickLog):
history.append(log)
elif log.sig == '0x4b43ed12':
history.append(Flipper.TendLog(log))
elif log.sig == '0x5ff3a382':
history.append(Flipper.DentLog(log))
elif log.sig == '0xc959c42b':
history.append(DealableAuctionContract.DealLog(log))
return history
def parse_event(self, event):
signature = Web3.toHex(event['topics'][0])
codec = ABICodec(default_registry)
if signature == "0xc84ce3a1172f0dec3173f04caaa6005151a4bfe40d4c9f3ea28dba5f719b2a7a":
event_data = get_event_data(codec, self.kick_abi, event)
return Flipper.KickLog(event_data)
else:
event_data = get_event_data(codec, self.log_note_abi, event)
return LogNote(event_data)
def __repr__(self):
return f"Flipper('{self.address}')"
class Flapper(DealableAuctionContract):
"""A client for the `Flapper` contract, used to interact with surplus auctions.
You can find the source code of the `Flapper` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Flapper` contract.
Event signatures:
0x65fae35e: (deployment-related)
0x9c52a7f1: (deployment-related)
0xe6dde59cbc017becba89714a037778d234a84ce7f0a137487142a007e580d609: kick
0x29ae8114: file
0x4b43ed12: tend
0xc959c42b: deal
"""
abi = Contract._load_abi(__name__, 'abi/Flapper.abi')
bin = Contract._load_bin(__name__, 'abi/Flapper.bin')
class Bid:
def __init__(self, id: int, bid: Wad, lot: Rad, guy: Address, tic: int, end: int):
assert(isinstance(id, int))
assert(isinstance(bid, Wad)) # MKR
assert(isinstance(lot, Rad)) # DAI
assert(isinstance(guy, Address))
assert(isinstance(tic, int))
assert(isinstance(end, int))
self.id = id
self.bid = bid
self.lot = lot
self.guy = guy
self.tic = tic
self.end = end
def __repr__(self):
return f"Flapper.Bid({pformat(vars(self))})"
class KickLog:
def __init__(self, log):
args = log['args']
self.id = args['id']
self.lot = Rad(args['lot'])
self.bid = Wad(args['bid'])
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
def __repr__(self):
return f"Flapper.KickLog({pformat(vars(self))})"
class TendLog:
def __init__(self, lognote: LogNote):
self.guy = Address(lognote.usr)
self.id = Web3.toInt(lognote.arg1)
self.lot = Rad(Web3.toInt(lognote.arg2))
self.bid = Wad(Web3.toInt(lognote.get_bytes_at_index(2)))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"Flapper.TendLog({pformat(vars(self))})"
def __init__(self, web3: Web3, address: Address):
super(Flapper, self).__init__(web3, address, Flapper.abi, self.bids)
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def bids(self, id: int) -> Bid:
"""Returns the auction details.
Args:
id: Auction identifier.
Returns:
The auction details.
"""
assert(isinstance(id, int))
array = self._contract.functions.bids(id).call()
return Flapper.Bid(id=id,
bid=Wad(array[0]),
lot=Rad(array[1]),
guy=Address(array[2]),
tic=int(array[3]),
end=int(array[4]))
def tend(self, id: int, lot: Rad, bid: Wad) -> Transact:
assert(isinstance(id, int))
assert(isinstance(lot, Rad))
assert(isinstance(bid, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'tend', [id, lot.value, bid.value])
def yank(self, id: int) -> Transact:
"""While `cage`d, refund current bid to the bidder"""
assert (isinstance(id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'yank', [id])
def past_logs(self, from_block: int, to_block: int = None, chunk_size=20000):
logs = super().get_past_lognotes(Flapper.abi, from_block, to_block, chunk_size)
history = []
for log in logs:
if log is None:
continue
elif isinstance(log, Flapper.KickLog):
history.append(log)
elif log.sig == '0x4b43ed12':
history.append(Flapper.TendLog(log))
elif log.sig == '0xc959c42b':
history.append(DealableAuctionContract.DealLog(log))
return history
def parse_event(self, event):
signature = Web3.toHex(event['topics'][0])
codec = ABICodec(default_registry)
if signature == "0xe6dde59cbc017becba89714a037778d234a84ce7f0a137487142a007e580d609":
event_data = get_event_data(codec, self.kick_abi, event)
return Flapper.KickLog(event_data)
else:
event_data = get_event_data(codec, self.log_note_abi, event)
return LogNote(event_data)
def __repr__(self):
return f"Flapper('{self.address}')"
class Flopper(DealableAuctionContract):
"""A client for the `Flopper` contract, used to interact with debt auctions.
You can find the source code of the `Flopper` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Flopper` contract.
Event signatures:
0x65fae35e: (deployment-related)
0x9c52a7f1: (deployment-related)
0x29ae8114: file
0x7e8881001566f9f89aedb9c5dc3d856a2b81e5235a8196413ed484be91cc0df6: kick
0x5ff3a382: dent
0xc959c42b: deal
"""
abi = Contract._load_abi(__name__, 'abi/Flopper.abi')
bin = Contract._load_bin(__name__, 'abi/Flopper.bin')
class Bid:
def __init__(self, id: int, bid: Rad, lot: Wad, guy: Address, tic: int, end: int):
assert(isinstance(id, int))
assert(isinstance(bid, Rad))
assert(isinstance(lot, Wad))
assert(isinstance(guy, Address))
assert(isinstance(tic, int))
assert(isinstance(end, int))
self.id = id
self.bid = bid
self.lot = lot
self.guy = guy
self.tic = tic
self.end = end
def __repr__(self):
return f"Flopper.Bid({pformat(vars(self))})"
class KickLog:
def __init__(self, log):
args = log['args']
self.id = args['id']
self.lot = Wad(args['lot'])
self.bid = Rad(args['bid'])
self.gal = Address(args['gal'])
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
def __repr__(self):
return f"Flopper.KickLog({pformat(vars(self))})"
class DentLog:
def __init__(self, lognote: LogNote):
self.guy = Address(lognote.usr)
self.id = Web3.toInt(lognote.arg1)
self.lot = Wad(Web3.toInt(lognote.arg2))
self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2)))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"Flopper.DentLog({pformat(vars(self))})"
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
super(Flopper, self).__init__(web3, address, Flopper.abi, self.bids)
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def pad(self) -> Wad:
"""Returns the lot increase applied after an auction has been `tick`ed."""
return Wad(self._contract.functions.pad().call())
def bids(self, id: int) -> Bid:
"""Returns the auction details.
Args:
id: Auction identifier.
Returns:
The auction details.
"""
assert(isinstance(id, int))
array = self._contract.functions.bids(id).call()
return Flopper.Bid(id=id,
bid=Rad(array[0]),
lot=Wad(array[1]),
guy=Address(array[2]),
tic=int(array[3]),
end=int(array[4]))
def dent(self, id: int, lot: Wad, bid: Rad) -> Transact:
assert(isinstance(id, int))
assert(isinstance(lot, Wad))
assert(isinstance(bid, Rad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'dent', [id, lot.value, bid.value])
def yank(self, id: int) -> Transact:
"""While `cage`d, refund current bid to the bidder"""
assert (isinstance(id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'yank', [id])
def past_logs(self, from_block: int, to_block: int = None, chunk_size=20000):
logs = super().get_past_lognotes(Flopper.abi, from_block, to_block, chunk_size)
history = []
for log in logs:
if log is None:
continue
elif isinstance(log, Flopper.KickLog):
history.append(log)
elif log.sig == '0x5ff3a382':
history.append(Flopper.DentLog(log))
elif log.sig == '0xc959c42b':
history.append(DealableAuctionContract.DealLog(log))
return history
def parse_event(self, event):
signature = Web3.toHex(event['topics'][0])
codec = ABICodec(default_registry)
if signature == "0x7e8881001566f9f89aedb9c5dc3d856a2b81e5235a8196413ed484be91cc0df6":
event_data = get_event_data(codec, self.kick_abi, event)
return Flopper.KickLog(event_data)
else:
event_data = get_event_data(codec, self.log_note_abi, event)
return LogNote(event_data)
def __repr__(self):
return f"Flopper('{self.address}')"
class Clipper(AuctionContract):
"""A client for the `Clipper` contract, used to interact with collateral auctions.
You can find the source code of the `Clipper` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Clipper` contract.
"""
abi = Contract._load_abi(__name__, 'abi/Clipper.abi')
bin = Contract._load_bin(__name__, 'abi/Clipper.bin')
class KickLog:
def __init__(self, log):
args = log['args']
self.id = args['id']
self.top = Ray(args['top']) # starting price
self.tab = Rad(args['tab']) # debt
self.lot = Wad(args['lot']) # collateral
self.usr = Address(args['usr']) # liquidated vault
self.kpr = Address(args['kpr']) # keeper who barked
self.coin = Rad(args['coin']) # total kick incentive (tip + tab*chip)
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
def __repr__(self):
return f"Clipper.KickLog({pformat(vars(self))})"
class TakeLog:
def __init__(self, log, sender):
args = log['args']
self.id = args['id']
self.max = Ray(args['max']) # Max bid price specified
self.price = Ray(args['price']) # Calculated bid price
self.owe = Rad(args['owe']) # Dai needed to satisfy the calculated bid price
self.tab = Rad(args['tab']) # Remaining debt
self.lot = Wad(args['lot']) # Remaining lot
self.usr = Address(args['usr']) # Liquidated vault
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
self.sender = sender
def __repr__(self):
return f"Clipper.TakeLog({pformat(vars(self))})"
class RedoLog(KickLog):
# Same fields as KickLog
def __repr__(self):
return f"Clipper.RedoLog({pformat(vars(self))})"
class Sale:
def __init__(self, id: int, pos: int, tab: Rad, lot: Wad, usr: Address, tic: int, top: Ray):
assert(isinstance(id, int))
assert(isinstance(pos, int))
assert(isinstance(tab, Rad))
assert(isinstance(lot, Wad))
assert(isinstance(usr, Address))
assert(isinstance(tic, int))
assert(isinstance(top, Ray))
self.id = id # auction identifier
self.pos = pos # active index
self.tab = tab # dai to raise
self.lot = lot # collateral to sell
self.usr = usr # liquidated urn address
self.tic = tic # auction start time
self.top = top # starting price
def __repr__(self):
return f"Clipper.Sale({pformat(vars(self))})"
def __init__(self, web3: Web3, address: Address):
super(Clipper, self).__init__(web3, address, Clipper.abi)
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
# Albeit more elegant, this is inconsistent with AuctionContract.vat(), a method call
self.calc = Address(self._contract.functions.calc().call())
self.dog = Dog(web3, Address(self._contract.functions.dog().call()))
self.vat = Vat(web3, Address(self._contract.functions.vat().call()))
self.take_abi = None
self.redo_abi = None
for member in self.abi:
if not self.take_abi and member.get('name') == 'Take':
self.take_abi = member
if not self.redo_abi and member.get('name') == 'Redo':
self.redo_abi = member
def active_auctions(self) -> list:
active_auctions = []
for index in range(1, self.kicks()+1):
sale = self.sales(index)
if sale.usr != Address.zero():
active_auctions.append(sale)
index += 1
return active_auctions
def ilk_name(self) -> str:
ilk = self._contract.functions.ilk().call()
return Web3.toText(ilk.strip(bytes(1)))
def buf(self) -> Ray:
"""Multiplicative factor to increase starting price"""
return Ray(self._contract.functions.buf().call())
def tail(self) -> int:
"""Time elapsed before auction reset"""
return int(self._contract.functions.tail().call())
def cusp(self) -> Ray:
"""Percentage drop before auction reset"""
return Ray(self._contract.functions.cusp().call())
def chip(self) -> Wad:
"""Percentage of tab to suck from vow to incentivize keepers"""
return Wad(self._contract.functions.chip().call())
def tip(self) -> Rad:
"""Flat fee to suck from vow to incentivize keepers"""
return Rad(self._contract.functions.tip().call())
def chost(self) -> Rad:
"""Ilk dust times the ilk chop"""
return Rad(self._contract.functions.chost().call())
def kicks(self) -> int:
"""Number of auctions started so far."""
return int(self._contract.functions.kicks().call())
def active_count(self) -> int:
"""Number of active and redoable auctions."""
return int(self._contract.functions.count().call())
def status(self, id: int) -> (bool, Ray, Wad, Rad):
"""Indicates current state of the auction
Args:
id: Auction identifier.
"""
assert isinstance(id, int)
(needs_redo, price, lot, tab) = self._contract.functions.getStatus(id).call()
logging.debug(f"Auction {id} {'needs redo ' if needs_redo else ''}with price={float(Ray(price))} "
f"lot={float(Wad(lot))} tab={float(Rad(tab))}")
return needs_redo, Ray(price), Wad(lot), Rad(tab)
def sales(self, id: int) -> Sale:
"""Returns the auction details.
Args:
id: Auction identifier.
Returns:
The auction details.
"""
assert(isinstance(id, int))
array = self._contract.functions.sales(id).call()
return Clipper.Sale(id=id,
pos=int(array[0]),
tab=Rad(array[1]),
lot=Wad(array[2]),
usr=Address(array[3]),
tic=int(array[4]),
top=Ray(array[5]))
def validate_take(self, id: int, amt: Wad, max: Ray, our_address: Address = None):
"""Raise assertion if collateral cannot be purchased from an auction as desired"""
assert isinstance(id, int)
assert isinstance(amt, Wad)
assert isinstance(max, Ray)
if our_address:
assert isinstance(our_address, Address)
else:
our_address = Address(self.web3.eth.defaultAccount)
(done, price, lot, tab) = self.status(id)
assert not done
assert max >= price
slice: Wad = min(lot, amt) # Purchase as much as possible, up to amt
owe: Rad = Rad(slice) * Rad(price) # DAI needed to buy a slice of this sale
chost = self.chost()
if Rad(owe) > tab:
owe = Rad(tab)
slice = Wad(owe / Rad(price))
elif owe < tab and slice < lot:
if (tab - owe) < chost:
assert tab > chost
owe = tab - chost
slice = Wad(owe / Rad(price))
tab: Rad = tab - owe
lot: Wad = lot - slice
assert self.vat.dai(our_address) >= owe
logger.debug(f"Validated clip.take which will leave tab={float(tab)} and lot={float(lot)}")
def take(self, id: int, amt: Wad, max: Ray, who: Address = None, data=b'') -> Transact:
"""Buy amount of collateral from auction indexed by id.
Args:
id: Auction id
amt: Upper limit on amount of collateral to buy
max: Maximum acceptable price (DAI / collateral)
who: Receiver of collateral and external call address
data: Data to pass in external call; if length 0, no call is done
"""
assert isinstance(id, int)
assert isinstance(amt, Wad)
assert isinstance(max, Ray)
if who:
assert isinstance(who, Address)
else:
who = Address(self.web3.eth.defaultAccount)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'take',
[id, amt.value, max.value, who.address, data])
def redo(self, id: int, kpr: Address = None) -> Transact:
"""Restart an auction which ended without liquidating all collateral.
id: Auction id
kpr: Keeper that called dog.bark()
"""
assert isinstance(id, int)
assert isinstance(kpr, Address) or kpr is None
if kpr:
assert isinstance(kpr, Address)
else:
kpr = Address(self.web3.eth.defaultAccount)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'redo', [id, kpr.address])
def upchost(self):
"""Update the the cached dust*chop value following a governance change"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'upchost', [])
def past_logs(self, from_block: int, to_block: int = None, chunk_size=20000):
logs = super().get_past_lognotes(Clipper.abi, from_block, to_block, chunk_size)
history = []
for log in logs:
if log is None:
continue
elif isinstance(log, Clipper.KickLog) \
or isinstance(log, Clipper.TakeLog) \
or isinstance(log, Clipper.RedoLog):
history.append(log)
else:
logger.debug(f"Found log with signature {log.sig}")
return history
def parse_event(self, event):
signature = Web3.toHex(event['topics'][0])
codec = ABICodec(default_registry)
if signature == "0x7c5bfdc0a5e8192f6cd4972f382cec69116862fb62e6abff8003874c58e064b8":
event_data = get_event_data(codec, self.kick_abi, event)
return Clipper.KickLog(event_data)
elif signature == "0x05e309fd6ce72f2ab888a20056bb4210df08daed86f21f95053deb19964d86b1":
event_data = get_event_data(codec, self.take_abi, event)
self._get_sender_for_eventlog(event_data)
return Clipper.TakeLog(event_data, self._get_sender_for_eventlog(event_data))
elif signature == "0x275de7ecdd375b5e8049319f8b350686131c219dd4dc450a08e9cf83b03c865f":
event_data = get_event_data(codec, self.redo_abi, event)
return Clipper.RedoLog(event_data)
else:
logger.debug(f"Found event signature {signature}")
def _get_sender_for_eventlog(self, event_data) -> Address:
tx_hash = event_data['transactionHash'].hex()
receipt = self.web3.eth.getTransactionReceipt(tx_hash)
return Address(receipt['from'])
def __repr__(self):
return f"Clipper('{self.address}')"
================================================
FILE: pymaker/auth.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.util import int_to_bytes32
class DSGuard(Contract):
"""A client for the `DSGuard` contract.
You can find the source code of the `DSGuard` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSGuard` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSGuard.abi')
bin = Contract._load_bin(__name__, 'abi/DSGuard.bin')
ANY = int_to_bytes32(2 ** 256 - 1)
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
return DSGuard(web3=web3, address=Contract._deploy(web3, DSGuard.abi, DSGuard.bin, []))
def permit(self, src, dst, sig: bytes) -> Transact:
"""Grant access to a function call.
Args:
src: Address of the caller, or `ANY`.
dst: Address of the called contract, or `ANY`.
sig: Signature of the called function, or `ANY`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(src, Address) or isinstance(src, bytes))
assert(isinstance(dst, Address) or isinstance(dst, bytes))
assert(isinstance(sig, bytes) and len(sig) in (4, 32))
if isinstance(src, Address) and isinstance(dst, Address):
method = 'permit(address,address,bytes32)'
src = src.address
dst = dst.address
else:
method = 'permit(bytes32,bytes32,bytes32)'
return Transact(self, self.web3, self.abi, self.address, self._contract, method, [src, dst, sig])
def __repr__(self):
return f"DSGuard('{self.address}')"
# TODO: Complete implementation and unit test
class DSAuth(Contract):
abi = Contract._load_abi(__name__, 'abi/DSAuth.abi')
bin = Contract._load_bin(__name__, 'abi/DSAuth.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
return DSAuth(web3=web3, address=Contract._deploy(web3, DSAuth.abi, DSAuth.bin, []))
def get_owner(self) -> Address:
return Address(self._contract.functions.owner().call())
def set_owner(self, owner: Address) -> Transact:
assert isinstance(owner, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract,
"setOwner", [owner.address])
def set_authority(self, ds_authority: Address):
assert isinstance(ds_authority, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract,
"setAuthority", [ds_authority.address])
================================================
FILE: pymaker/cdpmanager.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2020 ith-harvey
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.dss import Ilk, Urn, Vat
from pymaker.numeric import Wad
class CdpManager(Contract):
"""A client for the `DSCdpManger` contract, which is a wrapper around the cdp system, for easier use.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DssCdpManager.abi')
bin = Contract._load_bin(__name__, 'abi/DssCdpManager.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
self.vat = Vat(self.web3, Address(self._contract.functions.vat().call()))
def open(self, ilk: Ilk, address: Address) -> Transact:
assert isinstance(ilk, Ilk)
assert isinstance(address, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'open',
[ilk.toBytes(), address.address])
def urn(self, cdpid: int) -> Urn:
'''Returns Urn for respective CDP ID'''
assert isinstance(cdpid, int)
urn_address = Address(self._contract.functions.urns(cdpid).call())
ilk = self.ilk(cdpid)
urn = self.vat.urn(ilk, Address(urn_address))
return urn
def owns(self, cdpid: int) -> Address:
'''Returns owner Address of respective CDP ID'''
assert isinstance(cdpid, int)
owner = Address(self._contract.functions.owns(cdpid).call())
return owner
def ilk(self, cdpid: int) -> Ilk:
'''Returns Ilk for respective CDP ID'''
assert isinstance(cdpid, int)
ilk = Ilk.fromBytes(self._contract.functions.ilks(cdpid).call())
return ilk
def first(self, address: Address) -> int:
'''Returns first CDP Id created by owner address'''
assert isinstance(address, Address)
cdpid = int(self._contract.functions.first(address.address).call())
return cdpid
def last(self, address: Address) -> int:
'''Returns last CDP Id created by owner address'''
assert isinstance(address, Address)
cdpid = self._contract.functions.last(address.address).call()
return int(cdpid)
def count(self, address: Address) -> int:
'''Returns number of CDP's created using the DS-Cdp-Manager contract specifically'''
assert isinstance(address, Address)
count = int(self._contract.functions.count(address.address).call())
return count
def __repr__(self):
return f"CdpManager('{self.address}')"
================================================
FILE: pymaker/collateral.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019-2021 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from pymaker import Address, Contract
from pymaker.approval import directly, hope_directly
from pymaker.auctions import AuctionContract, Clipper, Flipper
from pymaker.ilk import Ilk
from pymaker.gas import DefaultGasPrice
from pymaker.join import GemJoin
from pymaker.token import DSToken, ERC20Token
logger = logging.getLogger()
class Collateral:
"""The `Collateral` object wraps accounting information in the Ilk with token-wide artifacts shared across
multiple collateral types for the same token. For example, ETH-A and ETH-B are represented by different Ilks,
but will share the same gem (WETH token), GemJoin instance, and Flipper contract.
"""
def __init__(self, ilk: Ilk, gem: ERC20Token, adapter: GemJoin, auction: AuctionContract, pip, vat: Contract):
assert isinstance(ilk, Ilk)
assert isinstance(gem, ERC20Token)
assert isinstance(adapter, GemJoin)
assert isinstance(auction, AuctionContract)
assert isinstance(vat, Contract)
self.ilk = ilk
self.gem = gem
self.adapter = adapter
if isinstance(auction, Flipper):
self.flipper = auction
self.clipper = None
elif isinstance(auction, Clipper):
self.flipper = None
self.clipper = auction
# Points to `median` for official deployments, `DSValue` for testing purposes.
# Users generally have no need to interact with the pip.
self.pip = pip
self.vat = vat
def approve(self, usr: Address, **kwargs):
"""
Allows the user to move this collateral into and out of their CDP.
Args
usr: User making transactions with this collateral
"""
gas_price = kwargs['gas_price'] if 'gas_price' in kwargs else DefaultGasPrice()
self.adapter.approve(hope_directly(from_address=usr, gas_price=gas_price), self.vat.address)
self.adapter.approve_token(directly(from_address=usr, gas_price=gas_price))
================================================
FILE: pymaker/deployment.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus, bargst
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import json
import os
import re
from typing import Dict, List, Optional
import pkg_resources
from pymaker.auctions import Clipper, Flapper, Flipper, Flopper
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly, hope_directly
from pymaker.auth import DSGuard
from pymaker.etherdelta import EtherDelta
from pymaker.collateral import Collateral
from pymaker.dss import Cat, Dog, Jug, Pot, Spotter, TokenFaucet, Vat, Vow
from pymaker.join import DaiJoin, GemJoin, GemJoin5
from pymaker.proxy import ProxyRegistry, DssProxyActionsDsr
from pymaker.feed import DSValue
from pymaker.gas import DefaultGasPrice
from pymaker.governance import DSPause, DSChief
from pymaker.numeric import Wad, Ray
from pymaker.oracles import OSM
from pymaker.sai import Tub, Tap, Top, Vox
from pymaker.shutdown import ShutdownModule, End
from pymaker.token import DSToken, DSEthToken
from pymaker.vault import DSVault
from pymaker.cdpmanager import CdpManager
from pymaker.dsrmanager import DsrManager
def deploy_contract(web3: Web3, contract_name: str, args: Optional[list] = None) -> Address:
"""Deploys a new contract.
Args:
web3: An instance of `Web` from `web3.py`.
contract_name: Name of the contract, used to find the `abi` and `bin` files.
args: Optional list of contract constructor parameters.
Returns:
Ethereum address of the newly deployed contract, as a :py:class:`pymaker.Address` instance.
"""
assert(isinstance(web3, Web3))
assert(isinstance(contract_name, str))
assert(isinstance(args, list) or (args is None))
abi = json.loads(pkg_resources.resource_string('pymaker.deployment', f'abi/{contract_name}.abi'))
bytecode = str(pkg_resources.resource_string('pymaker.deployment', f'abi/{contract_name}.bin'), 'utf-8')
if args is not None:
tx_hash = web3.eth.contract(abi=abi, bytecode=bytecode).constructor(*args).transact()
else:
tx_hash = web3.eth.contract(abi=abi, bytecode=bytecode).constructor().transact()
receipt = web3.eth.getTransactionReceipt(tx_hash)
return Address(receipt['contractAddress'])
class Deployment:
"""Represents a test deployment of the Maker smart contract ecosystem for single collateral Dai (SCD).
Creating an instance of this class creates a testrpc web3 provider with the entire set
of Maker smart contracts deployed to it. It is used in unit tests of PyMaker, and also in
unit tests for individual keepers.
"""
def __init__(self):
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = web3.eth.accounts[0]
our_address = Address(web3.eth.defaultAccount)
sai = DSToken.deploy(web3, 'DAI')
sin = DSToken.deploy(web3, 'SIN')
skr = DSToken.deploy(web3, 'PETH')
gem = DSToken.deploy(web3, 'WETH')
gov = DSToken.deploy(web3, 'MKR')
pip = DSValue.deploy(web3)
pep = DSValue.deploy(web3)
pit = DSVault.deploy(web3)
vox = Vox.deploy(web3, per=Ray.from_number(1))
tub = Tub.deploy(web3, sai=sai.address, sin=sin.address, skr=skr.address, gem=gem.address, gov=gov.address,
pip=pip.address, pep=pep.address, vox=vox.address, pit=pit.address)
tap = Tap.deploy(web3, tub.address)
top = Top.deploy(web3, tub.address, tap.address)
tub._contract.functions.turn(tap.address.address).transact()
etherdelta = EtherDelta.deploy(web3,
admin=Address('0x1111100000999998888877777666665555544444'),
fee_account=Address('0x8888877777666665555544444111110000099999'),
account_levels_addr=Address('0x0000000000000000000000000000000000000000'),
fee_make=Wad.from_number(0.01),
fee_take=Wad.from_number(0.02),
fee_rebate=Wad.from_number(0.03))
# set permissions
dad = DSGuard.deploy(web3)
dad.permit(DSGuard.ANY, DSGuard.ANY, DSGuard.ANY).transact()
tub.set_authority(dad.address).transact()
for auth in [sai, sin, skr, gem, gov, pit, tap, top]:
auth.set_authority(dad.address).transact()
# approve
tub.approve(directly())
tap.approve(directly())
# mint some GEMs
gem.mint(Wad.from_number(1000000)).transact()
self.snapshot_id = web3.manager.request_blocking("evm_snapshot", [])
self.web3 = web3
self.our_address = our_address
self.sai = sai
self.sin = sin
self.skr = skr
self.gem = gem
self.gov = gov
self.vox = vox
self.tub = tub
self.tap = tap
self.top = top
self.etherdelta = etherdelta
def reset(self):
"""Rollbacks all changes made since the initial deployment."""
self.web3.manager.request_blocking("evm_revert", [self.snapshot_id])
self.snapshot_id = self.web3.manager.request_blocking("evm_snapshot", [])
def time_travel_by(self, seconds: int):
assert(isinstance(seconds, int))
self.web3.manager.request_blocking("evm_increaseTime", [seconds])
class DssDeployment:
"""Represents a Dai Stablecoin System deployment for multi-collateral Dai (MCD).
Static method `from_json()` should be used to instantiate all the objet of
a deployment from a json description of all the system addresses.
"""
NETWORKS = {
"1": "mainnet",
"42": "kovan"
}
class Config:
def __init__(self, pause: DSPause, vat: Vat, vow: Vow, jug: Jug, cat: Cat, dog: Dog, flapper: Flapper,
flopper: Flopper, pot: Pot, dai: DSToken, dai_join: DaiJoin, mkr: DSToken,
spotter: Spotter, ds_chief: DSChief, esm: ShutdownModule, end: End,
proxy_registry: ProxyRegistry, dss_proxy_actions: DssProxyActionsDsr, cdp_manager: CdpManager,
dsr_manager: DsrManager, faucet: TokenFaucet, collaterals: Optional[Dict[str, Collateral]] = None):
self.pause = pause
self.vat = vat
self.vow = vow
self.jug = jug
self.cat = cat
self.dog = dog
self.flapper = flapper
self.flopper = flopper
self.pot = pot
self.dai = dai
self.dai_join = dai_join
self.mkr = mkr
self.spotter = spotter
self.ds_chief = ds_chief
self.esm = esm
self.end = end
self.proxy_registry = proxy_registry
self.dss_proxy_actions = dss_proxy_actions
self.cdp_manager = cdp_manager
self.dsr_manager = dsr_manager
self.faucet = faucet
self.collaterals = collaterals or {}
@staticmethod
def from_json(web3: Web3, conf: str):
def address_in_configs(key: str, conf: str) -> bool:
if key not in conf:
return False
elif not conf[key]:
return False
elif conf[key] == "0x0000000000000000000000000000000000000000":
return False
else:
return True
conf = json.loads(conf)
pause = DSPause(web3, Address(conf['MCD_PAUSE']))
vat = Vat(web3, Address(conf['MCD_VAT']))
vow = Vow(web3, Address(conf['MCD_VOW']))
jug = Jug(web3, Address(conf['MCD_JUG']))
cat = Cat(web3, Address(conf['MCD_CAT'])) if address_in_configs('MCD_CAT', conf) else None
dog = Dog(web3, Address(conf['MCD_DOG'])) if address_in_configs('MCD_DOG', conf) else None
dai = DSToken(web3, Address(conf['MCD_DAI']))
dai_adapter = DaiJoin(web3, Address(conf['MCD_JOIN_DAI']))
flapper = Flapper(web3, Address(conf['MCD_FLAP']))
flopper = Flopper(web3, Address(conf['MCD_FLOP']))
pot = Pot(web3, Address(conf['MCD_POT']))
mkr = DSToken(web3, Address(conf['MCD_GOV']))
spotter = Spotter(web3, Address(conf['MCD_SPOT']))
ds_chief = DSChief(web3, Address(conf['MCD_ADM']))
esm = ShutdownModule(web3, Address(conf['MCD_ESM']))
end = End(web3, Address(conf['MCD_END']))
proxy_registry = ProxyRegistry(web3, Address(conf['PROXY_REGISTRY']))
dss_proxy_actions = DssProxyActionsDsr(web3, Address(conf['PROXY_ACTIONS_DSR']))
cdp_manager = CdpManager(web3, Address(conf['CDP_MANAGER']))
dsr_manager = DsrManager(web3, Address(conf['DSR_MANAGER']))
faucet = TokenFaucet(web3, Address(conf['FAUCET'])) if address_in_configs('FAUCET', conf) else None
collaterals = {}
for name in DssDeployment.Config._infer_collaterals_from_addresses(conf.keys()):
ilk = vat.ilk(name[0].replace('_', '-'))
if name[1] == "ETH":
gem = DSEthToken(web3, Address(conf[name[1]]))
else:
gem = DSToken(web3, Address(conf[name[1]]))
if name[1] in ['USDC', 'WBTC', 'TUSD', 'USDT', 'GUSD', 'RENBTC']:
adapter = GemJoin5(web3, Address(conf[f'MCD_JOIN_{name[0]}']))
else:
adapter = GemJoin(web3, Address(conf[f'MCD_JOIN_{name[0]}']))
# PIP contract may be a DSValue, OSM, or bogus address.
pip_name = f'PIP_{name[1]}'
pip_address = Address(conf[pip_name]) if pip_name in conf and conf[pip_name] else None
val_name = f'VAL_{name[1]}'
val_address = Address(conf[val_name]) if val_name in conf and conf[val_name] else None
if pip_address: # Configure OSM as price source
pip = OSM(web3, pip_address)
elif val_address: # Configure price using DSValue
pip = DSValue(web3, val_address)
else:
pip = None
auction = None
if f'MCD_FLIP_{name[0]}' in conf:
auction = Flipper(web3, Address(conf[f'MCD_FLIP_{name[0]}']))
elif f'MCD_CLIP_{name[0]}' in conf:
auction = Clipper(web3, Address(conf[f'MCD_CLIP_{name[0]}']))
collateral = Collateral(ilk=ilk, gem=gem, adapter=adapter, auction=auction, pip=pip, vat=vat)
collaterals[ilk.name] = collateral
return DssDeployment.Config(pause, vat, vow, jug, cat, dog, flapper, flopper, pot,
dai, dai_adapter, mkr, spotter, ds_chief, esm, end,
proxy_registry, dss_proxy_actions, cdp_manager,
dsr_manager, faucet, collaterals)
@staticmethod
def _infer_collaterals_from_addresses(keys: []) -> List:
collaterals = []
for key in keys:
match = re.search(r'MCD_[CF]LIP_(?!CALC)((\w+)_\w+)', key)
if match:
collaterals.append((match.group(1), match.group(2)))
continue
match = re.search(r'MCD_[CF]LIP_(?!CALC)(\w+)', key)
if match:
collaterals.append((match.group(1), match.group(1)))
return collaterals
def to_dict(self) -> dict:
conf_dict = {
'MCD_PAUSE': self.pause.address.address,
'MCD_VAT': self.vat.address.address,
'MCD_VOW': self.vow.address.address,
'MCD_JUG': self.jug.address.address,
'MCD_FLAP': self.flapper.address.address,
'MCD_FLOP': self.flopper.address.address,
'MCD_POT': self.pot.address.address,
'MCD_DAI': self.dai.address.address,
'MCD_JOIN_DAI': self.dai_join.address.address,
'MCD_GOV': self.mkr.address.address,
'MCD_SPOT': self.spotter.address.address,
'MCD_ADM': self.ds_chief.address.address,
'MCD_ESM': self.esm.address.address,
'MCD_END': self.end.address.address,
'PROXY_REGISTRY': self.proxy_registry.address.address,
'PROXY_ACTIONS_DSR': self.dss_proxy_actions.address.address,
'CDP_MANAGER': self.cdp_manager.address.address,
'DSR_MANAGER': self.dsr_manager.address.address
}
if self.cat:
conf_dict['MCD_CAT'] = self.cat.address.address
if self.dog:
conf_dict['MCD_DOG'] = self.dog.address.address
if self.faucet:
conf_dict['FAUCET'] = self.faucet.address.address
for collateral in self.collaterals.values():
match = re.search(r'(\w+)(?:-\w+)?', collateral.ilk.name)
name = (collateral.ilk.name.replace('-', '_'), match.group(1))
conf_dict[name[1]] = collateral.gem.address.address
if collateral.pip:
conf_dict[f'PIP_{name[1]}'] = collateral.pip.address.address
conf_dict[f'MCD_JOIN_{name[0]}'] = collateral.adapter.address.address
if collateral.flipper:
conf_dict[f'MCD_FLIP_{name[0]}'] = collateral.flipper.address.address
elif collateral.clipper:
conf_dict[f'MCD_CLIP_{name[0]}'] = collateral.clipper.address.address
return conf_dict
def to_json(self) -> str:
return json.dumps(self.to_dict())
def __init__(self, web3: Web3, config: Config):
assert isinstance(web3, Web3)
assert isinstance(config, DssDeployment.Config)
self.web3 = web3
self.config = config
self.pause = config.pause
self.vat = config.vat
self.vow = config.vow
self.jug = config.jug
self.cat = config.cat
self.dog = config.dog
self.flapper = config.flapper
self.flopper = config.flopper
self.pot = config.pot
self.dai = config.dai
self.dai_adapter = config.dai_join
self.mkr = config.mkr
self.collaterals = config.collaterals
self.spotter = config.spotter
self.ds_chief = config.ds_chief
self.esm = config.esm
self.end = config.end
self.proxy_registry = config.proxy_registry
self.dss_proxy_actions = config.dss_proxy_actions
self.cdp_manager = config.cdp_manager
self.dsr_manager = config.dsr_manager
self.faucet = config.faucet
@staticmethod
def from_json(web3: Web3, conf: str):
return DssDeployment(web3, DssDeployment.Config.from_json(web3, conf))
def to_json(self) -> str:
return self.config.to_json()
@staticmethod
def from_node(web3: Web3):
assert isinstance(web3, Web3)
network = DssDeployment.NETWORKS.get(web3.net.version, "testnet")
return DssDeployment.from_network(web3=web3, network=network)
@staticmethod
def from_network(web3: Web3, network: str):
assert isinstance(web3, Web3)
assert isinstance(network, str)
cwd = os.path.dirname(os.path.realpath(__file__))
addresses_path = os.path.join(cwd, "../config", f"{network}-addresses.json")
return DssDeployment.from_json(web3=web3, conf=open(addresses_path, "r").read())
def approve_dai(self, usr: Address, **kwargs):
"""
Allows the user to draw Dai from and repay Dai to their CDPs.
Args
usr: Recipient of Dai from one or more CDPs
"""
assert isinstance(usr, Address)
gas_price = kwargs['gas_price'] if 'gas_price' in kwargs else DefaultGasPrice()
self.dai_adapter.approve(approval_function=hope_directly(from_address=usr, gas_price=gas_price),
source=self.vat.address)
self.dai.approve(self.dai_adapter.address).transact(from_address=usr, gas_price=gas_price)
def active_auctions(self) -> dict:
flips = {}
clips = {}
for collateral in self.collaterals.values():
# Each collateral has it's own liquidation contract; add auctions from each.
if collateral.flipper:
flips[collateral.ilk.name] = collateral.flipper.active_auctions()
elif collateral.clipper:
clips[collateral.ilk.name] = collateral.clipper.active_auctions()
return {
"flips": flips,
"clips": clips,
"flaps": self.flapper.active_auctions(),
"flops": self.flopper.active_auctions()
}
def __repr__(self):
return f'DssDeployment({self.config.to_json()})'
================================================
FILE: pymaker/dsr.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C)2019 grandizzy
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from pymaker import Address, Transact, Calldata
from pymaker.numeric import Wad, Ray
from pymaker.proxy import DSProxy
from pymaker.deployment import DssDeployment
logger = logging.getLogger()
class Dsr:
""" DSR Client implementation
"""
_ZERO_ADDRESS = Address("0x0000000000000000000000000000000000000000")
def __init__(self, mcd: DssDeployment, owner: Address):
assert (isinstance(mcd, DssDeployment))
assert (isinstance(owner, Address))
self.owner = owner
self.mcd = mcd
def has_proxy(self) -> bool:
return self.mcd.proxy_registry.proxies(self.owner) != self._ZERO_ADDRESS
def get_proxy(self) -> DSProxy:
return DSProxy(self.mcd.web3, Address(self.mcd.proxy_registry.proxies(self.owner)))
def build_proxy(self) -> Transact:
return self.mcd.proxy_registry.build(self.owner)
def chi(self) -> Ray:
return self.mcd.pot.chi()
def get_total_dai(self) -> Wad:
return self.mcd.pot.pie() * self.chi()
def dsr(self) -> Ray:
return self.mcd.pot.dsr()
def get_balance(self, proxy: Address) -> Wad:
assert (isinstance(proxy, Address))
total_pie = self.mcd.pot.pie()
if total_pie == Wad.from_number(0):
return Wad.from_number(0)
slice = self.mcd.pot.pie_of(proxy)
portion = slice / total_pie
dai_in_pot = self.mcd.vat.dai(self.mcd.pot.address)
return Wad(dai_in_pot * portion)
def join(self, amount: Wad, proxy: DSProxy) -> Transact:
assert (isinstance(amount, Wad))
assert (isinstance(proxy, DSProxy))
return proxy.execute_at(self.mcd.dss_proxy_actions.address,
Calldata.from_signature(
self.mcd.web3,
"join(address,address,uint256)",
[
self.mcd.dai_adapter.address.address,
self.mcd.pot.address.address,
amount.value
])
)
def exit(self, amount: Wad, proxy: DSProxy) -> Transact:
assert (isinstance(amount, Wad))
assert (isinstance(proxy, DSProxy))
return proxy.execute_at(self.mcd.dss_proxy_actions.address,
Calldata.from_signature(
self.mcd.web3,
"exit(address,address,uint256)",
[
self.mcd.dai_adapter.address.address,
self.mcd.pot.address.address,
amount.value
])
)
def exit_all(self, proxy: DSProxy) -> Transact:
assert (isinstance(proxy, DSProxy))
return proxy.execute_at(self.mcd.dss_proxy_actions.address,
Calldata.from_signature(
self.mcd.web3,
"exitAll(address,address)",
[
self.mcd.dai_adapter.address.address,
self.mcd.pot.address.address
])
)
================================================
FILE: pymaker/dsrmanager.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.dss import Pot
from pymaker.join import DaiJoin
from pymaker.numeric import Wad, Rad
from pymaker.token import DSToken
class DsrManager(Contract):
"""
A client for the `DsrManger` contract, which reduces the need for proxies
when interacting with the Pot contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DsrManager.abi')
bin = Contract._load_bin(__name__, 'abi/DsrManager.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def pot(self) -> Pot:
address = Address(self._contract.functions.pot().call())
return Pot(self.web3, address)
def dai(self) -> DSToken:
address = Address(self._contract.functions.dai().call())
return DSToken(self.web3, address)
def dai_adapter(self) -> DaiJoin:
address = Address(self._contract.functions.daiJoin().call())
return DaiJoin(self.web3, address)
def supply(self) -> Wad:
"""Total supply of pie locked in Pot through DsrManager"""
return Wad(self._contract.functions.supply().call())
def pie_of(self, usr: Address) -> Wad:
"""Pie balance of a given usr address"""
assert isinstance(usr, Address)
return Wad(self._contract.functions.pieOf(usr.address).call())
def dai_of(self, usr: Address) -> Rad:
"""
Internal Dai balance of a given usr address - current Chi is used
i.e. Dai balance potentially stale
"""
assert isinstance(usr, Address)
pie = self.pie_of(usr)
chi = self.pot().chi()
dai = Rad(pie) * Rad(chi)
return dai
def join(self, dst: Address, dai: Wad) -> Transact:
"""Lock a given amount of ERC20 Dai into the DSR Contract and give to dst address """
assert isinstance(dst, Address)
assert isinstance(dai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'join',
[dst.address, dai.value])
def exit(self, dst: Address, dai: Wad) -> Transact:
""" Free a given amount of ERC20 Dai from the DSR Contract and give to dst address """
assert isinstance(dst, Address)
assert isinstance(dai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'exit',
[dst.address, dai.value])
def exitAll(self, dst: Address) -> Transact:
""" Free all ERC20 Dai from the DSR Contract and give to dst address """
assert isinstance(dst, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'exitAll', [dst.address])
def __repr__(self):
return f"DsrManager('{self.address}')"
================================================
FILE: pymaker/dss.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018-2021 bargst, EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from datetime import datetime
from pprint import pformat
from typing import List
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.ilk import Ilk
from pymaker.logging import LogNote
from pymaker.token import DSToken, ERC20Token
from pymaker.numeric import Wad, Ray, Rad
logger = logging.getLogger()
class Urn:
"""Models one CDP for a single collateral type and account. Note the "address of the Urn" is merely the address
of the CDP holder.
"""
def __init__(self, address: Address, ilk: Ilk = None, ink: Wad = None, art: Wad = None):
assert isinstance(address, Address)
assert isinstance(ilk, Ilk) or (ilk is None)
assert isinstance(ink, Wad) or (ink is None)
assert isinstance(art, Wad) or (art is None)
self.address = address
self.ilk = ilk
self.ink = ink
self.art = art
def toBytes(self):
addr_str = self.address.address
return Web3.toBytes(hexstr='0x' + addr_str[2:].zfill(64))
@staticmethod
def fromBytes(urn: bytes):
assert isinstance(urn, bytes)
address = Address(Web3.toHex(urn[-20:]))
return Urn(address)
def __eq__(self, other):
assert isinstance(other, Urn)
return (self.address == other.address) and (self.ilk == other.ilk)
def __repr__(self):
repr = ''
if self.ilk:
repr += f'[{self.ilk.name}]'
if self.ink:
repr += f' ink={self.ink}'
if self.art:
repr += f' art={self.art}'
if repr:
repr = f'[{repr.strip()}]'
return f"Urn('{self.address}'){repr}"
class Vat(Contract):
"""A client for the `Vat` contract, which manages accounting for all Urns (CDPs).
Ref.
"""
# Identifies vault holders and collateral types they have frobbed
class LogFrob:
def __init__(self, lognote: LogNote):
assert isinstance(lognote, LogNote)
self.ilk = str(Web3.toText(lognote.arg1)).replace('\x00', '')
self.urn = Address(Web3.toHex(lognote.arg2)[26:])
self.collateral_owner = Address(Web3.toHex(lognote.arg3)[26:])
self.dai_recipient = Address(Web3.toHex(lognote.get_bytes_at_index(3))[26:])
self.dink = Wad(int.from_bytes(lognote.get_bytes_at_index(4), byteorder="big", signed=True))
self.dart = Wad(int.from_bytes(lognote.get_bytes_at_index(5), byteorder="big", signed=True))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"LogFrob({pformat(vars(self))})"
# Tracks movement of stablecoin between urns
class LogMove:
def __init__(self, lognote: LogNote):
assert isinstance(lognote, LogNote)
self.src = Address(Web3.toHex(lognote.arg1)[26:])
self.dst = Address(Web3.toHex(lognote.arg2)[26:])
self.dart = Rad(int.from_bytes(lognote.get_bytes_at_index(2), byteorder="big", signed=True))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"LogMove({pformat(vars(self))})"
# Shows vaults being split or merged
class LogFork:
def __init__(self, lognote: LogNote):
assert isinstance(lognote, LogNote)
self.ilk = str(Web3.toText(lognote.arg1)).replace('\x00', '')
self.src = Address(Web3.toHex(lognote.arg2)[26:])
self.dst = Address(Web3.toHex(lognote.arg3)[26:])
self.dink = Wad(int.from_bytes(lognote.get_bytes_at_index(3), byteorder="big", signed=True))
self.dart = Wad(int.from_bytes(lognote.get_bytes_at_index(4), byteorder="big", signed=True))
self.block = lognote.block
self.tx_hash = lognote.tx_hash
def __repr__(self):
return f"LogFork({pformat(vars(self))})"
abi = Contract._load_abi(__name__, 'abi/Vat.abi')
bin = Contract._load_bin(__name__, 'abi/Vat.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def init(self, ilk: Ilk) -> Transact:
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'init', [ilk.toBytes()])
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def wards(self, address: Address):
assert isinstance(address, Address)
return bool(self._contract.functions.wards(address.address).call())
def hope(self, address: Address):
assert isinstance(address, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'hope', [address.address])
def can(self, sender: Address, usr: Address):
assert isinstance(sender, Address)
assert isinstance(usr, Address)
return bool(self._contract.functions.can(sender.address, usr.address).call())
def ilk(self, name: str) -> Ilk:
assert isinstance(name, str)
b32_ilk = Ilk(name).toBytes()
(art, rate, spot, line, dust) = self._contract.functions.ilks(b32_ilk).call()
# We could get "ink" from the urn, but caller must provide an address.
return Ilk(name, rate=Ray(rate), ink=Wad(0), art=Wad(art), spot=Ray(spot), line=Rad(line), dust=Rad(dust))
def gem(self, ilk: Ilk, urn: Address) -> Wad:
assert isinstance(ilk, Ilk)
assert isinstance(urn, Address)
return Wad(self._contract.functions.gem(ilk.toBytes(), urn.address).call())
def dai(self, urn: Address) -> Rad:
assert isinstance(urn, Address)
return Rad(self._contract.functions.dai(urn.address).call())
def sin(self, urn: Address) -> Rad:
assert isinstance(urn, Address)
return Rad(self._contract.functions.sin(urn.address).call())
def urn(self, ilk: Ilk, address: Address) -> Urn:
assert isinstance(ilk, Ilk)
assert isinstance(address, Address)
(ink, art) = self._contract.functions.urns(ilk.toBytes(), address.address).call()
return Urn(address, ilk, Wad(ink), Wad(art))
def debt(self) -> Rad:
return Rad(self._contract.functions.debt().call())
def vice(self) -> Rad:
return Rad(self._contract.functions.vice().call())
def line(self) -> Rad:
""" Total debt ceiling """
return Rad(self._contract.functions.Line().call())
def flux(self, ilk: Ilk, src: Address, dst: Address, wad: Wad) -> Transact:
"""Move Ilk balance in Vat from source address to destiny address
Args:
ilk: Identifies the type of collateral.
src: Source of the collateral (address of the source).
dst: Destiny of the collateral (address of the recipient).
wad: Amount of collateral to move.
"""
assert isinstance(ilk, Ilk)
assert isinstance(src, Address)
assert isinstance(dst, Address)
assert isinstance(wad, Wad)
flux_args = [ilk.toBytes(), src.address, dst.address, wad.value]
return Transact(self, self.web3, self.abi, self.address, self._contract, 'flux', flux_args)
def move(self, src: Address, dst: Address, rad: Rad) -> Transact:
"""Move Dai balance in Vat from source address to destiny address
Args:
src: Source of the dai (address of the source).
dst: Destiny of the dai (address of the recipient).
rad: Amount of dai to move.
"""
assert isinstance(src, Address)
assert isinstance(dst, Address)
assert isinstance(rad, Rad)
move_args = [src.address, dst.address, rad.value]
return Transact(self, self.web3, self.abi, self.address, self._contract, 'move', move_args)
def fork(self, ilk: Ilk, src: Address, dst: Address, dink: Wad, dart: Wad) -> Transact:
"""Split a Vault - binary approval or splitting/merging Vault's
Args:
ilk: Identifies the type of collateral.
src: Address of the source Urn.
dst: Address of the destiny Urn.
dink: Amount of collateral to exchange.
dart: Amount of stable coin debt to exchange.
"""
assert isinstance(ilk, Ilk)
assert isinstance(src, Address)
assert isinstance(dst, Address)
assert isinstance(dink, Wad)
assert isinstance(dart, Wad)
fork_args = [ilk.toBytes(), src.address, dst.address, dink.value, dart.value]
return Transact(self, self.web3, self.abi, self.address, self._contract, 'fork', fork_args)
def frob(self, ilk: Ilk, urn_address: Address, dink: Wad, dart: Wad, collateral_owner=None, dai_recipient=None):
"""Adjust amount of collateral and reserved amount of Dai for the CDP
Args:
ilk: Identifies the type of collateral.
urn_address: CDP holder (address of the Urn).
dink: Amount of collateral to add/remove.
dart: Adjust CDP debt (amount of Dai available for borrowing).
collateral_owner: Holder of the collateral used to fund the CDP.
dai_recipient: Party receiving the Dai.
"""
assert isinstance(ilk, Ilk)
assert isinstance(urn_address, Address)
assert isinstance(dink, Wad)
assert isinstance(dart, Wad)
assert isinstance(collateral_owner, Address) or (collateral_owner is None)
assert isinstance(dai_recipient, Address) or (dai_recipient is None)
# Usually these addresses are the same as the account holding the urn
v = collateral_owner or urn_address
w = dai_recipient or urn_address
assert isinstance(v, Address)
assert isinstance(w, Address)
self.validate_frob(ilk, urn_address, dink, dart)
if v == urn_address and w == urn_address:
logger.info(f"frobbing {ilk.name} urn {urn_address.address} with dink={dink}, dart={dart}")
else:
logger.info(f"frobbing {ilk.name} urn {urn_address.address} "
f"with dink={dink} from {v.address}, "
f"dart={dart} for {w.address}")
return Transact(self, self.web3, self.abi, self.address, self._contract,
'frob', [ilk.toBytes(), urn_address.address, v.address, w.address, dink.value, dart.value])
def get_wipe_all_dart(self, ilk: Ilk, urn: Address) -> Wad:
"""Returns the amount of Dai required to wipe an urn without leaving any dust
adapted from https://github.com/makerdao/dss-proxy-actions/blob/master/src/DssProxyActions.sol#L200"""
assert isinstance(urn, Address)
assert isinstance(ilk, Ilk)
assert ilk.rate >= Ray.from_number(1)
rad: Rad = Rad(self.urn(ilk, urn).art) * Rad(ilk.rate)
wad: Wad = Wad(rad)
wad = wad + Wad(1) if Rad(wad) < rad else wad
return wad
def validate_frob(self, ilk: Ilk, address: Address, dink: Wad, dart: Wad):
"""Helps diagnose `frob` transaction failures by asserting on `require` conditions in the contract"""
def r(value, decimals=1): # rounding function
return round(float(value), decimals)
def f(value, decimals=1): # formatting function
return f"{r(value):16,.{decimals}f}"
assert isinstance(ilk, Ilk)
assert isinstance(address, Address)
assert isinstance(dink, Wad)
assert isinstance(dart, Wad)
assert self.live() # system is live
urn = self.urn(ilk, address)
ilk = self.ilk(ilk.name)
assert ilk.rate != Ray(0) # ilk has been initialised
ink = urn.ink + dink
art = urn.art + dart
ilk_art = ilk.art + dart
logger.debug(f"System | debt {f(self.debt())} | ceiling {f(self.line())}")
logger.debug(f"Collateral | debt {f(Ray(ilk_art) * ilk.rate)} | ceiling {f(ilk.line)}")
dtab = Rad(ilk.rate * Ray(dart))
tab = ilk.rate * art
debt = self.debt() + dtab
logger.debug(f"Frobbing ink={r(urn.ink)}, art={urn.art}, dtab={r(dtab)}, tab={tab}, "
f"ilk.rate={r(ilk.rate,8)}, ilk.spot={r(ilk.spot, 4)}, vat.debt={r(debt)}")
# either debt has decreased, or debt ceilings are not exceeded
under_collateral_debt_ceiling = Rad(Ray(ilk_art) * ilk.rate) <= ilk.line
under_system_debt_ceiling = debt < self.line()
calm = dart <= Wad(0) or (under_collateral_debt_ceiling and under_system_debt_ceiling)
# urn is either less risky than before, or it is safe
safe = (dart <= Wad(0) and dink >= Wad(0)) or tab <= Ray(ink) * ilk.spot
# urn has no debt, or a non-dusty amount
neat = art == Wad(0) or Rad(tab) >= ilk.dust
if not under_collateral_debt_ceiling:
logger.warning("collateral debt ceiling would be exceeded")
if not under_system_debt_ceiling:
logger.warning("system debt ceiling would be exceeded")
if not safe:
logger.warning("urn would be unsafe")
if not neat:
logger.warning("debt would not exceed dust cutoff")
assert calm and safe and neat
def past_frobs(self, from_block: int, to_block: int = None, ilk: Ilk = None, chunk_size=20000) -> List[LogFrob]:
"""Synchronously retrieve a list showing which ilks and urns have been frobbed.
Args:
from_block: Oldest Ethereum block to retrieve the events from.
to_block: Optional newest Ethereum block to retrieve the events from, defaults to current block
ilk: Optionally filter frobs by ilk.name
chunk_size: Number of blocks to fetch from chain at one time, for performance tuning
Returns:
List of past `LogFrob` events represented as :py:class:`pymaker.dss.Vat.LogFrob` class.
"""
return self.past_logs(from_block, to_block, ilk,
include_forks=False, include_moves=False, chunk_size=chunk_size)
def past_logs(self, from_block: int, to_block: int = None, ilk: Ilk = None,
include_forks=True, include_moves=True, chunk_size=20000) -> List[object]:
"""Synchronously retrieve a unordered list of vat activity, optionally filtered by collateral type.
Args:
from_block: Oldest Ethereum block to retrieve the events from.
to_block: Optional newest Ethereum block to retrieve the events from, defaults to current block
ilk: Optionally filter frobs by ilk.name
chunk_size: Number of blocks to fetch from chain at one time, for performance tuning
Returns:
Unordered list of past `LogFork`, `LogFrob`, and `LogMove` events.
"""
current_block = self._contract.web3.eth.blockNumber
assert isinstance(from_block, int)
assert from_block <= current_block
if to_block is None:
to_block = current_block
else:
assert isinstance(to_block, int)
assert to_block >= from_block
assert to_block <= current_block
assert isinstance(ilk, Ilk) or ilk is None
assert chunk_size > 0
logger.debug(f"Consumer requested frob data from block {from_block} to {to_block}")
start = from_block
end = None
chunks_queried = 0
retval = []
while end is None or start <= to_block:
chunks_queried += 1
end = min(to_block, start+chunk_size)
filter_params = {
'address': self.address.address,
'fromBlock': start,
'toBlock': end
}
logger.debug(f"Querying logs from block {start} to {end} ({end-start} blocks); "
f"accumulated {len(retval)} logs in {chunks_queried-1} requests")
logs = self.web3.eth.getLogs(filter_params)
lognotes = list(map(lambda l: LogNote.from_event(l, Vat.abi), logs))
# '0x7cdd3fde' is Vat.slip (from GemJoin.join) and '0x76088703' is Vat.frob
logfrobs = list(filter(lambda l: l.sig == '0x76088703', lognotes))
logfrobs = list(map(lambda l: Vat.LogFrob(l), logfrobs))
if ilk is not None:
logfrobs = list(filter(lambda l: l.ilk == ilk.name, logfrobs))
retval.extend(logfrobs)
# '0xbb35783b' is Vat.move
if include_moves:
logmoves = list(filter(lambda l: l.sig == '0xbb35783b', lognotes))
logmoves = list(map(lambda l: Vat.LogMove(l), logmoves))
retval.extend(logmoves)
# '0x870c616d' is Vat.fork
if include_forks:
logforks = list(filter(lambda l: l.sig == '0x870c616d', lognotes))
logforks = list(map(lambda l: Vat.LogFork(l), logforks))
if ilk is not None:
logforks = list(filter(lambda l: l.ilk == ilk.name, logforks))
retval.extend(logforks)
start += chunk_size
logger.debug(f"Found {len(retval)} logs in {chunks_queried} requests")
return retval
def heal(self, vice: Rad) -> Transact:
assert isinstance(vice, Rad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'heal', [vice.value])
def __eq__(self, other):
assert isinstance(other, Vat)
return self.address == other.address
def __repr__(self):
return f"Vat('{self.address}')"
class Spotter(Contract):
"""A client for the `Spotter` contract, which interacts with Vat for the purpose of managing collateral prices.
Users generally have no need to interact with this contract; it is included for unit testing purposes.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/Spotter.abi')
bin = Contract._load_bin(__name__, 'abi/Spotter.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def poke(self, ilk: Ilk) -> Transact:
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'poke', [ilk.toBytes()])
def vat(self) -> Address:
return Address(self._contract.functions.vat().call())
def par(self) -> Ray:
return Ray(self._contract.functions.par().call())
def mat(self, ilk: Ilk) -> Ray:
assert isinstance(ilk, Ilk)
(pip, mat) = self._contract.functions.ilks(ilk.toBytes()).call()
return Ray(mat)
def __repr__(self):
return f"Spotter('{self.address}')"
class Vow(Contract):
"""A client for the `Vow` contract, which manages liquidation of surplus Dai and settlement of collateral debt.
Specifically, this contract is useful for Flap and Flop auctions.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/Vow.abi')
bin = Contract._load_bin(__name__, 'abi/Vow.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
self.vat = Vat(web3, Address(self._contract.functions.vat().call()))
def rely(self, guy: Address) -> Transact:
assert isinstance(guy, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'rely', [guy.address])
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def flapper(self) -> Address:
return Address(self._contract.functions.flapper().call())
def flopper(self) -> Address:
return Address(self._contract.functions.flopper().call())
def sin(self) -> Rad:
return Rad(self._contract.functions.Sin().call())
def sin_of(self, era: int) -> Rad:
return Rad(self._contract.functions.sin(era).call())
def ash(self) -> Rad:
return Rad(self._contract.functions.Ash().call())
def woe(self) -> Rad:
return (self.vat.sin(self.address) - self.sin()) - self.ash()
def wait(self) -> int:
return int(self._contract.functions.wait().call())
def dump(self) -> Wad:
return Wad(self._contract.functions.dump().call())
def sump(self) -> Rad:
return Rad(self._contract.functions.sump().call())
def bump(self) -> Rad:
return Rad(self._contract.functions.bump().call())
def hump(self) -> Rad:
return Rad(self._contract.functions.hump().call())
def flog(self, era: int) -> Transact:
assert isinstance(era, int)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'flog', [era])
def heal(self, rad: Rad) -> Transact:
assert isinstance(rad, Rad)
logger.info(f"Healing joy={self.vat.dai(self.address)} woe={self.woe()}")
return Transact(self, self.web3, self.abi, self.address, self._contract, 'heal', [rad.value])
def kiss(self, rad: Rad) -> Transact:
assert isinstance(rad, Rad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'kiss', [rad.value])
def flop(self) -> Transact:
"""Initiate a debt auction"""
logger.info(f"Initiating a flop auction with woe={self.woe()}")
return Transact(self, self.web3, self.abi, self.address, self._contract, 'flop', [])
def flap(self) -> Transact:
"""Initiate a surplus auction"""
logger.info(f"Initiating a flap auction with joy={self.vat.dai(self.address)}")
return Transact(self, self.web3, self.abi, self.address, self._contract, 'flap', [])
def __repr__(self):
return f"Vow('{self.address}')"
class Jug(Contract):
"""A client for the `Jug` contract, which manages stability fees.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/Jug.abi')
bin = Contract._load_bin(__name__, 'abi/Jug.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
self.vat = Vat(web3, Address(self._contract.functions.vat().call()))
self.vow = Vow(web3, Address(self._contract.functions.vow().call()))
def init(self, ilk: Ilk) -> Transact:
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'init', [ilk.toBytes()])
def wards(self, address: Address):
assert isinstance(address, Address)
return bool(self._contract.functions.wards(address.address).call())
def drip(self, ilk: Ilk) -> Transact:
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'drip', [ilk.toBytes()])
def base(self) -> Ray:
return Ray(self._contract.functions.base().call())
def duty(self, ilk: Ilk) -> Ray:
assert isinstance(ilk, Ilk)
return Ray(self._contract.functions.ilks(ilk.toBytes()).call()[0])
def rho(self, ilk: Ilk) -> int:
assert isinstance(ilk, Ilk)
return Web3.toInt(self._contract.functions.ilks(ilk.toBytes()).call()[1])
def __repr__(self):
return f"Jug('{self.address}')"
class Cat(Contract):
"""A client for the `Cat` contract, used to liquidate unsafe Urns (CDPs).
Specifically, this contract is useful for Flip auctions.
Ref.
"""
# This information is read from the `Bite` event emitted from `Cat.bite`
class LogBite:
def __init__(self, log):
self.ilk = Ilk.fromBytes(log['args']['ilk'])
self.urn = Urn(Address(log['args']['urn']))
self.ink = Wad(log['args']['ink'])
self.art = Wad(log['args']['art'])
self.tab = Rad(log['args']['tab'])
self.flip = Address(log['args']['flip'])
self.id = int(log['args']['id'])
self.raw = log
def era(self, web3: Web3):
return web3.eth.getBlock(self.raw['blockNumber'])['timestamp']
def __eq__(self, other):
assert isinstance(other, Cat.LogBite)
return self.__dict__ == other.__dict__
def __repr__(self):
return pformat(vars(self))
abi = Contract._load_abi(__name__, 'abi/Cat.abi')
bin = Contract._load_bin(__name__, 'abi/Cat.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
self.vat = Vat(web3, Address(self._contract.functions.vat().call()))
self.vow = Vow(web3, Address(self._contract.functions.vow().call()))
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def can_bite(self, ilk: Ilk, urn: Urn) -> bool:
""" Determine whether a vault can be liquidated
Args:
ilk: Collateral type
urn: Identifies the vault holder or proxy
"""
assert isinstance(ilk, Ilk)
assert isinstance(urn, Urn)
ilk = self.vat.ilk(ilk.name)
urn = self.vat.urn(ilk, urn.address)
rate = ilk.rate
# Collateral value should be less than the product of our stablecoin debt and the debt multiplier
safe = Ray(urn.ink) * ilk.spot >= Ray(urn.art) * rate
if safe:
return False
# Ensure there's room in the litter box
box: Rad = self.box()
litter: Rad = self.litter()
room: Rad = box - litter
if litter >= box:
logger.debug(f"biting {urn.address} would exceed maximum Dai out for liquidation")
return False
if room < ilk.dust:
return False
# Prevent null auction (ilk.dunk [Rad], ilk.rate [Ray], ilk.chop [Wad])
assert self.chop(ilk) > Wad(0) # ensure liquidations are enabled and this uses flipper instead of clipper
dart: Wad = min(urn.art, Wad(min(self.dunk(ilk), room) / Rad(ilk.rate) / Rad(self.chop(ilk))))
dink: Wad = min(urn.ink, urn.ink * dart / urn.art)
return dart > Wad(0) and dink > Wad(0)
def bite(self, ilk: Ilk, urn: Urn) -> Transact:
""" Initiate liquidation of a vault, kicking off a flip auction
Args:
ilk: Identifies the type of collateral.
urn: Address of the vault holder.
"""
assert isinstance(ilk, Ilk)
assert isinstance(urn, Urn)
ilk = self.vat.ilk(ilk.name)
urn = self.vat.urn(ilk, urn.address)
rate = self.vat.ilk(ilk.name).rate
logger.info(f'Biting {ilk.name} vault {urn.address.address} with ink={urn.ink} spot={ilk.spot} '
f'art={urn.art} rate={rate}')
return Transact(self, self.web3, self.abi, self.address, self._contract,
'bite', [ilk.toBytes(), urn.address.address])
def chop(self, ilk: Ilk) -> Wad:
assert isinstance(ilk, Ilk)
(flip, chop, dunk) = self._contract.functions.ilks(ilk.toBytes()).call()
return Wad(chop)
def dunk(self, ilk: Ilk) -> Rad:
assert isinstance(ilk, Ilk)
(flip, chop, dunk) = self._contract.functions.ilks(ilk.toBytes()).call()
return Rad(dunk)
def flipper(self, ilk: Ilk) -> Address:
assert isinstance(ilk, Ilk)
(flip, chop, dunk) = self._contract.functions.ilks(ilk.toBytes()).call()
return Address(flip)
def box(self) -> Rad:
return Rad(self._contract.functions.box().call())
def litter(self) -> Rad:
return Rad(self._contract.functions.litter().call())
def past_bites(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogBite]:
"""Synchronously retrieve past LogBite events.
`LogBite` events are emitted every time someone bites a CDP.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogBite` events represented as :py:class:`pymaker.dss.Cat.LogBite` class.
"""
assert isinstance(number_of_past_blocks, int)
assert isinstance(event_filter, dict) or (event_filter is None)
return self._past_events(self._contract, 'Bite', Cat.LogBite, number_of_past_blocks, event_filter)
def __repr__(self):
return f"Cat('{self.address}')"
class Dog(Contract):
"""A client for the `Dog` contract, used to liquidate unsafe vaults.
Specifically, this contract is useful for Clip auctions.
Ref.
"""
# This information is read from the `Bark` event emitted from `Dog.bark`
class LogBark:
def __init__(self, log):
self.ilk = Ilk.fromBytes(log['args']['ilk'])
self.urn = Urn(Address(log['args']['urn']))
self.ink = Wad(log['args']['ink'])
self.art = Wad(log['args']['art'])
self.due = Rad(log['args']['due'])
self.clip = Address(log['args']['clip'])
self.id = int(log['args']['id'])
self.raw = log
def era(self, web3: Web3):
return web3.eth.getBlock(self.raw['blockNumber'])['timestamp']
def __eq__(self, other):
assert isinstance(other, Cat.LogBite)
return self.__dict__ == other.__dict__
def __repr__(self):
return pformat(vars(self))
abi = Contract._load_abi(__name__, 'abi/Dog.abi')
bin = Contract._load_bin(__name__, 'abi/Dog.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
vat_address = Address(self._contract.functions.vat().call())
self.vat = Vat(web3, vat_address) if vat_address != Address.zero() else None
vow_address = Address(self._contract.functions.vow().call())
self.vow = Vow(web3, vow_address) if vow_address != Address.zero() else None
def live(self) -> bool:
return self._contract.functions.live().call() > 0
def clipper(self, ilk: Ilk) -> Address:
assert isinstance(ilk, Ilk)
(clip, chop, hole, dirt) = self._contract.functions.ilks(ilk.toBytes()).call()
return Address(clip)
def chop(self, ilk: Ilk) -> Wad:
assert isinstance(ilk, Ilk)
(clip, chop, hole, dirt) = self._contract.functions.ilks(ilk.toBytes()).call()
return Wad(chop)
def hole(self, ilk: Ilk) -> Rad:
assert isinstance(ilk, Ilk)
(clip, chop, hole, dirt) = self._contract.functions.ilks(ilk.toBytes()).call()
return Rad(hole)
def dirt(self, ilk: Ilk) -> Rad:
assert isinstance(ilk, Ilk)
(clip, chop, hole, dirt) = self._contract.functions.ilks(ilk.toBytes()).call()
return Rad(dirt)
def dog_hole(self) -> Rad:
return Rad(self._contract.functions.Hole().call())
def dog_dirt(self) -> Rad:
return Rad(self._contract.functions.Dirt().call())
def bark(self, ilk: Ilk, urn: Urn, kpr: Address = None) -> Transact:
""" Initiate liquidation of a vault, kicking off a flip auction
Args:
ilk: Identifies the type of collateral.
urn: Address of the vault holder.
kpr: Keeper address; leave empty to use web3 default.
"""
assert isinstance(ilk, Ilk)
assert isinstance(urn, Urn)
if kpr:
assert isinstance(kpr, Address)
else:
kpr = Address(self.web3.eth.defaultAccount)
ilk = self.vat.ilk(ilk.name)
urn = self.vat.urn(ilk, urn.address)
rate = self.vat.ilk(ilk.name).rate
logger.info(f'Barking {ilk.name} vault {urn.address.address} with ink={urn.ink} spot={ilk.spot} '
f'art={urn.art} rate={rate}')
return Transact(self, self.web3, self.abi, self.address, self._contract,
'bark', [ilk.toBytes(), urn.address.address, kpr.address])
def past_barks(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogBark]:
"""Synchronously retrieve past LogBark events.
`LogBark` events are emitted every time someone bites a vault.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogBark` events represented as :py:class:`pymaker.dss.Dog.LogBark` class.
"""
assert isinstance(number_of_past_blocks, int)
assert isinstance(event_filter, dict) or (event_filter is None)
return self._past_events(self._contract, 'Bark', Dog.LogBark, number_of_past_blocks, event_filter)
class Pot(Contract):
"""A client for the `Pot` contract, which implements the DSR.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/Pot.abi')
bin = Contract._load_bin(__name__, 'abi/Pot.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def approve(self, source: Address, approval_function, **kwargs):
"""Approve the pot to access Dai from our Urns"""
assert isinstance(source, Address)
assert(callable(approval_function))
approval_function(ERC20Token(web3=self.web3, address=source), self.address, self.__class__.__name__, **kwargs)
def pie_of(self, address: Address) -> Wad:
assert isinstance(address, Address)
return Wad(self._contract.functions.pie(address.address).call())
def pie(self) -> Wad:
pie = self._contract.functions.Pie().call()
return Wad(pie)
def dsr(self) -> Ray:
dsr = self._contract.functions.dsr().call()
return Ray(dsr)
def chi(self) -> Ray:
chi = self._contract.functions.chi().call()
return Ray(chi)
def rho(self) -> datetime:
rho = self._contract.functions.rho().call()
return datetime.fromtimestamp(rho)
def drip(self) -> Transact:
return Transact(self, self.web3, self.abi, self.address, self._contract, 'drip', [])
""" Join/Exit in Pot can be invoked through pymaker/dsrmanager.py and pymaker/dsr.py """
def __repr__(self):
return f"Pot('{self.address}')"
class TokenFaucet(Contract):
"""A client for the `TokenFaucet` contract, to obtain ERC-20 tokens on testnets for testing purposes.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/TokenFaucet.abi')
bin = Contract._load_bin(__name__, 'abi/TokenFaucet.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def gulp(self, address: Address):
return Transact(self, self.web3, self.abi, self.address, self._contract, 'gulp(address)', [address.address])
================================================
FILE: pymaker/etherdelta.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import hashlib
import json
import logging
import random
import threading
from pprint import pformat
from subprocess import Popen, PIPE
from typing import List
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.numeric import Wad
from pymaker.sign import eth_sign, to_vrs
from pymaker.tightly_packed import encode_address, encode_uint256
from pymaker.token import ERC20Token
from pymaker.util import bytes_to_hexstring, hexstring_to_bytes
class Order:
"""An off-chain order placed on the EtherDelta exchange.
Attributes:
maker: Order creator.
pay_token: Address of the ERC20 token put on sale.
pay_amount: Amount of the `pay_token` token put on sale.
buy_token: Address of the ERC20 token to be bought.
buy_amount: Amount of the `buy_token` to be bought.
expires: The block number after which the order will expire.
nonce: Nonce number, used to make orders similar unique and randomize signatures.
v: V component of the order signature.
r: R component of the order signature.
s: S component of the order signature.
"""
def __init__(self, ether_delta, maker: Address, pay_token: Address, pay_amount: Wad, buy_token: Address,
buy_amount: Wad, expires: int, nonce: int, v: int, r: bytes, s: bytes):
assert(isinstance(maker, Address))
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(isinstance(expires, int))
assert(isinstance(nonce, int))
assert(isinstance(v, int))
assert(isinstance(r, bytes))
assert(isinstance(s, bytes))
self._ether_delta = ether_delta
self.maker = maker
self.pay_token = pay_token
self.pay_amount = pay_amount
self.buy_token = buy_token
self.buy_amount = buy_amount
self.expires = expires
self.nonce = nonce
self.v = v
self.r = r
self.s = s
@property
def sell_to_buy_price(self) -> Wad:
return self.pay_amount / self.buy_amount
@property
def buy_to_sell_price(self) -> Wad:
return self.buy_amount / self.pay_amount
@property
def remaining_buy_amount(self) -> Wad:
return self.buy_amount - self._ether_delta.amount_filled(self)
@property
def remaining_sell_amount(self) -> Wad:
return self.pay_amount - (self._ether_delta.amount_filled(self) * self.pay_amount / self.buy_amount)
@staticmethod
def from_json(ether_delta, data: dict):
assert(isinstance(data, dict))
return Order(ether_delta=ether_delta, maker=Address(data['user']), pay_token=Address(data['tokenGive']),
pay_amount=Wad(int(data['amountGive'])), buy_token=Address(data['tokenGet']),
buy_amount=Wad(int(data['amountGet'])), expires=int(data['expires']), nonce=int(data['nonce']),
v=int(data['v']), r=hexstring_to_bytes(data['r']), s=hexstring_to_bytes(data['s']))
def to_json(self) -> dict:
return {'contractAddr': self._ether_delta.address.address,
'tokenGet': self.buy_token.address,
'amountGet': self.buy_amount.value,
'tokenGive': self.pay_token.address,
'amountGive': self.pay_amount.value,
'expires': self.expires,
'nonce': self.nonce,
'v': self.v,
'r': bytes_to_hexstring(self.r),
's': bytes_to_hexstring(self.s),
'user': self.maker.address}
def __eq__(self, other):
assert(isinstance(other, Order))
return self.maker == other.maker and \
self.pay_token == other.pay_token and \
self.pay_amount == other.pay_amount and \
self.buy_token == other.buy_token and \
self.buy_amount == other.buy_amount and \
self.expires == other.expires and \
self.nonce == other.nonce and \
self.v == other.v and \
self.r == other.r and \
self.s == other.s
def __hash__(self):
return hash((self.maker,
self.pay_token,
self.pay_amount,
self.buy_token,
self.buy_amount,
self.expires,
self.nonce,
self.v,
self.r,
self.s))
def __str__(self):
return f"('{self.buy_token}', '{self.buy_amount}'," \
f" '{self.pay_token}', '{self.pay_amount}'," \
f" '{self.expires}', '{self.nonce}')"
def __repr__(self):
return pformat(vars(self))
class LogTrade:
def __init__(self, log):
self.maker = Address(log['args']['get'])
self.taker = Address(log['args']['give'])
self.pay_token = Address(log['args']['tokenGive'])
self.take_amount = Wad(log['args']['amountGive'])
self.buy_token = Address(log['args']['tokenGet'])
self.give_amount = Wad(log['args']['amountGet'])
self.raw = log
def __repr__(self):
return pformat(vars(self))
class EtherDelta(Contract):
"""A client for the EtherDelta exchange contract.
You can find the source code of the `EtherDelta` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `EtherDelta` contract.
"""
abi = Contract._load_abi(__name__, 'abi/EtherDelta.abi')
bin = Contract._load_bin(__name__, 'abi/EtherDelta.bin')
ETH_TOKEN = Address('0x0000000000000000000000000000000000000000')
@staticmethod
def deploy(web3: Web3,
admin: Address,
fee_account: Address,
account_levels_addr: Address,
fee_make: Wad,
fee_take: Wad,
fee_rebate: Wad):
"""Deploy a new instance of the `EtherDelta` contract.
Args:
web3: An instance of `Web` from `web3.py`.
Returns:
A `EtherDelta` class instance.
"""
return EtherDelta(web3=web3,
address=Contract._deploy(web3, EtherDelta.abi, EtherDelta.bin, [
admin.address,
fee_account.address,
account_levels_addr.address,
fee_make.value,
fee_take.value,
fee_rebate.value
]))
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def approve(self, tokens: List[ERC20Token], approval_function):
"""Approve the EtherDelta contract to fully access balances of specified tokens.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances.
approval_function: Approval function (i.e. approval mode).
"""
assert(isinstance(tokens, list))
assert(callable(approval_function))
for token in tokens:
approval_function(token, self.address, 'EtherDelta')
def admin(self) -> Address:
"""Returns the address of the admin account.
Returns:
The address of the admin account.
"""
return Address(self._contract.functions.admin().call())
def fee_account(self) -> Address:
"""Returns the address of the fee account i.e. the account that receives all fees collected.
Returns:
The address of the fee account.
"""
return Address(self._contract.functions.feeAccount().call())
def account_levels_addr(self) -> Address:
"""Returns the address of the AccountLevels contract.
Returns:
The address of the AccountLevels contract.
"""
return Address(self._contract.functions.accountLevelsAddr().call())
def fee_make(self) -> Wad:
"""Returns the maker fee configured in the contract.
Returns:
The maker fee.
"""
return Wad(self._contract.functions.feeMake().call())
def fee_take(self) -> Wad:
"""Returns the taker fee configured in the contract.
Returns:
The taker fee.
"""
return Wad(self._contract.functions.feeTake().call())
def fee_rebate(self) -> Wad:
"""Returns the rebate fee configured in the contract.
Plase see the contract source code for more details.
Returns:
The rebate fee.
"""
return Wad(self._contract.functions.feeRebate().call())
def past_trade(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogTrade]:
"""Synchronously retrieve past LogTrade events.
`LogTrade` events are emitted by the EtherDelta contract every time someone takes an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogTrade` events represented as :py:class:`pymaker.etherdelta.LogTrade` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'Trade', LogTrade, number_of_past_blocks, event_filter)
def deposit(self, amount: Wad) -> Transact:
"""Deposits `amount` of raw ETH to EtherDelta.
Args:
amount: Amount of raw ETH to be deposited on EtherDelta.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value})
def withdraw(self, amount: Wad) -> Transact:
"""Withdraws `amount` of raw ETH from EtherDelta.
The withdrawn ETH will get transferred to the calling account.
Args:
amount: Amount of raw ETH to be withdrawn from EtherDelta.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value])
def balance_of(self, user: Address) -> Wad:
"""Returns the amount of raw ETH deposited by the specified user.
Args:
user: Address of the user to check the balance of.
Returns:
The raw ETH balance kept in the EtherDelta contract by the specified user.
"""
assert(isinstance(user, Address))
return Wad(self._contract.functions.balanceOf('0x0000000000000000000000000000000000000000', user.address).call())
def deposit_token(self, token: Address, amount: Wad) -> Transact:
"""Deposits `amount` of ERC20 token `token` to EtherDelta.
Tokens will be pulled from the calling account, so the EtherDelta contract needs
to have appropriate allowance. Either call `approve()` or set the allowance manually
before trying to deposit tokens.
Args:
token: Address of the ERC20 token to be deposited.
amount: Amount of token `token` to be deposited to EtherDelta.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(token, Address))
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'depositToken',
[token.address, amount.value])
def withdraw_token(self, token: Address, amount: Wad) -> Transact:
"""Withdraws `amount` of ERC20 token `token` from EtherDelta.
Tokens will get transferred to the calling account.
Args:
token: Address of the ERC20 token to be withdrawn.
amount: Amount of token `token` to be withdrawn from EtherDelta.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(token, Address))
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdrawToken',
[token.address, amount.value])
def balance_of_token(self, token: Address, user: Address) -> Wad:
"""Returns the amount of ERC20 token `token` deposited by the specified user.
Args:
token: Address of the ERC20 token return the balance of.
user: Address of the user to check the balance of.
Returns:
The ERC20 token `token` balance kept in the EtherDelta contract by the specified user.
"""
assert(isinstance(token, Address))
assert(isinstance(user, Address))
return Wad(self._contract.functions.balanceOf(token.address, user.address).call())
def create_order(self,
pay_token: Address,
pay_amount: Wad,
buy_token: Address,
buy_amount: Wad,
expires: int) -> Order:
"""Creates a new off-chain order.
Although it's not necessary to have any amount of `pay_token` deposited to EtherDelta
before placing an order, nobody will be able to take this order until some balance of
'pay_token' is provided.
If you want to trade raw ETH, pass `Address('0x0000000000000000000000000000000000000000')`
as either `pay_token` or `buy_token`.
Args:
pay_token: Address of the ERC20 token you want to put on sale.
pay_amount: Amount of the `pay_token` token you want to put on sale.
buy_token: Address of the ERC20 token you want to be paid with.
buy_amount: Amount of the `buy_token` you want to receive.
expires: The block number after which the order will expire.
Returns:
Newly created order as an instance of the :py:class:`pymaker.etherdelta.Order` class.
"""
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(isinstance(expires, int) and (expires > 0))
assert(pay_amount > Wad(0))
assert(buy_amount > Wad(0))
nonce = self.random_nonce()
order_hash = hashlib.sha256(encode_address(self.address) +
encode_address(buy_token) +
encode_uint256(buy_amount.value) +
encode_address(pay_token) +
encode_uint256(pay_amount.value) +
encode_uint256(expires) +
encode_uint256(nonce)).digest()
signature = eth_sign(order_hash, self.web3)
v, r, s = to_vrs(signature)
return Order(self, Address(self.web3.eth.defaultAccount), pay_token, pay_amount, buy_token, buy_amount,
expires, nonce, v, r, s)
def amount_available(self, order: Order) -> Wad:
"""Returns the amount that is still available (tradeable) for an order.
The result will never be greater than `order.buy_amount - amount_filled(order)`.
It can be lower though if the order maker does not have enough balance on EtherDelta.
Args:
order: The order object you want to know the available amount of.
Returns:
The available amount for the order, in terms of `buy_token`.
"""
assert(isinstance(order, Order))
return Wad(self._contract.functions.availableVolume(order.buy_token.address,
order.buy_amount.value,
order.pay_token.address,
order.pay_amount.value,
order.expires,
order.nonce,
order.maker.address,
order.v if hasattr(order, 'v') else 0,
order.r if hasattr(order, 'r') else bytes(),
order.s if hasattr(order, 's') else bytes()).call())
def amount_filled(self, order: Order) -> Wad:
"""Returns the amount that has been already filled for an order.
The result will never be greater than `order.buy_amount`. It can be lower though
if the order maker does not have enough balance on EtherDelta.
If an order has been cancelled, `amount_filled(order)` will be always equal
to `order.buy_amount`. Cancelled orders basically look like completely filled ones.
Args:
order: The order object you want to know the filled amount of.
Returns:
The amount already filled for the order, in terms of `buy_token`.
"""
assert(isinstance(order, Order))
return Wad(self._contract.functions.amountFilled(order.buy_token.address,
order.buy_amount.value,
order.pay_token.address,
order.pay_amount.value,
order.expires,
order.nonce,
order.maker.address,
order.v if hasattr(order, 'v') else 0,
order.r if hasattr(order, 'r') else bytes(),
order.s if hasattr(order, 's') else bytes()).call())
def trade(self, order: Order, amount: Wad) -> Transact:
"""Takes (buys) an order.
`amount` is in `buy_token` terms, it is the amount you want to buy with. It can not be higher
than `amount_available(order)`.
The 'amount' of `buy_token` tokens will get deducted from your EtherDelta balance if the trade was
successful. The corresponding amount of `pay_token` tokens will be added to your EtherDelta balance.
Args:
order: The order you want to take (buy).
amount: Amount of `buy_token` tokens that you want to be deducted from your EtherDelta balance
in order to buy a corresponding amount of `pay_token` tokens.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'trade',
[order.buy_token.address,
order.buy_amount.value,
order.pay_token.address,
order.pay_amount.value,
order.expires,
order.nonce,
order.maker.address,
order.v if hasattr(order, 'v') else 0,
order.r if hasattr(order, 'r') else bytes(),
order.s if hasattr(order, 's') else bytes(),
amount.value])
def can_trade(self, order: Order, amount: Wad) -> bool:
"""Verifies whether a trade can be executed.
Verifies whether amount `amount` can be traded on order `order` i.e. whether the `trade()`
method executed with exactly the same parameters should succeed.
Args:
order: The order you want to verify the trade for.
amount: Amount expressed in terms of `buy_token` that you want to verify the trade for.
Returns:
'True' if the given amount can be traded on this order. `False` otherwise.
"""
assert(isinstance(order, Order))
assert(isinstance(amount, Wad))
return self._contract.functions.testTrade(order.buy_token.address,
order.buy_amount.value,
order.pay_token.address,
order.pay_amount.value,
order.expires,
order.nonce,
order.maker.address,
order.v if hasattr(order, 'v') else 0,
order.r if hasattr(order, 'r') else bytes(),
order.s if hasattr(order, 's') else bytes(),
amount.value,
self.web3.eth.defaultAccount).call()
def cancel_order(self, order: Order) -> Transact:
"""Cancels an existing order.
Orders can be cancelled only by their owners.
Args:
order: The order you want to cancel.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cancelOrder',
[order.buy_token.address,
order.buy_amount.value,
order.pay_token.address,
order.pay_amount.value,
order.expires,
order.nonce,
order.v if hasattr(order, 'v') else 0,
order.r if hasattr(order, 'r') else bytes(),
order.s if hasattr(order, 's') else bytes()])
@staticmethod
def random_nonce():
return random.randint(1, 2**32 - 1)
def __repr__(self):
return f"EtherDelta('{self.address}')"
class EtherDeltaApi:
"""A client for the EtherDelta API backend.
Attributes:
client_tool_directory: Directory containing the `etherdelta-client` tool.
client_tool_command: Command for running the `etherdelta-client` tool.
api_server: Base URL of the EtherDelta API backend server.
number_of_attempts: Number of attempts to run the `etherdelta-client` tool.
retry_interval: Interval between subsequent retries if order placement failed,
within one `etherdelta-client` run.
timeout: Timeout after which publish order is considered as failed by the
`etherdelta-client` tool. If number_of_attempts > 1, this tool will be
run several times though.
"""
logger = logging.getLogger()
def __init__(self,
client_tool_directory: str,
client_tool_command: str,
api_server: str,
number_of_attempts: int,
retry_interval: int,
timeout: int):
assert(isinstance(client_tool_directory, str))
assert(isinstance(client_tool_command, str))
assert(isinstance(api_server, str))
assert(isinstance(number_of_attempts, int))
assert(isinstance(retry_interval, int))
assert(isinstance(timeout, int))
self.client_tool_directory = client_tool_directory
self.client_tool_command = client_tool_command
self.api_server = api_server
self.number_of_attempts = number_of_attempts
self.retry_interval = retry_interval
self.timeout = timeout
def publish_order(self, order: Order):
assert(isinstance(order, Order))
def _publish_order_via_client() -> bool:
process = Popen(self.client_tool_command.split() + ['--url', self.api_server,
'--timeout', str(self.timeout),
'--retry-interval', str(self.retry_interval),
json.dumps(order.to_json())],
cwd=self.client_tool_directory, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=False)
result = process.communicate(None, timeout=self.timeout+15)
stdout = result[0].decode("utf-8").rstrip().replace('\n', ' -> ')
stderr = result[1].decode("utf-8").rstrip().replace('\n', ' -> ')
if len(stdout) > 0:
if process.returncode == 0:
self.logger.info(f"Output from 'etherdelta-client': {stdout}")
else:
self.logger.warning(f"Non-zero exit code output from 'etherdelta-client': {stdout}")
if len(stderr) > 0:
self.logger.fatal(f"Error from 'etherdelta-client': {stderr}")
return process.returncode == 0
def _run():
for attempt in range(self.number_of_attempts):
self.logger.info(f"Sending order (attempt #{attempt+1}): {order}")
if _publish_order_via_client():
self.logger.info(f"Order {order} sent successfully")
return
self.logger.warning(f"Failed to send order {order}")
threading.Thread(target=_run, daemon=True).start()
def __repr__(self):
return f"EtherDeltaApi()"
================================================
FILE: pymaker/feed.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.auth import DSAuth
class DSValue(DSAuth):
"""A client for the `DSValue` contract, a single-value data feed.
`DSValue` is a single-value data feed, which means it can be in one of two states.
It can either contain a value (in which case `has_value()` returns `True` and the read methods
return that value) or be empty (in which case `has_value()` returns `False` and the read
methods throw exceptions).
`DSValue` can be populated with a new value using `poke()` and cleared using `void()`.
Everybody can read from a `DSValue`.
Calling `poke()` and `void()` is usually whitelisted to some addresses only.
The `DSValue` contract keeps the value as a 32-byte array (Ethereum `bytes32` type).
Methods have been provided to cast it into `int`, read as hex etc.
You can find the source code of the `DSValue` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSValue` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSValue.abi')
bin = Contract._load_bin(__name__, 'abi/DSValue.bin')
@staticmethod
def deploy(web3: Web3):
return DSValue(web3=web3, address=Contract._deploy(web3, DSValue.abi, DSValue.bin, []))
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def has_value(self) -> bool:
"""Checks whether this instance contains a value.
Returns:
`True` if this instance contains a value, which can be read. `False` otherwise.
"""
return self._contract.functions.peek().call()[1]
def read(self) -> bytes:
"""Reads the current value from this instance as a byte array.
If this instance does not contain a value, throws an exception.
Returns:
A 32-byte array with the current value of this instance.
"""
return self._contract.functions.read().call()
def read_as_hex(self) -> str:
"""Reads the current value from this instance and converts it to a hex string.
If this instance does not contain a value, throws an exception.
Returns:
A string with a hexadecimal representation of the current value of this instance.
"""
return ''.join(hex(x)[2:].zfill(2) for x in self.read())
def read_as_int(self) -> int:
"""Reads the current value from this instance and converts it to an int.
If the value is actually a `Ray` or a `Wad`, you can convert it to one using `Ray(...)`
or `Wad(...)`. Please see `Ray` or `Wad` for more details.
If this instance does not contain a value, throws an exception.
Returns:
An integer representation of the current value of this instance.
"""
return int(self.read_as_hex(), 16)
def poke(self, new_value: bytes) -> Transact:
"""Populates this instance with a new value.
Args:
new_value: A 32-byte array with the new value to be set.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(new_value, bytes))
assert(len(new_value) == 32)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'poke', [new_value])
def poke_with_int(self, new_value: int) -> Transact:
"""Populates this instance with a new value.
Handles the conversion of a Python `int` into the Solidity `bytes32` type automatically.
If the value you want to set is actually a `Ray` or a `Wad`, you can get the integer value from them
by accessing their `value` property. Please see `Ray` or `Wad` for more details.
Args:
new_value: A non-negative integer with the new value to be set.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(new_value, int))
assert(new_value >= 0)
return self.poke(new_value.to_bytes(32, byteorder='big'))
def void(self) -> Transact:
"""Removes the current value from this instance.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'void', [])
def __repr__(self):
return f"DSValue('{self.address}')"
================================================
FILE: pymaker/gas.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import math
from typing import Optional
from web3 import Web3
class GasPrice(object):
GWEI = 1000000000
"""Abstract class, which can be inherited for implementing different gas price strategies.
`GasPrice` class contains only one method, `get_gas_price`, which is responsible for
returning the gas price (in Wei) for a specific point in time. It is possible to build
custom gas price strategies by implementing this method so the gas price returned
increases over time. The piece of code responsible for sending Ethereum transactions
(please see :py:class:`pymaker.Transact`) will in this case overwrite the transaction
with another one, using the same `nonce` but increasing gas price. If the value returned
by `get_gas_price` does not go up, no new transaction gets submitted to the network.
An example custom gas price strategy my be: start with 10 GWei. If transaction has not been
confirmed within 10 minutes, try again with 15 GWei. If still no confirmation, increase
to 30 GWei and then wait indefinitely for confirmation.
"""
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
"""Return gas price applicable for a given point in time.
Bear in mind that Parity (don't know about other Ethereum nodes) requires the gas
price for overwritten transactions to go up by at least 10%. Also, you may return
`None` which will make the node use the default gas price, but once you returned
a numeric value (gas price in Wei), you shouldn't switch back to `None` as such
transaction also may not get properly overwritten.
Args:
time_elapsed: Number of seconds since this specific Ethereum transaction
has been originally sent for the first time.
Returns:
Gas price in Wei, or `None` if default gas price should be used. Default gas price
means it's the Ethereum node the keeper is connected to will decide on the gas price.
"""
raise NotImplementedError("Please implement this method")
class DefaultGasPrice(GasPrice):
"""Default gas price.
Uses the default gas price i.e. gas price will be decided by the Ethereum node
the keeper is connected to.
"""
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
return None
class NodeAwareGasPrice(GasPrice):
"""Abstract baseclass which is Web3-aware.
Retrieves the default gas price provided by the Ethereum node to be consumed by subclasses.
"""
def __init__(self, web3: Web3):
assert isinstance(web3, Web3)
if self.__class__ == NodeAwareGasPrice:
raise NotImplementedError('This class is not intended to be used directly')
self.web3 = web3
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
"""If user wants node to choose gas price, they should use DefaultGasPrice for the same functionality
without an additional HTTP request. This baseclass exists to let a subclass manipulate the node price."""
raise NotImplementedError("Please implement this method")
def get_node_gas_price(self):
return max(self.web3.manager.request_blocking("eth_gasPrice", []), 1 * self.GWEI)
class FixedGasPrice(GasPrice):
"""Fixed gas price.
Uses specified gas price instead of the default price suggested by the Ethereum
node the keeper is connected to. The gas price may be later changed (while the transaction
is still in progress) by calling the `update_gas_price` method.
Attributes:
gas_price: Gas price to be used (in Wei).
"""
def __init__(self, gas_price: int):
assert(isinstance(gas_price, int))
self.gas_price = gas_price
def update_gas_price(self, new_gas_price: int):
"""Changes the initial gas price to a higher value, preferably higher.
The only reason when calling this function makes sense is when an async transaction is in progress.
In this case, the loop waiting for the transaction to be mined (see :py:class:`pymaker.Transact`)
will resend the pending transaction again with the new gas price.
As Parity excepts the gas price to rise by at least 10% in replacement transactions, the price
argument supplied to this method should be accordingly higher.
Args:
new_gas_price: New gas price to be set (in Wei).
"""
assert(isinstance(new_gas_price, int))
self.gas_price = new_gas_price
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
assert(isinstance(time_elapsed, int))
return self.gas_price
class IncreasingGasPrice(GasPrice):
"""Constantly increasing gas price.
Start with `initial_price`, then increase it by fixed amount `increase_by` every `every_secs` seconds
until the transaction gets confirmed. There is an optional upper limit.
Attributes:
initial_price: The initial gas price in Wei i.e. the price the transaction
is originally sent with.
increase_by: Gas price increase in Wei, which will happen every `every_secs` seconds.
every_secs: Gas price increase interval (in seconds).
max_price: Optional upper limit.
"""
def __init__(self, initial_price: int, increase_by: int, every_secs: int, max_price: Optional[int]):
assert(isinstance(initial_price, int))
assert(isinstance(increase_by, int))
assert(isinstance(every_secs, int))
assert(isinstance(max_price, int) or max_price is None)
assert(initial_price > 0)
assert(increase_by > 0)
assert(every_secs > 0)
if max_price is not None:
assert(max_price > 0)
self.initial_price = initial_price
self.increase_by = increase_by
self.every_secs = every_secs
self.max_price = max_price
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
assert(isinstance(time_elapsed, int))
result = self.initial_price + int(time_elapsed/self.every_secs)*self.increase_by
if self.max_price is not None:
result = min(result, self.max_price)
return result
class GeometricGasPrice(GasPrice):
"""Geometrically increasing gas price.
Start with `initial_price`, then increase it every 'every_secs' seconds by a fixed coefficient.
Coefficient defaults to 1.125 (12.5%), the minimum increase for Parity to replace a transaction.
Coefficient can be adjusted, and there is an optional upper limit.
Attributes:
initial_price: The initial gas price in Wei i.e. the price the transaction is originally sent with.
every_secs: Gas price increase interval (in seconds).
coefficient: Gas price multiplier, defaults to 1.125.
max_price: Optional upper limit, defaults to None.
"""
def __init__(self, initial_price: int, every_secs: int, coefficient=1.125, max_price: Optional[int] = None):
assert (isinstance(initial_price, int))
assert (isinstance(every_secs, int))
assert (isinstance(max_price, int) or max_price is None)
assert (initial_price > 0)
assert (every_secs > 0)
assert (coefficient > 1)
if max_price is not None:
assert(max_price >= initial_price)
self.initial_price = initial_price
self.every_secs = every_secs
self.coefficient = coefficient
self.max_price = max_price
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
assert(isinstance(time_elapsed, int))
result = self.initial_price
if time_elapsed >= self.every_secs:
for second in range(math.floor(time_elapsed/self.every_secs)):
result *= self.coefficient
if self.max_price is not None:
result = min(result, self.max_price)
return math.ceil(result)
================================================
FILE: pymaker/governance.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import datetime
from web3 import Web3
from typing import List
from pprint import pformat
from pymaker import Contract, Address, Transact, Wad
from pymaker.auth import DSAuth
from pymaker.token import DSToken
# TODO: Complete implementation and unit test
class DSPause(Contract):
"""A client for the `DSPause` contract, which schedules function calls after a predefined delay.
You can find the source code of the `DSPause` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSPause` contract.
"""
class Plan:
def __init__(self, usr: Address, fax: bytes, eta: datetime):
"""Creates a plan to be executed later.
Args:
usr: Address of the caller
fax: Identifies the calldata
eta: Identifies the earliest time of execution
"""
assert isinstance(usr, Address)
assert isinstance(fax, bytes)
assert isinstance(eta, datetime.datetime)
self.usr = usr
self.fax = fax
self.eta = eta.timestamp()
abi = Contract._load_abi(__name__, 'abi/DSPause.abi')
bin = Contract._load_bin(__name__, 'abi/DSPause.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, delay: int, owner: Address, ds_auth: DSAuth):
return DSPause(web3=web3, address=Contract._deploy(web3, DSPause.abi, DSPause.bin,
[delay, owner.address, ds_auth.address.address]))
# TODO: Awaiting updated ABI/BIN from dss-deploy
# def plot(self, plan: Plan):
# return self._transact(plan, "plot")
def drop(self, plan: Plan):
return self._transact(plan, "drop")
def exec(self, plan: Plan) -> Transact:
return self._transact(plan, "exec")
def _transact(self, plan: Plan, function_name: str) -> Transact:
assert isinstance(plan, DSPause.Plan)
assert isinstance(function_name, str)
return Transact(self, self.web3, self.abi, self.address, self._contract, function_name,
[plan.usr.address, plan.fax, int(plan.eta)])
# TODO: Implement and unit test
class DSRoles(Contract):
"""A client for the `DSRoles` contract, which manages lists of user roles and capabilities.
You can find the source code of the `DSRoles` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSRoles` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSRoles.abi')
bin = Contract._load_bin(__name__, 'abi/DSRoles.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def is_root_user(self, who: Address) -> bool:
assert isinstance(who, Address)
return bool(self._contract.functions.isUserRoot(who.address).call())
def set_root_user(self, who: Address, enabled=True) -> Transact:
assert isinstance(who, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract,
"setRootUser", [who.address, enabled])
def has_user_role(self, who: Address, role: int) -> bool:
assert isinstance(who, Address)
assert isinstance(role, int)
assert 0 <= role <= int('0xFFFFFFFF')
return bool(self._contract.functions.hasUserRole(who.address, role).call())
def set_user_role(self, who: Address, role: int, enabled=True) -> Transact:
assert isinstance(who, Address)
assert isinstance(role, int)
assert 0 <= role <= int('0xFFFFFFFF')
return Transact(self, self.web3, self.abi, self.address, self._contract,
"setUserRole", [who.address, role, enabled])
class Etch:
def __init__(self, log):
self.slate = log['args']['slate']
self.address = log['address']
self.block_number = log['blockNumber']
self.log_index = log['logIndex']
self.tx_hash = log['transactionHash']
def __repr__(self):
return pformat(vars(self))
class DSChief(Contract):
"""A client for the `DSChief` contract, which manages lists of user roles and capabilities.
You can find the source code of the `DSChief` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSChief` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSChief.abi')
bin = Contract._load_bin(__name__, 'abi/DSChief.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def live(self) -> bool:
return self._contract.functions.live().call()
def iou(self) -> DSToken:
return DSToken(self.web3, Address(self._contract.functions.IOU().call()))
def get_votes(self, address):
return self._contract.functions.votes(address).call()
def get_yay(self, slate, position) -> str:
return self._contract.functions.slates(slate, position).call()
def get_deposits(self, address) -> Wad:
return Wad(self._contract.functions.deposits(address).call())
def get_approvals(self, address) -> Wad:
return Wad(self._contract.functions.approvals(address).call())
def get_hat(self) -> Address:
return Address(self._contract.functions.hat().call())
def get_max_yays(self) -> int:
return self._contract.functions.MAX_YAYS().call()
def launch(self) -> Transact:
return Transact(self, self.web3, self.abi, self.address, self._contract, 'launch', [])
def lock(self, amount: Wad) -> Transact:
assert isinstance(amount, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'lock', [amount.value])
def free(self, amount: Wad) -> Transact:
assert isinstance(amount, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'free', [amount.value])
def etch(self, yays: List) -> Transact:
assert isinstance(yays, List)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'etch(address[])', [yays])
def vote_yays(self, yays: List) -> Transact:
assert isinstance(yays, List)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'vote(address[])', [yays])
def vote_etch(self, etch: Etch) -> Transact:
assert isinstance(etch, Etch)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'vote(bytes32)', [etch.slate])
def lift(self, whom: Address) -> Transact:
assert isinstance(whom, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'lift', [whom.address])
def past_etch(self, number_of_past_blocks: int, event_filter: dict = None) -> List[Etch]:
"""Synchronously retrieve past Etch events.
`Etch` events are emitted by the ds-chief contract every time someone places a vote.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `Etch` events represented as :py:class:`pymaker.governance.Etch` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'Etch', Etch, number_of_past_blocks, event_filter)
def past_etch_in_range(self, from_block: int, to_block: int, event_filter: dict = None) -> List[Etch]:
"""Synchronously retrieve past Etch events.
`Etch` events are emitted by the ds-chief contract every time someone places a vote.
Args:
from_block: Starting block to retrieve the events from.
to_block: Last block to retrieve the events to.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `Etch` events represented as :py:class:`pymaker.governance.Etch` class.
"""
assert(isinstance(from_block, int))
assert(isinstance(to_block, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events_in_block_range(self._contract, 'Etch', Etch, from_block, to_block, event_filter)
================================================
FILE: pymaker/ilk.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019-2021 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from typing import Optional
from web3 import Web3
from pymaker.numeric import Wad, Ray, Rad
class Ilk:
"""Models one collateral type, the combination of a token and a set of risk parameters.
For example, ETH-A and ETH-B are different collateral types with the same underlying token (WETH) but with
different risk parameters.
"""
def __init__(self, name: str, rate: Optional[Ray] = None,
ink: Optional[Wad] = None,
art: Optional[Wad] = None,
spot: Optional[Ray] = None,
line: Optional[Rad] = None,
dust: Optional[Rad] = None):
assert (isinstance(name, str))
assert (isinstance(rate, Ray) or (rate is None))
assert (isinstance(ink, Wad) or (ink is None))
assert (isinstance(art, Wad) or (art is None))
assert (isinstance(spot, Ray) or (spot is None))
assert (isinstance(line, Rad) or (line is None))
assert (isinstance(dust, Rad) or (dust is None))
self.name = name
self.rate = rate
self.ink = ink
self.art = art
self.spot = spot
self.line = line
self.dust = dust
def toBytes(self):
return Web3.toBytes(text=self.name).ljust(32, bytes(1))
@staticmethod
def fromBytes(ilk: bytes):
assert (isinstance(ilk, bytes))
name = Web3.toText(ilk.strip(bytes(1)))
return Ilk(name)
def __eq__(self, other):
assert isinstance(other, Ilk)
return (self.name == other.name) \
and (self.rate == other.rate) \
and (self.ink == other.ink) \
and (self.art == other.art) \
and (self.spot == other.spot) \
and (self.line == other.line) \
and (self.dust == other.dust)
def __repr__(self):
repr = ''
if self.rate:
repr += f' rate={self.rate}'
if self.ink:
repr += f' Ink={self.ink}'
if self.art:
repr += f' Art={self.art}'
if self.spot:
repr += f' spot={self.spot}'
if self.line:
repr += f' line={self.line}'
if self.dust:
repr += f' dust={self.dust}'
if repr:
repr = f'[{repr.strip()}]'
return f"Ilk('{self.name}'){repr}"
================================================
FILE: pymaker/join.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019-2021 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.ilk import Ilk
from pymaker.token import DSToken, ERC20Token
from pymaker.numeric import Wad, Ray, Rad
logger = logging.getLogger()
class Join(Contract):
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
self._token: DSToken = None
def approve(self, approval_function, source: Address):
assert(callable(approval_function))
assert isinstance(source, Address)
approval_function(ERC20Token(web3=self.web3, address=source), self.address, self.__class__.__name__)
def approve_token(self, approval_function, **kwargs):
return self.approve(approval_function, self._token.address, **kwargs)
def join(self, usr: Address, value: Wad) -> Transact:
assert isinstance(usr, Address)
assert isinstance(value, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract,
'join', [usr.address, value.value])
def exit(self, usr: Address, value: Wad) -> Transact:
assert isinstance(usr, Address)
assert isinstance(value, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract,
'exit', [usr.address, value.value])
class DaiJoin(Join):
"""A client for the `DaiJoin` contract, which allows the CDP holder to draw Dai from their Urn and repay it.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DaiJoin.abi')
bin = Contract._load_bin(__name__, 'abi/DaiJoin.bin')
def __init__(self, web3: Web3, address: Address):
super(DaiJoin, self).__init__(web3, address)
self._token = self.dai()
def dai(self) -> DSToken:
address = Address(self._contract.functions.dai().call())
return DSToken(self.web3, address)
class GemJoin(Join):
"""A client for the `GemJoin` contract, which allows the user to deposit collateral into a new or existing vault.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/GemJoin.abi')
bin = Contract._load_bin(__name__, 'abi/GemJoin.bin')
def __init__(self, web3: Web3, address: Address):
super(GemJoin, self).__init__(web3, address)
self._token = self.gem()
def ilk(self):
return Ilk.fromBytes(self._contract.functions.ilk().call())
def gem(self) -> DSToken:
address = Address(self._contract.functions.gem().call())
return DSToken(self.web3, address)
def dec(self) -> int:
return 18
class GemJoin5(GemJoin):
"""A client for the `GemJoin5` contract, which allows the user to deposit collateral into a new or existing vault.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/GemJoin5.abi')
bin = Contract._load_bin(__name__, 'abi/GemJoin5.bin')
def __init__(self, web3: Web3, address: Address):
super(GemJoin5, self).__init__(web3, address)
self._token = self.gem()
def dec(self) -> int:
return int(self._contract.functions.dec().call())
================================================
FILE: pymaker/keys.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import getpass
from typing import Optional
from eth_account import Account
from web3 import Web3
from web3.middleware import construct_sign_and_send_raw_middleware
from pymaker import Address
_registered_accounts = {}
def register_keys(web3: Web3, keys: Optional[list]):
for key in keys or []:
register_key(web3, key)
def register_key(web3: Web3, key: str):
assert(isinstance(web3, Web3))
parsed = {}
for p in key.split(","):
var, val = p.split("=")
parsed[var] = val
register_key_file(web3, parsed.get('key_file'), parsed.get('pass_file', None))
def register_key_file(web3: Web3, key_file: str, pass_file: Optional[str] = None):
assert(isinstance(web3, Web3))
assert(isinstance(key_file, str))
assert(isinstance(pass_file, str) or (pass_file is None))
with open(key_file) as key_file_open:
read_key = key_file_open.read()
if pass_file:
with open(pass_file) as pass_file_open:
read_pass = pass_file_open.read().replace("\n", "")
else:
read_pass = getpass.getpass(prompt=f"Password for {key_file}: ")
private_key = Account.decrypt(read_key, read_pass)
register_private_key(web3, private_key)
def get_private_key(web3: Web3, key: str):
assert(isinstance(web3, Web3))
assert(isinstance(key, str))
parsed = {}
for p in key.split(","):
var, val = p.split("=")
parsed[var] = val
with open(parsed.get('key_file')) as key_file_open:
read_key = key_file_open.read()
private_key = web3
if parsed.get('pass_file'):
with open(parsed.get('pass_file')) as pass_file_open:
read_pass = pass_file_open.read().replace("\n", "")
else:
read_pass = getpass.getpass(prompt=f"Password for {key_file}: ")
private_key = Account.decrypt(read_key, read_pass).hex()
return private_key
def register_private_key(web3: Web3, private_key):
assert(isinstance(web3, Web3))
account = Account.privateKeyToAccount(private_key)
_registered_accounts[(web3, Address(account.address))] = account
web3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))
================================================
FILE: pymaker/lifecycle.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import datetime
import logging
import signal
import threading
import time
import pytz
from pymaker.sign import eth_sign
from web3 import Web3
from web3.exceptions import BlockNotFound, BlockNumberOutofRange
from pymaker import register_filter_thread, any_filter_thread_present, stop_all_filter_threads, all_filter_threads_alive
from pymaker.util import AsyncCallback
def trigger_event(event: threading.Event):
assert(isinstance(event, threading.Event))
event.set()
class Lifecycle:
"""Main keeper lifecycle controller.
This is a utility class helping to build a proper keeper lifecycle. Lifecycle
consists of startup phase, subscribing to Web3 events and/or timers, and
a shutdown phase at the end.
One could as well initialize the keeper and start listening for events themselves
i.e. without using `Lifecycle`, just that this class takes care of some quirks.
For example the listener threads of web3.py tend to die at times, which causes
the client to stop receiving events without even knowing something might be wrong.
`Lifecycle` does some tricks to monitor for it, and shutdowns the keeper the
moment it detects something may be wrong with the listener threads.
Other quirk is the new block filter callback taking more time to execute that
the time between subsequent blocks. If you do not handle it explicitly,
the event queue will pile up and the keeper won't work as expected.
`Lifecycle` used :py:class:`pymaker.util.AsyncCallback` to handle it properly.
It also handles:
- waiting for the node to have at least one peer and sync before starting the keeper,
- checking if the keeper account (`web3.eth.defaultAccount`) is unlocked.
Also, once the lifecycle is initialized, keeper starts listening for SIGINT/SIGTERM
signals and starts a graceful shutdown if it receives any of them.
The typical usage pattern is as follows:
with Web3Lifecycle(self.web3) as lifecycle:
lifecycle.on_startup(self.some_startup_function)
lifecycle.on_block(self.do_something)
lifecycle.every(15, self.do_something_else)
lifecycle.on_shutdown(self.some_shutdown_function)
once called like that, `Lifecycle` will enter an infinite loop.
Attributes:
web3: Instance of the `Web3` class from `web3.py`. Optional.
"""
logger = logging.getLogger()
def __init__(self, web3: Web3 = None):
self.web3 = web3
self.do_wait_for_sync = True
self.delay = 0
self.wait_for_functions = []
self.startup_function = None
self.shutdown_function = None
self.block_function = None
self.every_timers = []
self.event_timers = []
self.terminated_internally = False
self.terminated_externally = False
self.fatal_termination = False
self._at_least_one_every = False
self._last_block_time = None
self._on_block_callback = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Initialization phase
if self.web3:
self.logger.info(f"Keeper connected to {self.web3.provider}")
if self.web3.eth.defaultAccount and self.web3.eth.defaultAccount != "0x0000000000000000000000000000000000000000":
self.logger.info(f"Keeper operating as {self.web3.eth.defaultAccount}")
self._check_account_unlocked()
else:
self.logger.info(f"Keeper not operating as any particular account")
# web3 calls do not work correctly if defaultAccount is empty
self.web3.eth.defaultAccount = "0x0000000000000000000000000000000000000000"
else:
self.logger.info(f"Keeper initializing")
# Wait for sync and peers
if self.web3 and self.do_wait_for_sync:
self._wait_for_init()
# Initial delay
if self.delay > 0:
self.logger.info(f"Waiting for {self.delay} seconds of initial delay...")
time.sleep(self.delay)
# Initial checks
if len(self.wait_for_functions) > 0:
self.logger.info("Waiting for initial checks to pass...")
for index, (wait_for_function, max_wait) in enumerate(self.wait_for_functions, start=1):
start_time = time.time()
while True:
try:
result = wait_for_function()
except Exception as e:
self.logger.exception(f"Initial check #{index} failed with an exception: '{e}'")
result = False
if result:
break
if time.time() - start_time >= max_wait:
self.logger.warning(f"Initial check #{index} took more than {max_wait} seconds to pass, skipping")
break
time.sleep(0.1)
# Startup phase
if self.startup_function:
self.logger.info("Executing keeper startup logic")
self.startup_function()
# Bind `on_block`, bind `every`
# Enter the main loop
self._start_watching_blocks()
self._start_every_timers()
self._main_loop()
# Enter shutdown process
self.logger.info("Shutting down the keeper")
# Disable all filters
if any_filter_thread_present():
self.logger.info("Waiting for all threads to terminate...")
stop_all_filter_threads()
# If the `on_block` callback is still running, wait for it to terminate
if self._on_block_callback is not None:
self.logger.info("Waiting for outstanding callback to terminate...")
self._on_block_callback.wait()
# If any every (timer) callback is still running, wait for it to terminate
if len(self.every_timers) > 0:
self.logger.info("Waiting for outstanding timers to terminate...")
for timer in self.every_timers:
timer[1].wait()
# If any event callback is still running, wait for it to terminate
if len(self.event_timers) > 0:
self.logger.info("Waiting for outstanding events to terminate...")
for timer in self.event_timers:
timer[2].wait()
# Shutdown phase
if self.shutdown_function:
self.logger.info("Executing keeper shutdown logic...")
self.shutdown_function()
self.logger.info("Shutdown logic finished")
self.logger.info("Keeper terminated")
exit(10 if self.fatal_termination else 0)
def _wait_for_init(self):
# In unit-tests waiting for the node to sync does not work correctly.
# So we skip it.
if 'TestRPC' in self.web3.clientVersion:
return
# wait for the client to have at least one peer
if self.web3.net.peer_count == 0:
self.logger.info(f"Waiting for the node to have at least one peer...")
while self.web3.net.peer_count == 0:
time.sleep(0.25)
# wait for the client to sync completely,
# as we do not want to apply keeper logic to stale blocks
if self.web3.eth.syncing:
self.logger.info(f"Waiting for the node to sync...")
while self.web3.eth.syncing:
time.sleep(0.25)
def _check_account_unlocked(self):
try:
eth_sign(bytes("pymaker testing if account is unlocked", "utf-8"), self.web3)
except:
self.logger.exception(f"Account {self.web3.eth.defaultAccount} is not unlocked and no private key supplied for it")
self.logger.fatal(f"Unlocking the account or providing the private key is necessary for the keeper to operate")
exit(-1)
def wait_for_sync(self, wait_for_sync: bool):
assert(isinstance(wait_for_sync, bool))
self.do_wait_for_sync = wait_for_sync
def initial_delay(self, initial_delay: int):
"""Make the keeper wait for specified amount of time before startup.
The primary use case is to allow background threads to have a chance to pull necessary
information like prices, gas prices etc. At the same time we may not want to wait indefinitely
for that information to become available as the price source may be down etc.
Args:
initial_delay: Initial delay on keeper startup (in seconds).
"""
assert(isinstance(initial_delay, int))
self.delay = initial_delay
def wait_for(self, initial_check, max_wait: int):
"""Make the keeper wait for the function to turn true before startup.
The primary use case is to allow background threads to have a chance to pull necessary
information like prices, gas prices etc. At the same time we may not want to wait indefinitely
for that information to become available as the price source may be down etc.
Args:
initial_check: Function which will be evaluated and its result compared to True.
max_wait: Maximum waiting time (in seconds).
"""
assert(callable(initial_check))
assert(isinstance(max_wait, int))
self.wait_for_functions.append((initial_check, max_wait))
def on_startup(self, callback):
"""Register the specified callback to be run on keeper startup.
Args:
callback: Function to be called on keeper startup.
"""
assert(callable(callback))
assert(self.startup_function is None)
self.startup_function = callback
def on_shutdown(self, callback):
"""Register the specified callback to be run on keeper shutdown.
Args:
callback: Function to be called on keeper shutdown.
"""
assert(callable(callback))
assert(self.shutdown_function is None)
self.shutdown_function = callback
def terminate(self, message=None):
if message is not None:
self.logger.warning(message)
self.terminated_internally = True
def on_block(self, callback):
"""Register the specified callback to be run for each new block received by the node.
Args:
callback: Function to be called for each new blocks.
"""
assert(callable(callback))
assert(self.web3 is not None)
assert(self.block_function is None)
self.block_function = callback
def on_event(self, event: threading.Event, min_frequency_in_seconds: int, callback):
"""
Register the specified callback to be called every time event is triggered,
but at least once every `min_frequency_in_seconds`.
Args:
event: Event which should be monitored.
min_frequency_in_seconds: Minimum execution frequency (in seconds).
callback: Function to be called by the timer.
"""
assert(isinstance(event, threading.Event))
assert(isinstance(min_frequency_in_seconds, int))
assert(callable(callback))
self.event_timers.append((event, min_frequency_in_seconds, AsyncCallback(callback)))
def every(self, frequency_in_seconds: int, callback):
"""Register the specified callback to be called by a timer.
Args:
frequency_in_seconds: Execution frequency (in seconds).
callback: Function to be called by the timer.
"""
self.every_timers.append((frequency_in_seconds, AsyncCallback(callback)))
def _sigint_sigterm_handler(self, sig, frame):
if self.terminated_externally:
self.logger.warning("Graceful keeper termination due to SIGINT/SIGTERM already in progress")
else:
self.logger.warning("Keeper received SIGINT/SIGTERM signal, will terminate gracefully")
self.terminated_externally = True
def _start_watching_blocks(self):
def new_block_callback(block_hash):
self._last_block_time = datetime.datetime.now(tz=pytz.UTC)
block = self.web3.eth.getBlock(block_hash)
block_number = block['number']
if not self.web3.eth.syncing:
max_block_number = self.web3.eth.blockNumber
if block_number >= max_block_number:
def on_start():
self.logger.debug(f"Processing block #{block_number} ({block_hash.hex()})")
def on_finish():
self.logger.debug(f"Finished processing block #{block_number} ({block_hash.hex()})")
if not self.terminated_internally and not self.terminated_externally and not self.fatal_termination:
if not self._on_block_callback.trigger(on_start, on_finish):
self.logger.debug(f"Ignoring block #{block_number} ({block_hash.hex()}),"
f" as previous callback is still running")
else:
self.logger.debug(f"Ignoring block #{block_number} as keeper is already terminating")
else:
self.logger.debug(f"Ignoring block #{block_number} ({block_hash.hex()}),"
f" as there is already block #{max_block_number} available")
else:
self.logger.info(f"Ignoring block #{block_number} ({block_hash.hex()}), as the node is syncing")
def new_block_watch():
event_filter = self.web3.eth.filter('latest')
logging.debug(f"Created event filter: {event_filter}")
while True:
try:
for event in event_filter.get_new_entries():
new_block_callback(event)
except (BlockNotFound, BlockNumberOutofRange, ValueError) as ex:
self.logger.warning(f"Node dropped event emitter; recreating latest block filter: {ex}")
event_filter = self.web3.eth.filter('latest')
finally:
time.sleep(1)
if self.block_function:
self._on_block_callback = AsyncCallback(self.block_function)
block_filter = threading.Thread(target=new_block_watch, daemon=True)
block_filter.start()
register_filter_thread(block_filter)
self.logger.info("Watching for new blocks")
def _start_thread_safely(self, t: threading.Thread):
delay = 10
while True:
try:
t.start()
break
except Exception as e:
self.logger.critical(f"Failed to start a thread ({e}), trying again in {delay} seconds")
time.sleep(delay)
def _start_every_timers(self):
for idx, timer in enumerate(self.every_timers, start=1):
self._start_every_timer(idx, timer[0], timer[1])
for idx, event_timer in enumerate(self.event_timers, start=1):
self._start_event_timer(idx, event_timer[0], event_timer[1], event_timer[2])
if len(self.every_timers) > 0:
self.logger.info(f"Started {len(self.every_timers)} timer(s)")
if len(self.event_timers) > 0:
self.logger.info(f"Started {len(self.event_timers)} event(s)")
def _start_every_timer(self, idx: int, frequency_in_seconds: int, callback):
def setup_timer(delay):
timer = threading.Timer(delay, func)
timer.daemon = True
self._start_thread_safely(timer)
def func():
try:
if not self.terminated_internally and not self.terminated_externally and not self.fatal_termination:
def on_start():
self.logger.debug(f"Processing the timer #{idx}")
def on_finish():
self.logger.debug(f"Finished processing the timer #{idx}")
if not callback.trigger(on_start, on_finish):
self.logger.debug(f"Ignoring timer #{idx} as previous one is already running")
else:
self.logger.debug(f"Ignoring timer #{idx} as keeper is already terminating")
except:
setup_timer(frequency_in_seconds)
raise
setup_timer(frequency_in_seconds)
setup_timer(1)
self._at_least_one_every = True
def _start_event_timer(self, idx: int, event: threading.Event, min_frequency_in_seconds: int, callback):
def setup_thread():
self._start_thread_safely(threading.Thread(target=func, daemon=True))
def func():
event_happened = False
while True:
try:
if not self.terminated_internally and not self.terminated_externally and not self.fatal_termination:
def on_start():
self.logger.debug(f"Processing the event #{idx}" if event_happened
else f"Processing the event #{idx} because of minimum frequency")
def on_finish():
self.logger.debug(f"Finished processing the event #{idx}" if event_happened
else f"Finished processing the event #{idx} because of minimum frequency")
assert callback.trigger(on_start, on_finish)
callback.wait()
else:
self.logger.debug(f"Ignoring event #{idx} as keeper is terminating" if event_happened
else f"Ignoring event #{idx} because of minimum frequency as keeper is terminating")
except:
setup_thread()
raise
event_happened = event.wait(timeout=min_frequency_in_seconds)
event.clear()
setup_thread()
self._at_least_one_every = True
def _main_loop(self):
# terminate gracefully on either SIGINT or SIGTERM
signal.signal(signal.SIGINT, self._sigint_sigterm_handler)
signal.signal(signal.SIGTERM, self._sigint_sigterm_handler)
# in case at least one filter has been set up, we enter an infinite loop and let
# the callbacks do the job. in case of no filters, we will not enter this loop
# and the keeper will terminate soon after it started
while any_filter_thread_present() or self._at_least_one_every:
time.sleep(1)
# if the keeper logic asked us to terminate, we do so
if self.terminated_internally:
self.logger.warning("Keeper logic asked for termination, the keeper will terminate")
break
# if SIGINT/SIGTERM asked us to terminate, we do so
if self.terminated_externally:
self.logger.warning("The keeper is terminating due do SIGINT/SIGTERM signal received")
break
# if any exception is raised in filter handling thread (could be an HTTP exception
# while communicating with the node), web3.py does not retry and the filter becomes
# dysfunctional i.e. no new callbacks will ever be fired. we detect it and terminate
# the keeper so it can be restarted.
if not all_filter_threads_alive():
self.logger.fatal("One of filter threads is dead, the keeper will terminate")
self.fatal_termination = True
break
# if we are watching for new blocks and no new block has been reported during
# some time, we assume the watching filter died and terminate the keeper
# so it can be restarted.
#
# this used to happen when the machine that has the node and the keeper running
# was put to sleep and then woken up.
#
# TODO the same thing could possibly happen if we watch any event other than
# TODO a new block. if that happens, we have no reliable way of detecting it now.
if self._last_block_time and (datetime.datetime.now(tz=pytz.UTC) - self._last_block_time).total_seconds() > 300:
if not self.web3.eth.syncing:
self.logger.fatal("No new blocks received for 300 seconds, the keeper will terminate")
self.fatal_termination = True
break
================================================
FILE: pymaker/logging.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from pprint import pformat
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
# Shared between DSNote and many MCD contracts
class LogNote:
def __init__(self, log):
args = log['args']
self.sig = Web3.toHex(args['sig'])
self.usr = args['usr'] if 'usr' in args else None # vat.frob doesn't offer `usr`
self.arg1 = args['arg1'] if 'arg1' in args else None
self.arg2 = args['arg2'] if 'arg2' in args else None
self.arg3 = args['arg3'] if 'arg3' in args else None # Special variant used for vat.frob
self.block = log['blockNumber']
self.tx_hash = log['transactionHash'].hex()
self._data = args['data']
@classmethod
def from_event(cls, event: dict, contract_abi: list):
assert isinstance(event, dict)
assert isinstance(contract_abi, list)
log_note_abi = [abi for abi in contract_abi if abi.get('name') == 'LogNote'][0]
try:
codec = ABICodec(default_registry)
event_data = get_event_data(codec, log_note_abi, event)
return LogNote(event_data)
except ValueError:
# event is not a LogNote
return None
def get_bytes_at_index(self, index: int) -> bytes:
assert isinstance(index, int)
if index > 5:
raise ValueError("Only six words of calldata are provided")
start_index = len(self._data) - ((6-index) * 32) - 28
return self._data[start_index:start_index+32]
def __eq__(self, other):
assert isinstance(other, LogNote)
return self.__dict__ == other.__dict__
def __repr__(self):
return f"LogNote({pformat(vars(self))})"
================================================
FILE: pymaker/model.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2020 mitakash, MikeHathaway
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from pprint import pformat
from typing import Optional, List
from pymaker import Address
from pymaker.numeric import Wad
class Token:
def __init__(self, name: str, address: Optional[Address], decimals: int):
assert(isinstance(name, str))
assert(isinstance(address, Address) or (address is None))
assert(isinstance(decimals, int))
self.name = name
self.address = address
self.decimals = decimals
self.min_amount = Wad.from_number(10 ** -self.decimals)
def normalize_amount(self, amount: Wad) -> Wad:
assert(isinstance(amount, Wad))
return amount * Wad.from_number(10 ** (18 - self.decimals))
def unnormalize_amount(self, amount: Wad) -> Wad:
assert(isinstance(amount, Wad))
return amount * Wad.from_number(10 ** (self.decimals - 18))
def is_eth(self) -> bool:
return self.address == Address('0x0000000000000000000000000000000000000000')
def __eq__(self, other):
assert(isinstance(other, Token))
return self.name == other.name and \
self.address == other.address and \
self.decimals == other.decimals
def __hash__(self):
return hash((self.name, self.address, self.decimals))
def __str__(self):
return self.name
def __repr__(self):
return pformat(vars(self))
class TokenConfig:
def __init__(self, data: dict):
assert (isinstance(data, dict))
self.token_list = []
self.token_config = data['tokens']
def set_token_list(self, data):
assert (isinstance(data, dict))
self.token_list = [Token(name=key,
address=Address(value['tokenAddress']) if 'tokenAddress' in value else None,
decimals=value['tokenDecimals'] if 'tokenDecimals' in value else 18) for key, value in
data['tokens'].items()]
def get_token_list(self) -> List[Token]:
return self.token_list
def __repr__(self):
return pformat(vars(self))
================================================
FILE: pymaker/numeric.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
# Copyright (C) 2018 bargst
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import math
from functools import total_ordering, reduce
from decimal import *
_context = Context(prec=1000, rounding=ROUND_DOWN)
@total_ordering
class Wad:
"""Represents a number with 18 decimal places.
`Wad` implements comparison, addition, subtraction, multiplication and division operators. Comparison, addition,
subtraction and division only work with other instances of `Wad`. Multiplication works with instances
of `Wad` and `Ray` and also with `int` numbers. The result of multiplication is always a `Wad`.
`Wad`, along with `Ray`, are the two basic numeric types used by Maker contracts.
Notes:
The internal representation of `Wad` is an unbounded integer, the last 18 digits of it being treated
as decimal places. It is similar to the representation used in Maker contracts (`uint128`).
"""
def __init__(self, value):
"""Creates a new Wad number.
Args:
value: an instance of `Wad`, `Ray` or an integer. In case of an integer, the internal representation
of Maker contracts is used which means that passing `1` will create an instance of `Wad`
with a value of `0.000000000000000001'.
"""
if isinstance(value, Wad):
self.value = value.value
elif isinstance(value, Ray):
self.value = int((Decimal(value.value) // (Decimal(10)**Decimal(9))).quantize(1, context=_context))
elif isinstance(value, Rad):
self.value = int((Decimal(value.value) // (Decimal(10)**Decimal(27))).quantize(1, context=_context))
elif isinstance(value, int):
# assert(value >= 0)
self.value = value
else:
raise ArithmeticError
@classmethod
def from_number(cls, number):
# assert(number >= 0)
pwr = Decimal(10) ** 18
dec = Decimal(str(number)) * pwr
return Wad(int(dec.quantize(1, context=_context)))
def __repr__(self):
return "Wad(" + str(self.value) + ")"
def __str__(self):
tmp = str(self.value).zfill(19)
return (tmp[0:len(tmp)-18] + "." + tmp[len(tmp)-18:len(tmp)]).replace("-.", "-0.")
def __add__(self, other):
if isinstance(other, Wad):
return Wad(self.value + other.value)
else:
raise ArithmeticError
def __sub__(self, other):
if isinstance(other, Wad):
return Wad(self.value - other.value)
else:
raise ArithmeticError
def __mod__(self, other):
if isinstance(other, Wad):
return Wad(self.value % other.value)
else:
raise ArithmeticError
# z = cast((uint256(x) * y + WAD / 2) / WAD);
def __mul__(self, other):
if isinstance(other, Wad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(18))
return Wad(int(result.quantize(1, context=_context)))
elif isinstance(other, Ray):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(27))
return Wad(int(result.quantize(1, context=_context)))
elif isinstance(other, Rad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(45))
return Wad(int(result.quantize(1, context=_context)))
elif isinstance(other, int):
return Wad(int((Decimal(self.value) * Decimal(other)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __truediv__(self, other):
if isinstance(other, Wad):
return Wad(int((Decimal(self.value) * (Decimal(10) ** Decimal(18)) / Decimal(other.value)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __abs__(self):
return Wad(abs(self.value))
def __eq__(self, other):
if isinstance(other, Wad):
return self.value == other.value
else:
raise ArithmeticError
def __hash__(self):
return hash(self.value)
def __lt__(self, other):
if isinstance(other, Wad):
return self.value < other.value
else:
raise ArithmeticError
def __int__(self):
return int(self.value / 10**18)
def __float__(self):
return self.value / 10**18
def __round__(self, ndigits: int = 0):
return Wad(round(self.value, -18 + ndigits))
def __sqrt__(self):
return Wad.from_number(math.sqrt(self.__float__()))
@staticmethod
def min(*args):
"""Returns the lower of the Wad values"""
return reduce(lambda x, y: x if x < y else y, args[1:], args[0])
@staticmethod
def max(*args):
"""Returns the higher of the Wad values"""
return reduce(lambda x, y: x if x > y else y, args[1:], args[0])
@total_ordering
class Ray:
"""Represents a number with 27 decimal places.
`Ray` implements comparison, addition, subtraction, multiplication and division operators. Comparison, addition,
subtraction and division only work with other instances of `Ray`. Multiplication works with instances
of `Ray` and `Wad` and also with `int` numbers. The result of multiplication is always a `Ray`.
`Ray`, along with `Wad`, are the two basic numeric types used by Maker contracts.
Notes:
The internal representation of `Ray` is an unbounded integer, the last 27 digits of it being treated
as decimal places. It is similar to the representation used in Maker contracts (`uint128`).
"""
def __init__(self, value):
"""Creates a new Ray number.
Args:
value: an instance of `Ray`, `Wad` or an integer. In case of an integer, the internal representation
of Maker contracts is used which means that passing `1` will create an instance of `Ray`
with a value of `0.000000000000000000000000001'.
"""
if isinstance(value, Ray):
self.value = value.value
elif isinstance(value, Wad):
self.value = int((Decimal(value.value) * (Decimal(10)**Decimal(9))).quantize(1, context=_context))
elif isinstance(value, Rad):
self.value = int((Decimal(value.value) / (Decimal(10)**Decimal(18))).quantize(1, context=_context))
elif isinstance(value, int):
# assert(value >= 0)
self.value = value
else:
raise ArithmeticError
@classmethod
def from_number(cls, number):
# assert(number >= 0)
pwr = Decimal(10) ** 27
dec = Decimal(str(number)) * pwr
return Ray(int(dec.quantize(1, context=_context)))
def __repr__(self):
return "Ray(" + str(self.value) + ")"
def __str__(self):
tmp = str(self.value).zfill(28)
return (tmp[0:len(tmp)-27] + "." + tmp[len(tmp)-27:len(tmp)]).replace("-.", "-0.")
def __add__(self, other):
if isinstance(other, Ray):
return Ray(self.value + other.value)
else:
raise ArithmeticError
def __sub__(self, other):
if isinstance(other, Ray):
return Ray(self.value - other.value)
else:
raise ArithmeticError
def __mod__(self, other):
if isinstance(other, Ray):
return Ray(self.value % other.value)
else:
raise ArithmeticError
def __mul__(self, other):
if isinstance(other, Ray):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(27))
return Ray(int(result.quantize(1, context=_context)))
elif isinstance(other, Wad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(18))
return Ray(int(result.quantize(1, context=_context)))
elif isinstance(other, Rad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(45))
return Ray(int(result.quantize(1, context=_context)))
elif isinstance(other, int):
return Ray(int((Decimal(self.value) * Decimal(other)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __truediv__(self, other):
if isinstance(other, Ray):
return Ray(int((Decimal(self.value) * (Decimal(10) ** Decimal(27)) / Decimal(other.value)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __abs__(self):
return Ray(abs(self.value))
def __eq__(self, other):
if isinstance(other, Ray):
return self.value == other.value
else:
raise ArithmeticError
def __hash__(self):
return hash(self.value)
def __lt__(self, other):
if isinstance(other, Ray):
return self.value < other.value
else:
raise ArithmeticError
def __int__(self):
return int(self.value / 10**27)
def __float__(self):
return self.value / 10**27
def __round__(self, ndigits: int = 0):
return Ray(round(self.value, -27 + ndigits))
def __sqrt__(self):
return Ray.from_number(math.sqrt(self.__float__()))
@staticmethod
def min(*args):
"""Returns the lower of the Ray values"""
return reduce(lambda x, y: x if x < y else y, args[1:], args[0])
@staticmethod
def max(*args):
"""Returns the higher of the Ray values"""
return reduce(lambda x, y: x if x > y else y, args[1:], args[0])
@total_ordering
class Rad:
"""Represents a number with 45 decimal places.
`Rad` implements comparison, addition, subtraction, multiplication and division operators. Comparison, addition,
subtraction and division only work with other instances of `Rad`. Multiplication works with instances
of `Rad`, `Ray and `Wad` and also with `int` numbers. The result of multiplication is always a `Rad`.
`Rad` is rad is a new unit that exists to prevent precision loss in the core CDP engine of MCD.
Notes:
The internal representation of `Rad` is an unbounded integer, the last 45 digits of it being treated
as decimal places.
"""
def __init__(self, value):
"""Creates a new Rad number.
Args:
value: an instance of `Rad`, `Ray`, `Wad` or an integer. In case of an integer, the internal representation
of Maker contracts is used which means that passing `1` will create an instance of `Rad`
with a value of `0.000000000000000000000000000000000000000000001'.
"""
if isinstance(value, Rad):
self.value = value.value
elif isinstance(value, Ray):
self.value = int((Decimal(value.value) * (Decimal(10)**Decimal(18))).quantize(1, context=_context))
elif isinstance(value, Wad):
self.value = int((Decimal(value.value) * (Decimal(10)**Decimal(27))).quantize(1, context=_context))
elif isinstance(value, int):
# assert(value >= 0)
self.value = value
else:
raise ArithmeticError
@classmethod
def from_number(cls, number):
# assert(number >= 0)
pwr = Decimal(10) ** 45
dec = Decimal(str(number)) * pwr
return Rad(int(dec.quantize(1, context=_context)))
def __repr__(self):
return "Rad(" + str(self.value) + ")"
def __str__(self):
tmp = str(self.value).zfill(46)
return (tmp[0:len(tmp)-45] + "." + tmp[len(tmp)-45:len(tmp)]).replace("-.", "-0.")
def __add__(self, other):
if isinstance(other, Rad):
return Rad(self.value + other.value)
else:
raise ArithmeticError
def __sub__(self, other):
if isinstance(other, Rad):
return Rad(self.value - other.value)
else:
raise ArithmeticError
def __mod__(self, other):
if isinstance(other, Rad):
return Rad(self.value % other.value)
else:
raise ArithmeticError
def __mul__(self, other):
if isinstance(other, Rad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(45))
return Rad(int(result.quantize(1, context=_context)))
elif isinstance(other, Ray):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(27))
return Rad(int(result.quantize(1, context=_context)))
elif isinstance(other, Wad):
result = Decimal(self.value) * Decimal(other.value) / (Decimal(10) ** Decimal(18))
return Rad(int(result.quantize(1, context=_context)))
elif isinstance(other, int):
return Rad(int((Decimal(self.value) * Decimal(other)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __truediv__(self, other):
if isinstance(other, Rad):
return Rad(int((Decimal(self.value) * (Decimal(10) ** Decimal(45)) / Decimal(other.value)).quantize(1, context=_context)))
else:
raise ArithmeticError
def __abs__(self):
return Rad(abs(self.value))
def __eq__(self, other):
if isinstance(other, Rad):
return self.value == other.value
else:
raise ArithmeticError
def __hash__(self):
return hash(self.value)
def __lt__(self, other):
if isinstance(other, Rad):
return self.value < other.value
else:
raise ArithmeticError
def __int__(self):
return int(self.value / 10**45)
def __float__(self):
return self.value / 10**45
def __round__(self, ndigits: int = 0):
return Rad(round(self.value, -45 + ndigits))
def __sqrt__(self):
return Rad.from_number(math.sqrt(self.__float__()))
@staticmethod
def min(*args):
"""Returns the lower of the Rad values"""
return reduce(lambda x, y: x if x < y else y, args[1:], args[0])
@staticmethod
def max(*args):
"""Returns the higher of the Rad values"""
return reduce(lambda x, y: x if x > y else y, args[1:], args[0])
================================================
FILE: pymaker/oasis.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from pprint import pformat
from typing import Optional, List, Iterable, Iterator
from hexbytes import HexBytes
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker import Contract, Address, Transact, Receipt
from pymaker.numeric import Wad
from pymaker.token import ERC20Token
from pymaker.util import int_to_bytes32, bytes_to_int
from pymaker.model import Token
class Order:
"""Represents a single order on `OasisDEX`.
Instances of this class shouldn't be created directly. Instead of that, new orders can be queried
using methods of :py:class:`pymaker.oasis.SimpleMarket` or :py:class:`pymaker.oasis.MatchingMarket`.
Attributes:
order_id: Id of the order.
maker: Ethereum address of the owner of this order.
pay_token: The address of the token which is put on sale.
pay_amount: The amount of the `pay_token` token which is put on sale.
buy_token: The address of the token the order creator wants to be paid with.
buy_amount: The price the order creator wants to be paid, denominated in the `buy_token` token.
timestamp: Date and time when this order has been created, as a unix timestamp.
"""
def __init__(self, market, order_id: int, maker: Address, pay_token: Address, pay_amount: Wad, buy_token: Address,
buy_amount: Wad, timestamp: int):
assert(isinstance(order_id, int))
assert(isinstance(maker, Address))
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(isinstance(timestamp, int))
self._market = market
self.order_id = order_id
self.maker = maker
self.pay_token = pay_token
self.pay_amount = pay_amount
self.buy_token = buy_token
self.buy_amount = buy_amount
self.timestamp = timestamp
@property
def sell_to_buy_price(self) -> Wad:
return self.pay_amount / self.buy_amount
@property
def buy_to_sell_price(self) -> Wad:
return self.buy_amount / self.pay_amount
@property
def remaining_buy_amount(self) -> Wad:
return self.buy_amount
@property
def remaining_sell_amount(self) -> Wad:
return self.pay_amount
def __eq__(self, other):
assert(isinstance(other, Order))
return self._market.address == other._market.address and self.order_id == other.order_id
def __hash__(self):
return self.order_id
def __repr__(self):
return pformat(vars(self))
class LogMake:
def __init__(self, log):
self.order_id = bytes_to_int(log['args']['id'])
self.maker = Address(log['args']['maker'])
self.pay_token = Address(log['args']['pay_gem'])
self.pay_amount = Wad(log['args']['pay_amt'])
self.buy_token = Address(log['args']['buy_gem'])
self.buy_amount = Wad(log['args']['buy_amt'])
self.timestamp = log['args']['timestamp']
self.raw = log
@classmethod
def from_receipt(cls, receipt: Receipt):
assert(isinstance(receipt, Receipt))
if receipt.logs is not None:
for log in receipt.logs:
if len(log['topics']) > 0 and log['topics'][0] == HexBytes('0x773ff502687307abfa024ac9f62f9752a0d210dac2ffd9a29e38e12e2ea82c82'):
log_make_abi = [abi for abi in SimpleMarket.abi if abi.get('name') == 'LogMake'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, log_make_abi, log)
yield LogMake(event_data)
def __repr__(self):
return pformat(vars(self))
class LogBump:
def __init__(self, log):
self.order_id = bytes_to_int(log['args']['id'])
self.maker = Address(log['args']['maker'])
self.pay_token = Address(log['args']['pay_gem'])
self.pay_amount = Wad(log['args']['pay_amt'])
self.buy_token = Address(log['args']['buy_gem'])
self.buy_amount = Wad(log['args']['buy_amt'])
self.timestamp = log['args']['timestamp']
self.raw = log
def __repr__(self):
return pformat(vars(self))
class LogTake:
def __init__(self, log):
self.order_id = bytes_to_int(log['args']['id'])
self.maker = Address(log['args']['maker'])
self.taker = Address(log['args']['taker'])
self.pay_token = Address(log['args']['pay_gem'])
self.take_amount = Wad(log['args']['take_amt'])
self.buy_token = Address(log['args']['buy_gem'])
self.give_amount = Wad(log['args']['give_amt'])
self.timestamp = log['args']['timestamp']
self.raw = log
@classmethod
def from_event(cls, event: dict):
assert(isinstance(event, dict))
topics = event.get('topics')
if topics and topics[0] == HexBytes('0x3383e3357c77fd2e3a4b30deea81179bc70a795d053d14d5b7f2f01d0fd4596f'):
log_take_abi = [abi for abi in SimpleMarket.abi if abi.get('name') == 'LogTake'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, log_take_abi, event)
return LogTake(event_data)
def __eq__(self, other):
assert(isinstance(other, LogTake))
return self.__dict__ == other.__dict__
def __repr__(self):
return pformat(vars(self))
class LogKill:
def __init__(self, log):
self.order_id = bytes_to_int(log['args']['id'])
self.maker = Address(log['args']['maker'])
self.pay_token = Address(log['args']['pay_gem'])
self.pay_amount = Wad(log['args']['pay_amt'])
self.buy_token = Address(log['args']['buy_gem'])
self.buy_amount = Wad(log['args']['buy_amt'])
self.timestamp = log['args']['timestamp']
self.raw = log
def __repr__(self):
return pformat(vars(self))
class SimpleMarket(Contract):
"""A client for a `SimpleMarket` contract.
`SimpleMarket` is a simple on-chain OTC market for ERC20-compatible tokens.
It powers the `OasisDEX` decentralized exchange.
You can find the source code of the `OasisDEX` contracts here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `SimpleMarket` contract.
"""
abi = Contract._load_abi(__name__, 'abi/SimpleMarket.abi')
bin = Contract._load_bin(__name__, 'abi/SimpleMarket.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
"""Deploy a new instance of the `SimpleMarket` contract.
Args:
web3: An instance of `Web3` from `web3.py`.
Returns:
A `SimpleMarket` class instance.
"""
return SimpleMarket(web3=web3, address=Contract._deploy(web3, SimpleMarket.abi, SimpleMarket.bin, []))
def approve(self, tokens: List[ERC20Token], approval_function):
"""Approve the OasisDEX contract to fully access balances of specified tokens.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances.
approval_function: Approval function (i.e. approval mode).
"""
assert(isinstance(tokens, list))
assert(callable(approval_function))
for token in tokens:
approval_function(token, self.address, 'OasisDEX')
def past_make(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogMake]:
"""Synchronously retrieve past LogMake events.
`LogMake` events are emitted by the Oasis contract every time someone places an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogMake` events represented as :py:class:`pymaker.oasis.LogMake` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogMake', LogMake, number_of_past_blocks, event_filter)
def past_bump(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogBump]:
"""Synchronously retrieve past LogBump events.
`LogBump` events are emitted by the Oasis contract every time someone calls the `bump()` function.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogBump` events represented as :py:class:`pymaker.oasis.LogBump` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogBump', LogBump, number_of_past_blocks, event_filter)
def past_take(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogTake]:
"""Synchronously retrieve past LogTake events.
`LogTake` events are emitted by the Oasis contract every time someone takes an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogTake` events represented as :py:class:`pymaker.oasis.LogTake` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogTake', LogTake, number_of_past_blocks, event_filter)
def past_kill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogKill]:
"""Synchronously retrieve past LogKill events.
`LogKill` events are emitted by the Oasis contract every time someone cancels an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogKill` events represented as :py:class:`pymaker.oasis.LogKill` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogKill', LogKill, number_of_past_blocks, event_filter)
def get_last_order_id(self) -> int:
"""Get the id of the last order created on the market.
Returns:
The id of the last order. Returns `0` if no orders have been created at all.
"""
return self._contract.functions.last_offer_id().call()
def get_order(self, order_id: int, block_ident: Optional[str] = None) -> Optional[Order]:
"""Get order details.
Args:
order_id: The id of the order to get the details of.
`block_ident`: Block identifier can either be a str, int or None and will fallback to 'latest' block.
Used for historical retreival of orders.
Returns:
An instance of `Order` if the order is still active, or `None` if the order has been
either already completely taken or cancelled.
"""
assert(isinstance(order_id, int))
assert(isinstance(block_ident, str) or isinstance(block_ident, int) or (block_ident is None))
block_ident = 'latest' if block_ident is None else block_ident
array = self._contract.functions.offers(order_id).call(block_identifier=block_ident)
if array[5] == 0:
return None
else:
return Order(market=self, order_id=order_id, maker=Address(array[4]), pay_token=Address(array[1]),
pay_amount=Wad(array[0]), buy_token=Address(array[3]), buy_amount=Wad(array[2]),
timestamp=array[5])
def get_orders(self, pay_token: Address = None, buy_token: Address = None) -> List[Order]:
"""Get all active orders.
If both `pay_token` and `buy_token` are specified, orders will be filtered by these.
Either none or both of these parameters have to be specified.
Args:
`pay_token`: Address of the `pay_token` to filter the orders by.
`buy_token`: Address of the `buy_token` to filter the orders by.
Returns:
A list of `Order` objects representing all active orders on Oasis.
"""
assert((isinstance(pay_token, Address) and isinstance(buy_token, Address))
or (pay_token is None and buy_token is None))
orders = [self.get_order(order_id + 1) for order_id in range(self.get_last_order_id())]
orders = [order for order in orders if order is not None]
if pay_token is not None and buy_token is not None:
orders = list(filter(lambda order: order.pay_token == pay_token and order.buy_token == buy_token, orders))
return orders
def get_orders_by_maker(self, maker: Address) -> List[Order]:
"""Get all active orders created by `maker`.
Args:
maker: Address of the `maker` to filter the orders by.
Returns:
A list of `Order` objects representing all active orders belonging to this `maker`.
"""
assert(isinstance(maker, Address))
result = []
for order_id in range(self.get_last_order_id()):
# Query the order.
order = self.get_order(order_id + 1)
if order is None:
continue
# We are only interested in orders owned by `maker`. In case the order is not owned by `maker`,
# we add it to `_alien_orders[maker]` so the next time `get_orders_by_maker()` is called
# with the same parameter we will be able to rule out these orders straight away.
if order.maker != maker:
continue
result.append(order)
return result
def make(self, pay_token: Address, pay_amount: Wad, buy_token: Address, buy_amount: Wad) -> Transact:
"""Create a new order.
The `pay_amount` of `pay_token` token will be taken from you on order creation and deposited
in the market contract. Allowance needs to be set first - refer to the `approve()` method.
When complete, `receipt.result` will contain order_id of the new order.
Args:
pay_token: Address of the ERC20 token you want to put on sale.
pay_amount: Amount of the `pay_token` token you want to put on sale.
buy_token: Address of the ERC20 token you want to be paid with.
buy_amount: Amount of the `buy_token` you want to receive.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(pay_amount > Wad(0))
assert(buy_amount > Wad(0))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'make', [pay_token.address, buy_token.address, pay_amount.value, buy_amount.value], None,
self._make_order_id_result_function)
def bump(self, order_id: int) -> Transact:
"""Bumps an order.
Bumping an order generates a `LogBump` event, which can make the order reappear
in some front-ends relying on the events.
Args:
order_id: Id of the order you want to bump.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order_id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'bump',
[int_to_bytes32(order_id)])
def take(self, order_id: int, quantity: Wad) -> Transact:
"""Takes (buys) an order.
If `quantity` is equal to `pay_amount`, the whole order will be taken (bought) which will make it
disappear from the order book. If you want to buy a fraction of the order, set `quantity` to a number
lower than `pay_amount`.
Args:
order_id: Id of the order you want to take (buy).
quantity: Quantity of `pay_token` that you want to buy.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order_id, int))
assert(isinstance(quantity, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'take',
[int_to_bytes32(order_id), quantity.value])
def kill(self, order_id: int) -> Transact:
"""Cancels an existing order.
Orders can be cancelled only by their owners.
Args:
order_id: Id of the order you want to cancel.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order_id, int))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'kill(bytes32)', [int_to_bytes32(order_id)])
@staticmethod
def _make_order_id_result_function(receipt):
return next(map(lambda log_make: log_make.order_id, LogMake.from_receipt(receipt)), None)
def __repr__(self):
return f"SimpleMarket('{self.address}')"
class MatchingMarket(SimpleMarket):
"""A client for a `MatchingMarket` contract.
You can find the source code of the `OasisDEX` contracts here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `MatchingMarket` contract.
support_address: Ethereum address of the `MakerOtcSupportMethods` contract (optional).
"""
abi = Contract._load_abi(__name__, 'abi/MatchingMarket.abi')
bin = Contract._load_bin(__name__, 'abi/MatchingMarket.bin')
abi_support = Contract._load_abi(__name__, 'abi/MakerOtcSupportMethods.abi')
def __init__(self, web3: Web3, address: Address, support_address: Optional[Address] = None):
assert(isinstance(support_address, Address) or (support_address is None))
super(MatchingMarket, self).__init__(web3=web3, address=address)
self.support_address = support_address
self._support_contract = self._get_contract(web3, self.abi_support, self.support_address) \
if self.support_address else None
@staticmethod
def deploy(web3: Web3, dust_token: Address, dust_limit: Wad, price_oracle: Address,
support_address: Optional[Address] = None):
"""Deploy a new instance of the `MatchingMarket` contract.
Args:
web3: An instance of `Web` from `web3.py`.
dust_token: Address of token serving as unit of measurement for dust_limit
dust_limit: The limit itself
price_oracle: Exposes getPriceFor method for price conversion
support_address: Ethereum address of the `MakerOtcSupportMethods` contract (optional).
Returns:
A `MatchingMarket` class instance.
"""
assert isinstance(dust_token, Address)
assert isinstance(dust_limit, Wad)
assert isinstance(price_oracle, Address)
return MatchingMarket(web3=web3, address=Contract._deploy(web3, MatchingMarket.abi, MatchingMarket.bin,
[dust_token.address,
dust_limit.value,
price_oracle.address]),
support_address=support_address)
def add_token_pair_whitelist(self, base_token: Address, quote_token: Address) -> Transact:
"""Adds a token pair to the whitelist.
All newly created orders are checked against the whitelist.
Args:
base_token: Address of the ERC20 token.
quote_token: Address of the ERC20 token.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(base_token, Address))
assert(isinstance(quote_token, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'addTokenPairWhitelist', [base_token.address, quote_token.address])
def get_orders(self, p_token: Token = None, b_token: Token = None, block_ident: Optional[str] = None) -> List[Order]:
"""Get all active orders.
If both `p_token` and `b_token` are specified, orders will be filtered by these.
In case of the _MatchingMarket_ implementation, order enumeration will be much efficient
if these two parameters are supplied, as then orders can be fetched using `getBestOffer`
and a series of `getWorseOffer` calls. This approach will result in much lower number of calls
comparing to the naive 0..get_last_order_id approach, especially if the number of inactive orders
is very high.
Either none or both of these parameters have to be specified.
Args:
`p_token`: Token object (see `model.py`) of the `pay_token` to filter the orders by.
`b_token`: Token object (see `model.py`) of the `buy_token` to filter the orders by.
`block_ident`: Block identifier can either be a str, int or None and will fallback to 'latest' block.
Used for historical retreival of orders.
Returns:
A list of `Order` objects representing all active orders on Oasis.
"""
assert((isinstance(p_token, Token) and isinstance(b_token, Token))
or ((p_token is None) and (b_token is None)))
assert(isinstance(block_ident, int) or isinstance(block_ident, str) or (block_ident is None))
block_ident = 'latest' if block_ident is None else block_ident
if (p_token is None) or (b_token is None):
pay_token = None
buy_token = None
else:
pay_token = p_token.address
buy_token = b_token.address
if pay_token is not None and buy_token is not None:
orders = []
if self._support_contract:
result = self._support_contract.functions.getOffers(self.address.address, pay_token.address, buy_token.address).call(block_identifier=block_ident)
while True:
count = 0
for i in range(0, 100):
if result[3][i] != '0x0000000000000000000000000000000000000000':
count += 1
orders.append(Order(market=self,
order_id=result[0][i],
maker=Address(result[3][i]),
pay_token=pay_token,
pay_amount=p_token.normalize_amount(Wad(result[1][i])),
buy_token=buy_token,
buy_amount=b_token.normalize_amount(Wad(result[2][i])),
timestamp=result[4][i]))
if count == 100:
next_order_id = self._contract.functions.getWorseOffer(orders[-1].order_id).call(block_identifier=block_ident)
result = self._support_contract.functions.getOffers(self.address.address, next_order_id).call(block_identifier=block_ident)
else:
break
else:
order_id = self._contract.functions.getBestOffer(pay_token.address, buy_token.address).call(block_identifier=block_ident)
while order_id != 0:
order = self.get_order(order_id, block_ident)
if order is not None:
orders.append(order)
order_id = self._contract.functions.getWorseOffer(order_id).call(block_identifier=block_ident)
return sorted(orders, key=lambda order: order.order_id)
else:
return super(MatchingMarket, self).get_orders(pay_token, buy_token)
def make(self, p_token: Token, pay_amount: Wad, b_token: Token, buy_amount: Wad, pos: int = None) -> Transact:
"""Create a new order.
The `have_amount` of `have_token` token will be taken from you on order creation and deposited
in the market contract. Allowance needs to be set first. Refer to the `approve()` method
in the `ERC20Token` class.
The `MatchingMarket` contract maintains an internal ordered linked list of orders, which allows the contract
to do automated matching. Client placing a new order can either let the contract find the correct
position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position
itself and just pass the right value to the contract (this will happen if you omit the `pos`
argument of `make`). The latter should always use less gas. If the client decides not to calculate the
position or it does get it wrong and the number of open orders is high at the same time, the new order
may not even be placed at all as the attempt to calculate the position by the contract will likely fail
due to high gas usage.
When complete, `receipt.result` will contain order_id of the new order.
Args:
p_token: Token object (see `model.py`) of the ERC20 token you want to put on sale.
pay_amount: Amount of the `pay_token` token you want to put on sale.
b_token: Token object (see `model.py`) of the ERC20 token you want to be paid with.
buy_amount: Amount of the `buy_token` you want to receive.
pos: The position to insert the order at in the sorted list.
If `None`, the optimal position will automatically get calculated.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(p_token, Token))
assert(isinstance(pay_amount, Wad))
assert(isinstance(b_token, Token))
assert(isinstance(buy_amount, Wad))
assert(isinstance(pos, int) or (pos is None))
assert(pay_amount > Wad(0))
assert(buy_amount > Wad(0))
pay_token = p_token.address
buy_token = b_token.address
if pos is None:
pos = self.position(pay_amount=pay_amount,
p_token=p_token,
b_token=b_token,
buy_amount=buy_amount)
else:
assert(pos >= 0)
return Transact(self, self.web3, self.abi, self.address, self._contract,
'offer(uint256,address,uint256,address,uint256)',
[pay_amount.value, pay_token.address, buy_amount.value, buy_token.address, pos], None,
self._make_order_id_result_function)
def position(self, p_token: Token, pay_amount: Wad, b_token: Token, buy_amount: Wad) -> int:
"""Calculate the position (`pos`) new order should be inserted at to minimize gas costs.
The `MatchingMarket` contract maintains an internal ordered linked list of orders, which allows the contract
to do automated matching. Client placing a new order can either let the contract find the correct
position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position
itself and just pass the right value to the contract (this will happen if you omit the `pos`
argument of `make`). The latter should always use less gas. If the client decides not to calculate the
position or it does get it wrong and the number of open orders is high at the same time, the new order
may not even be placed at all as the attempt to calculate the position by the contract will likely fail
due to high gas usage.
This method is responsible for calculating the correct insertion position. It is used internally
by `make` when `pos` argument is omitted (or is `None`).
Args:
p_token: Token object (see `model.py`) of the token you want to put on sale.
pay_amount: Amount of the `pay_token` token you want to put on sale.
b_token: Token object (see `model.py`) of the token you want to be paid with.
buy_amount: Amount of the `buy_token` you want to receive.
Returns:
The position (`pos`) new order should be inserted at.
"""
assert(isinstance(p_token, Token))
assert(isinstance(pay_amount, Wad))
assert(isinstance(b_token, Token))
assert(isinstance(buy_amount, Wad))
pay_token = p_token.address
buy_token = b_token.address
self.logger.debug("Enumerating orders for position calculation...")
orders = filter(lambda order: order.pay_amount / order.buy_amount >= p_token.normalize_amount(pay_amount) / b_token.normalize_amount(buy_amount),
self.get_orders(p_token, b_token))
self.logger.debug("Enumerating orders for position calculation finished")
sorted_orders = sorted(orders, key=lambda o: o.pay_amount / o.buy_amount)
return sorted_orders[0].order_id if len(sorted_orders) > 0 else 0
def __repr__(self):
return f"MatchingMarket('{self.address}')"
================================================
FILE: pymaker/oracles.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 grandizzy
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.numeric import Wad
# TODO: Complete implementation and unit test
class OSM(Contract):
"""A client for the `OSM` contract.
You can find the source code of the `OSM` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `OSM` contract.
"""
abi = Contract._load_abi(__name__, 'abi/OSM.abi')
bin = Contract._load_bin(__name__, 'abi/OSM.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def poke(self) -> Transact:
return Transact(self, self.web3, self.abi, self.address, self._contract, 'poke', [])
def peek(self) -> Wad:
return Wad(self._extract_price(3))
def peep(self) -> Wad:
return Wad(self._extract_price(4))
def zzz(self) -> int:
return self._contract.functions.zzz().call()
def _extract_price(self, storage_slot: int) -> int:
assert isinstance(storage_slot, int)
return Web3.toInt(self.web3.eth.getStorageAt(self.address.address, storage_slot)[16:])
def __repr__(self):
return f"OSM('{self.address}')"
class OldUniv2LpOSM(OSM):
"""A custom `OSM` contract for Uniswap LP tokens which used different storage slots, obsolete as of dss-1.7.0
You can find the source code of the `OSM` contract here:
.
"""
def __init__(self, web3: Web3, address: Address):
super().__init__(web3, address)
def peek(self) -> Wad:
return Wad(self._extract_price(6))
def peep(self) -> Wad:
return Wad(self._extract_price(7))
================================================
FILE: pymaker/proxy.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018,2019 bargst
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from typing import List, Optional
from hexbytes import HexBytes
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker import Address, Contract, Transact, Receipt, Calldata
from pymaker.util import hexstring_to_bytes
class DSProxyCache(Contract):
"""A client for the `DSProxyCache` contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DSProxyCache.abi')
bin = Contract._load_bin(__name__, 'abi/DSProxyCache.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@classmethod
def deploy(cls, web3: Web3):
return cls(web3=web3, address=Contract._deploy(web3, cls.abi, cls.bin, []))
def read(self, code: str) -> Optional[Address]:
assert (isinstance(code, str))
if code.startswith('0x'):
b32_code = hexstring_to_bytes(code)
else:
b32_code = hexstring_to_bytes('0x' + code)
address = Address(self._contract.functions.read(b32_code).call())
if address == Address('0x0000000000000000000000000000000000000000'):
return None
else:
return address
def write(self, code: str):
assert (isinstance(code, str))
if code.startswith('0x'):
b32_code = hexstring_to_bytes(code)
else:
b32_code = hexstring_to_bytes('0x' + code)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'write', [b32_code])
def __repr__(self):
return f"DSProxyCache('{self.address}')"
class DSProxy(Contract):
"""A client for the `DSProxy` contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DSProxy.abi')
bin = Contract._load_bin(__name__, 'abi/DSProxy.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def authority(self) -> Address:
"""Return the current `authority` of a `DSAuth`-ed contract.
Returns:
The address of the current `authority`.
"""
return Address(self._contract.functions.authority().call())
def set_authority(self, address: Address) -> Transact:
"""Set the `authority` of a `DSAuth`-ed contract.
Args:
address: The address of the new `authority`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
@classmethod
def deploy(cls, web3: Web3, cache: Address):
return cls(web3=web3, address=Contract._deploy(web3, cls.abi, cls.bin, [cache.address]))
def execute(self, code: str, calldata: Calldata) -> Transact:
assert (isinstance(code, str))
assert (isinstance(calldata, Calldata))
if code.startswith('0x'):
b32_code = hexstring_to_bytes(code)
else:
b32_code = hexstring_to_bytes('0x' + code)
return Transact(self, self.web3, self.abi, self.address, self._contract,
'execute(bytes,bytes)', [b32_code, calldata.as_bytes()])
def call(self, code: str, calldata: Calldata) -> (Address, HexBytes):
assert (isinstance(code, str))
assert (isinstance(calldata, Calldata))
fn = self._contract.get_function_by_signature('execute(bytes,bytes)')
target, response = fn(code, calldata.value).call()
return Address(target), HexBytes(response)
def execute_at(self, address: Address, calldata: Calldata) -> Transact:
assert (isinstance(address, Address))
assert (isinstance(calldata, Calldata))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'execute(address,bytes)', [address.address, calldata.as_bytes()])
def call_at(self, address: Address, calldata: Calldata) -> Transact:
assert (isinstance(address, Address))
assert (isinstance(calldata, Calldata))
fn = self._contract.get_function_by_signature('execute(address,bytes)')
response = fn(address.address, calldata.value).call()
return HexBytes(response)
def set_cache(self, address: Address) -> Transact:
assert (isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setCache', [address.address])
def cache(self) -> Address:
return Address(self._contract.functions.cache().call())
def __repr__(self):
return f"DSProxy('{self.address}')"
# event Created(address indexed sender, address indexed owner, address proxy, address cache);
class LogCreated:
def __init__(self, log):
self.sender = Address(log['args']['sender'])
self.owner = Address(log['args']['owner'])
self.proxy = Address(log['args']['proxy'])
self.cache = Address(log['args']['cache'])
self.raw = log
@classmethod
def from_event(cls, event: dict):
assert (isinstance(event, dict))
topics = event.get('topics')
if topics and topics[0] == HexBytes('0x259b30ca39885c6d801a0b5dbc988640f3c25e2f37531fe138c5c5af8955d41b'):
log_created_abi = [abi for abi in DSProxyFactory.abi if abi.get('name') == 'Created'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, log_created_abi, event)
return LogCreated(event_data)
else:
raise Exception(f'[from_event] Invalid topic in {event}')
def __eq__(self, other):
assert (isinstance(other, LogCreated))
return self.__dict__ == other.__dict__
class DSProxyFactory(Contract):
"""A client for the `DSProxyFactory` contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DSProxyFactory.abi')
bin = Contract._load_bin(__name__, 'abi/DSProxyFactory.bin')
def __init__(self, web3: Web3, address: Address):
assert (isinstance(web3, Web3))
assert (isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@classmethod
def deploy(cls, web3: Web3):
return cls(web3=web3, address=Contract._deploy(web3, cls.abi, cls.bin, []))
def build(self) -> Transact:
return Transact(self, self.web3, self.abi, self.address, self._contract, 'build()', [])
def build_for(self, address: Address) -> Transact:
assert (isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'build(address)', [address.address])
def cache(self) -> Address:
return Address(self._contract.functions.cache().call())
def is_proxy(self, address: Address) -> bool:
assert (isinstance(address, Address))
return self._contract.functions.isProxy(address.address).call()
def past_build(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogCreated]:
"""Synchronously retrieve past LogCreated events.
`LogCreated` events are emitted every time someone build a proxy from the factory.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogCreated` events represented as :py:class:`pymaker.proxy.LogCreated` class.
"""
assert isinstance(number_of_past_blocks, int)
assert isinstance(event_filter, dict) or (event_filter is None)
return self._past_events(self._contract, 'Created', LogCreated, number_of_past_blocks, event_filter)
@classmethod
def log_created(cls, receipt: Receipt) -> List[LogCreated]:
assert isinstance(receipt, Receipt)
events = []
for log in receipt.raw_receipt.logs:
try:
event = LogCreated.from_event(dict(log))
events.append(event)
except:
pass
return events
def __repr__(self):
return f"DSProxyFactory('{self.address}')"
class ProxyRegistry(Contract):
"""A client for the `ProxyRegistry` contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/ProxyRegistry.abi')
bin = Contract._load_bin(__name__, 'abi/ProxyRegistry.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def build(self, owner: Address) -> Transact:
assert isinstance(owner, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'build(address)', [owner.address])
def proxies(self, owner: Address) -> Address:
assert isinstance(owner, Address)
return Address(self._contract.functions.proxies(owner.address).call())
def __repr__(self):
return f"ProxyRegistry('{self.address}')"
class DssProxyActionsDsr(Contract):
"""A client for the `DssProxyActionsDsr` contract.
Ref.
"""
abi = Contract._load_abi(__name__, 'abi/DssProxyActionsDsr.abi')
bin = Contract._load_bin(__name__, 'abi/DssProxyActionsDsr.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
================================================
FILE: pymaker/reloadable_config.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 MikeHathaway
#
# This is proprietary (closed source) software. Unauthorized copying,
# distributing, downloading, sharing, conveying, modifying, or use, via any
# medium or in any manner whatsoever are strictly prohibited. No license or
# any other rights are provided with this file.
import json
import _jsonnet
import logging
import os
import zlib
from typing import Optional, List
class ReloadableConfig:
"""Reloadable JSON config file reader.
This reader will always read most up-to-date version of the config file from disk
on each call to `get_config()`. In addition to that, whenever the config file changes,
a log event is emitted.
Attributes:
filename: Filename of the configuration file.
"""
logger = logging.getLogger('reloadable-config')
def __init__(self, filename: str):
assert(isinstance(filename, str))
self.filename = filename
self._checksum = None
self._checksum_config = None
self._config = None
self._mtime = None
self._imported_paths_to_mtimes = {}
def _import_callback(self, paths: list):
def callback(path, file):
abs_path = os.path.join(os.path.dirname(self.filename), file)
paths.append(abs_path)
with open(abs_path) as file_obj:
return file, file_obj.read()
return callback
def _load_mtimes(self, imported_paths: List[str]) -> dict:
return {path: os.path.getmtime(path) for path in imported_paths}
def _mtimes_changed(self, imported_paths_to_mtimes: dict) -> bool:
try:
return any(os.path.getmtime(path) != mtime for path, mtime in imported_paths_to_mtimes.items())
except:
return True
def get_config(self):
"""Reads the JSON config file from disk and returns it as a Python object.
Returns:
Current configuration as a `dict` or `list` object.
"""
mtime = os.path.getmtime(self.filename)
# If the modification time has not changed since the last time we have read the file,
# we return the last content without opening and parsing it. It saves us around ~ 30ms.
#
# Ultimately something like `watchdog` ()
# should be used to watch the filesystem changes asynchronously.
if self._config is not None and self._mtime is not None:
if mtime == self._mtime \
and not self._mtimes_changed(self._imported_paths_to_mtimes):
return self._config
with open(self.filename) as data_file:
content_file = data_file.read()
imported_paths = []
content_config = _jsonnet.evaluate_snippet("snippet", content_file, ext_vars={},
import_callback=self._import_callback(imported_paths))
result = None
try:
result = json.loads(content_config)
except ValueError as ex:
logging.error(f"Failed to read config: {ex}")
raise ex
# Report if file has been newly loaded or reloaded
checksum = zlib.crc32(content_file.encode('utf-8'))
checksum_config = zlib.crc32(content_config.encode('utf-8'))
if self._checksum is None:
self.logger.info(f"Loaded configuration from '{self.filename}'")
self.logger.debug(f"Config file is: " + json.dumps(result, indent=4))
elif self._checksum != checksum:
self.logger.info(f"Reloaded configuration from '{self.filename}'")
self.logger.debug(f"Reloaded config file is: " + json.dumps(result, indent=4))
elif self._imported_paths_to_mtimes != self._load_mtimes(imported_paths):
self.logger.info(f"Reloaded configuration from '{self.filename}' (due to imported file changed)")
self.logger.debug(f"Reloaded config file is: " + json.dumps(result, indent=4))
elif self._checksum_config != checksum_config:
self.logger.debug(f"Parsed configuration from '{self.filename}'")
self.logger.debug(f"Parsed config file is: " + json.dumps(result, indent=4))
self._checksum = checksum
self._checksum_config = checksum_config
self._config = result
self._mtime = mtime
self._imported_paths_to_mtimes = self._load_mtimes(imported_paths)
return result
================================================
FILE: pymaker/sai.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from typing import Optional
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.numeric import Wad, Ray
from pymaker.token import ERC20Token
from pymaker.util import int_to_bytes32
class Cup:
"""Represents details of a single cup managed by a `Tub`.
Notes:
`art` is denominated in internal debt units and should not be used directly, unless you really
know what you're doing and you know what `chi()` and `rho()` are.
Attributes:
cup_id: The identifier of the cup.
lad: Address of the owner of the cup.
art: The amount of outstanding debt (denominated in internal debt units).
ink: The amount of SKR collateral locked in the cup.
"""
def __init__(self, cup_id: int, lad: Address, ink: Wad, art: Wad):
assert(isinstance(cup_id, int))
assert(isinstance(lad, Address))
assert(isinstance(art, Wad))
assert(isinstance(ink, Wad))
self.cup_id = cup_id
self.lad = lad
self.art = art
self.ink = ink
def __repr__(self):
return f"Cup(cup_id={self.cup_id}, lad={repr(self.lad)}, art={self.art}, ink={self.ink})"
class Tub(Contract):
"""A client for the `Tub` contract.
SAI is a simple version of the diversely collateralized DAI stablecoin.
In this model there is one type of underlying collateral (called gems).
The SKR token represents claims on the system's excess gems, and is the
only admissible type of collateral. Gems can be converted to/from SKR.
Any transfers of SAI or SKR are done using the normal ERC20 interface;
until settlement mode is triggered, SAI users should only need ERC20.
``ERC20Token`` class may be used for it.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Tub` contract.
"""
abi = Contract._load_abi(__name__, 'abi/SaiTub.abi')
bin = Contract._load_bin(__name__, 'abi/SaiTub.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, sai: Address, sin: Address, skr: Address, gem: Address, gov: Address, pip: Address, pep: Address, vox: Address, pit: Address):
assert(isinstance(sai, Address))
assert(isinstance(sin, Address))
assert(isinstance(skr, Address))
assert(isinstance(gem, Address))
assert(isinstance(gov, Address))
assert(isinstance(pip, Address))
assert(isinstance(pep, Address))
assert(isinstance(vox, Address))
assert(isinstance(pit, Address))
return Tub(web3=web3, address=Contract._deploy(web3, Tub.abi, Tub.bin,
[sai.address, sin.address, skr.address, gem.address, gov.address,
pip.address, pep.address, vox.address, pit.address]))
def set_authority(self, address: Address) -> Transact:
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def approve(self, approval_function):
"""Approve the `Tub` to access our GEM, SKR, SAI and GOV balances.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
approval_function: Approval function (i.e. approval mode).
"""
assert(callable(approval_function))
approval_function(ERC20Token(web3=self.web3, address=self.gem()), self.address, 'Tub')
approval_function(ERC20Token(web3=self.web3, address=self.skr()), self.address, 'Tub')
approval_function(ERC20Token(web3=self.web3, address=self.sai()), self.address, 'Tub')
approval_function(ERC20Token(web3=self.web3, address=self.gov()), self.address, 'Tub')
def era(self) -> int:
"""Return the current `Tub` timestamp.
Returns:
Timestamp as a unix timestamp.
"""
return self._contract.functions.era().call()
def tap(self) -> Address:
"""Get the address of the `Tap` contract.
Returns:
The address of the `Tap` contract.
"""
return Address(self._contract.functions.tap().call())
def sai(self) -> Address:
"""Get the SAI token.
Returns:
The address of the SAI token.
"""
return Address(self._contract.functions.sai().call())
def sin(self) -> Address:
"""Get the SIN token.
Returns:
The address of the SIN token.
"""
return Address(self._contract.functions.sin().call())
def gov(self) -> Address:
"""Get the MKR token.
Returns:
The address of the MKR token.
"""
return Address(self._contract.functions.gov().call())
def vox(self) -> Address:
"""Get the address of the `Vox` contract.
Returns:
The address of the `Vox` contract.
"""
return Address(self._contract.functions.vox().call())
def pit(self) -> Address:
"""Get the governance vault.
Returns:
The address of the `DSVault` holding the governance tokens awaiting burn.
"""
return Address(self._contract.functions.pit().call())
def skr(self) -> Address:
"""Get the SKR token.
Returns:
The address of the SKR token.
"""
return Address(self._contract.functions.skr().call())
def gem(self) -> Address:
"""Get the collateral token (eg. W-ETH).
Returns:
The address of the collateral token.
"""
return Address(self._contract.functions.gem().call())
def pip(self) -> Address:
"""Get the reference (GEM) price feed.
Returns:
The address of the reference (GEM) price feed, which could be a `DSValue`, a `DSCache`, `Mednianizer` etc.
"""
return Address(self._contract.functions.pip().call())
def pep(self) -> Address:
"""Get the governance (MKR) price feed.
Returns:
The address of the governance (MKR) price feed, which could be a `DSValue`, a `DSCache`, `Mednianizer` etc.
"""
return Address(self._contract.functions.pep().call())
def axe(self) -> Ray:
"""Get the liquidation penalty.
Returns:
The liquidation penalty. `1.0` means no penalty. `1.2` means 20% penalty.
"""
return Ray(self._contract.functions.axe().call())
def cap(self) -> Wad:
"""Get the debt ceiling.
Returns:
The debt ceiling in SAI.
"""
return Wad(self._contract.functions.cap().call())
def mat(self) -> Ray:
"""Get the liquidation ratio.
Returns:
The liquidation ratio. `1.5` means the liquidation ratio is 150%.
"""
return Ray(self._contract.functions.mat().call())
def tax(self) -> Ray:
"""Get the stability fee.
Returns:
Per-second value of the stability fee. `1.0` means no stability fee.
"""
return Ray(self._contract.functions.tax().call())
def reg(self) -> int:
"""Get the Tub stage ('register').
Returns:
The current Tub stage (0=Usual, 1=Caged).
"""
return self._contract.functions.reg().call()
def fit(self) -> Ray:
"""Get the GEM per SKR settlement price.
Returns:
The GEM per SKR settlement (kill) price.
"""
return Ray(self._contract.functions.fit().call())
def rho(self) -> int:
"""Get the time of the last drip.
Returns:
The time of the last drip as a unix timestamp.
"""
return self._contract.functions.rho().call()
def tau(self) -> int:
"""Get the time of the last prod.
Returns:
The time of the last prod as a unix timestamp.
"""
return self._contractTip.functions.tau().call()
def chi(self) -> Ray:
"""Get the internal debt price.
Every invocation of this method calls `drip()` internally, so the value you receive is always up-to-date.
But as calling it doesn't result in an Ethereum transaction, the actual `_chi` value in the smart
contract storage does not get updated.
Returns:
The internal debt price in SAI.
"""
return Ray(self._contract.functions.chi().call())
def mold_axe(self, new_axe: Ray) -> Transact:
"""Update the liquidation penalty.
Args:
new_axe: The new value of the liquidation penalty (`axe`). `1.0` means no penalty. `1.2` means 20% penalty.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_axe, Ray)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('axe', 'utf-8'), new_axe.value])
def mold_cap(self, new_cap: Wad) -> Transact:
"""Update the debt ceiling.
Args:
new_cap: The new value of the debt ceiling (`cap`), in SAI.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_cap, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('cap', 'utf-8'), new_cap.value])
def mold_mat(self, new_mat: Ray) -> Transact:
"""Update the liquidation ratio.
Args:
new_mat: The new value of the liquidation ratio (`mat`). `1.5` means the liquidation ratio is 150%.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_mat, Ray)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('mat', 'utf-8'), new_mat.value])
def mold_tax(self, new_tax: Ray) -> Transact:
"""Update the stability fee.
Args:
new_tax: The new per-second value of the stability fee (`tax`). `1.0` means no stability fee.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_tax, Ray)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('tax', 'utf-8'), new_tax.value])
def mold_gap(self, new_gap: Wad) -> Transact:
"""Update the current spread (`gap`) for `join` and `exit`.
Args:
new_tax: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_gap, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('gap', 'utf-8'), new_gap.value])
def drip(self) -> Transact:
"""Recalculate the internal debt price (`chi`).
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'drip', [])
def prod(self) -> Transact:
"""Recalculate the accrued holder fee (`par`).
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abiTip, self.tip(), self._contractTip, 'prod', [])
def din(self) -> Wad:
"""Get the amount of total debt.
Returns:
The amount of total debt in SAI.
"""
return Wad(self._contract.functions.din().call())
def pie(self) -> Wad:
"""Get the amount of raw collateral.
Returns:
The amount of raw collateral in GEM.
"""
return Wad(self._contract.functions.pie().call())
def air(self) -> Wad:
"""Get the amount of backing collateral.
Returns:
The amount of backing collateral in SKR.
"""
return Wad(self._contract.functions.air().call())
def tag(self) -> Ray:
"""Get the reference price (REF per SKR).
The price is read from the price feed (`tip()`) every time this method gets called.
Its value is actually the value from the feed (REF per GEM) multiplied by `per()` (GEM per SKR).
Returns:
The reference price (REF per SKR).
"""
return Ray(self._contract.functions.tag().call())
def per(self) -> Ray:
"""Get the current average entry/exit price (GEM per SKR).
In order to get the price that will be actually used on `join()` or `exit()`, see
`ask()` and `bid()` respectively. The difference is due to the spread (`gap`).
Returns:
The current GEM per SKR price.
"""
return Ray(self._contract.functions.per().call())
def gap(self) -> Wad:
"""Get the current spread for `join` and `exit`.
Returns:
The current spread for `join` and `exit`. `1.0` means no spread, `1.01` means 1% spread.
"""
return Wad(self._contract.functions.gap().call())
def bid(self, amount: Wad) -> Wad:
"""Get the current `exit()`.
Returns:
The amount of GEM you will get for `amount` SKR in `join()`.
"""
assert(isinstance(amount, Wad))
return Wad(self._contract.functions.bid(amount.value).call())
def ask(self, amount: Wad) -> Wad:
"""Get the current `join()` price.
Returns:
The amount of GEM you will have to pay to get `amount` SKR fromm `join()`.
"""
assert(isinstance(amount, Wad))
return Wad(self._contract.functions.ask(amount.value).call())
def cupi(self) -> int:
"""Get the last cup id
Returns:
The id of the last cup created. Zero if no cups have been created so far.
"""
return self._contract.functions.cupi().call()
def cups(self, cup_id: int) -> Cup:
"""Get the cup details.
Args:
cup_id: Id of the cup to get the details of.
Returns:
Class encapsulating cup details.
"""
assert isinstance(cup_id, int)
array = self._contract.functions.cups(int_to_bytes32(cup_id)).call()
return Cup(cup_id, Address(array[0]), Wad(array[1]), Wad(array[2]))
def tab(self, cup_id: int) -> Wad:
"""Get the amount of debt in a cup.
Args:
cup_id: Id of the cup.
Returns:
Amount of debt in the cup, in SAI.
"""
assert isinstance(cup_id, int)
return Wad(self._contract.functions.tab(int_to_bytes32(cup_id)).call())
def ink(self, cup_id: int) -> Wad:
"""Get the amount of SKR collateral locked in a cup.
Args:
cup_id: Id of the cup.
Returns:
Amount of SKR collateral locked in the cup, in SKR.
"""
assert isinstance(cup_id, int)
return Wad(self._contract.functions.ink(int_to_bytes32(cup_id)).call())
def lad(self, cup_id: int) -> Address:
"""Get the owner of a cup.
Args:
cup_id: Id of the cup.
Returns:
Address of the owner of the cup.
"""
assert isinstance(cup_id, int)
return Address(self._contract.functions.lad(int_to_bytes32(cup_id)).call())
def safe(self, cup_id: int) -> bool:
"""Determine if a cup is safe.
Args:
cup_id: Id of the cup
Returns:
`True` if the cup is safe. `False` otherwise.
"""
assert isinstance(cup_id, int)
return self._contract.functions.safe(int_to_bytes32(cup_id)).call()
def join(self, amount_in_skr: Wad) -> Transact:
"""Buy SKR for GEMs.
Args:
amount_in_skr: The amount of SKRs to buy for GEM.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'join', [amount_in_skr.value])
def exit(self, amount_in_skr: Wad) -> Transact:
"""Sell SKR for GEMs.
Args:
amount_in_skr: The amount of SKR to sell for GEMs.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'exit', [amount_in_skr.value])
#TODO make it return the id of the newly created cup
def open(self) -> Transact:
"""Create a new cup.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'open', [])
def shut(self, cup_id: int) -> Transact:
"""Close a cup.
Involves calling `wipe()` and `free()` internally in order to clear all remaining SAI debt and free
all remaining SKR collateral.
Args:
cup_id: Id of the cup to close.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'shut', [int_to_bytes32(cup_id)])
def lock(self, cup_id: int, amount_in_skr: Wad) -> Transact:
"""Post additional SKR collateral to a cup.
Args:
cup_id: Id of the cup to post the collateral into.
amount_in_skr: The amount of collateral to post, in SKR.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'lock',
[int_to_bytes32(cup_id), amount_in_skr.value])
def free(self, cup_id: int, amount_in_skr: Wad) -> Transact:
"""Remove excess SKR collateral from a cup.
Args:
cup_id: Id of the cup to remove the collateral from.
amount_in_skr: The amount of collateral to remove, in SKR.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'free',
[int_to_bytes32(cup_id), amount_in_skr.value])
def draw(self, cup_id: int, amount_in_sai: Wad) -> Transact:
"""Issue the specified amount of SAI stablecoins.
Args:
cup_id: Id of the cup to issue the SAI from.
amount_in_sai: The amount SAI to be issued.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
assert isinstance(amount_in_sai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'draw',
[int_to_bytes32(cup_id), amount_in_sai.value])
def wipe(self, cup_id: int, amount_in_sai: Wad) -> Transact:
"""Repay some portion of existing SAI debt.
Args:
cup_id: Id of the cup to repay the SAI to.
amount_in_sai: The amount SAI to be repaid.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
assert isinstance(amount_in_sai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'wipe',
[int_to_bytes32(cup_id), amount_in_sai.value])
def give(self, cup_id: int, new_lad: Address) -> Transact:
"""Transfer ownership of a cup.
Args:
cup_id: Id of the cup to transfer ownership of.
new_lad: New owner of the cup.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
assert isinstance(new_lad, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'give',
[int_to_bytes32(cup_id), new_lad.address])
def bite(self, cup_id: int) -> Transact:
"""Initiate liquidation of an undercollateralized cup.
Args:
cup_id: Id of the cup to liquidate.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(cup_id, int)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'bite', [int_to_bytes32(cup_id)])
def __eq__(self, other):
assert(isinstance(other, Tub))
return self.address == other.address
def __repr__(self):
return f"Tub('{self.address}')"
class Tap(Contract):
"""A client for the `Tap` contract.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Tap` contract.
"""
abi = Contract._load_abi(__name__, 'abi/SaiTap.abi')
bin = Contract._load_bin(__name__, 'abi/SaiTap.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, tub: Address):
assert(isinstance(tub, Address))
return Tap(web3=web3, address=Contract._deploy(web3, Tap.abi, Tap.bin, [tub.address]))
def set_authority(self, address: Address) -> Transact:
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def approve(self, approval_function):
"""Approve the `Tap` to access our SAI, SKR and GEM balances.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
approval_function: Approval function (i.e. approval mode).
"""
assert(callable(approval_function))
tub = Tub(web3=self.web3, address=self.tub())
approval_function(ERC20Token(web3=self.web3, address=self.sai()), self.address, 'Tap')
approval_function(ERC20Token(web3=self.web3, address=self.skr()), self.address, 'Tap')
approval_function(ERC20Token(web3=self.web3, address=tub.gem()), self.address, 'Tap')
def tub(self) -> Address:
"""Get the address of the `Tub` contract.
Returns:
The address of the `Tub` contract.
"""
return Address(self._contract.functions.tub().call())
def sai(self) -> Address:
"""Get the SAI token.
Returns:
The address of the SAI token.
"""
return Address(self._contract.functions.sai().call())
def sin(self) -> Address:
"""Get the SIN token.
Returns:
The address of the SIN token.
"""
return Address(self._contract.functions.sin().call())
def skr(self) -> Address:
"""Get the SKR token.
Returns:
The address of the SKR token.
"""
return Address(self._contract.functions.skr().call())
def woe(self) -> Wad:
"""Get the amount of bad debt.
Returns:
The amount of bad debt in SAI.
"""
return Wad(self._contract.functions.woe().call())
def fog(self) -> Wad:
"""Get the amount of SKR pending liquidation.
Returns:
The amount of SKR pending liquidation, in SKR.
"""
return Wad(self._contract.functions.fog().call())
#TODO beware that it doesn't call drip() underneath so if `tax`>1.0 we won't get an up-to-date value of joy()
def joy(self) -> Wad:
"""Get the amount of surplus SAI.
Surplus SAI can be processed using `boom()`.
Returns:
The amount of surplus SAI accumulated in the Tub.
"""
return Wad(self._contract.functions.joy().call())
def gap(self) -> Wad:
"""Get the current spread for `boom` and `bust`.
Returns:
The current spread for `boom` and `bust`. `1.0` means no spread, `1.01` means 1% spread.
"""
return Wad(self._contract.functions.gap().call())
def mold_gap(self, new_gap: Wad) -> Transact:
"""Update the current spread (`gap`) for `boom` and `bust`.
Args:
new_gap: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(new_gap, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', [bytes('gap', 'utf-8'), new_gap.value])
def s2s(self) -> Ray:
"""Get the current SKR per SAI rate (for `boom` and `bust`).
Returns:
The current SKR per SAI rate.
"""
return Ray(self._contract.functions.s2s().call())
def bid(self, amount_in_skr: Wad) -> Wad:
"""Get the current price of `amount_in_skr` SKR in SAI for `boom`.
Returns:
The amount in SAI which will be received from `boom` in return of
`amount_in_skr` SKR.
"""
return Wad(self._contract.functions.bid(amount_in_skr.value).call())
def ask(self, amount_in_skr: Wad) -> Wad:
"""Get the current price of `amount_in_skr` SKR in SAI for `bust`.
Returns:
The amount in SAI which will be consumed by `bust` if we want
to receive `amount_in_skr` SKR from it.
"""
return Wad(self._contract.functions.ask(amount_in_skr.value).call())
def boom(self, amount_in_skr: Wad) -> Transact:
"""Buy some amount of SAI to process `joy` (surplus).
Args:
amount_in_skr: The amount of SKR we want to send in order to receive SAI.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'boom', [amount_in_skr.value])
def bust(self, amount_in_skr: Wad) -> Transact:
"""Sell some amount of SAI to process `woe` (bad debt).
Args:
amount_in_skr: The amount of SKR we want to receive in exchange for our SAI.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert isinstance(amount_in_skr, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'bust', [amount_in_skr.value])
def cash(self, amount_in_sai: Wad) -> Transact:
"""Exchange SAI to GEM after cage.
Args:
amount_in_sai: The amount of SAI to exchange to GEM.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cash', [amount_in_sai.value])
def mock(self, amount_in_sai: Wad) -> Transact:
"""Exchange GEM to SAI after cage.
Args:
amount_in_sai: The amount of SAI to buy for GEM.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mock', [amount_in_sai.value])
def __eq__(self, other):
assert(isinstance(other, Tap))
return self.address == other.address
def __repr__(self):
return f"Tap('{self.address}')"
class Top(Contract):
"""A client for the `Top` contract.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Top` contract.
"""
abi = Contract._load_abi(__name__, 'abi/SaiTop.abi')
bin = Contract._load_bin(__name__, 'abi/SaiTop.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, tub: Address, tap: Address):
assert(isinstance(tub, Address))
assert(isinstance(tap, Address))
return Top(web3=web3, address=Contract._deploy(web3, Top.abi, Top.bin, [tub.address, tap.address]))
def set_authority(self, address: Address) -> Transact:
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def fix(self) -> Ray:
"""Get the GEM per SAI settlement price.
Returns:
The GEM per SAI settlement (kill) price.
"""
return Ray(self._contract.functions.fix().call())
def cage(self) -> Transact:
"""Force settlement of the system at a current price.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cage', [])
# TODO vent
def __eq__(self, other):
assert(isinstance(other, Top))
return self.address == other.address
def __repr__(self):
return f"Top('{self.address}')"
class Vox(Contract):
"""A client for the `Vox` contract.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `Vox` contract.
"""
abi = Contract._load_abi(__name__, 'abi/SaiVox.abi')
bin = Contract._load_bin(__name__, 'abi/SaiVox.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, per: Ray):
assert(isinstance(per, Ray))
return Vox(web3=web3, address=Contract._deploy(web3, Vox.abi, Vox.bin, [per.value]))
def set_authority(self, address: Address) -> Transact:
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def era(self) -> int:
"""Return the current `Vox` timestamp.
Returns:
Timestamp as a unix timestamp.
"""
return self._contract.functions.era().call()
def par(self) -> Ray:
"""Get the accrued holder fee (REF per SAI).
Every invocation of this method calls `prod()` internally, so the value you receive is always up-to-date.
But as calling it doesn't result in an Ethereum transaction, the actual `_par` value in the smart
contract storage does not get updated.
Returns:
The accrued holder fee.
"""
return Ray(self._contract.functions.par().call())
def __eq__(self, other):
assert(isinstance(other, Vox))
return self.address == other.address
def __repr__(self):
return f"Vox('{self.address}')"
================================================
FILE: pymaker/shutdown.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
from datetime import datetime
from typing import Optional, List
from web3 import Web3
from pymaker import Address, Contract, Transact
from pymaker.approval import directly, hope_directly
from pymaker.dss import Ilk
from pymaker.numeric import Wad, Ray, Rad
from pymaker.token import DSToken, ERC20Token
logger = logging.getLogger()
class ShutdownModule(Contract):
"""A client for the `ESM` contract, which allows users to call `end.cage()` and thereby trigger a shutdown.
Ref.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `ESM` contract."""
abi = Contract._load_abi(__name__, 'abi/ESM.abi')
bin = Contract._load_bin(__name__, 'abi/ESM.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def sum(self) -> Wad:
"""Total balance of MKR `join`ed to this contract"""
return Wad(self._contract.functions.Sum().call())
def sum_of(self, address: Address) -> Wad:
"""MKR `join`ed to this contract by a specific account"""
assert isinstance(address, Address)
return Wad(self._contract.functions.sum(address.address).call())
def min(self) -> Wad:
"""Minimum amount of MKR required to call `fire`"""
return Wad(self._contract.functions.min().call())
def join(self, value: Wad) -> Transact:
"""Before `fire` can be called, sufficient MKR must be `join`ed to this contract"""
assert isinstance(value, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'join', [value.value])
def fire(self):
"""Calls `cage` on the `end` contract, initiating a shutdown."""
logger.info("Calling fire to cage the end")
return Transact(self, self.web3, self.abi, self.address, self._contract, 'fire', [])
def deny(self, address: Address):
"""Removes the Pause proxy's privileges from address"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'deny', [address.address])
def burn(self):
logger.info("Calling burn to burn all the joined MKR")
return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn', [])
class End(Contract):
"""A client for the `End` contract, used to orchestrate a shutdown.
Ref.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `ESM` contract."""
abi = Contract._load_abi(__name__, 'abi/End.abi')
bin = Contract._load_bin(__name__, 'abi/End.bin')
def __init__(self, web3: Web3, address: Address):
assert isinstance(web3, Web3)
assert isinstance(address, Address)
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def live(self) -> bool:
"""False when caged, true when uncaged"""
return self._contract.functions.live().call() > 0
def when(self) -> datetime:
"""Time of cage"""
timestamp = self._contract.functions.when().call()
return datetime.utcfromtimestamp(timestamp)
def wait(self) -> int:
"""Processing cooldown length, in seconds"""
return int(self._contract.functions.wait().call())
def debt(self) -> Rad:
"""total outstanding dai following processing"""
return Rad(self._contract.functions.debt().call())
def tag(self, ilk: Ilk) -> Ray:
"""Cage price for the collateral"""
assert isinstance(ilk, Ilk)
return Ray(self._contract.functions.tag(ilk.toBytes()).call())
def gap(self, ilk: Ilk) -> Wad:
"""Collateral shortfall (difference of debt and collateral"""
assert isinstance(ilk, Ilk)
return Wad(self._contract.functions.gap(ilk.toBytes()).call())
def art(self, ilk: Ilk) -> Wad:
"""Total debt for the collateral"""
assert isinstance(ilk, Ilk)
return Wad(self._contract.functions.Art(ilk.toBytes()).call())
def fix(self, ilk: Ilk) -> Ray:
"""Final cash price for the collateral"""
assert isinstance(ilk, Ilk)
return Ray(self._contract.functions.fix(ilk.toBytes()).call())
def bag(self, address: Address) -> Wad:
"""Amount of Dai `pack`ed for retrieving collateral in return"""
assert isinstance(address, Address)
return Wad(self._contract.functions.bag(address.address).call())
def out(self, ilk: Ilk, address: Address) -> Wad:
assert isinstance(ilk, Ilk)
assert isinstance(address, Address)
return Wad(self._contract.functions.out(ilk.toBytes(), address.address).call())
def cage(self, ilk: Ilk) -> Transact:
"""Set the `cage` price for the collateral"""
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cage(bytes32)', [ilk.toBytes()])
def snip(self, ilk: Ilk, clip_id: int) -> Transact:
"""Cancel a clip auction and seize it's collateral"""
assert isinstance(ilk, Ilk)
assert isinstance(clip_id, int)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'snip', [ilk.toBytes(), clip_id])
def skip(self, ilk: Ilk, flip_id: int) -> Transact:
"""Cancel a flip auction and seize it's collateral"""
assert isinstance(ilk, Ilk)
assert isinstance(flip_id, int)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'skip', [ilk.toBytes(), flip_id])
def skim(self, ilk: Ilk, address: Address) -> Transact:
"""Cancels undercollateralized CDP debt to determine collateral shortfall"""
assert isinstance(ilk, Ilk)
assert isinstance(address, Address)
return Transact(self, self.web3, self.abi, self.address, self._contract,
'skim', [ilk.toBytes(), address.address])
def free(self, ilk: Ilk) -> Transact:
"""Releases excess collateral after `skim`ming"""
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'free', [ilk.toBytes()])
def thaw(self):
"""Fix the total outstanding supply of Dai"""
return Transact(self, self.web3, self.abi, self.address, self._contract, 'thaw', [])
def flow(self, ilk: Ilk) -> Transact:
"""Calculate the `fix`, the cash price for a given collateral"""
assert isinstance(ilk, Ilk)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'flow', [ilk.toBytes()])
def pack(self, dai: Wad) -> Transact:
"""Deposit Dai into the `bag`, from which it cannot be withdrawn"""
assert isinstance(dai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'pack', [dai.value])
def cash(self, ilk: Ilk, dai: Wad):
"""Exchange an amount of dai (already `pack`ed in the `bag`) for collateral"""
assert isinstance(ilk, Ilk)
assert isinstance(dai, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cash', [ilk.toBytes(), dai.value])
================================================
FILE: pymaker/sign.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
import time
from typing import Tuple
from eth_account.messages import defunct_hash_message
from eth_utils import encode_hex
from web3 import Web3
from pymaker import Address
from pymaker.keys import _registered_accounts
from pymaker.util import bytes_to_hexstring
def eth_sign(message: bytes, web3: Web3, key=None, in_hexbytes=False, account=None):
assert(isinstance(message, bytes))
assert(isinstance(web3, Web3))
local_account = _registered_accounts.get((web3, Address(web3.eth.defaultAccount)))
if local_account or (account is not None):
if key is None:
pkey = local_account.privateKey
else:
pkey = key
start_time = time.time()
start_clock = time.process_time()
try:
if in_hexbytes:
message_hash = message
else:
message_hash = defunct_hash_message(primitive=message)
signature = web3.eth.account.signHash(message_hash, private_key=pkey).signature.hex()
finally:
end_time = time.time()
end_clock = time.process_time()
logging.debug(f"Local signing took {end_time - start_time:.3f}s time, {end_clock - start_clock:.3f}s clock")
return signature
else:
signature = bytes_to_hexstring(web3.manager.request_blocking(
"eth_sign", [web3.eth.defaultAccount, encode_hex(message)],
))
# for `EthereumJS TestRPC/v2.2.1/ethereum-js`
if signature.endswith("00"):
signature = signature[:-2] + "1b"
if signature.endswith("01"):
signature = signature[:-2] + "1c"
return signature
def to_vrs(signature: str) -> Tuple[int, bytes, bytes]:
assert(isinstance(signature, str))
assert(signature.startswith("0x"))
signature_hex = signature[2:]
r = bytes.fromhex(signature_hex[0:64])
s = bytes.fromhex(signature_hex[64:128])
v = ord(bytes.fromhex(signature_hex[128:130]))
return v, r, s
================================================
FILE: pymaker/tightly_packed.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from eth_abi.encoding import encode_uint_256, BytesEncoder, AddressEncoder
from pymaker import Address
def encode_address(address: Address) -> bytes:
return AddressEncoder().encode(address.address)[12:]
def encode_uint256(value: int) -> bytes:
return encode_uint_256.encode(value)
def encode_bytes(value: bytes) -> bytes:
return BytesEncoder().encode(value)
================================================
FILE: pymaker/token.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import json
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.numeric import Wad
class ERC20Token(Contract):
"""A client for a standard ERC20 token contract.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the ERC20 token.
"""
abi = Contract._load_abi(__name__, 'abi/ERC20Token.abi')
registry = {}
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def name(self) -> str:
abi_with_string = json.loads("""[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]""")
abi_with_bytes32 = json.loads("""[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}]""")
contract_with_string = self._get_contract(self.web3, abi_with_string, self.address)
contract_with_bytes32 = self._get_contract(self.web3, abi_with_bytes32, self.address)
try:
return contract_with_string.functions.name().call()
except:
return str(contract_with_bytes32.functions.name().call(), "utf-8").strip('\x00')
def symbol(self) -> str:
abi_with_string = json.loads("""[{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]""")
abi_with_bytes32 = json.loads("""[{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}]""")
contract_with_string = self._get_contract(self.web3, abi_with_string, self.address)
contract_with_bytes32 = self._get_contract(self.web3, abi_with_bytes32, self.address)
try:
return contract_with_string.functions.symbol().call()
except:
return str(contract_with_bytes32.functions.symbol().call(), "utf-8").strip('\x00')
def total_supply(self) -> Wad:
"""Returns the total supply of the token.
Returns:
The total supply of the token.
"""
return Wad(self._contract.functions.totalSupply().call())
def balance_of(self, address: Address) -> Wad:
"""Returns the token balance of a given address.
Args:
address: The address to check the balance of.
Returns:
The token balance of the address specified.
"""
assert(isinstance(address, Address))
return Wad(self._contract.functions.balanceOf(address.address).call())
def balance_at_block(self, address: Address, block_identifier: int = 'latest') -> Wad:
"""Returns the token balance of a given address.
Args:
address: The address to check the balance of.
block_identifier: block at which to retrieve the balance
Returns:
The token balance of the address specified.
"""
assert(isinstance(address, Address))
assert(isinstance(block_identifier, int) or block_identifier == 'latest')
return Wad(self._contract.functions.balanceOf(address.address).call(block_identifier=block_identifier))
def allowance_of(self, address: Address, payee: Address) -> Wad:
"""Returns the current allowance of a specified `payee` (delegate account).
Allowance is an ERC20 concept allowing the `payee` (delegate account) to spend a fixed amount of tokens
on behalf of the token owner (`address`).
Args:
address: The address to check the allowance for (it's the address the tokens can be spent from).
payee: The address of the delegate account (it's the address that can spend the tokens).
Returns:
The allowance of the `payee` specified in regards to the `address`.
"""
assert(isinstance(address, Address))
assert(isinstance(payee, Address))
return Wad(self._contract.functions.allowance(address.address, payee.address).call())
def transfer(self, address: Address, value: Wad) -> Transact:
"""Transfers tokens to a specified address.
Args:
address: Destination address to transfer the tokens to.
value: The value of tokens to transfer.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(address, Address))
assert(isinstance(value, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'transfer', [address.address, value.value])
def transfer_from(self, source_address: Address, destination_address: Address, value: Wad) -> Transact:
"""Transfers tokens to a specified address.
Args:
source_address: Source address to transfer the tokens from.
destination_address: Destination address to transfer the tokens to.
value: The value of tokens to transfer.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(source_address, Address))
assert(isinstance(destination_address, Address))
assert(isinstance(value, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'transferFrom', [source_address.address,
destination_address.address,
value.value])
def approve(self, payee: Address, limit: Wad = Wad(2**256 - 1)) -> Transact:
"""Modifies the current allowance of a specified `payee` (delegate account).
Allowance is an ERC20 concept allowing the `payee` (delegate account) to spend a fixed amount of tokens
(`limit`) on behalf of the token owner.
If `limit` is omitted, a maximum possible value is granted.
Args:
payee: The address of the delegate account (it's the address that can spend the tokens).
limit: The value of the allowance i.e. the value of tokens that the `payee` (delegate account)
can spend on behalf of their owner.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(payee, Address))
assert(isinstance(limit, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'approve(address,uint256)', [payee.address, limit.value])
def __eq__(self, other):
return self.address == other.address
def __repr__(self):
return f"ERC20Token('{self.address}')"
class DSToken(ERC20Token):
"""A client for the `DSToken` contract.
You can find the source code of the `DSToken` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSToken` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSToken.abi')
bin = Contract._load_bin(__name__, 'abi/DSToken.bin')
@staticmethod
def deploy(web3: Web3, symbol: str):
"""Deploy a new instance of the `DSToken` contract.
Args:
web3: An instance of `Web` from `web3.py`.
symbol: Symbol of the new token.
Returns:
A `DSToken` class instance.
"""
assert(isinstance(symbol, str))
return DSToken(web3=web3, address=Contract._deploy(web3, DSToken.abi, DSToken.bin, [bytes(symbol, "utf-8")]))
def authority(self) -> Address:
"""Return the current `authority` of a `DSAuth`-ed contract.
Returns:
The address of the current `authority`.
"""
return Address(self._contract.functions.authority().call())
def set_authority(self, address: Address) -> Transact:
"""Set the `authority` of a `DSAuth`-ed contract.
Args:
address: The address of the new `authority`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def mint(self, amount: Wad) -> Transact:
"""Increase the total supply of the token.
Args:
amount: The amount to increase the total supply by.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mint(uint256)', [amount.value])
def mint_to(self, address: Address, amount: Wad) -> Transact:
"""Increase the total supply of the token.
Args:
address: The address to credit the new tokens to.
amount: The amount to increase the total supply by.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'mint(address,uint256)', [address.address,
amount.value])
def burn(self, amount: Wad) -> Transact:
"""Decrease the total supply of the token.
Args:
amount: The amount to decrease the total supply by.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn(uint256)', [amount.value])
def burn_from(self, address: Address, amount: Wad) -> Transact:
"""Decrease the total supply of the token.
Args:
address: The address to burn the tokens from.
amount: The amount to decrease the total supply by.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn(address,uint256)', [address.address,
amount.value])
def __repr__(self):
return f"DSToken('{self.address}')"
class DSEthToken(ERC20Token):
"""A client for the `DSEthToken` contract.
`DSEthToken`, also known as ETH Wrapper or W-ETH, is a contract into which you can deposit
raw ETH and then deal with it like with any other ERC20 token. In addition to the `deposit()`
and `withdraw()` methods, it implements the standard ERC20 token API.
You can find the source code of the `DSEthToken` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSEthToken` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSEthToken.abi')
bin = Contract._load_bin(__name__, 'abi/DSEthToken.bin')
@staticmethod
def deploy(web3: Web3):
"""Deploy a new instance of the `DSEthToken` contract.
Args:
web3: An instance of `Web` from `web3.py`.
Returns:
A `DSEthToken` class instance.
"""
return DSEthToken(web3=web3, address=Contract._deploy(web3, DSEthToken.abi, DSEthToken.bin, []))
def __init__(self, web3, address):
super().__init__(web3, address)
self._contract = self._get_contract(web3, self.abi, address)
def deposit(self, amount: Wad) -> Transact:
"""Deposits `amount` of raw ETH to `DSEthToken`.
Args:
amount: Amount of raw ETH to be deposited to `DSEthToken`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value})
def withdraw(self, amount: Wad) -> Transact:
"""Withdraws `amount` of raw ETH from `DSEthToken`.
The withdrawn ETH will get transferred to the calling account.
Args:
amount: Amount of raw ETH to be withdrawn from `DSEthToken`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value])
def __repr__(self):
return f"DSEthToken('{self.address}')"
class EthToken():
"""Basic ETH token.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the original ETH token.
"""
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
def balance_of(self, address):
"""Returns the ETH balance of a given Ethereum address.
Args:
address: The address to check the balance of.
Returns:
The ETH balance of the address specified.
"""
assert(isinstance(address, Address))
return Wad(self.web3.eth.getBalance(address.address))
================================================
FILE: pymaker/transactional.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import operator
from functools import reduce
from typing import List
from web3 import Web3
from pymaker import Contract, Address, Invocation, Transact
from pymaker.token import ERC20Token
class TxManager(Contract):
"""A client for the `TxManager` contract.
`TxManager` allows to invoke multiple contract methods in one Ethereum transaction.
Each invocation is represented as an instance of the `Invocation` class, containing a
contract address and a calldata.
In addition to that, these invocations can use ERC20 token balances. In order to do that,
the entire allowance of each token involved is transferred from the caller to the `TxManager`
contract at the beginning of the transaction and all the remaining balances are returned
to the caller at the end of it. In order to use this feature, ERC20 token allowances
have to be granted to the `TxManager`.
You can find the source code of the `TxManager` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `TxManager` contract.
"""
abi = Contract._load_abi(__name__, 'abi/TxManager.abi')
bin = Contract._load_bin(__name__, 'abi/TxManager.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
return TxManager(web3=web3, address=Contract._deploy(web3, TxManager.abi, TxManager.bin, []))
def approve(self, tokens: List[ERC20Token], approval_function):
"""Approve the TxManager contract to fully access balances of specified tokens.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances.
approval_function: Approval function (i.e. approval mode).
"""
assert(isinstance(tokens, list))
assert(callable(approval_function))
for token in tokens:
approval_function(token, self.address, 'TxManager')
def owner(self) -> Address:
return Address(self._contract.functions.owner().call())
def execute(self, tokens: List[Address], invocations: List[Invocation]) -> Transact:
"""Executes multiple contract methods in one Ethereum transaction.
Args:
tokens: List of addresses of ERC20 token the invocations should be able to access.
invocations: A list of invocations (contract methods) to be executed.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
def token_addresses() -> list:
return list(map(lambda address: address.address, tokens))
def script() -> bytes:
return reduce(operator.add, map(lambda invocation: script_entry(invocation), invocations), bytes())
def script_entry(invocation: Invocation) -> bytes:
address = invocation.address.as_bytes()
calldata = invocation.calldata.as_bytes()
calldata_length = len(calldata).to_bytes(32, byteorder='big')
return address + calldata_length + calldata
assert(isinstance(tokens, list))
assert(isinstance(invocations, list))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'execute', [token_addresses(), script()])
def __repr__(self):
return f"TxManager('{self.address}')"
================================================
FILE: pymaker/util.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import logging
import threading
from web3 import Web3
from pymaker.numeric import Wad
def chain(web3: Web3) -> str:
block_0 = web3.eth.getBlock(0)['hash']
if block_0 == "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3":
return "ethlive"
elif block_0 == "0xa3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9":
return "kovan"
elif block_0 == "0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d":
return "ropsten"
elif block_0 == "0x0cd786a2425d16f152c658316c423e6ce1181e15c3295826d7c9904cba9ce303":
return "morden"
else:
return "unknown"
def http_response_summary(response) -> str:
text = response.text.replace('\r', '').replace('\n', '')[:2048]
return f"{response.status_code} {response.reason} ({text})"
# CAUTION: Used by Transact class, this breaks applications running their own asyncio event loop.
def synchronize(futures) -> list:
if len(futures) > 0:
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(asyncio.gather(*futures, loop=loop))
finally:
loop.close()
else:
return []
def eth_balance(web3: Web3, address) -> Wad:
return Wad(web3.eth.getBalance(address.address))
def is_contract_at(web3: Web3, address):
code = web3.eth.getCode(address.address)
return (code is not None) and (code != "0x") and (code != "0x0") and (code != b"\x00") and (code != b"")
def int_to_bytes32(value: int) -> bytes:
assert(isinstance(value, int))
return value.to_bytes(32, byteorder='big')
def bytes_to_int(value) -> int:
if isinstance(value, bytes) or isinstance(value, bytearray):
return int.from_bytes(value, byteorder='big')
elif isinstance(value, str):
b = bytearray()
b.extend(map(ord, value))
return int.from_bytes(b, byteorder='big')
else:
raise AssertionError
def bytes_to_hexstring(value) -> str:
if isinstance(value, bytes) or isinstance(value, bytearray):
return "0x" + "".join(map(lambda b: format(b, "02x"), value))
elif isinstance(value, str):
b = bytearray()
b.extend(map(ord, value))
return "0x" + "".join(map(lambda b: format(b, "02x"), b))
else:
raise AssertionError
def hexstring_to_bytes(value: str) -> bytes:
assert(isinstance(value, str))
assert(value.startswith("0x"))
return Web3.toBytes(hexstr=value)
class AsyncCallback:
"""Decouples callback invocation from the web3.py filter.
Decouples callback invocation from the web3.py filter by executing the callback
in a dedicated thread. If we make web3.py trigger the callback directly, and the callback
execution takes more than 60 seconds, the `eth_getFilterChanges` call also will not
get called for 60 seconds and more which will make the filter expire in Parity side.
It's 60 seconds for Parity, this could be a different value for other nodes,
but the filter will eventually expire sooner or later anyway.
Invoking the callback logic in a separate thread allows the web3.py Filter thread
to keep calling `eth_getFilterChanges` regularly, so the filter stays active.
Attributes:
callback: The callback function to be invoked in a separate thread.
"""
def __init__(self, callback):
self.callback = callback
self.thread = None
def trigger(self, on_start=None, on_finish=None) -> bool:
"""Invokes the callback in a separate thread, unless one is already running.
If callback isn't currently running, invokes it in a separate thread and returns `True`.
If the previous callback invocation still hasn't finished, doesn't do anything
and returns `False`.
Arguments:
on_start: Optional method to be called before the actual callback. Can be `None`.
on_finish: Optional method to be called after the actual callback. Can be `None`.
Returns:
`True` if callback has been invoked, or if it invocation attempt failed.
`False` if the previous callback invocation still hasn't finished.
"""
if self.thread is None or not self.thread.is_alive():
def thread_target():
if on_start is not None:
on_start()
self.callback()
if on_finish is not None:
on_finish()
self.thread = threading.Thread(target=thread_target)
try:
self.thread.start()
except Exception as e:
self.thread = None
logging.critical(f"Failed to start the async callback thread ({e})")
return True
else:
return False
def wait(self):
"""Waits for the currently running callback to finish.
If the callback isn't running or hasn't even been invoked once, returns instantly."""
if self.thread is not None:
self.thread.join()
================================================
FILE: pymaker/vault.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Contract, Address, Transact
class DSVault(Contract):
"""A client for the `DSVault` contract.
You can find the source code of the `DSVault` contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the `DSVault` contract.
"""
abi = Contract._load_abi(__name__, 'abi/DSVault.abi')
bin = Contract._load_bin(__name__, 'abi/DSVault.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
"""Deploy a new instance of the `DSVault` contract.
Args:
web3: An instance of `Web` from `web3.py`.
Returns:
A `DSVault` class instance.
"""
return DSVault(web3=web3, address=Contract._deploy(web3, DSVault.abi, DSVault.bin, []))
def authority(self) -> Address:
"""Return the current `authority` of a `DSAuth`-ed contract.
Returns:
The address of the current `authority`.
"""
return Address(self._contract.functions.authority().call())
def set_authority(self, address: Address) -> Transact:
"""Set the `authority` of a `DSAuth`-ed contract.
Args:
address: The address of the new `authority`.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(address, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])
def __repr__(self):
return f"DSVault('{self.address}')"
================================================
FILE: pymaker/zrx.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import array
import copy
import logging
import random
from pprint import pformat
from typing import List, Optional
import requests
from hexbytes import HexBytes
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker import Contract, Address, Transact
from pymaker.numeric import Wad
from pymaker.sign import eth_sign, to_vrs
from pymaker.token import ERC20Token
from pymaker.util import bytes_to_hexstring, hexstring_to_bytes, http_response_summary
class Order:
def __init__(self, exchange, maker: Address, taker: Address, maker_fee: Wad, taker_fee: Wad, pay_token: Address,
pay_amount: Wad, buy_token: Address, buy_amount: Wad, salt: int, fee_recipient: Address,
expiration: int, exchange_contract_address: Address, ec_signature_r: Optional[str],
ec_signature_s: Optional[str], ec_signature_v: Optional[int]):
assert(isinstance(maker, Address))
assert(isinstance(taker, Address))
assert(isinstance(maker_fee, Wad))
assert(isinstance(taker_fee, Wad))
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(isinstance(salt, int))
assert(isinstance(fee_recipient, Address))
assert(isinstance(expiration, int))
assert(isinstance(exchange_contract_address, Address))
assert((isinstance(ec_signature_r, str) and isinstance(ec_signature_s, str) and isinstance(ec_signature_v, int))
or (ec_signature_r is None and ec_signature_s is None and ec_signature_v is None))
self._exchange = exchange
self.maker = maker
self.taker = taker
self.maker_fee = maker_fee
self.taker_fee = taker_fee
self.pay_token = pay_token
self.pay_amount = pay_amount
self.buy_token = buy_token
self.buy_amount = buy_amount
self.salt = salt
self.fee_recipient = fee_recipient
self.expiration = expiration
self.exchange_contract_address = exchange_contract_address
self.ec_signature_r = ec_signature_r
self.ec_signature_s = ec_signature_s
self.ec_signature_v = ec_signature_v
# this is not a proper 0x order_id, it's just so `OrderBookManager` can uniquely identify orders
@property
def order_id(self):
return hash(self)
@property
def sell_to_buy_price(self) -> Wad:
return self.pay_amount / self.buy_amount
@property
def buy_to_sell_price(self) -> Wad:
return self.buy_amount / self.pay_amount
@property
def remaining_buy_amount(self) -> Wad:
return Wad.max(self.buy_amount - self._exchange.get_unavailable_buy_amount(self), Wad(0))
@property
def remaining_sell_amount(self) -> Wad:
unavailable_buy_amount = self._exchange.get_unavailable_buy_amount(self)
if unavailable_buy_amount >= self.buy_amount:
return Wad(0)
else:
return Wad.max(self.pay_amount - (unavailable_buy_amount * self.pay_amount / self.buy_amount), Wad(0))
@staticmethod
def from_json(exchange, data: dict):
assert(isinstance(data, dict))
return Order(exchange=exchange,
maker=Address(data['maker']),
taker=Address(data['taker']),
maker_fee=Wad(int(data['makerFee'])),
taker_fee=Wad(int(data['takerFee'])),
pay_token=Address(data['makerTokenAddress']),
pay_amount=Wad(int(data['makerTokenAmount'])),
buy_token=Address(data['takerTokenAddress']),
buy_amount=Wad(int(data['takerTokenAmount'])),
salt=int(data['salt']),
fee_recipient=Address(data['feeRecipient']),
expiration=int(data['expirationUnixTimestampSec']),
exchange_contract_address=Address(data['exchangeContractAddress']),
ec_signature_r=data['ecSignature']['r'] if 'ecSignature' in data else None,
ec_signature_s=data['ecSignature']['s'] if 'ecSignature' in data else None,
ec_signature_v=data['ecSignature']['v'] if 'ecSignature' in data else None)
def to_json_without_fees(self) -> dict:
return {
"exchangeContractAddress": self.exchange_contract_address.address.lower(),
"maker": self.maker.address.lower(),
"taker": self.taker.address.lower(),
"makerTokenAddress": self.pay_token.address.lower(),
"takerTokenAddress": self.buy_token.address.lower(),
"makerTokenAmount": str(self.pay_amount.value),
"takerTokenAmount": str(self.buy_amount.value),
"expirationUnixTimestampSec": str(self.expiration),
"salt": str(self.salt)
}
def to_json(self) -> dict:
return {
"exchangeContractAddress": self.exchange_contract_address.address.lower(),
"maker": self.maker.address.lower(),
"taker": self.taker.address.lower(),
"makerTokenAddress": self.pay_token.address.lower(),
"takerTokenAddress": self.buy_token.address.lower(),
"feeRecipient": self.fee_recipient.address.lower(),
"makerTokenAmount": str(self.pay_amount.value),
"takerTokenAmount": str(self.buy_amount.value),
"makerFee": str(self.maker_fee.value),
"takerFee": str(self.taker_fee.value),
"expirationUnixTimestampSec": str(self.expiration),
"salt": str(self.salt),
"ecSignature": {
"r": self.ec_signature_r,
"s": self.ec_signature_s,
"v": self.ec_signature_v
}
}
def __eq__(self, other):
assert(isinstance(other, Order))
return self.maker == other.maker and \
self.taker == other.taker and \
self.maker_fee == other.maker_fee and \
self.taker_fee == other.taker_fee and \
self.pay_token == other.pay_token and \
self.pay_amount == other.pay_amount and \
self.buy_token == other.buy_token and \
self.buy_amount == other.buy_amount and \
self.salt == other.salt and \
self.fee_recipient == other.fee_recipient and \
self.expiration == other.expiration and \
self.exchange_contract_address == other.exchange_contract_address and \
self.ec_signature_r == other.ec_signature_r and \
self.ec_signature_s == other.ec_signature_s and \
self.ec_signature_v == other.ec_signature_v
def __hash__(self):
return hash((self.maker,
self.taker,
self.maker_fee,
self.taker_fee,
self.pay_token,
self.pay_amount,
self.buy_token,
self.buy_amount,
self.salt,
self.fee_recipient,
self.expiration,
self.exchange_contract_address,
self.ec_signature_r,
self.ec_signature_s,
self.ec_signature_v))
def __str__(self):
return f"('{self.buy_token}', '{self.buy_amount}'," \
f" '{self.pay_token}', '{self.pay_amount}'," \
f" '{self.exchange_contract_address}', '{self.salt}')"
def __repr__(self):
return pformat(vars(self))
class LogCancel:
def __init__(self, log):
self.maker = Address(log['args']['maker'])
self.fee_recipient = Address(log['args']['feeRecipient'])
self.pay_token = Address(log['args']['makerToken'])
self.buy_token = Address(log['args']['takerToken'])
self.cancelled_pay_amount = Wad(int(log['args']['cancelledMakerTokenAmount']))
self.cancelled_buy_amount = Wad(int(log['args']['cancelledTakerTokenAmount']))
self.tokens = bytes_to_hexstring(log['args']['tokens'])
self.order_hash = bytes_to_hexstring(log['args']['orderHash'])
self.raw = log
def __repr__(self):
return pformat(vars(self))
class LogFill:
def __init__(self, log):
self.maker = Address(log['args']['maker'])
self.taker = Address(log['args']['taker'])
self.fee_recipient = Address(log['args']['feeRecipient'])
self.pay_token = Address(log['args']['makerToken'])
self.buy_token = Address(log['args']['takerToken'])
self.filled_pay_amount = Wad(int(log['args']['filledMakerTokenAmount']))
self.filled_buy_amount = Wad(int(log['args']['filledTakerTokenAmount']))
self.paid_maker_fee = Wad(int(log['args']['paidMakerFee']))
self.paid_taker_fee = Wad(int(log['args']['paidTakerFee']))
self.tokens = bytes_to_hexstring(log['args']['tokens'])
self.order_hash = bytes_to_hexstring(log['args']['orderHash'])
self.raw = log
@classmethod
def from_event(cls, event: dict):
assert(isinstance(event, dict))
codec = ABICodec(default_registry)
topics = event.get('topics')
if topics and topics[0] == HexBytes('0x0bcc4c97732e47d9946f229edb95f5b6323f601300e4690de719993f3c371129'):
log_fill_abi = [abi for abi in ZrxExchange.abi if abi.get('name') == 'LogFill'][0]
event_data = get_event_data(codec, log_fill_abi, event)
return LogFill(event_data)
def __eq__(self, other):
assert(isinstance(other, LogFill))
return self.__dict__ == other.__dict__
def __repr__(self):
return pformat(vars(self))
class ZrxExchange(Contract):
"""A client for the 0x exchange contract.
You can find the source code of the `0x` exchange contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the _0x_ `Exchange` contract.
"""
abi = Contract._load_abi(__name__, 'abi/Exchange.abi')
bin = Contract._load_bin(__name__, 'abi/Exchange.bin')
_ZERO_ADDRESS = Address("0x0000000000000000000000000000000000000000")
@staticmethod
def deploy(web3: Web3, zrx_token: Address, token_transfer_proxy: Address):
"""Deploy a new instance of the 0x `Exchange` contract.
Args:
web3: An instance of `Web` from `web3.py`.
zrx_token: The address of the ZRX token this exchange will use.
token_transfer_proxy: The address of the token transfer proxy this exchange will use.
Returns:
A `ZrxExchange` class instance.
"""
return ZrxExchange(web3=web3,
address=Contract._deploy(web3, ZrxExchange.abi, ZrxExchange.bin, [
zrx_token.address,
token_transfer_proxy.address
]))
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def zrx_token(self) -> Address:
"""Get the address of the ZRX token contract associated with this `Exchange` contract.
Returns:
The address of the `ZRX` token.
"""
return Address(self._contract.functions.ZRX_TOKEN_CONTRACT().call())
def token_transfer_proxy(self) -> Address:
"""Get the address of the `TokenTransferProxy` contract associated with this `Exchange` contract.
Returns:
The address of the `TokenTransferProxy` token.
"""
return Address(self._contract.functions.TOKEN_TRANSFER_PROXY_CONTRACT().call())
def approve(self, tokens: List[ERC20Token], approval_function):
"""Approve the 0x Exchange TokenTransferProxy contract to fully access balances of specified tokens.
In case of 0x, it's the TokenTransferProxy contract that actually gets the approvals,
not the 0x Exchange contract itself. In addition to the tokens specified as the `tokens`
parameter, the ZRX token always gets approved as well as without it the 0x Exchange
contract wouldn't be able to charge maker and taker fees.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances.
approval_function: Approval function (i.e. approval mode).
"""
assert(isinstance(tokens, list))
assert(callable(approval_function))
for token in tokens + [ERC20Token(web3=self.web3, address=self.zrx_token())]:
approval_function(token, self.token_transfer_proxy(), '0x Exchange contract')
def past_fill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogFill]:
"""Synchronously retrieve past LogFill events.
`LogFill` events are emitted by the 0x contract every time someone fills an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogFill` events represented as :py:class:`pymaker.zrx.LogFill` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogFill', LogFill, number_of_past_blocks, event_filter)
def past_cancel(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogCancel]:
"""Synchronously retrieve past LogCancel events.
`LogCancel` events are emitted by the 0x contract every time someone cancels an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogCancel` events represented as :py:class:`pymaker.zrx.LogCancel` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'LogCancel', LogCancel, number_of_past_blocks, event_filter)
def create_order(self,
pay_token: Address,
pay_amount: Wad,
buy_token: Address,
buy_amount: Wad,
expiration: int) -> Order:
"""Creates a new order.
The `maker_fee`, `taker_fee` and `fee_recipient` fields are by default set to zero.
Before signing the order and submitting it to the relayer, they may need to be
populated using the `calculate_fees()` method of the `ZrxRelayerApi` class.
Args:
pay_token: Address of the ERC20 token you want to put on sale.
pay_amount: Amount of the `pay_token` token you want to put on sale.
buy_token: Address of the ERC20 token you want to be paid with.
buy_amount: Amount of the `buy_token` you want to receive.
expiration: Unix timestamp (in seconds) when the order will expire.
Returns:
New order as an instance of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(pay_token, Address))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_token, Address))
assert(isinstance(buy_amount, Wad))
assert(isinstance(expiration, int))
return Order(exchange=self,
maker=Address(self.web3.eth.defaultAccount),
taker=self._ZERO_ADDRESS,
maker_fee=Wad(0),
taker_fee=Wad(0),
pay_token=pay_token,
pay_amount=pay_amount,
buy_token=buy_token,
buy_amount=buy_amount,
salt=self.generate_salt(),
fee_recipient=self._ZERO_ADDRESS,
expiration=expiration,
exchange_contract_address=self.address,
ec_signature_r=None,
ec_signature_s=None,
ec_signature_v=None)
def get_order_hash(self, order: Order) -> str:
"""Calculates hash of an order.
Args:
order: Order you want to calculate the hash of.
Returns:
Order hash as a hex string starting with `0x`.
"""
assert(isinstance(order, Order))
# the hash depends on the exchange contract address as well
assert(order.exchange_contract_address == self.address)
result = self._contract.functions.getOrderHash(self._order_addresses(order), self._order_values(order)).call()
return bytes_to_hexstring(result)
def get_unavailable_buy_amount(self, order: Order) -> Wad:
"""Return the order amount which was either taken or cancelled.
Args:
order: Order you want to get the unavailable amount of.
Returns:
The unavailable amount of the order (i.e. the amount which was either taken or cancelled),
expressed in terms of the `buy_token` token.
"""
assert(isinstance(order, Order))
return Wad(self._contract.functions.getUnavailableTakerTokenAmount(
hexstring_to_bytes(self.get_order_hash(order))).call())
def sign_order(self, order: Order) -> Order:
"""Signs an order so it can be submitted to the relayer.
Order will be signed by the `web3.eth.defaultAccount` account.
Args:
order: Order you want to sign.
Returns:
Signed order. Copy of the order passed as a parameter with the `ec_signature_r`, `ec_signature_s`
and `ec_signature_v` fields filled with signature values.
"""
assert(isinstance(order, Order))
signature = eth_sign(hexstring_to_bytes(self.get_order_hash(order)), self.web3)
v, r, s = to_vrs(signature)
signed_order = copy.copy(order)
signed_order.ec_signature_r = bytes_to_hexstring(r)
signed_order.ec_signature_s = bytes_to_hexstring(s)
signed_order.ec_signature_v = v
return signed_order
def fill_order(self, order: Order, fill_buy_amount: Wad) -> Transact:
"""Fills an order.
Args:
order: The order to be filled.
fill_buy_amount: The amount (in terms of `buy_token` of the original order) to be filled.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
assert(isinstance(fill_buy_amount, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'fillOrder',
[self._order_addresses(order), self._order_values(order), fill_buy_amount.value,
True, order.ec_signature_v,
hexstring_to_bytes(order.ec_signature_r),
hexstring_to_bytes(order.ec_signature_s)])
def cancel_order(self, order: Order) -> Transact:
"""Cancels an order.
Args:
order: Order you want to cancel.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
return Transact(self, self.web3, self.abi, self.address, self._contract, 'cancelOrder',
[self._order_addresses(order), self._order_values(order), order.buy_amount.value])
@staticmethod
def _order_values(order):
return [order.pay_amount.value,
order.buy_amount.value,
order.maker_fee.value,
order.taker_fee.value,
order.expiration,
order.salt]
@staticmethod
def _order_addresses(order):
return [order.maker.address,
order.taker.address,
order.pay_token.address,
order.buy_token.address,
order.fee_recipient.address]
@staticmethod
def generate_salt() -> int:
return random.randint(1, 2**256 - 1)
def __repr__(self):
return f"ZrxExchange('{self.address}')"
class ZrxRelayerApi:
"""A client for the Standard 0x Relayer API V0.
Attributes:
exchange: The 0x Exchange contract.
api_server: Base URL of the Standard Relayer API server.
"""
logger = logging.getLogger()
timeout = 15.5
def __init__(self, exchange: ZrxExchange, api_server: str):
assert(isinstance(exchange, ZrxExchange))
assert(isinstance(api_server, str))
self.exchange = exchange
self.api_server = api_server
def get_orders(self, pay_token: Address, buy_token: Address, per_page: int = 100) -> List[Order]:
"""Returns active orders filtered by token pair (one side).
In order to get them, issues a `/v0/orders` call to the Standard Relayer API.
Args:
per_page: Maximum number of orders to be downloaded per page. 0x Standard Relayer API
limitation is 100, but some relayers can handle more so that's why this parameter
is exposed.
Returns:
Orders, as a list of instances of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(pay_token, Address))
assert(isinstance(buy_token, Address))
url = f"{self.api_server}/v0/orders?" \
f"exchangeContractAddress={str(self.exchange.address.address).lower()}&" \
f"makerTokenAddress={str(pay_token.address).lower()}&" \
f"takerTokenAddress={str(buy_token.address).lower()}&" \
f"per_page={per_page}"
response = requests.get(url, timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to fetch 0x orders from the relayer: {http_response_summary(response)}")
return list(map(lambda item: Order.from_json(self.exchange, item), response.json()))
def get_orders_by_maker(self, maker: Address, per_page: int = 100) -> List[Order]:
"""Returns all active orders created by `maker`.
In order to get them, issues a `/v0/orders` call to the Standard Relayer API.
Args:
maker: Address of the `maker` to filter the orders by.
per_page: Maximum number of orders to be downloaded per page. 0x Standard Relayer API
limitation is 100, but some relayers can handle more so that's why this parameter
is exposed.
Returns:
Active orders created by `maker`, as a list of instances of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(maker, Address))
url = f"{self.api_server}/v0/orders?" \
f"exchangeContractAddress={str(self.exchange.address.address).lower()}&" \
f"maker={str(maker.address).lower()}&" \
f"per_page={per_page}"
response = requests.get(url, timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to fetch 0x orders from the relayer: {http_response_summary(response)}")
return list(map(lambda item: Order.from_json(self.exchange, item), response.json()))
def calculate_fees(self, order: Order) -> Order:
"""Takes and order and returns the same order with proper relayer fees.
Issues a call to the `/v0/fees` endpoint of the Standard Relayer API, as a result of it
new order is returned being the copy of the original one with the `maker_fee`, `taker_fee`
and `fee_recipient` fields filled in according to the relayer.
Relayers will very likely reject orders submitted if proper fees are not set first.
The standard approach is to call `calculate_fees()` first and then call `submit_order()`
passing the order received from `calculate_fees()` as parameter.
Args:
order: Order which should have fees calculated. The values of `maker_fee`, `taker_fee`
and `fee_recipient` are irrelevant and may as well be zeros as they will be overwritten
by this method anyway.
Returns:
Copy of the order received as a parameter with the `maker_fee`, `taker_fee` and `fee_recipient`
fields updated according to the relayer.
"""
assert(isinstance(order, Order))
response = requests.post(f"{self.api_server}/v0/fees", json=order.to_json_without_fees(), timeout=self.timeout)
if response.status_code == 200:
data = response.json()
order_with_fees = copy.copy(order)
order_with_fees.maker_fee = Wad(int(data['makerFee']))
order_with_fees.taker_fee = Wad(int(data['takerFee']))
order_with_fees.fee_recipient = Address(data['feeRecipient'])
return order_with_fees
else:
raise Exception(f"Failed to fetch fees for 0x order: {http_response_summary(response)}")
def submit_order(self, order: Order) -> bool:
"""Submits the order to the relayer.
Posts the order to the `/v0/order` endpoint of the Standard Relayer API
Args:
order: Order to be submitted.
Return:
`True` if order submission was successful. `False` otherwise.
"""
assert(isinstance(order, Order))
response = requests.post(f"{self.api_server}/v0/order", json=order.to_json(), timeout=self.timeout)
if response.status_code in [200, 201]:
self.logger.info(f"Placed 0x order: {order}")
return True
else:
self.logger.warning(f"Failed to place 0x order: {http_response_summary(response)}")
return False
def __repr__(self):
return f"ZrxRelayerApi()"
================================================
FILE: pymaker/zrxv2.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import array
import copy
import logging
import random
import time
from pprint import pformat
from typing import List, Optional, Tuple
import requests
from eth_abi import encode_single, encode_abi, decode_single
from hexbytes import HexBytes
from web3 import Web3
from web3._utils.events import get_event_data
from eth_abi.codec import ABICodec
from eth_abi.registry import registry as default_registry
from pymaker import Contract, Address, Transact
from pymaker.numeric import Wad
from pymaker.sign import eth_sign, to_vrs
from pymaker.token import ERC20Token
from pymaker.util import bytes_to_hexstring, hexstring_to_bytes, http_response_summary
class Asset:
@staticmethod
def deserialize(asset: str):
assert(isinstance(asset, str))
if ERC20Asset.ID.upper() == asset[0:10].upper():
return ERC20Asset(token_address=Address("0x" + asset[-40:]))
else:
return UnknownAsset(asset=asset)
def serialize(self) -> str:
raise Exception("serialize() not implemented")
def __repr__(self):
return pformat(vars(self))
class ERC20Asset(Asset):
ID = "0xf47261b0"
def __init__(self, token_address: Address):
assert(isinstance(token_address, Address))
self.token_address = token_address
def serialize(self) -> str:
return self.ID + self.token_address.address[2:].zfill(64).lower()
def __hash__(self):
return hash(self.token_address)
def __eq__(self, other):
return isinstance(other, ERC20Asset) and self.token_address == other.token_address
class UnknownAsset(Asset):
def __init__(self, asset: str):
assert(isinstance(asset, str))
self.asset = asset
def serialize(self) -> str:
return self.asset
def __hash__(self):
return hash(self.asset)
def __eq__(self, other):
return isinstance(other, UnknownAsset) and self.asset == other.asset
class Order:
def __init__(self, exchange, sender: Address, maker: Address, taker: Address, maker_fee: Wad, taker_fee: Wad,
pay_asset: Asset, pay_amount: Wad, buy_asset: Asset, buy_amount: Wad, salt: int, fee_recipient: Address,
expiration: int, exchange_contract_address: Address, signature: Optional[str]):
assert(isinstance(sender, Address))
assert(isinstance(maker, Address))
assert(isinstance(taker, Address))
assert(isinstance(maker_fee, Wad))
assert(isinstance(taker_fee, Wad))
assert(isinstance(pay_asset, Asset))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_asset, Asset))
assert(isinstance(buy_amount, Wad))
assert(isinstance(salt, int))
assert(isinstance(fee_recipient, Address))
assert(isinstance(expiration, int))
assert(isinstance(exchange_contract_address, Address))
assert(isinstance(signature, str) or (signature is None))
self._exchange = exchange
self.sender = sender
self.maker = maker
self.taker = taker
self.maker_fee = maker_fee
self.taker_fee = taker_fee
self.pay_asset = pay_asset
self.pay_amount = pay_amount
self.buy_asset = buy_asset
self.buy_amount = buy_amount
self.salt = salt
self.fee_recipient = fee_recipient
self.expiration = expiration
self.exchange_contract_address = exchange_contract_address
self.signature = signature
# this is not a proper 0x order_id, it's just so `OrderBookManager` can uniquely identify orders
@property
def order_id(self):
return hash(self)
@property
def sell_to_buy_price(self) -> Wad:
return self.pay_amount / self.buy_amount
@property
def buy_to_sell_price(self) -> Wad:
return self.buy_amount / self.pay_amount
@property
def remaining_buy_amount(self) -> Wad:
return Wad.max(self.buy_amount - self._exchange.get_unavailable_buy_amount(self), Wad(0))
@property
def remaining_sell_amount(self) -> Wad:
unavailable_buy_amount = self._exchange.get_unavailable_buy_amount(self)
if unavailable_buy_amount >= self.buy_amount:
return Wad(0)
else:
return Wad.max(self.pay_amount - (unavailable_buy_amount * self.pay_amount / self.buy_amount), Wad(0))
@staticmethod
def from_json(exchange, data: dict):
assert(isinstance(data, dict))
return Order(exchange=exchange,
sender=Address(data['senderAddress']),
maker=Address(data['makerAddress']),
taker=Address(data['takerAddress']),
maker_fee=Wad(int(data['makerFee'])),
taker_fee=Wad(int(data['takerFee'])),
pay_asset=Asset.deserialize(str(data['makerAssetData'])),
pay_amount=Wad(int(data['makerAssetAmount'])),
buy_asset=Asset.deserialize(str(data['takerAssetData'])),
buy_amount=Wad(int(data['takerAssetAmount'])),
salt=int(data['salt']),
fee_recipient=Address(data['feeRecipientAddress']),
expiration=int(data['expirationTimeSeconds']),
exchange_contract_address=Address(data['exchangeAddress']),
signature=data['signature'] if 'signature' in data else None)
def to_json_without_fees(self) -> dict:
return {
"exchangeAddress": self.exchange_contract_address.address.lower(),
"makerAddress": self.maker.address.lower(),
"takerAddress": self.taker.address.lower(),
"makerAssetData": self.pay_asset.serialize(),
"takerAssetData": self.buy_asset.serialize(),
"makerAssetAmount": str(self.pay_amount.value),
"takerAssetAmount": str(self.buy_amount.value),
"expirationTimeSeconds": str(self.expiration)
}
def to_json(self) -> dict:
return {
"exchangeAddress": self.exchange_contract_address.address.lower(),
"senderAddress": self.sender.address.lower(),
"makerAddress": self.maker.address.lower(),
"takerAddress": self.taker.address.lower(),
"makerAssetData": self.pay_asset.serialize(),
"takerAssetData": self.buy_asset.serialize(),
"makerAssetAmount": str(self.pay_amount.value),
"takerAssetAmount": str(self.buy_amount.value),
"feeRecipientAddress": self.fee_recipient.address.lower(),
"makerFee": str(self.maker_fee.value),
"takerFee": str(self.taker_fee.value),
"expirationTimeSeconds": str(self.expiration),
"salt": str(self.salt),
"signature": self.signature
}
def __eq__(self, other):
assert(isinstance(other, Order))
return self.sender == other.sender and \
self.maker == other.maker and \
self.taker == other.taker and \
self.maker_fee == other.maker_fee and \
self.taker_fee == other.taker_fee and \
self.pay_asset == other.pay_asset and \
self.pay_amount == other.pay_amount and \
self.buy_asset == other.buy_asset and \
self.buy_amount == other.buy_amount and \
self.salt == other.salt and \
self.fee_recipient == other.fee_recipient and \
self.expiration == other.expiration and \
self.exchange_contract_address == other.exchange_contract_address and \
self.signature == other.signature
def __hash__(self):
return hash((self.sender,
self.maker,
self.taker,
self.maker_fee,
self.taker_fee,
self.pay_asset,
self.pay_amount,
self.buy_asset,
self.buy_amount,
self.salt,
self.fee_recipient,
self.expiration,
self.exchange_contract_address,
self.signature))
def __str__(self):
return f"('{self.buy_asset}', '{self.buy_amount}'," \
f" '{self.pay_asset}', '{self.pay_amount}'," \
f" '{self.exchange_contract_address}', '{self.salt}')"
def __repr__(self):
return pformat(vars(self))
class LogCancel:
def __init__(self, log):
self.maker = Address(log['args']['makerAddress'])
self.fee_recipient = Address(log['args']['feeRecipientAddress'])
self.sender = Address(log['args']['senderAddress'])
self.pay_asset = Asset.deserialize(bytes_to_hexstring(log['args']['makerAssetData']))
self.buy_asset = Asset.deserialize(bytes_to_hexstring(log['args']['takerAssetData']))
self.order_hash = bytes_to_hexstring(log['args']['orderHash'])
self.raw = log
def __repr__(self):
return pformat(vars(self))
class LogFill:
def __init__(self, log):
self.sender = Address(log['args']['senderAddress'])
self.maker = Address(log['args']['makerAddress'])
self.taker = Address(log['args']['takerAddress'])
self.fee_recipient = Address(log['args']['feeRecipientAddress'])
self.pay_asset = Asset.deserialize(bytes_to_hexstring(log['args']['makerAssetData']))
self.buy_asset = Asset.deserialize(bytes_to_hexstring(log['args']['takerAssetData']))
self.filled_pay_amount = Wad(int(log['args']['makerAssetFilledAmount']))
self.filled_buy_amount = Wad(int(log['args']['takerAssetFilledAmount']))
self.paid_maker_fee = Wad(int(log['args']['makerFeePaid']))
self.paid_taker_fee = Wad(int(log['args']['takerFeePaid']))
self.order_hash = bytes_to_hexstring(log['args']['orderHash'])
self.raw = log
@classmethod
def from_event(cls, event: dict):
assert(isinstance(event, dict))
topics = event.get('topics')
if topics and topics[0] == HexBytes('0x0bcc4c97732e47d9946f229edb95f5b6323f601300e4690de719993f3c371129'):
log_fill_abi = [abi for abi in ZrxExchangeV2.abi if abi.get('name') == 'Fill'][0]
codec = ABICodec(default_registry)
event_data = get_event_data(codec, log_fill_abi, event)
return LogFill(event_data)
else:
return None
def __eq__(self, other):
assert(isinstance(other, LogFill))
return self.__dict__ == other.__dict__
def __repr__(self):
return pformat(vars(self))
class ZrxExchangeV2(Contract):
"""A client for the 0x V2 exchange contract.
You can find the `0x V2` exchange contract here:
.
Attributes:
web3: An instance of `Web` from `web3.py`.
address: Ethereum address of the _0x_ `Exchange` contract.
"""
abi = Contract._load_abi(__name__, 'abi/ExchangeV2.abi')
bin = Contract._load_bin(__name__, 'abi/ExchangeV2.bin')
_ZERO_ADDRESS = Address("0x0000000000000000000000000000000000000000")
ORDER_INFO_TYPE = '(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)'
@staticmethod
def deploy(web3: Web3, zrx_asset: str):
"""Deploy a new instance of the 0x `Exchange` contract.
Args:
web3: An instance of `Web` from `web3.py`.
zrx_token: The address of the ZRX token this exchange will use.
Returns:
A `ZrxExchange` class instance.
"""
return ZrxExchangeV2(web3=web3,
address=Contract._deploy(web3, ZrxExchangeV2.abi, ZrxExchangeV2.bin, []))
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
def zrx_asset(self) -> str:
"""Get the asset data of the ZRX token contract associated with this `ExchangeV2` contract.
Returns:
The asset data of the `ZRX` token.
"""
return str(bytes_to_hexstring(self._contract.functions.ZRX_ASSET_DATA().call()))
def zrx_token(self) -> Address:
"""Get the address of the ZRX token contract associated with this `ExchangeV2` contract.
Returns:
The address of the `ZRX` token.
"""
return Address("0x" + self.zrx_asset()[-40:])
def asset_transfer_proxy(self, proxy_id: str) -> Address:
"""Get the address of the `ERC20Proxy` contract associated with this `Exchange` contract.
Returns:
The address of the `ERC20Proxy` token.
"""
assert(isinstance(proxy_id, str))
return Address(self._contract.functions.getAssetProxy(hexstring_to_bytes(proxy_id)).call())
def approve(self, tokens: List[ERC20Token], approval_function):
"""Approve the 0x ERC20Proxy contract to fully access balances of specified tokens.
In case of 0x V2, it's the ERC20Proxy contract that actually gets the approvals,
not the 0x Exchange contract itself. In addition to the tokens specified as the `tokens`
parameter, the ZRX token always gets approved as well as without it the 0x Exchange
contract wouldn't be able to charge maker and taker fees.
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager`
in `pymaker.approval`.
Args:
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances.
approval_function: Approval function (i.e. approval mode).
"""
assert(isinstance(tokens, list))
assert(callable(approval_function))
for token in tokens: # TODO + [ERC20Token(web3=self.web3, address=self.zrx_token())]
approval_function(token, self.asset_transfer_proxy(ERC20Asset.ID), '0x ERC20Proxy contract')
def past_fill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogFill]:
"""Synchronously retrieve past LogFill events.
`LogFill` events are emitted by the 0x contract every time someone fills an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogFill` events represented as :py:class:`pymaker.zrx.LogFill` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'Fill', LogFill, number_of_past_blocks, event_filter)
def past_cancel(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogCancel]:
"""Synchronously retrieve past LogCancel events.
`LogCancel` events are emitted by the 0x contract every time someone cancels an order.
Args:
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from.
event_filter: Filter which will be applied to returned events.
Returns:
List of past `LogCancel` events represented as :py:class:`pymaker.zrx.LogCancel` class.
"""
assert(isinstance(number_of_past_blocks, int))
assert(isinstance(event_filter, dict) or (event_filter is None))
return self._past_events(self._contract, 'Cancel', LogCancel, number_of_past_blocks, event_filter)
def create_order(self,
pay_asset: Asset,
pay_amount: Wad,
buy_asset: Asset,
buy_amount: Wad,
expiration: int) -> Order:
"""Creates a new order.
The `maker_fee`, `taker_fee` and `fee_recipient` fields are by default set to zero.
Before signing the order and submitting it to the relayer, they may need to be
populated using the `calculate_fees()` method of the `ZrxRelayerApi` class.
Args:
pay_asset: The asset you want to put on sale.
pay_amount: Amount of the `pay_asset` token you want to put on sale.
buy_asset: The asset you want to be paid with.
buy_amount: Amount of the `buy_asset` you want to receive.
expiration: Unix timestamp (in seconds) when the order will expire.
Returns:
New order as an instance of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(pay_asset, Asset))
assert(isinstance(pay_amount, Wad))
assert(isinstance(buy_asset, Asset))
assert(isinstance(buy_amount, Wad))
assert(isinstance(expiration, int))
return Order(exchange=self,
sender=self._ZERO_ADDRESS,
maker=Address(self.web3.eth.defaultAccount),
taker=self._ZERO_ADDRESS,
maker_fee=Wad(0),
taker_fee=Wad(0),
pay_asset=pay_asset,
pay_amount=pay_amount,
buy_asset=buy_asset,
buy_amount=buy_amount,
salt=self.generate_salt(),
fee_recipient=self._ZERO_ADDRESS,
expiration=expiration,
exchange_contract_address=self.address,
signature=None)
def _get_order_info(self, order):
assert(isinstance(order, Order))
method_signature = self.web3.keccak(text=f"getOrderInfo({self.ORDER_INFO_TYPE})")[0:4]
method_parameters = encode_single(f"({self.ORDER_INFO_TYPE})", [self._order_tuple(order)])
request = bytes_to_hexstring(method_signature + method_parameters)
response = self.web3.eth.call({'to': self.address.address, 'data': request})
response_decoded = decode_single("((uint8,bytes32,uint256))", response)
return response_decoded
def get_order_hash(self, order: Order) -> str:
"""Calculates hash of an order.
Args:
order: Order you want to calculate the hash of.
Returns:
Order hash as a hex string starting with `0x`.
"""
assert(isinstance(order, Order))
# the hash depends on the exchange contract address as well
assert(order.exchange_contract_address == self.address)
return bytes_to_hexstring(self._get_order_info(order)[0][1])
def get_unavailable_buy_amount(self, order: Order) -> Wad:
"""Return the order amount which was either taken or cancelled.
Args:
order: Order you want to get the unavailable amount of.
Returns:
The unavailable amount of the order (i.e. the amount which was either taken or cancelled),
expressed in terms of the `buy_token` token.
"""
assert(isinstance(order, Order))
order_info = self._get_order_info(order)[0]
if order_info[0] in [0, # INVALID, // Default value
1, # INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount
2, # INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount
4, # EXPIRED, // Order has already expired
5, # FULLY_FILLED, // Order is fully filled
6]: # CANCELLED // Order has been cancelled
return order.buy_amount
else:
return Wad(order_info[2])
def sign_order(self, order: Order) -> Order:
"""Signs an order so it can be submitted to the relayer.
Order will be signed by the `web3.eth.defaultAccount` account.
Args:
order: Order you want to sign.
Returns:
Signed order. Copy of the order passed as a parameter with the `signature` field filled with signature.
"""
assert(isinstance(order, Order))
signature = eth_sign(hexstring_to_bytes(self.get_order_hash(order)), self.web3)
v, r, s = to_vrs(signature)
signed_order = copy.copy(order)
signed_order.signature = bytes_to_hexstring(bytes([v])) + \
bytes_to_hexstring(r)[2:] + \
bytes_to_hexstring(s)[2:] + \
"03" # EthSign
return signed_order
def fill_order(self, order: Order, fill_buy_amount: Wad) -> Transact:
"""Fills an order.
Args:
order: The order to be filled.
fill_buy_amount: The amount (in terms of `buy_token` of the original order) to be filled.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
assert(isinstance(fill_buy_amount, Wad))
method_signature = self.web3.keccak(text=f"fillOrder({self.ORDER_INFO_TYPE},uint256,bytes)")[0:4]
method_parameters = encode_single(f"({self.ORDER_INFO_TYPE},uint256,bytes)", [self._order_tuple(order),
fill_buy_amount.value,
hexstring_to_bytes(order.signature)])
request = bytes_to_hexstring(method_signature + method_parameters)
return Transact(self, self.web3, self.abi, self.address, self._contract, None,
[request])
def cancel_order(self, order: Order) -> Transact:
"""Cancels an order.
Args:
order: Order you want to cancel.
Returns:
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction.
"""
assert(isinstance(order, Order))
method_signature = self.web3.keccak(text=f"cancelOrder({self.ORDER_INFO_TYPE})")[0:4]
method_parameters = encode_single(f"({self.ORDER_INFO_TYPE})", [self._order_tuple(order)])
request = bytes_to_hexstring(method_signature + method_parameters)
return Transact(self, self.web3, self.abi, self.address, self._contract, None,
[request])
@staticmethod
def _order_tuple(order):
return (order.maker.address,
order.taker.address,
order.fee_recipient.address,
order.sender.address,
order.pay_amount.value,
order.buy_amount.value,
order.maker_fee.value,
order.taker_fee.value,
order.expiration,
order.salt,
hexstring_to_bytes(order.pay_asset.serialize()),
hexstring_to_bytes(order.buy_asset.serialize()))
@staticmethod
def generate_salt() -> int:
return int(time.time() * 1000)
def __repr__(self):
return f"ZrxExchangeV2('{self.address}')"
class ZrxRelayerApiV2:
"""A client for the Standard 0x Relayer API V2.
Attributes:
exchange: The 0x Exchange V2 contract.
api_server: Base URL of the Standard Relayer API server.
"""
logger = logging.getLogger()
timeout = 15.5
def __init__(self, exchange: ZrxExchangeV2, api_server: str):
assert(isinstance(exchange, ZrxExchangeV2))
assert(isinstance(api_server, str))
self.exchange = exchange
self.api_server = api_server
def get_book(self, pay_token: Address, buy_token: Address, depth: int = 100) -> Tuple[List[Order], List[Order]]:
assert(isinstance(pay_token, Address))
assert(isinstance(buy_token, Address))
assert(isinstance(depth, int))
params = {"baseAssetData": ERC20Asset(pay_token).serialize(),
"quoteAssetData": ERC20Asset(buy_token).serialize(),
"perPage": depth}
response = requests.get(f"{self.api_server}/v2/orderbook", params, timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to fetch 0x orderbook from the relayer: {http_response_summary(response)}")
data = response.json()
return list(map(lambda item: Order.from_json(self.exchange, item['order']), data['asks']['records'])), \
list(map(lambda item: Order.from_json(self.exchange, item['order']), data['bids']['records']))
def get_orders(self, pay_token: Address, buy_token: Address, per_page: int = 100) -> List[Order]:
"""Returns active orders filtered by token pair (one side).
In order to get them, issues a `/v2/orders` call to the Standard Relayer API.
Args:
per_page: Maximum number of orders to be downloaded per page. 0x Standard Relayer API
limitation is 100, but some relayers can handle more so that's why this parameter
is exposed.
Returns:
Orders, as a list of instances of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(pay_token, Address))
assert(isinstance(buy_token, Address))
params = { "exchangeAddress": self.exchange.address.address.lower(),
"makerAssetData": ERC20Asset(pay_token).serialize(),
"takerAssetData": ERC20Asset(buy_token).serialize(),
"per_page": per_page,
}
response = requests.get(f"{self.api_server}/v2/orders", params=params, timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to fetch 0x orders from the relayer: {http_response_summary(response)}")
data = response.json()
if 'records' in data:
return list(map(lambda item: Order.from_json(self.exchange, item['order']), data['records']))
else:
return []
def get_order(self, order_hash: str) -> Order:
assert(isinstance(order_hash, str))
response = requests.get(f"{self.api_server}/v2/order/{order_hash}", timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to 0x order from the relayer: {http_response_summary(response)}")
return Order.from_json(self.exchange, response.json()['order'])
def get_orders_by_maker(self, maker: Address, per_page: int = 100) -> List[Order]:
"""Returns all active orders created by `maker`.
In order to get them, issues a `/v2/orders` call to the Standard Relayer API.
Args:
maker: Address of the `maker` to filter the orders by.
per_page: Maximum number of orders to be downloaded per page. 0x Standard Relayer API
limitation is 100, but some relayers can handle more so that's why this parameter
is exposed.
Returns:
Active orders created by `maker`, as a list of instances of the :py:class:`pymaker.zrx.Order` class.
"""
assert(isinstance(maker, Address))
params = { "exchangeAddress": self.exchange.address.address.lower(),
"makerAddress": str(maker).lower(),
"per_page": per_page,
}
response = requests.get(f"{self.api_server}/v2/orders", params=params, timeout=self.timeout)
if not response.ok:
raise Exception(f"Failed to fetch 0x orders from the relayer: {http_response_summary(response)}")
data = response.json()
if 'records' in data:
return list(map(lambda item: Order.from_json(self.exchange, item['order']), data['records']))
else:
return []
def configure_order(self, order: Order) -> Order:
"""Takes a partial order and receive information required to complete the order:
senderAddress, feeRecipientAddress, makerFee, takerFee
Issues a call to the `/v2/order_config` endpoint of the Standard Relayer API V2, as a result
of it new order is returned being the copy of the original one with the `senderAddress`,
`maker_fee`, `taker_fee` and `fee_recipient` fields filled in according to the relayer.
Relayers will very likely reject orders submitted if proper fields are not set first.
The standard approach is to call `configure_order()` first and then call `submit_order()`
passing the order received from `configure_order()` as parameter.
Args:
order: Order which should be configured. The values of `senderAddress`, `maker_fee`, `taker_fee`
and `fee_recipient` could be overwritten by this method.
Returns:
Copy of the order received as a parameter with the `senderAddress`, `maker_fee`, `taker_fee`
and `fee_recipient` fields updated according to the relayer.
"""
assert(isinstance(order, Order))
response = requests.get(f"{self.api_server}/v2/order_config", params=order.to_json_without_fees(), timeout=self.timeout)
if response.status_code == 200:
data = response.json()
#{"senderAddress":"0xc8924d8cd9a758a4150afe7cc7030effaff1aecc","feeRecipientAddress":"0xc8924d8cd9a758a4150afe7cc7030effaff1aecc","makerFee":"0","takerFee":"0"}
configured_order = copy.copy(order)
configured_order.sender = Address(data['senderAddress'])
configured_order.maker_fee = Wad(int(data['makerFee']))
configured_order.taker_fee = Wad(int(data['takerFee']))
configured_order.fee_recipient = Address(data['feeRecipientAddress'])
return configured_order
else:
raise Exception(f"Failed to configure order with 0x SRAv2: {http_response_summary(response)}")
def submit_order(self, order: Order) -> bool:
"""Submits the order to the relayer.
Posts the order to the `/v2/order` endpoint of the Standard Relayer API
Args:
order: Order to be submitted.
Return:
`True` if order submission was successful. `False` otherwise.
"""
assert(isinstance(order, Order))
response = requests.post(f"{self.api_server}/v2/order", json=order.to_json(), timeout=self.timeout)
if response.status_code in [200, 201]:
self.logger.info(f"Placed 0x order: {order}")
return True
else:
self.logger.warning(f"Failed to place 0x order: {http_response_summary(response)}")
return False
def __repr__(self):
return f"ZrxRelayerApiV2()"
================================================
FILE: requirements-dev.txt
================================================
attrs == 19.1.0
codecov == 2.0.9
mock == 2.0.0
pytest == 3.3.0
pytest-asyncio == 0.9.0
pytest-cov == 2.5.1
pytest-mock == 1.6.3
pytest-timeout == 1.2.1
asynctest == 0.13.0
Sphinx == 1.6.2
zipp == 3.4.1
================================================
FILE: requirements.txt
================================================
pytz == 2017.3
web3 == 5.12.0
requests == 2.22.0
eth-keys<0.3.0,>=0.2.1
jsonnet == 0.9.5
================================================
FILE: setup.py
================================================
"""A setuptools based setup module.
See:
https://packaging.python.org/guides/distributing-packages-using-setuptools/
https://github.com/pypa/sampleproject
https://github.com/pypa/sampleproject/blob/master/setup.py
"""
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
# Read requirements.txt
with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f:
requirements = f.read().split('\n')
# Arguments marked as "Required" below must be included for upload to PyPI.
# Fields marked as "Optional" may be commented out.
setup(
name='pymaker',
# Versions should comply with PEP 440:
# https://www.python.org/dev/peps/pep-0440/
#
# For a discussion on single-sourcing the version across setup.py and the
# project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version='1.1.3', # Required
description='Python API for Maker contracts',
license='COPYING',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/makerdao/pymaker',
author='MakerDAO',
packages=find_packages(include=['pymaker', 'pymaker.*']), # Required
package_data={'pymaker': ['abi/*', '../config/*']},
include_package_data=True,
python_requires='~=3.6',
# This field lists other packages that your project depends on to run.
# Any package you put here will be installed by pip when your project is
# installed, so they must be valid existing projects.
#
# For an analysis of "install_requires" vs pip's requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=requirements
)
================================================
FILE: test-dss.sh
================================================
#!/bin/bash
# Pull the docker image
docker pull makerdao/testchain-pymaker:unit-testing
# Remove existing container if tests not gracefully stopped
docker-compose down
# Start ganache
docker-compose up -d ganache
# Start parity and wait to initialize
docker-compose up -d parity
sleep 2
# Run the tests
py.test --cov=pymaker --cov-report=term --cov-append tests/test_auctions.py tests/test_cdpmanager.py \
tests/test_dsrmanager.py tests/test_dss.py tests/test_savings.py tests/test_shutdown.py $@
TEST_RESULT=$?
# Cleanup
docker-compose down
exit $TEST_RESULT
================================================
FILE: test.sh
================================================
#!/bin/bash
# Pull the docker image
docker pull makerdao/testchain-pymaker:unit-testing
# Remove existing container if tests not gracefully stopped
docker-compose down
# Start ganache
docker-compose up -d ganache
# Start parity and wait to initialize
docker-compose up -d parity
sleep 2
# Run the tests
py.test --cov=pymaker --cov-report=term --cov-append tests/ $@
TEST_RESULT=$?
# Cleanup
docker-compose down
exit $TEST_RESULT
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/abi/DaiMock.abi
================================================
[{"constant":true,"inputs":[],"name":"vat","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"can","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"hope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"move","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"nope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"urn","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"join","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"exit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"dai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"vat_","type":"address"},{"name":"dai_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]
================================================
FILE: tests/abi/DaiMock.sol
================================================
pragma solidity ^0.4.24;
// Fusion between a DaiJoin and a DaiMove
contract GemLike {
function transferFrom(address,address,uint) public returns (bool);
function mint(address,uint) public;
function burn(address,uint) public;
}
contract VatLike {
function slip(bytes32,bytes32,int) public;
function move(bytes32,bytes32,int) public;
function flux(bytes32,bytes32,bytes32,int) public;
}
contract DaiMock {
VatLike public vat;
GemLike public dai;
constructor(address vat_, address dai_) public {
vat = VatLike(vat_);
dai = GemLike(dai_);
}
uint constant ONE = 10 ** 27;
function mul(uint x, uint y) internal pure returns (int z) {
z = int(x * y);
require(int(z) >= 0);
require(y == 0 || uint(z) / y == x);
}
mapping(address => mapping (address => bool)) public can;
function hope(address guy) public { can[msg.sender][guy] = true; }
function nope(address guy) public { can[msg.sender][guy] = false; }
function move(address src, address dst, uint wad) public {
require(src == msg.sender || can[src][msg.sender]);
vat.move(bytes32(src), bytes32(dst), mul(ONE, wad));
}
function join(bytes32 urn, uint wad) public {
vat.move(bytes32(address(this)), urn, mul(ONE, wad));
dai.burn(msg.sender, wad);
}
function exit(address guy, uint wad) public {
vat.move(bytes32(msg.sender), bytes32(address(this)), mul(ONE, wad));
dai.mint(guy, wad);
}
}
================================================
FILE: tests/abi/GemMock.abi
================================================
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"mint","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"can","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"name_","type":"bytes32"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"stopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"mint","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"hope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"push","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"move","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"start","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"push","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"}],"name":"nope","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"wad","type":"uint256"}],"name":"pull","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"symbol_","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}]
================================================
FILE: tests/abi/GemMock.sol
================================================
pragma solidity ^0.4.24;
import "ds-token/token.sol";
contract GemMock is DSToken('') {
constructor(bytes32 symbol_) public {
symbol = symbol_;
}
function can(address src, address guy) public view returns (bool) {
if (allowance(src, guy) > 0) {
return true;
}
return false;
}
function push(bytes32 guy, uint wad) public { push(address(guy), wad); }
function hope(address guy) public { approve(guy); }
function nope(address guy) public { approve(guy, 0); }
}
================================================
FILE: tests/abi/OasisMockPriceOracle.abi
================================================
[{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"getPriceFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"_price","type":"uint256"}],"name":"setPrice","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
================================================
FILE: tests/abi/OasisMockPriceOracle.sol
================================================
pragma solidity ^0.5.12;
contract OasisMockPriceOracle {
uint256 price;
function setPrice(address, uint256 _price) public {
price = _price;
}
function getPriceFor(address, address, uint256) public view returns (uint256) {
return price;
}
}
================================================
FILE: tests/accounts/0_0x9596c16d7bf9323265c2f2e22f43e6c80eb3d943.json
================================================
{"version":3,"id":"375c1ad3-b203-4e32-b32a-dd5cad819a4c","address":"9596c16d7bf9323265c2f2e22f43e6c80eb3d943","crypto":{"ciphertext":"4a24fa91bd5c9652ca0a3e9b03c8376d1c4cc1beda2641ea5e2642519b1614c6","cipherparams":{"iv":"39a62791ce1c0e80a3b8a159b1d1c2dd"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"e1e7167139d333de7897971f90f6af69c3befe0a02604775c55d109e82659156","n":262144,"r":8,"p":1},"mac":"945369f4bf11492703ff7793038ab45f7f20a07f559be0b3721f159e8152d00e"}}
================================================
FILE: tests/accounts/1_0xe415482ca06eeb684ad3f758c2129fca4b1eb1f4.json
================================================
{"version":3,"id":"d2275fb9-533f-4c63-b970-9767bb45d38d","address":"e415482ca06eeb684ad3f758c2129fca4b1eb1f4","crypto":{"ciphertext":"9807f801eae14580b0641c6474e26e2ac2876f3903d0705965d2c5c5f1cfc4ab","cipherparams":{"iv":"ede22e7e1e3f74560328258df631f7d7"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"9cdab7c0c88539b75946b03a0a3e3d637faf42cf0e99d92540d269b0bc358fa0","n":262144,"r":8,"p":1},"mac":"38c3c786a8b8d61d5169ae854b18c9e4968b6fb596028a71a78fab05477bf889"}}
================================================
FILE: tests/accounts/2_0x270b0e8d873e858abd698a000b0da0b94e21d84c.json
================================================
{"version":3,"id":"3088cf66-27a4-4da4-a5de-bb03fdfc1751","address":"270b0e8d873e858abd698a000b0da0b94e21d84c","crypto":{"ciphertext":"92da68991a680c24e806ce49bb3034c63c0597ef0e13b669c5d2bf3c4f8d4d03","cipherparams":{"iv":"758f2d23967c5d2a2b7e816c259647d5"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"e5a6c8b70e5df30da1e53c075b243872c2cc404038a4ac76208aa481a629e377","n":262144,"r":8,"p":1},"mac":"c8316865a833729218c55fac5d709d3d2bfe37c893724c8fa879e9ef3abe0171"}}
================================================
FILE: tests/accounts/3_0x812e87be5d4198fca55cb52fa60cb46620617474.json
================================================
{"version":3,"id":"c0870e2f-9f3b-48b6-a670-cd056d3c6931","address":"812e87be5d4198fca55cb52fa60cb46620617474","crypto":{"ciphertext":"03a7f102806b520d17847f6dd75cd0b357fce4cd3097be0f3e40e93e55021b6d","cipherparams":{"iv":"fc81b85e0efded67ae9d6a90283f1886"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"d7e85edd551f7d3fd836dfbd94430c78c2484e51eb5d1d314b3bba21a59a8694","n":262144,"r":8,"p":1},"mac":"6bc6b12ac5e9d2e61069aa76bda242fb2b07f08d07ccfc1437b318f5aaf61dd6"}}
================================================
FILE: tests/accounts/4_0x13314e21cd6d343ceb857073f3f6d9368919d1ef.json
================================================
{"version":3,"id":"f9455bbb-482b-46f7-9e29-503838c386be","address":"13314e21cd6d343ceb857073f3f6d9368919d1ef","crypto":{"ciphertext":"e16d398307f5cfc1331db001f1a9b0392eae1bb80299c3b7cf24fc56b3a24755","cipherparams":{"iv":"488f5f4c06909ccec258ff3aa9a40caf"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"f054d8267209ef8e28ef30c3ed3bbb5291ae8738efd0448c7f8993f4c0505388","n":262144,"r":8,"p":1},"mac":"cf3a13bc8e6b6a4794f3890e350b972afe782fe092df7b99e443a34d18ad8ce8"}}
================================================
FILE: tests/accounts/5_0x176087fea5c41fc370fabbd850521bc4451690ca.json
================================================
{"version":3,"id":"aaf50a37-d4d1-41ef-ad91-ac5e20a0f5b4","address":"176087fea5c41fc370fabbd850521bc4451690ca","crypto":{"ciphertext":"e9429a2801c7a16ad339bd748fc352639e7380c72fa9bea984b2955664c4350d","cipherparams":{"iv":"5457ac288d8340c984f753e8f21f3bb5"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"5eb1234e8d3ad07a3989c33f68378d7cffd314cee79f988d76623fe083f251f0","n":262144,"r":8,"p":1},"mac":"9470221ed35c56d8798fe238e276ab731b2f14f7671f69fcca16dfd9ccc8bccd"}}
================================================
FILE: tests/accounts/pass
================================================
12345678
================================================
FILE: tests/config/keys/UnlimitedChain/key.json
================================================
{"id":"032b64c7-38c6-dfb4-714a-25eacd2daecc","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"38f902a7336bf5160c427f499a6ded5c"},"ciphertext":"afcc67f82dd5327181a75cbd33319301575577f42aa42083f4f6620dcbb6fa60","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"c7b2c641170cfb25a06c60e51d08bdf4e11d51acdfd65aaf832e2d7b13470812"},"mac":"dca205a615725a79073baec0e015a7a5c1d51c72d861081f2c26296abe7cec09"},"address":"00a329c0648769a73afac7f9381e08fb43dbea72","name":"Development Account","meta":"{\"description\":\"Never use this account outside of development chain!\",\"passwordHint\":\"Password is empty string\"}"}
================================================
FILE: tests/config/keys/UnlimitedChain/key1.json
================================================
{"id":"09068027-f04d-f8fb-79d9-9df6f03ac604","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"4ac591b30b76366cf71de779fd32b5fe"},"ciphertext":"6eabb1137e388761f879d85f1f48ff48252ef657ba723e274b04c8b86c676f29","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"e16ce22f791ffa5b8e9da1236e6c943e7988f82ba63a540892e12b5d874241bb"},"mac":"b81515fe8c2ccc669f35b859128bf431c100f6eeeb853f942c73bb7fba9db44c"},"address":"6c626f45e3b7ae5a3998478753634790fd0e82ee","name":"","meta":"{}"}
================================================
FILE: tests/config/keys/UnlimitedChain/key2.json
================================================
{"id":"6e7c69aa-e8d3-e381-ed85-36a08df7cfbe","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"e19eee7d928a8d7df75bc58fd31ff05a"},"ciphertext":"b6f9bd7de52fbf59677de26a81b8a4b03bbf4eb0f849e126eae4a36b0ac5a676","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"e9580e3282bcf4bf517d1eb78bf86b41e41bd9777f43afa517b2283c5a52ed9c"},"mac":"906d301903aae013fd98b184f153118a42af29e043865b2845e846eb5c5b4678"},"address":"50ff810797f75f6bfbf2227442e0c961a8562f4c","name":"","meta":"{}"}
================================================
FILE: tests/config/keys/UnlimitedChain/key3.json
================================================
{"id":"ecf98a52-7a41-0b18-a39a-e57ddc7a41d9","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c40c8be2726d367cdc885f59a1158488"},"ciphertext":"0ce8de6e23d2ebdb4ac559af6270e92a01668f11a20868caee672200d7f80c0c","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"b69e7ff92bdb5e10963221ae4379f5a4bfc1e3856001e5a78cf26c1aaeea0d54"},"mac":"c067ac59f17e52586de59a9f4542c11149d84825e9b027f7145455124784337b"},"address":"5beb2d3aa2333a524703af18310acff462c04723","name":"","meta":"{}"}
================================================
FILE: tests/config/keys/UnlimitedChain/key4.json
================================================
{"id":"d2bed4ab-c313-7430-be81-8a0d04bfc9aa","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"b7b2722e5e15d41ab83d0350fd72d6f6"},"ciphertext":"7dc899d78c010324b8600a729078d894517c28b5a4fef85e60d0d95c91d51a75","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"fa15aef30d031c59d28794c1aec05797d47da1a5a0661d1a8e68841a613881b5"},"mac":"3f8c47c68a3c2428c44674e770dd1c16b47a968a2ec6f5f59901cdbbe994acb0"},"address":"57da1b8f38a5ecf91e9fee8a047df0f0a88716a1","name":"","meta":"{}"}
================================================
FILE: tests/config/parity-dev-constantinopole.json
================================================
{
"name": "UnlimitedChain",
"engine": {
"instantSeal": {
"params": {}
}
},
"params": {
"gasLimitBoundDivisor": "0x0400",
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x11",
"registrar" : "0x0000000000000000000000000000000000001337",
"eip150Transition": "0x0",
"eip160Transition": "0x0",
"eip161abcTransition": "0x0",
"eip161dTransition": "0x0",
"eip155Transition": "0x0",
"eip98Transition": "0x7fffffffffffff",
"maxCodeSize": "0xFFFFFF",
"maxCodeSizeTransition": "0x0",
"eip140Transition": "0x0",
"eip211Transition": "0x0",
"eip214Transition": "0x0",
"eip658Transition": "0x0",
"eip145Transition": "0x0",
"eip1014Transition": "0x0",
"eip1052Transition": "0x0",
"wasmActivationTransition": "0x0"
},
"genesis": {
"seal": {
"generic": "0x0"
},
"difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0xFFFFFF"
},
"accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"0000000000000000000000000000000000000005": { "balance": "1", "builtin": { "name": "modexp", "activate_at": 0, "pricing": { "modexp": { "divisor": 20 } } } },
"0000000000000000000000000000000000000006": { "balance": "1", "builtin": { "name": "alt_bn128_add", "activate_at": 0, "pricing": { "linear": { "base": 500, "word": 0 } } } },
"0000000000000000000000000000000000000007": { "balance": "1", "builtin": { "name": "alt_bn128_mul", "activate_at": 0, "pricing": { "linear": { "base": 40000, "word": 0 } } } },
"0000000000000000000000000000000000000008": { "balance": "1", "builtin": { "name": "alt_bn128_pairing", "activate_at": 0, "pricing": { "alt_bn128_pairing": { "base": 100000, "pair": 80000 } } } },
"0000000000000000000000000000000000001337": { "balance": "1", "constructor": "0x606060405233600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550670de0b6b3a764000060035534610000575b612904806100666000396000f3006060604052361561013c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306b2ff471461014157806313af40351461018c57806319362a28146101bf5780633f3935d114610248578063432ced04146102b75780634f39ca59146102eb5780636795dbcd1461032457806369fe0e2d146103c857806379ce9fac146103fd5780638da5cb5b1461045557806390b97fc1146104a457806392698814146105245780639890220b1461055d578063ac4e73f914610584578063ac72c12014610612578063c3a358251461064b578063ddca3f43146106c3578063deb931a2146106e6578063df57b74214610747578063e30bd740146107a8578063eadf976014610862578063ef5454d6146108e7578063f25eb5c114610975578063f6d339e414610984575b610000565b3461000057610172600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610a1f565b604051808215151515815260200191505060405180910390f35b34610000576101bd600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610a81565b005b346100005761022e60048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803560001916906020019091905050610ba2565b604051808215151515815260200191505060405180910390f35b346100005761029d600480803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050610dc9565b604051808215151515815260200191505060405180910390f35b6102d1600480803560001916906020019091905050611035565b604051808215151515815260200191505060405180910390f35b346100005761030a60048080356000191690602001909190505061115f565b604051808215151515815260200191505060405180910390f35b346100005761038660048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611378565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34610000576103e3600480803590602001909190505061140d565b604051808215151515815260200191505060405180910390f35b346100005761043b60048080356000191690602001909190803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506114b4565b604051808215151515815260200191505060405180910390f35b34610000576104626115fb565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b346100005761050660048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611621565b60405180826000191660001916815260200191505060405180910390f35b34610000576105436004808035600019169060200190919050506116b2565b604051808215151515815260200191505060405180910390f35b346100005761056a611715565b604051808215151515815260200191505060405180910390f35b34610000576105f8600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611824565b604051808215151515815260200191505060405180910390f35b3461000057610631600480803560001916906020019091905050611d8b565b604051808215151515815260200191505060405180910390f35b34610000576106ad60048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611dee565b6040518082815260200191505060405180910390f35b34610000576106d0611e83565b6040518082815260200191505060405180910390f35b3461000057610705600480803560001916906020019091905050611e89565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3461000057610766600480803560001916906020019091905050611ed2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34610000576107d9600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611f1b565b6040518080602001828103825283818151815260200191508051906020019080838360008314610828575b80518252602083111561082857602082019150602081019050602083039250610804565b505050905090810190601f1680156108545780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34610000576108cd60048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803590602001909190505061200c565b604051808215151515815260200191505060405180910390f35b346100005761095b600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050612236565b604051808215151515815260200191505060405180910390f35b3461000057610982612425565b005b3461000057610a0560048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050612698565b604051808215151515815260200191505060405180910390f35b60006000600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290049050141590505b919050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515610add57610b9f565b8073ffffffffffffffffffffffffffffffffffffffff16600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f70aea8d848e8a90fb7661b227dc522eb6395c3dac71b63cb59edd5c9899b236460405180905060405180910390a380600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505b5b50565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610c1d57610dc1565b82600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b60208310610c705780518252602082019150602081019050602083039250610c4d565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b60208310610cdf5780518252602082019150602081019050602083039250610cbc565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea866040518080602001828103825283818151815260200191508051906020019080838360008314610d82575b805182526020831115610d8257602082019150602081019050602083039250610d5e565b505050905090810190601f168015610dae5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b509392505050565b6000813373ffffffffffffffffffffffffffffffffffffffff1660016000836040518082805190602001908083835b60208310610e1b5780518252602082019150602081019050602083039250610df8565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390206000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610ea45761102f565b82600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10610f2d57805160ff1916838001178555610f5b565b82800160010185558215610f5b579182015b82811115610f5a578251825591602001919060010190610f3f565b5b509050610f8091905b80821115610f7c576000816000905550600101610f64565b5090565b50503373ffffffffffffffffffffffffffffffffffffffff16836040518082805190602001908083835b60208310610fcd5780518252602082019150602081019050602083039250610faa565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f098ae8581bb8bd9af1beaf7f2e9f51f31a8e5a8bfada4e303a645d71d9c9192060405180905060405180910390a3600191505b5b50919050565b600081600060016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561109b57611159565b6003543410156110aa57611158565b3360016000856000191660001916815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055503373ffffffffffffffffffffffffffffffffffffffff1683600019167f4963513eca575aba66fdcd25f267aae85958fe6fb97e75fa25d783f1a091a22160405180905060405180910390a3600191505b5b5b50919050565b6000813373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161415156111da57611372565b6002600060016000866000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f1061127c57506112b3565b601f0160209004906000526020600020908101906112b291905b808211156112ae576000816000905550600101611296565b5090565b5b5060016000846000191660001916815260200190815260200160002060006000820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556001820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff021916905550503373ffffffffffffffffffffffffffffffffffffffff1683600019167fef1961b4d2909dc23643b309bfe5c3e5646842d98c3a58517037ef3871185af360405180905060405180910390a3600191505b5b50919050565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b602083106113cc57805182526020820191506020810190506020830392506113a9565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020546001900490505b92915050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561146b576114af565b816003819055507f6bbc57480a46553fa4d156ce702beef5f3ad66303b0ed1a5d4cb44966c6584c3826040518082815260200191505060405180910390a1600190505b5b919050565b6000823373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561152f576115f4565b8260016000866000191660001916815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1685600019167f7b97c62130aa09acbbcbf7482630e756592496f1759eaf702f469cf64dfb779460405180905060405180910390a4600191505b5b5092915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b602083106116755780518252602082019150602081019050602083039250611652565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390205490505b92915050565b6000600060016000846000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141590505b919050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561177357611821565b7fdef931299fe61d176f949118058530c1f3f539dcb6950b4e372c9b835c33ca073073ffffffffffffffffffffffffffffffffffffffff16316040518082815260200191505060405180910390a13373ffffffffffffffffffffffffffffffffffffffff166108fc3073ffffffffffffffffffffffffffffffffffffffff16319081150290604051809050600060405180830381858888f19350505050151561181b57610000565b600190505b5b90565b60006000836040518082805190602001908083835b6020831061185c5780518252602082019150602081019050602083039250611839565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390203373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561190157611d83565b846040518082805190602001908083835b602083106119355780518252602082019150602081019050602083039250611912565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390209150600060016000846000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614158015611ab4575081600019166002600060016000866000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206040518082805460018160011615610100020316600290048015611aa15780601f10611a7f576101008083540402835291820191611aa1565b820191906000526020600020905b815481529060010190602001808311611a8d575b5050915050604051809103902060001916145b15611c79576002600060016000856000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f10611b5b5750611b92565b601f016020900490600052602060002090810190611b9191905b80821115611b8d576000816000905550600101611b75565b5090565b5b5060016000836000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16856040518082805190602001908083835b60208310611c1c5780518252602082019150602081019050602083039250611bf9565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f12491ad95fd945e444d88a894ffad3c21959880a4dcd8af99d4ae4ffc71d4abd60405180905060405180910390a35b8360016000846000191660001916815260200190815260200160002060010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508373ffffffffffffffffffffffffffffffffffffffff16856040518082805190602001908083835b60208310611d215780518252602082019150602081019050602083039250611cfe565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f728435a0031f6a04538fcdd24922a7e06bc7bc945db03e83d22122d1bc5f28df60405180905060405180910390a3600192505b5b505092915050565b6000600060016000846000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141590505b919050565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b60208310611e425780518252602082019150602081019050602083039250611e1f565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020546001900490505b92915050565b60035481565b600060016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505b919050565b600060016000836000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505b919050565b6020604051908101604052806000815250600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015611fff5780601f10611fd457610100808354040283529160200191611fff565b820191906000526020600020905b815481529060010190602001808311611fe257829003601f168201915b505050505090505b919050565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161415156120875761222e565b82600102600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b602083106120dd57805182526020820191506020810190506020830392506120ba565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b6020831061214c5780518252602082019150602081019050602083039250612129565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea8660405180806020018281038252838181518152602001915080519060200190808383600083146121ef575b8051825260208311156121ef576020820191506020810190506020830392506121cb565b505050905090810190601f16801561221b5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b509392505050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156122945761241f565b82600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061231d57805160ff191683800117855561234b565b8280016001018555821561234b579182015b8281111561234a57825182559160200191906001019061232f565b5b50905061237091905b8082111561236c576000816000905550600101612354565b5090565b50508173ffffffffffffffffffffffffffffffffffffffff16836040518082805190602001908083835b602083106123bd578051825260208201915060208101905060208303925061239a565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f098ae8581bb8bd9af1beaf7f2e9f51f31a8e5a8bfada4e303a645d71d9c9192060405180905060405180910390a3600190505b5b92915050565b3373ffffffffffffffffffffffffffffffffffffffff16600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060405180828054600181600116156101000203166002900480156124d65780601f106124b45761010080835404028352918201916124d6565b820191906000526020600020905b8154815290600101906020018083116124c2575b505091505060405180910390207f12491ad95fd945e444d88a894ffad3c21959880a4dcd8af99d4ae4ffc71d4abd60405180905060405180910390a360016000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060405180828054600181600116156101000203166002900480156125b05780601f1061258e5761010080835404028352918201916125b0565b820191906000526020600020905b81548152906001019060200180831161259c575b505091505060405180910390206000191660001916815260200190815260200160002060010160006101000a81549073ffffffffffffffffffffffffffffffffffffffff0219169055600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f1061265d5750612694565b601f01602090049060005260206000209081019061269391905b8082111561268f576000816000905550600101612677565b5090565b5b505b565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515612713576128d0565b8273ffffffffffffffffffffffffffffffffffffffff16600102600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b6020831061277f578051825260208201915060208101905060208303925061275c565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b602083106127ee57805182526020820191506020810190506020830392506127cb565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea866040518080602001828103825283818151815260200191508051906020019080838360008314612891575b8051825260208311156128915760208201915060208101905060208303925061286d565b505050905090810190601f1680156128bd5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b5093925050505600a165627a7a7230582066b2da4773a0f1d81efe071c66b51c46868a871661efd18c0f629353ff4c1f9b0029" },
"00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376" },
"6c626f45e3b7ae5a3998478753634790fd0e82ee": { "balance": "1606938044258990275541962092341162602522202993782792835301376" },
"50ff810797f75f6bfbf2227442e0c961a8562f4c": { "balance": "1606938044258990275541962092341162602522202993782792835301376" },
"5beb2d3aa2333a524703af18310acff462c04723": { "balance": "1606938044258990275541962092341162602522202993782792835301376" },
"57da1b8f38a5ecf91e9fee8a047df0f0a88716a1": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
}
}
================================================
FILE: tests/conftest.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2019 reverendus, EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address, web3_via_http
from pymaker.auctions import Flipper, Flapper, Flopper
from pymaker.deployment import Deployment, DssDeployment
from pymaker.dss import Vat, Vow, Cat, Dog, Jug, Pot
from pymaker.keys import register_keys
@pytest.fixture(scope='session')
def new_deployment() -> Deployment:
return Deployment()
@pytest.fixture()
def deployment(new_deployment: Deployment) -> Deployment:
new_deployment.reset()
return new_deployment
@pytest.fixture(scope="session")
def web3() -> Web3:
# for local dockerized parity testchain
web3 = web3_via_http("http://0.0.0.0:8545")
web3.eth.defaultAccount = "0x50FF810797f75f6bfbf2227442e0c961a8562F4C"
register_keys(web3,
["key_file=tests/config/keys/UnlimitedChain/key1.json,pass_file=/dev/null",
"key_file=tests/config/keys/UnlimitedChain/key2.json,pass_file=/dev/null",
"key_file=tests/config/keys/UnlimitedChain/key3.json,pass_file=/dev/null",
"key_file=tests/config/keys/UnlimitedChain/key4.json,pass_file=/dev/null",
"key_file=tests/config/keys/UnlimitedChain/key.json,pass_file=/dev/null"])
# reduce logspew
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
assert len(web3.eth.accounts) > 3
return web3
@pytest.fixture(scope="session")
def our_address(web3) -> Address:
return Address(web3.eth.accounts[0])
@pytest.fixture(scope="session")
def other_address(web3) -> Address:
return Address(web3.eth.accounts[1])
@pytest.fixture(scope="session")
def deployment_address(web3) -> Address:
# FIXME: Unsure why it isn't added to web3.eth.accounts list
return Address("0x00a329c0648769A73afAc7F9381E08FB43dBEA72")
@pytest.fixture(scope="session")
def mcd(web3) -> DssDeployment:
# for local dockerized parity testchain
deployment = DssDeployment.from_node(web3=web3)
validate_contracts_loaded(deployment)
initialize_collaterals(deployment)
return deployment
def validate_contracts_loaded(deployment: DssDeployment):
assert isinstance(deployment.vat, Vat)
assert deployment.vat.address is not None
assert isinstance(deployment.vow, Vow)
assert deployment.vow.address is not None
assert isinstance(deployment.cat, Cat)
assert deployment.cat.address is not None
assert isinstance(deployment.dog, Dog)
assert deployment.dog.address is not None
assert isinstance(deployment.jug, Jug)
assert deployment.jug.address is not None
assert isinstance(deployment.flapper, Flapper)
assert deployment.flapper.address is not None
assert isinstance(deployment.flopper, Flopper)
assert deployment.flopper.address is not None
assert isinstance(deployment.pot, Pot)
assert deployment.pot.address is not None
def initialize_collaterals(deployment: DssDeployment):
for collateral in deployment.collaterals.values():
if collateral.clipper:
collateral.clipper.upchost().transact(from_address=deployment_address(deployment.web3))
================================================
FILE: tests/dss_token.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from web3 import Web3
from pymaker import Contract, Address, Transact
from pymaker.dss import Urn, Ilk
from pymaker.numeric import Wad
class GemMock(Contract):
"""A client for `GemMock` contract.
"""
abi = Contract._load_abi(__name__, 'abi/GemMock.abi')
bin = Contract._load_bin(__name__, 'abi/GemMock.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3, vat: Address, ilk: Ilk, gem: Address):
assert isinstance(web3, Web3)
assert isinstance(vat, Address)
assert isinstance(ilk, Ilk)
assert isinstance(gem, Address)
return GemMock(web3=web3, address=Contract._deploy(web3, GemMock.abi, GemMock.bin, [vat.address,
ilk.toBytes(),
gem.address]))
def ilk(self):
return Ilk.fromBytes(self._contract.functions.ilk().call())
def join(self, urn: Urn, value: Wad) -> Transact:
assert(isinstance(urn, Urn))
assert(isinstance(value, Wad))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'join', [urn.toBytes() , value.value])
def hope(self, guy: Address) -> Transact:
assert(isinstance(guy, Address))
return Transact(self, self.web3, self.abi, self.address, self._contract,
'hope', [guy.address])
def __eq__(self, other):
return self.address == other.address
def __repr__(self):
return f"GemMock('{self.address}')"
================================================
FILE: tests/helpers.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import time
from unittest.mock import Mock
from web3 import Web3
def is_hashable(v):
"""Determine whether `v` can be hashed."""
try:
hash(v)
except TypeError:
return False
return True
def wait_until_mock_called(mock: Mock):
while not mock.called:
pass
return mock.call_args[0]
def time_travel_by(web3: Web3, seconds: int):
assert(isinstance(web3, Web3))
assert(isinstance(seconds, int))
if "Parity" in web3.clientVersion:
print(f"time travel unsupported by parity; waiting {seconds} seconds")
time.sleep(seconds)
# force a block mining to have a correct timestamp in latest block
web3.eth.sendTransaction({'from': web3.eth.accounts[0], 'to': web3.eth.accounts[1], 'value': 1})
else:
web3.manager.request_blocking("evm_increaseTime", [seconds])
# force a block mining to have a correct timestamp in latest block
web3.manager.request_blocking("evm_mine", [])
def snapshot(web3: Web3):
assert(isinstance(web3, Web3))
return web3.manager.request_blocking("evm_snapshot", [])
def reset(web3: Web3, snap_id):
assert(isinstance(web3, Web3))
return web3.manager.request_blocking("evm_revert", [snap_id])
================================================
FILE: tests/manual_test_async_tx.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import logging
import os
import sys
import threading
import time
from pymaker import Address, web3_via_http
from pymaker.deployment import DssDeployment
from pymaker.gas import FixedGasPrice, GeometricGasPrice
from pymaker.keys import register_keys
from pymaker.numeric import Wad
logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.DEBUG)
# reduce logspew
logging.getLogger('urllib3').setLevel(logging.INFO)
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("requests").setLevel(logging.INFO)
pool_size = int(sys.argv[3]) if len(sys.argv) > 3 else 10
web3 = web3_via_http(endpoint_uri=os.environ['ETH_RPC_URL'], http_pool_size=pool_size)
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
mcd = DssDeployment.from_node(web3)
our_address = Address(web3.eth.defaultAccount)
weth = DssDeployment.from_node(web3).collaterals['ETH-A'].gem
GWEI = 1000000000
slow_gas = GeometricGasPrice(initial_price=int(15 * GWEI), every_secs=42, max_price=200 * GWEI)
fast_gas = GeometricGasPrice(initial_price=int(30 * GWEI), every_secs=42, max_price=200 * GWEI)
class TestApp:
def main(self):
self.test_replacement()
self.test_simultaneous()
self.shutdown()
def test_replacement(self):
first_tx = weth.deposit(Wad(4))
logging.info(f"Submitting first TX with gas price deliberately too low")
self._run_future(first_tx.transact_async(gas_price=slow_gas))
time.sleep(0.5)
second_tx = weth.deposit(Wad(6))
logging.info(f"Replacing first TX with legitimate gas price")
second_tx.transact(replace=first_tx, gas_price=fast_gas)
assert first_tx.replaced
def test_simultaneous(self):
self._run_future(weth.deposit(Wad(1)).transact_async(gas_price=fast_gas))
self._run_future(weth.deposit(Wad(3)).transact_async(gas_price=fast_gas))
self._run_future(weth.deposit(Wad(5)).transact_async(gas_price=fast_gas))
self._run_future(weth.deposit(Wad(7)).transact_async(gas_price=fast_gas))
time.sleep(33)
def shutdown(self):
balance = weth.balance_of(our_address)
if Wad(0) < balance < Wad(100): # this account's tiny WETH balance came from this test
logging.info(f"Unwrapping {balance} WETH")
assert weth.withdraw(balance).transact(gas_price=fast_gas)
elif balance >= Wad(22): # user already had a balance, so unwrap what a successful test would have consumed
logging.info(f"Unwrapping 12 WETH")
assert weth.withdraw(Wad(22)).transact(gas_price=fast_gas)
@staticmethod
def _run_future(future):
def worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
asyncio.get_event_loop().run_until_complete(future)
finally:
loop.close()
thread = threading.Thread(target=worker, daemon=True)
thread.start()
if __name__ == '__main__':
TestApp().main()
================================================
FILE: tests/manual_test_cdpmanager.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 ith-harvey
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import sys
import os
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.numeric import Wad
from pymaker.dsr import Dsr
endpoint_uri = f"{os.environ['SERVER_ETH_RPC_HOST']}:{os.environ['SERVER_ETH_RPC_PORT']}"
web3 = Web3(HTTPProvider(endpoint_uri=endpoint_uri,
request_kwargs={"timeout": 10}))
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
cdpid = int(sys.argv[3])
mcd = DssDeployment.from_network(web3, "kovan")
our_address = Address(web3.eth.defaultAccount)
dsr_client = Dsr(mcd, our_address)
print(our_address)
print(f"Default account: {our_address.address}")
if dsr_client.has_proxy():
proxy = dsr_client.get_proxy()
print(f"{our_address} has a DS-Proxy - {proxy.address.address}, test will continue")
print(f"Urn of Cdp ID {cdpid} - {mcd.cdp_manager.urn(cdpid)}")
print(f"Owner of CDP ID {cdpid} - {mcd.cdp_manager.owns(cdpid)}")
print(f"List of CDP IDs next to and previous to {cdpid} - {mcd.cdp_manager.list(cdpid)}")
print(f"Ilk of CDP ID {cdpid} - {mcd.cdp_manager.ilk(cdpid)}")
print(f"First of Cdp ID for account {proxy.address.address} - {mcd.cdp_manager.first(proxy.address)}")
print(f"Last of Cdp ID for account {proxy.address.address} - {mcd.cdp_manager.last(proxy.address)}")
print(f"Number of all CDPs created via DS-Cdp-Manager contract {proxy.address.address} - {mcd.cdp_manager.count(proxy.address)}")
else:
print(f"{our_address} does not have a DS-Proxy. Please create a cdp on kovan via Oasis.app (to create a proxy) to perform this test")
================================================
FILE: tests/manual_test_dsr.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 grandizzy
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import sys
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.numeric import Wad
from pymaker.dsr import Dsr
web3 = Web3(HTTPProvider(endpoint_uri="http://0.0.0.0:8545",
request_kwargs={"timeout": 10}))
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
mcd = DssDeployment.from_network(web3, "kovan")
our_address = Address(web3.eth.defaultAccount)
print(our_address)
dsr_client = Dsr(mcd, our_address)
print(f"Chi: {dsr_client.chi()}")
print(f"Total DAI: {dsr_client.get_total_dai()}")
print(f"DSR: {dsr_client.dsr()}")
proxy = dsr_client.get_proxy()
print(f"Has Proxy: {dsr_client.has_proxy()}")
if not dsr_client.has_proxy():
dsr_client.build_proxy().transact()
proxy = dsr_client.get_proxy()
print(f"Proxy address: {proxy.address.address}")
print(f"Balance: {dsr_client.get_balance(proxy.address)}")
# approve proxy to use 10 DAI from account
dsr_client.mcd.dai.approve(proxy.address, Wad.from_number(10)).transact()
dsr_client.join(Wad.from_number(2.2), proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")
dsr_client.exit(Wad.from_number(1.01), proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")
dsr_client.exit_all(proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")
================================================
FILE: tests/manual_test_goerli.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020-2021 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
import os
import sys
from web3 import Web3, HTTPProvider
from pymaker import Address, eth_transfer, web3_via_http
from pymaker.gas import GeometricGasPrice
from pymaker.lifecycle import Lifecycle
from pymaker.keys import register_keys
from pymaker.numeric import Wad
from pymaker.token import EthToken
logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.DEBUG)
# reduce logspew
logging.getLogger('urllib3').setLevel(logging.INFO)
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("requests").setLevel(logging.INFO)
endpoint_uri = sys.argv[1]
web3 = web3_via_http(endpoint_uri, timeout=10)
print(web3.clientVersion)
"""
Argument: Reqd? Example:
Ethereum node URI yes https://localhost:8545
Ethereum address no 0x0000000000000000000000000000000aBcdef123
Private key no key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
Gas price (GWEI) no 9
"""
if len(sys.argv) > 3:
web3.eth.defaultAccount = sys.argv[2]
register_keys(web3, [sys.argv[3]])
our_address = Address(web3.eth.defaultAccount)
run_transactions = True
elif len(sys.argv) > 2:
our_address = Address(sys.argv[2])
run_transactions = False
else:
our_address = None
run_transactions = False
gas_price = None if len(sys.argv) <= 4 else \
GeometricGasPrice(initial_price=int(float(sys.argv[4]) * GeometricGasPrice.GWEI),
every_secs=15,
max_price=100 * GeometricGasPrice.GWEI)
eth = EthToken(web3, Address.zero())
class TestApp:
def main(self):
with Lifecycle(web3) as lifecycle:
lifecycle.on_block(self.on_block)
def on_block(self):
block = web3.eth.blockNumber
logging.info(f"Found block; web3.eth.blockNumber={block}")
if run_transactions and block % 3 == 0:
# dummy transaction: send 0 ETH to ourself
eth_transfer(web3=web3, to=our_address, amount=Wad(0)).transact(
from_address=our_address, gas=21000, gas_price=gas_price)
if our_address:
logging.info(f"Eth balance is {eth.balance_of(our_address)}")
if __name__ == '__main__':
TestApp().main()
================================================
FILE: tests/manual_test_mcd.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019-2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import os
import sys
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.numeric import Wad
from pymaker.oracles import OSM
assert os.environ['ETH_RPC_URL']
web3 = Web3(HTTPProvider(endpoint_uri=os.environ['ETH_RPC_URL'], request_kwargs={"timeout": 10}))
our_address = None
if len(sys.argv) > 1:
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
our_address = Address(web3.eth.defaultAccount)
if len(sys.argv) > 2:
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
run_transactions = True
else:
run_transactions = False
mcd = DssDeployment.from_node(web3)
# Print a list of collaterals available for this deployment of MCD
for collateral in mcd.collaterals.values():
osm: OSM = collateral.pip
liquidation = "clip" if collateral.clipper else "flip"
print(f"Found {collateral.ilk.name:>15} - {collateral.gem.name():<21} with {collateral.adapter.dec():>2} decimals, "
f"OSM price {round(float(osm.peek()), 3):>14}, "
f"rate {float(collateral.ilk.rate):<18}, using {liquidation} liquidations")
# Choose the desired collateral; in this case we'll wrap some Eth
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
if run_transactions:
# Determine minimum amount of Dai which can be drawn
dai_amount = Wad(ilk.dust)
# Set an amount of collateral to join and an amount of Dai to draw
collateral_amount = Wad.from_number(105)
if collateral.gem.balance_of(our_address) > collateral_amount:
if collateral.ilk.name.startswith("ETH"):
# Wrap ETH to produce WETH
assert collateral.gem.deposit(collateral_amount).transact()
# Add collateral and allocate the desired amount of Dai
collateral.approve(our_address)
assert collateral.adapter.join(our_address, collateral_amount).transact()
assert mcd.vat.frob(ilk, our_address, dink=collateral_amount, dart=Wad(0)).transact()
assert mcd.vat.frob(ilk, our_address, dink=Wad(0), dart=dai_amount).transact()
print(f"Urn balance: {mcd.vat.urn(ilk, our_address)}")
print(f"Dai balance: {mcd.vat.dai(our_address)}")
# Mint and withdraw our Dai
mcd.approve_dai(our_address)
assert mcd.dai_adapter.exit(our_address, dai_amount).transact()
print(f"Dai balance after withdrawal: {mcd.vat.dai(our_address)}")
# Repay (and burn) our Dai
assert mcd.dai_adapter.join(our_address, dai_amount).transact()
print(f"Dai balance after repayment: {mcd.vat.dai(our_address)}")
# Withdraw our collateral; stability fee accumulation may make these revert
assert mcd.vat.frob(ilk, our_address, dink=Wad(0), dart=dai_amount*-1).transact()
assert mcd.vat.frob(ilk, our_address, dink=collateral_amount*-1, dart=Wad(0)).transact()
assert collateral.adapter.exit(our_address, collateral_amount).transact()
print(f"Dai balance w/o collateral: {mcd.vat.dai(our_address)}")
else:
print(f"Not enough {ilk.name} to join to the vat")
if our_address:
print(f"Collateral balance: {mcd.vat.gem(ilk, our_address)}")
print(f"Urn balance: {mcd.vat.urn(ilk, our_address)}")
================================================
FILE: tests/manual_test_node.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import logging
import os
import sys
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.lifecycle import Lifecycle
from pymaker.deployment import DssDeployment
from pymaker.keys import register_keys
from pymaker.numeric import Wad
logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.DEBUG)
# reduce logspew
logging.getLogger('urllib3').setLevel(logging.INFO)
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("requests").setLevel(logging.INFO)
endpoint_uri = sys.argv[1] # ex: https://localhost:8545
web3 = Web3(HTTPProvider(endpoint_uri=endpoint_uri, request_kwargs={"timeout": 30}))
if len(sys.argv) > 3:
web3.eth.defaultAccount = sys.argv[2] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[3]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
our_address = Address(web3.eth.defaultAccount)
run_transactions = True
elif len(sys.argv) > 2:
our_address = Address(sys.argv[2])
run_transactions = False
else:
our_address = None
run_transactions = False
mcd = DssDeployment.from_node(web3)
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
if run_transactions:
collateral.approve(our_address)
past_blocks = 100
class TestApp:
def __init__(self):
self.amount = Wad(3)
self.joined = Wad(0)
def main(self):
with Lifecycle(web3) as lifecycle:
lifecycle.on_shutdown(self.on_shutdown)
lifecycle.on_block(self.on_block)
def on_block(self):
if run_transactions:
logging.info(f"Found block {web3.eth.blockNumber}, joining {self.amount} {ilk.name} to our urn")
collateral.gem.deposit(self.amount).transact()
assert collateral.adapter.join(our_address, self.amount).transact()
self.joined += self.amount
else:
logging.info(f"Found block; web3.eth.blockNumber={web3.eth.blockNumber}")
if our_address:
logging.info(f"Urn balance is {mcd.vat.gem(ilk, our_address)} {ilk.name}")
# self.request_history()
def request_history(self):
logs = mcd.vat.past_frobs(web3.eth.blockNumber - past_blocks)
logging.info(f"Found {len(logs)} frobs in the past {past_blocks} blocks")
def on_shutdown(self):
if run_transactions and self.joined > Wad(0):
logging.info(f"Exiting {self.joined} {ilk.name} from our urn")
assert collateral.adapter.exit(our_address, self.joined).transact()
assert collateral.gem.withdraw(self.joined).transact()
if __name__ == '__main__':
TestApp().main()
================================================
FILE: tests/manual_test_tx_recovery.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import logging
import os
import sys
import time
import threading
from pprint import pprint
from pymaker import Address, get_pending_transactions, Wad, web3_via_http
from pymaker.deployment import DssDeployment
from pymaker.gas import FixedGasPrice, GeometricGasPrice
from pymaker.keys import register_keys
logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=logging.DEBUG)
# reduce logspew
logging.getLogger('urllib3').setLevel(logging.INFO)
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("requests").setLevel(logging.INFO)
web3 = web3_via_http(endpoint_uri=os.environ['ETH_RPC_URL'])
web3.eth.defaultAccount = sys.argv[1] # ex: 0x0000000000000000000000000000000aBcdef123
register_keys(web3, [sys.argv[2]]) # ex: key_file=~keys/default-account.json,pass_file=~keys/default-account.pass
our_address = Address(web3.eth.defaultAccount)
weth = DssDeployment.from_node(web3).collaterals['ETH-A'].gem
stuck_txes_to_submit = int(sys.argv[3]) if len(sys.argv) > 3 else 1
GWEI = 1000000000
increasing_gas = GeometricGasPrice(initial_price=int(1 * GWEI), every_secs=30, coefficient=1.5, max_price=100 * GWEI)
class TestApp:
def main(self):
self.startup()
pending_txes = get_pending_transactions(web3)
pprint(list(map(lambda t: f"{t.name()} with gas {t.current_gas}", pending_txes)))
if len(pending_txes) > 0:
while len(pending_txes) > 0:
pending_txes[0].cancel(gas_price=increasing_gas)
# After the synchronous cancel, wait to see if subsequent transactions get mined
time.sleep(15)
pending_txes = get_pending_transactions(web3)
else:
logging.info(f"No pending transactions were found; submitting {stuck_txes_to_submit}")
for i in range(1, stuck_txes_to_submit+1):
self._run_future(weth.deposit(Wad(i)).transact_async(gas_price=FixedGasPrice(int(0.4 * i * GWEI))))
time.sleep(2)
self.shutdown()
def startup(self):
pass
def shutdown(self):
pass
@staticmethod
def _run_future(future):
def worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
asyncio.get_event_loop().run_until_complete(future)
finally:
loop.close()
thread = threading.Thread(target=worker, daemon=True)
thread.start()
if __name__ == '__main__':
TestApp().main()
================================================
FILE: tests/manual_test_zrxv2.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import sys
import time
from web3 import EthereumTesterProvider, Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly
from pymaker.numeric import Wad
from pymaker.token import DSToken, ERC20Token
from pymaker.util import bytes_to_hexstring
from pymaker.zrxv2 import ZrxExchangeV2, Order, ZrxRelayerApiV2, ERC20Asset
from tests.helpers import is_hashable, wait_until_mock_called
#EXCHANGE_ADDR = '0x4f833a24e1f95d70f028921e27040ca56e09ab0b' # Mainnet
EXCHANGE_ADDR = sys.argv[1]
SRAV2_URL = 'https://kovan-staging.ercdex.com/api'
KOVAN_DAI = Address('0xc4375b7de8af5a38a93548eb8453a498222c4ff2')
KOVAN_WETH = Address('0xd0a1e359811322d97991e03f863a0c30c2cf029c')
web3 = Web3(HTTPProvider('http://localhost:8545'))
web3.eth.defaultAccount = web3.eth.accounts[0]
exchange = ZrxExchangeV2(web3=web3, address=Address(EXCHANGE_ADDR))
#exchange.approve([ERC20Token(web3=web3, address=KOVAN_DAI),
# ERC20Token(web3=web3, address=KOVAN_WETH)], directly())
order = exchange.create_order(pay_asset=ERC20Asset(KOVAN_WETH),
pay_amount=Wad.from_number(0.1),
buy_asset=ERC20Asset(KOVAN_DAI),
buy_amount=Wad.from_number(25),
expiration=int(time.time())+60*35)
api = ZrxRelayerApiV2(exchange=exchange, api_server=SRAV2_URL)
order = api.configure_order(order)
order = exchange.sign_order(order)
#print(order)
#print(api.submit_order(order))
#print(api.get_orders(KOVAN_WETH, KOVAN_DAI))
print(api.get_orders_by_maker(Address(web3.eth.defaultAccount)))
#print(exchange.past_fill(500))
================================================
FILE: tests/test_approval.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
from unittest.mock import MagicMock
import pytest
from web3 import HTTPProvider
from web3 import Web3
from pymaker import Address
from pymaker import Wad
from pymaker.approval import directly, via_tx_manager
from pymaker.gas import FixedGasPrice
from pymaker.token import DSToken
from pymaker.transactional import TxManager
class FailingTransact:
def transact(self):
return None
async def transact_async(self):
return None
def setup_module():
global web3, our_address, second_address, third_address
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = web3.eth.accounts[0]
our_address = Address(web3.eth.defaultAccount)
second_address = Address(web3.eth.accounts[1])
third_address = Address(web3.eth.accounts[2])
def setup_function():
global token
token = DSToken.deploy(web3, 'ABC')
def test_direct_approval():
# given
global web3, our_address, second_address, token
# when
directly()(token, second_address, "some-name")
# then
assert token.allowance_of(our_address, second_address) == Wad(2**256-1)
def test_direct_approval_should_obey_from_address():
# given
global web3, our_address, second_address, third_address, token
# and
# [there is already approval from the `defaultAccount`]
# [so that we make sure we check for the existing approval properly]
directly()(token, second_address, "some-name")
# when
directly(from_address=third_address)(token, second_address, "some-name")
# then
assert token.allowance_of(third_address, second_address) == Wad(2**256-1)
def test_direct_approval_should_obey_gas_price():
# given
global web3, our_address, second_address, token
# when
directly(gas_price=FixedGasPrice(25000000000))(token, second_address, "some-name")
# then
assert web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 25000000000
def test_direct_approval_should_not_approve_if_already_approved():
# given
global web3, our_address, second_address, token
token.approve(second_address, Wad(2**248+17)).transact()
# when
directly()(token, second_address, "some-name")
# then
assert token.allowance_of(our_address, second_address) == Wad(2**248+17)
def test_direct_approval_should_raise_exception_if_approval_fails():
# given
global web3, our_address, second_address, token
token.approve = MagicMock(return_value=FailingTransact())
# expect
with pytest.raises(Exception):
directly()(token, second_address, "some-name")
def test_via_tx_manager_approval():
# given
global web3, our_address, second_address, token
tx = TxManager.deploy(web3)
# when
via_tx_manager(tx)(token, second_address, "some-name")
# then
assert token.allowance_of(tx.address, second_address) == Wad(2**256-1)
def test_via_tx_manager_approval_should_obey_gas_price():
# given
global web3, our_address, second_address, token
tx = TxManager.deploy(web3)
# when
via_tx_manager(tx, gas_price=FixedGasPrice(15000000000))(token, second_address, "some-name")
# then
assert web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15000000000
def test_via_tx_manager_approval_should_not_approve_if_already_approved():
# given
global web3, our_address, second_address, token
tx = TxManager.deploy(web3)
tx.execute([], [token.approve(second_address, Wad(2**248+19)).invocation()]).transact()
# when
via_tx_manager(tx)(token, second_address, "some-name")
# then
assert token.allowance_of(tx.address, second_address) == Wad(2**248+19)
def test_via_tx_manager_approval_should_raise_exception_if_approval_fails():
# given
global web3, our_address, second_address, token
tx = TxManager.deploy(web3)
tx.execute = MagicMock(return_value=FailingTransact())
# when
with pytest.raises(Exception):
via_tx_manager(tx)(token, second_address, "some-name")
================================================
FILE: tests/test_auctions.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018-2019 reverendus, bargst, EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from datetime import datetime
from web3 import Web3
from pymaker import Address
from pymaker.approval import directly, hope_directly
from pymaker.auctions import DealableAuctionContract, Clipper, Flapper, Flipper, Flopper
from pymaker.deployment import Collateral, DssDeployment
from pymaker.numeric import Wad, Ray, Rad
from tests.helpers import time_travel_by
from tests.test_dss import wrap_eth, mint_mkr, set_collateral_price, frob, cleanup_urn, max_dart
def create_surplus(mcd: DssDeployment, flapper: Flapper, deployment_address: Address):
assert isinstance(mcd, DssDeployment)
assert isinstance(flapper, Flapper)
assert isinstance(deployment_address, Address)
joy = mcd.vat.dai(mcd.vow.address)
if joy < mcd.vat.sin(mcd.vow.address) + mcd.vow.hump() + mcd.vow.bump():
print('Creating a vault with surplus')
collateral = mcd.collaterals['ETH-C']
ink = Wad.from_number(200)
wrap_eth(mcd, deployment_address, ink)
collateral.approve(deployment_address)
assert collateral.adapter.join(deployment_address, ink).transact(
from_address=deployment_address)
# CAUTION: dart needs to be adjusted over time to keep tests happy
frob(mcd, collateral, deployment_address, dink=ink, dart=Wad.from_number(3000))
assert mcd.jug.drip(collateral.ilk).transact(from_address=deployment_address)
joy = mcd.vat.dai(mcd.vow.address)
# total surplus > total debt + surplus auction lot size + surplus buffer
assert float(joy) > float(mcd.vat.sin(mcd.vow.address)) + float(mcd.vow.bump()) + float(mcd.vow.hump())
else:
print(f'Surplus of {joy} already exists; skipping CDP creation')
def create_debt(web3: Web3, mcd: DssDeployment, our_address: Address, deployment_address: Address):
assert isinstance(web3, Web3)
assert isinstance(mcd, DssDeployment)
assert isinstance(our_address, Address)
assert isinstance(deployment_address, Address)
# Create a vault
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
ink = Wad.from_number(10)
wrap_eth(mcd, deployment_address, ink)
collateral.approve(deployment_address)
assert collateral.adapter.join(deployment_address, ink).transact(from_address=deployment_address)
frob(mcd, collateral, deployment_address, dink=ink, dart=Wad(0))
dart = max_dart(mcd, collateral, deployment_address) - Wad(1)
frob(mcd, collateral, deployment_address, dink=Wad(0), dart=dart)
assert not mcd.cat.can_bite(ilk, mcd.vat.urn(collateral.ilk, deployment_address))
# Undercollateralize by dropping the spot price, and then bite the vault
to_price = Wad(Web3.toInt(collateral.pip.read())) / Wad.from_number(2)
set_collateral_price(mcd, collateral, to_price)
urn = mcd.vat.urn(collateral.ilk, deployment_address)
assert urn.ink is not None and urn.art is not None
assert ilk.spot is not None
assert mcd.cat.can_bite(collateral.ilk, urn)
assert mcd.cat.bite(collateral.ilk, urn).transact()
flip_kick = collateral.flipper.kicks()
# Generate some Dai, submit a zero bid to win the flip auction without covering all the debt
wrap_eth(mcd, our_address, Wad.from_number(10))
collateral.approve(our_address)
assert collateral.adapter.join(our_address, Wad.from_number(10)).transact(from_address=our_address)
web3.eth.defaultAccount = our_address.address
frob(mcd, collateral, our_address, dink=Wad.from_number(10), dart=Wad.from_number(20))
collateral.flipper.approve(mcd.vat.address, approval_function=hope_directly())
current_bid = collateral.flipper.bids(flip_kick)
TestFlipper.tend(collateral.flipper, flip_kick, our_address, current_bid.lot, Rad(1))
mcd.vat.can(our_address, collateral.flipper.address)
time_travel_by(web3, collateral.flipper.ttl() + 1)
assert collateral.flipper.deal(flip_kick).transact()
# Raise debt from the queue (note that vow.wait is 0 on our testchain)
assert mcd.vow.wait() == 0
bites = mcd.cat.past_bites(100)
assert len(bites) > 0
for bite in bites:
era_bite = bite.era(web3)
assert era_bite > int(datetime.now().timestamp()) - 120
assert mcd.vow.sin_of(era_bite) > Rad(0)
assert mcd.vow.flog(era_bite).transact()
assert mcd.vow.sin_of(era_bite) == Rad(0)
barks = mcd.dog.past_barks(100)
assert len(barks) > 0
for bark in barks:
era_bark = bark.era(web3)
assert era_bark > int(datetime.now().timestamp()) - 120
assert mcd.vow.sin_of(era_bark) > Rad(0)
assert mcd.vow.flog(era_bark).transact()
assert mcd.vow.sin_of(era_bark) == Rad(0)
# Cancel out surplus and debt
dai_vow = mcd.vat.dai(mcd.vow.address)
# to do fix heal
#assert dai_vow <= mcd.vow.woe()
#assert mcd.vow.heal(dai_vow).transact()
#assert mcd.vow.woe() >= mcd.vow.sump()
def check_active_auctions(auction: DealableAuctionContract):
for bid in auction.active_auctions():
assert bid.id > 0
assert auction.kicks() >= bid.id
assert isinstance(bid.guy, Address)
assert bid.guy != Address("0x0000000000000000000000000000000000000000")
class TestFlapper:
@pytest.fixture(scope="session")
def flapper(self, mcd: DssDeployment) -> Flapper:
return mcd.flapper
@staticmethod
def tend(flapper: Flapper, id: int, address: Address, lot: Rad, bid: Wad):
assert (isinstance(flapper, Flapper))
assert (isinstance(id, int))
assert (isinstance(lot, Rad))
assert (isinstance(bid, Wad))
assert flapper.live() == 1
current_bid = flapper.bids(id)
assert current_bid.guy != Address("0x0000000000000000000000000000000000000000")
assert current_bid.tic > datetime.now().timestamp() or current_bid.tic == 0
assert current_bid.end > datetime.now().timestamp()
assert lot == current_bid.lot
assert bid > current_bid.bid
assert bid >= flapper.beg() * current_bid.bid
assert flapper.tend(id, lot, bid).transact(from_address=address)
log = TestFlapper.last_log(flapper)
assert isinstance(log, Flapper.TendLog)
assert log.guy == address
assert log.id == id
assert log.lot == lot
assert log.bid == bid
@staticmethod
def last_log(flapper: Flapper):
current_block = flapper.web3.eth.blockNumber
return flapper.past_logs(current_block-1, current_block)[0]
def test_getters(self, mcd, flapper):
assert flapper.vat() == mcd.vat.address
assert flapper.beg() > Wad.from_number(1)
assert flapper.ttl() > 0
assert flapper.tau() > flapper.ttl()
assert flapper.kicks() >= 0
def test_scenario(self, web3, mcd, flapper, our_address, other_address, deployment_address):
create_surplus(mcd, flapper, deployment_address)
joy_before = mcd.vat.dai(mcd.vow.address)
# total surplus > total debt + surplus auction lot size + surplus buffer
assert joy_before > mcd.vat.sin(mcd.vow.address) + mcd.vow.bump() + mcd.vow.hump()
assert (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() == Rad(0)
assert mcd.vow.flap().transact()
kick = flapper.kicks()
assert kick == 1
assert len(flapper.active_auctions()) == 1
check_active_auctions(flapper)
current_bid = flapper.bids(1)
assert current_bid.lot > Rad(0)
log = self.last_log(flapper)
assert isinstance(log, Flapper.KickLog)
assert log.id == kick
assert log.lot == current_bid.lot
assert log.bid == current_bid.bid
# Allow the auction to expire, and then resurrect it
time_travel_by(web3, flapper.tau()+1)
assert flapper.tick(kick).transact()
# Bid on the resurrected auction
mint_mkr(mcd.mkr, our_address, Wad.from_number(10))
flapper.approve(mcd.mkr.address, directly(from_address=our_address))
bid = Wad.from_number(0.001)
assert mcd.mkr.balance_of(our_address) > bid
TestFlapper.tend(flapper, kick, our_address, current_bid.lot, bid)
current_bid = flapper.bids(kick)
assert current_bid.bid == bid
assert current_bid.guy == our_address
# Exercise _deal_ after bid has expired
time_travel_by(web3, flapper.ttl()+1)
now = datetime.now().timestamp()
assert 0 < current_bid.tic < now or current_bid.end < now
assert flapper.deal(kick).transact(from_address=our_address)
joy_after = mcd.vat.dai(mcd.vow.address)
print(f'joy_before={str(joy_before)}, joy_after={str(joy_after)}')
assert joy_before - joy_after == mcd.vow.bump()
log = self.last_log(flapper)
assert isinstance(log, Flapper.DealLog)
assert log.usr == our_address
assert log.id == kick
# Grab our dai
mcd.approve_dai(our_address)
assert mcd.dai_adapter.exit(our_address, Wad(current_bid.lot)).transact(from_address=our_address)
assert mcd.dai.balance_of(our_address) >= Wad(current_bid.lot)
assert (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() == Rad(0)
class TestFlipper:
@pytest.fixture(scope="session")
def collateral(self, mcd: DssDeployment) -> Collateral:
return mcd.collaterals['ETH-A']
@pytest.fixture(scope="session")
def flipper(self, collateral, deployment_address) -> Flipper:
return collateral.flipper
@staticmethod
def tend(flipper: Flipper, id: int, address: Address, lot: Wad, bid: Rad):
assert (isinstance(flipper, Flipper))
assert (isinstance(id, int))
assert (isinstance(lot, Wad))
assert (isinstance(bid, Rad))
current_bid = flipper.bids(id)
assert current_bid.guy != Address("0x0000000000000000000000000000000000000000")
assert current_bid.tic > datetime.now().timestamp() or current_bid.tic == 0
assert current_bid.end > datetime.now().timestamp()
assert lot == current_bid.lot
assert bid <= current_bid.tab
assert bid > current_bid.bid
assert (bid >= Rad(flipper.beg()) * current_bid.bid) or (bid == current_bid.tab)
assert flipper.tend(id, lot, bid).transact(from_address=address)
@staticmethod
def dent(flipper: Flipper, id: int, address: Address, lot: Wad, bid: Rad):
assert (isinstance(flipper, Flipper))
assert (isinstance(id, int))
assert (isinstance(lot, Wad))
assert (isinstance(bid, Rad))
current_bid = flipper.bids(id)
assert current_bid.guy != Address("0x0000000000000000000000000000000000000000")
assert current_bid.tic > datetime.now().timestamp() or current_bid.tic == 0
assert current_bid.end > datetime.now().timestamp()
assert bid == current_bid.bid
assert bid == current_bid.tab
assert lot < current_bid.lot
assert flipper.beg() * lot <= current_bid.lot
assert flipper.dent(id, lot, bid).transact(from_address=address)
@staticmethod
def last_log(flipper: Flipper):
current_block = flipper.web3.eth.blockNumber
return flipper.past_logs(current_block-1, current_block)[0]
def test_getters(self, mcd, flipper):
assert flipper.vat() == mcd.vat.address
assert flipper.beg() > Wad.from_number(1)
assert flipper.ttl() > 0
assert flipper.tau() > flipper.ttl()
assert flipper.kicks() >= 0
def test_scenario(self, web3, mcd, collateral, flipper, our_address, other_address, deployment_address):
# Create a vault
kicks_before = flipper.kicks()
ilk = collateral.ilk
ink = Wad.from_number(0.5)
wrap_eth(mcd, deployment_address, ink)
collateral.approve(deployment_address)
assert collateral.adapter.join(deployment_address, ink).transact(
from_address=deployment_address)
frob(mcd, collateral, deployment_address, dink=ink, dart=Wad(0))
dart = max_dart(mcd, collateral, deployment_address) - Wad(1)
frob(mcd, collateral, deployment_address, dink=Wad(0), dart=dart)
# Mint and withdraw all the Dai
mcd.approve_dai(deployment_address)
assert mcd.dai_adapter.exit(deployment_address, dart).transact(from_address=deployment_address)
assert mcd.dai.balance_of(deployment_address) == dart
# Undercollateralize the vault
to_price = Wad(Web3.toInt(collateral.pip.read())) / Wad.from_number(2)
set_collateral_price(mcd, collateral, to_price)
urn = mcd.vat.urn(collateral.ilk, deployment_address)
ilk = mcd.vat.ilk(ilk.name)
assert ilk.rate is not None
assert ilk.spot is not None
safe = Ray(urn.art) * mcd.vat.ilk(ilk.name).rate <= Ray(urn.ink) * ilk.spot
assert not safe
assert len(flipper.active_auctions()) == 0
litter_before = mcd.cat.litter()
# Bite the vault, which moves debt to the vow and kicks the flipper
urn = mcd.vat.urn(collateral.ilk, deployment_address)
assert urn.ink > Wad(0)
art = min(urn.art, Wad(mcd.cat.dunk(ilk))) # Wad
tab = art * ilk.rate # Wad
assert tab == dart
assert mcd.cat.can_bite(ilk, urn)
assert mcd.cat.bite(ilk, urn).transact()
kick = flipper.kicks()
assert kick == kicks_before + 1
urn = mcd.vat.urn(collateral.ilk, deployment_address)
# Check vat, vow, and cat
assert urn.ink == Wad(0)
assert urn.art == dart - art
assert mcd.vat.vice() > Rad(0)
assert mcd.vow.sin() == Rad(tab)
bites = mcd.cat.past_bites(1)
assert len(bites) == 1
last_bite = bites[0]
assert last_bite.tab > Rad(0)
assert last_bite.id == 1
litter_after = mcd.cat.litter()
assert litter_before < litter_after
# Check the flipper
current_bid = flipper.bids(kick)
assert isinstance(current_bid, Flipper.Bid)
assert current_bid.lot > Wad(0)
assert current_bid.tab > Rad(0)
assert current_bid.bid == Rad(0)
# Cat doesn't incorporate the liquidation penalty (chop), but the kicker includes it.
# Awaiting word from @dc why this is so.
#assert last_bite.tab == current_bid.tab
log = self.last_log(flipper)
assert isinstance(log, Flipper.KickLog)
assert log.id == kick
assert log.lot == current_bid.lot
assert log.bid == current_bid.bid
assert log.tab == current_bid.tab
assert log.usr == deployment_address
assert log.gal == mcd.vow.address
# Allow the auction to expire, and then resurrect it
time_travel_by(web3, flipper.tau()+1)
assert flipper.tick(kick).transact()
# Wrap some eth and handle approvals before bidding
eth_required = Wad(current_bid.tab / Rad(ilk.spot)) * Wad.from_number(1.13)
wrap_eth(mcd, other_address, eth_required)
collateral.approve(other_address)
assert collateral.adapter.join(other_address, eth_required).transact(from_address=other_address)
wrap_eth(mcd, our_address, eth_required)
collateral.approve(our_address)
assert collateral.adapter.join(our_address, eth_required).transact(from_address=our_address)
# Test the _tend_ phase of the auction
flipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=other_address))
# Add Wad(1) to counter precision error converting tab from Rad to Wad
frob(mcd, collateral, other_address, dink=eth_required, dart=Wad(current_bid.tab) + Wad(1))
urn = mcd.vat.urn(collateral.ilk, other_address)
assert Rad(urn.art) >= current_bid.tab
# Bid the tab to instantly transition to dent stage
TestFlipper.tend(flipper, kick, other_address, current_bid.lot, current_bid.tab)
current_bid = flipper.bids(kick)
assert current_bid.guy == other_address
assert current_bid.bid == current_bid.tab
assert len(flipper.active_auctions()) == 1
check_active_auctions(flipper)
log = self.last_log(flipper)
assert isinstance(log, Flipper.TendLog)
assert log.guy == current_bid.guy
assert log.id == current_bid.id
assert log.lot == current_bid.lot
assert log.bid == current_bid.bid
# Test the _dent_ phase of the auction
flipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=our_address))
frob(mcd, collateral, our_address, dink=eth_required, dart=Wad(current_bid.tab) + Wad(1))
lot = current_bid.lot - Wad.from_number(0.3)
assert flipper.beg() * lot <= current_bid.lot
TestFlipper.dent(flipper, kick, our_address, lot, current_bid.tab)
current_bid = flipper.bids(kick)
assert current_bid.guy == our_address
assert current_bid.bid == current_bid.tab
assert current_bid.lot == lot
log = self.last_log(flipper)
assert isinstance(log, Flipper.DentLog)
assert log.guy == current_bid.guy
assert log.id == current_bid.id
assert log.lot == current_bid.lot
assert log.bid == current_bid.bid
# Exercise _deal_ after bid has expired
time_travel_by(web3, flipper.ttl()+1)
now = datetime.now().timestamp()
assert 0 < current_bid.tic < now or current_bid.end < now
assert flipper.deal(kick).transact(from_address=our_address)
assert len(flipper.active_auctions()) == 0
log = self.last_log(flipper)
assert isinstance(log, Flipper.DealLog)
assert log.usr == our_address
# Grab our collateral
collateral_before = collateral.gem.balance_of(our_address)
assert collateral.adapter.exit(our_address, current_bid.lot).transact(from_address=our_address)
collateral_after = collateral.gem.balance_of(our_address)
assert collateral_before < collateral_after
# Cleanup
set_collateral_price(mcd, collateral, Wad.from_number(230))
cleanup_urn(mcd, collateral, other_address)
class TestClipper:
@pytest.fixture(scope="session")
def collateral(self, mcd: DssDeployment) -> Collateral:
return mcd.collaterals['ETH-B']
@pytest.fixture(scope="session")
def clipper(self, collateral, deployment_address) -> Clipper:
return collateral.clipper
@staticmethod
def last_log(clipper: Clipper):
current_block = clipper.web3.eth.blockNumber
return clipper.past_logs(current_block-1, current_block)[0]
def test_getters(self, mcd, clipper):
assert clipper.ilk_name() == "ETH-B"
collateral = mcd.collaterals[clipper.ilk_name()]
assert clipper.kicks() == 0
assert clipper.buf() == Ray.from_number(1.50)
assert clipper.tail() == 10800
assert clipper.cusp() == Ray.from_number(0.3333)
assert clipper.chip() == Wad.from_number(0.02)
assert clipper.tip() == Rad.from_number(100)
assert clipper.chost() == collateral.ilk.dust * Rad(mcd.dog.chop(collateral.ilk))
assert isinstance(clipper.calc, Address)
assert clipper.calc != Address.zero()
assert clipper.dog.address == mcd.dog.address
assert clipper.vat.address == mcd.vat.address
assert clipper.active_auctions() == []
def test_scenario(self, web3, mcd, collateral, clipper, our_address, other_address, deployment_address):
dirt_before = mcd.dog.dog_dirt()
vice_before = mcd.vat.vice()
sin_before = mcd.vow.sin()
# Create a vault
ilk = collateral.ilk
ink = Wad.from_number(1)
wrap_eth(mcd, deployment_address, ink)
collateral.approve(deployment_address)
assert collateral.adapter.join(deployment_address, ink).transact(
from_address=deployment_address)
frob(mcd, collateral, deployment_address, dink=ink, dart=Wad(0))
dart = max_dart(mcd, collateral, deployment_address) - Wad(1)
frob(mcd, collateral, deployment_address, dink=Wad(0), dart=dart)
# Mint and withdraw all the Dai
mcd.approve_dai(deployment_address)
assert mcd.dai_adapter.exit(deployment_address, dart).transact(from_address=deployment_address)
# Undercollateralize the vault
to_price = Wad(Web3.toInt(collateral.pip.read())) / Wad.from_number(2)
set_collateral_price(mcd, collateral, to_price)
urn = mcd.vat.urn(collateral.ilk, deployment_address)
ilk = mcd.vat.ilk(ilk.name)
safe = Ray(urn.art) * mcd.vat.ilk(ilk.name).rate <= Ray(urn.ink) * ilk.spot
assert not safe
assert clipper.active_count() == 0
# Bark the vault, which moves debt to the vow and kicks the clipper
dai_before_bark: Rad = mcd.vat.dai(our_address)
urn = mcd.vat.urn(collateral.ilk, deployment_address)
assert urn.ink > Wad(0)
tab = urn.art * ilk.rate # Wad
assert tab == dart
assert mcd.dog.bark(ilk, urn).transact()
barks = mcd.dog.past_barks(1)
assert len(barks) == 1
last_bite = barks[0]
assert last_bite.due > Rad(0)
assert clipper.active_count() == 1
kick = clipper.kicks()
assert kick == 1
assert len(clipper.active_auctions()) == 1
assert clipper.active_auctions()[0].id == 1
assert mcd.active_auctions()['clips']['ETH-B'][0].id == 1
urn = mcd.vat.urn(collateral.ilk, deployment_address)
(needs_redo, price, lot, tab) = clipper.status(kick)
assert not needs_redo
assert price == Ray.from_number(172.5)
assert lot == ink
assert float(tab) == 105.0
# Check vat, vow, and dog
assert urn.ink == Wad(0)
assert vice_before < mcd.vat.vice()
assert sin_before < mcd.vow.sin()
assert dirt_before < mcd.dog.dog_dirt()
# Check the clipper
current_sale = clipper.sales(kick)
assert isinstance(current_sale, Clipper.Sale)
assert current_sale.pos == 0
assert float(current_sale.tab) == 105.0
assert current_sale.lot == ink
assert current_sale.usr == deployment_address
assert current_sale.tic > 0
assert round(current_sale.top, 1) == price
coin = Rad(clipper.tip() + (current_sale.tab * clipper.chip()))
# Confirm we received our liquidation reward
dai_after_bark: Rad = mcd.vat.dai(our_address)
assert dai_after_bark == dai_before_bark + coin
kick_log = self.last_log(clipper)
assert isinstance(kick_log, Clipper.KickLog)
assert kick_log.id == kick
assert kick_log.top == current_sale.top
assert kick_log.tab == current_sale.tab
assert kick_log.lot == current_sale.lot
assert kick_log.usr == deployment_address
assert kick_log.kpr == our_address
assert kick_log.coin == coin
# Wrap some eth and handle approvals before bidding
eth_required = Wad(current_sale.tab / Rad(ilk.spot)) * Wad.from_number(1.1)
wrap_eth(mcd, our_address, eth_required)
collateral.approve(our_address)
assert collateral.adapter.join(our_address, eth_required).transact(from_address=our_address)
clipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=our_address))
# Ensure we cannot take collateral below the current price
(needs_redo, price, lot, tab) = clipper.status(kick)
with pytest.raises(AssertionError):
clipper.validate_take(kick, ink, price - Ray.from_number(1))
assert not clipper.take(kick, ink, Ray.from_number(140)).transact(from_address=our_address)
# Take some collateral with max above the top price
clipper.validate_take(kick, Wad.from_number(0.07), Ray.from_number(180))
assert web3.eth.defaultAccount == our_address.address
assert clipper.take(kick, Wad.from_number(0.07), Ray.from_number(180)).transact(from_address=our_address)
(needs_redo, price, lot, tab) = clipper.status(kick)
assert not needs_redo
current_sale = clipper.sales(kick)
assert current_sale.lot > Wad(0)
assert current_sale.top > price
assert Rad(0) < current_sale.tab < kick_log.tab
first_take_log = self.last_log(clipper)
assert first_take_log.id == 1
assert first_take_log.max == Ray.from_number(180)
assert first_take_log.price == price
assert first_take_log.lot == current_sale.lot
assert first_take_log.usr == deployment_address
assert first_take_log.sender == our_address
assert round(first_take_log.owe, 18) == round(Rad.from_number(0.07) * Rad(price), 18)
# Allow the auction to expire, and then resurrect it
# TODO: If abaci contract is ever wrapped, read tau from it
time_travel_by(web3, 24)
(needs_redo, price, lot, tab) = clipper.status(kick)
assert needs_redo
assert len(clipper.active_auctions()) == clipper.active_count()
assert clipper.redo(kick, our_address).transact()
(needs_redo, price, lot, tab) = clipper.status(kick)
assert not needs_redo
current_sale = clipper.sales(kick)
assert current_sale.lot > Wad(0)
redo_log = self.last_log(clipper)
assert isinstance(redo_log, Clipper.RedoLog)
assert redo_log.id == kick
assert redo_log.top == current_sale.top
assert redo_log.tab == current_sale.tab
assert redo_log.lot == current_sale.lot
assert redo_log.usr == deployment_address
assert redo_log.kpr == our_address
coin = Rad(clipper.tip() + (current_sale.tab * clipper.chip()))
assert round(float(redo_log.coin), 18) == round(float(coin), 18)
# Sleep until price has gone down enough to bid with remaining Dai
dai = mcd.vat.dai(our_address)
last_price = price
print(f"Bid cost={float(price * Ray(current_sale.lot))}, Dai balance={float(dai)}")
while price * Ray(current_sale.lot) > Ray(dai):
print(f"Bid cost {price * Ray(current_sale.lot)} exceeds Dai balance {dai}")
time_travel_by(web3, 2)
(needs_redo, price, lot, tab) = clipper.status(kick)
assert price < last_price
assert not needs_redo
last_price = price
clipper.validate_take(kick, current_sale.lot, price)
assert clipper.take(kick, current_sale.lot, price).transact(from_address=our_address)
current_sale = clipper.sales(kick)
assert current_sale.lot == Wad(0)
assert current_sale.tab == Rad(0)
assert clipper.active_count() == 0
assert len(clipper.active_auctions()) == 0
# Ensure we can retrieve our collateral
collateral_before = collateral.gem.balance_of(our_address)
assert collateral.adapter.exit(our_address, ink).transact(from_address=our_address)
collateral_after = collateral.gem.balance_of(our_address)
assert collateral_before < collateral_after
# Cleanup
set_collateral_price(mcd, collateral, Wad.from_number(230))
cleanup_urn(mcd, collateral, our_address)
class TestFlopper:
@pytest.fixture(scope="session")
def flopper(self, mcd: DssDeployment) -> Flopper:
return mcd.flopper
@staticmethod
def dent(flopper: Flopper, id: int, address: Address, lot: Wad, bid: Rad):
assert (isinstance(flopper, Flopper))
assert (isinstance(id, int))
assert (isinstance(lot, Wad))
assert (isinstance(bid, Rad))
assert flopper.live() == 1
current_bid = flopper.bids(id)
assert current_bid.guy != Address("0x0000000000000000000000000000000000000000")
assert current_bid.tic > datetime.now().timestamp() or current_bid.tic == 0
assert current_bid.end > datetime.now().timestamp()
assert bid == current_bid.bid
assert Wad(0) < lot < current_bid.lot
assert flopper.beg() * lot <= current_bid.lot
assert flopper.dent(id, lot, bid).transact(from_address=address)
log = TestFlopper.last_log(flopper)
assert isinstance(log, Flopper.DentLog)
assert log.guy == address
assert log.id == id
assert log.lot == lot
assert log.bid == bid
@staticmethod
def last_log(flopper: Flopper):
current_block = flopper.web3.eth.blockNumber
return flopper.past_logs(current_block-1, current_block)[0]
def test_getters(self, mcd, flopper):
assert flopper.vat() == mcd.vat.address
assert flopper.beg() > Wad.from_number(1)
assert flopper.ttl() > 0
assert flopper.tau() > flopper.ttl()
assert flopper.kicks() >= 0
@pytest.mark.skip(reason="fix flop tests")
def test_scenario(self, web3, mcd, flopper, our_address, other_address, deployment_address):
create_debt(web3, mcd, our_address, deployment_address)
# Kick off the flop auction
assert flopper.kicks() == 0
assert len(flopper.active_auctions()) == 0
assert mcd.vat.dai(mcd.vow.address) == Rad(0)
assert mcd.vow.flop().transact()
kick = flopper.kicks()
assert kick == 1
assert len(flopper.active_auctions()) == 1
check_active_auctions(flopper)
current_bid = flopper.bids(kick)
log = self.last_log(flopper)
assert isinstance(log, Flopper.KickLog)
assert log.id == kick
assert log.lot == current_bid.lot
assert log.bid == current_bid.bid
assert log.gal == mcd.vow.address
# Allow the auction to expire, and then resurrect it
time_travel_by(web3, flopper.tau()+1)
assert flopper.tick(kick).transact()
assert flopper.bids(kick).lot == current_bid.lot * flopper.pad()
# Bid on the resurrected auction
bid = Wad.from_number(0.000005)
flopper.approve(mcd.vat.address, hope_directly())
assert mcd.vat.can(our_address, flopper.address)
TestFlopper.dent(flopper, kick, our_address, bid, current_bid.bid)
current_bid = flopper.bids(kick)
assert current_bid.guy == our_address
# Confirm victory
time_travel_by(web3, flopper.ttl()+1)
assert flopper.live()
now = int(datetime.now().timestamp())
assert (current_bid.tic < now and current_bid.tic != 0) or current_bid.end < now
mkr_before = mcd.mkr.balance_of(our_address)
assert flopper.deal(kick).transact(from_address=our_address)
mkr_after = mcd.mkr.balance_of(our_address)
assert mkr_after > mkr_before
log = self.last_log(flopper)
assert isinstance(log, Flopper.DealLog)
assert log.usr == our_address
assert log.id == kick
# Cleanup
collateral = mcd.collaterals['ETH-A']
set_collateral_price(mcd, collateral, Wad.from_number(230))
================================================
FILE: tests/test_auth.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.auth import DSGuard, DSAuth
from pymaker.util import hexstring_to_bytes
class TestDSGuard:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.ds_guard = DSGuard.deploy(self.web3)
def can_call(self, src: str, dst: str, sig: str) -> bool:
return self.ds_guard._contract.functions.canCall(src, dst, hexstring_to_bytes(sig)).call()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
DSGuard(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_no_permit_by_default(self):
# expect
assert not self.can_call(src='0x1111111111222222222211111111112222222222',
dst='0x3333333333444444444433333333334444444444',
sig='0xab121fd7')
def test_permit_any_to_any_with_any_sig(self):
# when
self.ds_guard.permit(DSGuard.ANY, DSGuard.ANY, DSGuard.ANY).transact()
# then
assert self.can_call(src='0x1111111111222222222211111111112222222222',
dst='0x3333333333444444444433333333334444444444',
sig='0xab121fd7')
def test_permit_specific_addresses_and_sig(self):
# when
self.ds_guard.permit(src=Address('0x1111111111222222222211111111112222222222'),
dst=Address('0x3333333333444444444433333333334444444444'),
sig=hexstring_to_bytes('0xab121fd7')).transact()
# then
assert self.can_call(src='0x1111111111222222222211111111112222222222',
dst='0x3333333333444444444433333333334444444444',
sig='0xab121fd7')
# and
assert not self.can_call(src='0x3333333333444444444433333333334444444444',
dst='0x1111111111222222222211111111112222222222',
sig='0xab121fd7') # different addresses
assert not self.can_call(src='0x1111111111222222222211111111112222222222',
dst='0x3333333333444444444433333333334444444444',
sig='0xab121fd8') # different sig
class TestDSAuth:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.ds_auth = DSAuth.deploy(self.web3)
@pytest.mark.skip(reason="calls to ABI/BIN are not working on ganache")
def test_owner(self):
owner = self.ds_auth.get_owner()
assert isinstance(owner, Address)
assert owner == self.web3.eth.accounts[0]
assert self.ds_auth.set_owner(self.web3.eth.accounts[1])
assert self.ds_auth.get_owner() == self.web3.eth.accounts[1]
================================================
FILE: tests/test_cdpmanager.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.cdpmanager import Urn
class TestCdpManager:
def test_none(self, our_address: Address, mcd: DssDeployment):
assert mcd.cdp_manager.first(our_address) == 0
assert mcd.cdp_manager.last(our_address) == 0
assert mcd.cdp_manager.count(our_address) == 0
def test_open(self, our_address: Address, mcd: DssDeployment):
ilk = mcd.collaterals['ETH-A'].ilk
assert mcd.cdp_manager.open(ilk, our_address).transact()
assert mcd.cdp_manager.last(our_address) == 1
assert mcd.cdp_manager.ilk(1).name == ilk.name
assert mcd.cdp_manager.owns(1) == our_address
assert isinstance(mcd.cdp_manager.urn(1), Urn)
def test_one(self, our_address: Address, mcd: DssDeployment):
assert mcd.cdp_manager.first(our_address) == 1
assert mcd.cdp_manager.last(our_address) == 1
assert mcd.cdp_manager.count(our_address) == 1
================================================
FILE: tests/test_dsrmanager.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 Maker Ecosystem Growth Holdings, INC
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from pymaker import Address
from tests.helpers import time_travel_by
from pymaker.numeric import Wad, Rad, Ray
from pymaker.deployment import DaiJoin, DssDeployment
from pymaker.dss import Pot
from pymaker.token import DSToken
from tests.test_dss import wrap_eth, frob
def mint_dai(mcd: DssDeployment, amount: Wad, ilkName: str, our_address: Address):
startingAmount = mcd.dai.balance_of(our_address)
dai = amount
# Add collateral to our CDP and draw internal Dai
collateral=mcd.collaterals[ilkName]
ilk = mcd.vat.ilk(collateral.ilk.name)
dink = Wad.from_number(1)
dart = Wad( Rad(dai) / Rad(ilk.rate))
wrap_eth(mcd, our_address, dink)
assert collateral.gem.balance_of(our_address) >= dink
assert collateral.gem.approve(collateral.adapter.address).transact(from_address=our_address)
assert collateral.adapter.join(our_address, dink).transact(from_address=our_address)
frob(mcd, collateral, our_address, dink=dink, dart=dart)
# Exit to Dai Token and make some checks
assert mcd.vat.hope(mcd.dai_adapter.address).transact(from_address=our_address)
assert mcd.dai_adapter.exit(our_address, dai).transact(from_address=our_address)
assert mcd.dai.balance_of(our_address) == dai + startingAmount
pytest.global_dai = Wad(0)
class TestDsrManager:
def test_getters(self, mcd: DssDeployment):
assert isinstance(mcd.dsr_manager.pot(), Pot)
assert mcd.dsr_manager.pot().address.address == mcd.pot.address.address
assert isinstance(mcd.dsr_manager.dai(), DSToken)
assert mcd.dsr_manager.dai().address.address == mcd.dai.address.address
assert isinstance(mcd.dsr_manager.dai_adapter(), DaiJoin)
assert mcd.dsr_manager.dai_adapter().address.address == mcd.dai_adapter.address.address
def test_join(self, mcd: DssDeployment, our_address: Address):
# Mint 58 Dai and lock it in the Pot contract through DsrManager
more_dai = Wad.from_number(58)
mint_dai(mcd=mcd, amount=more_dai, ilkName='ETH-A', our_address=our_address)
dai = mcd.dai.balance_of(our_address)
assert mcd.dai.approve(mcd.dsr_manager.address).transact(from_address=our_address)
assert mcd.dsr_manager.supply() == Wad(0)
# Join through DsrManager an ensure Dai Token balance is depleted
assert mcd.dsr_manager.join(our_address, dai).transact(from_address=our_address)
assert mcd.dai.balance_of(our_address) == Wad(0)
pytest.global_dai = dai
def test_supply_pie_dai(self, mcd: DssDeployment, our_address: Address):
dai = pytest.global_dai
chi1 = mcd.pot.chi()
# assert chi1 == Ray.from_number(1) Commented out in case there's some initial state on testchain
pie = Wad(Rad(dai) / Rad(chi1))
assert mcd.dsr_manager.supply() == pie
assert mcd.dsr_manager.pie_of(our_address) == pie
time_travel_by(web3=mcd.web3, seconds=10)
assert mcd.pot.drip().transact(from_address=our_address)
chi2 = mcd.pot.chi()
assert chi1 != chi2
dai = Rad(pie) * Rad(chi2)
assert mcd.dsr_manager.dai_of(our_address) == dai
def test_exit(self, mcd: DssDeployment, our_address: Address):
dai = pytest.global_dai
assert mcd.dai.balance_of(our_address) == Wad.from_number(0)
# since drip was called in previous test, there should be some amount left
assert mcd.dsr_manager.exit(our_address, dai).transact(from_address=our_address)
assert mcd.dai.balance_of(our_address) == dai
assert mcd.dsr_manager.supply() != Wad(0)
assert mcd.dsr_manager.exitAll(our_address).transact(from_address=our_address)
assert mcd.dai.balance_of(our_address) > dai
assert mcd.dsr_manager.supply() == Wad(0)
================================================
FILE: tests/test_dss.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018-2019 bargst, EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import json
import pytest
import time
from datetime import datetime
from web3 import Web3
from pymaker import Address
from pymaker.approval import hope_directly
from pymaker.deployment import Collateral, DssDeployment
from pymaker.dss import Ilk, Urn, Vat, Vow
from pymaker.feed import DSValue
from pymaker.join import DaiJoin, GemJoin, GemJoin5
from pymaker.numeric import Wad, Ray, Rad
from pymaker.oracles import OSM
from pymaker.token import DSToken, DSEthToken, ERC20Token
from tests.conftest import validate_contracts_loaded
@pytest.fixture
def urn(our_address: Address, mcd: DssDeployment):
collateral = mcd.collaterals['ETH-A']
urn = mcd.vat.urn(collateral.ilk, our_address)
assert urn.ilk is not None
assert urn.ilk == collateral.ilk
return urn
def wrap_eth(mcd: DssDeployment, address: Address, amount: Wad):
assert isinstance(mcd, DssDeployment)
assert isinstance(address, Address)
assert isinstance(amount, Wad)
assert amount > Wad(0)
collateral = mcd.collaterals['ETH-A']
assert isinstance(collateral.gem, DSEthToken)
assert collateral.gem.deposit(amount).transact(from_address=address)
def mint_mkr(mkr: DSToken, recipient_address: Address, amount: Wad):
assert isinstance(mkr, DSToken)
assert isinstance(recipient_address, Address)
assert isinstance(amount, Wad)
assert amount > Wad(0)
deployment_address = Address("0x00a329c0648769A73afAc7F9381E08FB43dBEA72")
assert mkr.mint(amount).transact(from_address=deployment_address)
assert mkr.balance_of(deployment_address) > Wad(0)
assert mkr.approve(recipient_address).transact(from_address=deployment_address)
assert mkr.transfer(recipient_address, amount).transact(from_address=deployment_address)
def get_collateral_price(collateral: Collateral):
assert isinstance(collateral, Collateral)
return Wad(Web3.toInt(collateral.pip.read()))
def set_collateral_price(mcd: DssDeployment, collateral: Collateral, price: Wad):
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(price, Wad)
assert price > Wad(0)
pip = collateral.pip
assert isinstance(pip, DSValue)
print(f"Changing price of {collateral.ilk.name} to {price}")
assert pip.poke_with_int(price.value).transact(from_address=pip.get_owner())
assert mcd.spotter.poke(ilk=collateral.ilk).transact(from_address=pip.get_owner())
assert get_collateral_price(collateral) == price
def frob(mcd: DssDeployment, collateral: Collateral, address: Address, dink: Wad, dart: Wad):
"""Wraps vat.frob for debugging purposes"""
# given
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(address, Address)
assert isinstance(dink, Wad)
assert isinstance(dart, Wad)
ilk = collateral.ilk
# when
ink_before = mcd.vat.urn(ilk, address).ink
art_before = mcd.vat.urn(ilk, address).art
# then
if dart < Wad(0):
assert mcd.vat.dai(address) >= Rad(dart*-1)
assert mcd.vat.frob(ilk=ilk, urn_address=address, dink=dink, dart=dart).transact(from_address=address)
assert mcd.vat.urn(ilk, address).ink == ink_before + dink
assert mcd.vat.urn(ilk, address).art == art_before + dart
def max_dart(mcd: DssDeployment, collateral: Collateral, our_address: Address) -> Wad:
"""Determines how much stablecoin should be reserved in an `urn` to make it as poorly collateralized as
possible, such that a small change to the collateral price could trip the liquidation ratio."""
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(our_address, Address)
urn = mcd.vat.urn(collateral.ilk, our_address)
ilk = mcd.vat.ilk(collateral.ilk.name)
# change in art = (collateral balance * collateral price with safety margin) - CDP's stablecoin debt
dart = urn.ink * ilk.spot - Wad(Ray(urn.art) * ilk.rate)
# change in debt must also take the rate into account
dart = dart * Wad(Ray.from_number(1) / ilk.rate)
# prevent the change in debt from exceeding the collateral debt ceiling
if (Rad(urn.art) + Rad(dart)) >= ilk.line:
print("max_dart is avoiding collateral debt ceiling")
dart = Wad(ilk.line - Rad(urn.art))
# prevent the change in debt from exceeding the total debt ceiling
debt = mcd.vat.debt() + Rad(ilk.rate * dart)
line = Rad(ilk.line)
if (debt + Rad(dart)) >= line:
print("max_dart is avoiding total debt ceiling")
dart = Wad(debt - Rad(urn.art))
assert dart > Wad(0)
return dart
def cleanup_urn(mcd: DssDeployment, collateral: Collateral, address: Address):
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(address, Address)
urn = mcd.vat.urn(collateral.ilk, address)
ilk = mcd.vat.ilk(collateral.ilk.name)
# If jug.drip has been called, we won't have sufficient dai to repay the CDP
if ilk.rate > Ray.from_number(1):
return
# Repay borrowed Dai
mcd.approve_dai(address)
# Put all the user's Dai back into the vat
if mcd.dai.balance_of(address) >= Wad(0):
assert mcd.dai_adapter.join(address, mcd.dai.balance_of(address)).transact(from_address=address)
# tab = Ray(urn.art) * ilk.rate
# print(f'tab={str(tab)}, rate={str(ilk.rate)}, dai={str(mcd.vat.dai(address))}')
if urn.art > Wad(0) and mcd.vat.dai(address) >= Rad(urn.art):
frob(mcd, collateral, address, Wad(0), urn.art * -1)
# Withdraw collateral
collateral.approve(address)
urn = mcd.vat.urn(collateral.ilk, address)
# dink = Wad((Ray(urn.art) * ilk.rate) / ilk.spot)
# print(f'dink={str(dink)}, ink={str(urn.ink)}')
if urn.art == Wad(0) and urn.ink > Wad(0):
frob(mcd, collateral, address, urn.ink * -1, Wad(0))
assert collateral.adapter.exit(address, mcd.vat.gem(collateral.ilk, address)).transact(from_address=address)
# TestVat.ensure_clean_urn(mcd, collateral, address)
@pytest.fixture(scope="session")
def bite(web3: Web3, mcd: DssDeployment, our_address: Address):
collateral = mcd.collaterals['ETH-A']
# Add collateral to our CDP
dink = Wad.from_number(1)
wrap_eth(mcd, our_address, dink)
assert collateral.gem.balance_of(our_address) >= dink
assert collateral.adapter.join(our_address, dink).transact()
frob(mcd, collateral, our_address, dink, Wad(0))
# Define required bite parameters
to_price = Wad(Web3.toInt(collateral.pip.read())) / Wad.from_number(2)
# Manipulate price to make our CDP underwater
# Note this will only work on a testchain deployed with fixed prices, where PIP is a DSValue
frob(mcd, collateral, our_address, Wad(0), max_dart(mcd, collateral, our_address))
set_collateral_price(mcd, collateral, to_price)
# Bite the CDP
assert mcd.cat.can_bite(collateral.ilk, Urn(our_address))
assert mcd.cat.bite(collateral.ilk, Urn(our_address)).transact()
@pytest.fixture(scope="session")
def bite_event(web3: Web3, mcd: DssDeployment, our_address: Address):
bite(web3, mcd, our_address)
# Return the corresponding event
return mcd.cat.past_bites(1)[0]
class TestConfig:
def test_from_json(self, web3: Web3, mcd: DssDeployment):
# fixture calls DssDeployment.from_json
assert len(mcd.config.collaterals) >= 3
assert len(mcd.collaterals) >= 3
assert len(mcd.config.to_dict()) > 10
assert len(mcd.collaterals) == len(mcd.config.collaterals)
def test_to_json(self, web3: Web3, mcd: DssDeployment):
config_out = mcd.to_json()
dict = json.loads(config_out)
assert "MCD_GOV" in dict
assert "MCD_DAI" in dict
assert len(dict) > 20
def test_from_node(self, web3: Web3):
mcd_testnet = DssDeployment.from_node(web3)
validate_contracts_loaded(mcd_testnet)
def test_collaterals(self, mcd):
for collateral in mcd.collaterals.values():
assert isinstance(collateral.ilk, Ilk)
assert isinstance(collateral.gem, ERC20Token)
assert len(collateral.ilk.name) > 0
assert len(collateral.gem.name()) > 0
assert len(collateral.gem.symbol()) > 0
assert collateral.adapter
assert collateral.flipper or collateral.clipper
assert collateral.pip
def test_account_transfers(self, web3: Web3, mcd, our_address, other_address):
print(mcd.collaterals)
collateral = mcd.collaterals['ETH-A']
token = collateral.gem
amount = Wad(10)
assert web3.eth.defaultAccount == our_address.address
assert our_address != other_address
wrap_eth(mcd, our_address, amount)
# Move eth between each account to confirm keys are properly set up
before = token.balance_of(our_address)
assert token.transfer_from(our_address, other_address, amount).transact()
after = token.balance_of(our_address)
assert (before - amount) == after
assert token.transfer_from(other_address, our_address, amount).transact(from_address=other_address)
assert token.balance_of(our_address) == before
def test_get_active_auctions(self, mcd):
auctions = mcd.active_auctions()
assert "flips" in auctions
assert "flaps" in auctions
assert "flops" in auctions
class TestVat:
@staticmethod
def ensure_clean_urn(mcd: DssDeployment, collateral: Collateral, address: Address):
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(address, Address)
urn = mcd.vat.urn(collateral.ilk, address)
assert urn.ink == Wad(0)
assert urn.art == Wad(0)
assert mcd.vat.gem(collateral.ilk, address) == Wad(0)
def test_getters(self, mcd):
assert isinstance(mcd.vat.live(), bool)
def test_ilk(self, mcd):
assert mcd.vat.ilk('XXX') == Ilk('XXX',
rate=Ray(0), ink=Wad(0), art=Wad(0), spot=Ray(0), line=Rad(0), dust=Rad(0))
ilk = mcd.collaterals["ETH-C"].ilk
assert ilk.line == Rad.from_number(15000000)
assert ilk.dust == Rad(0)
representation = repr(ilk)
assert "ETH-C" in representation
def test_gem(self, web3: Web3, mcd: DssDeployment, our_address: Address):
# given
collateral = mcd.collaterals['ETH-A']
amount_to_join = Wad(10)
our_urn = mcd.vat.urn(collateral.ilk, our_address)
assert isinstance(collateral.ilk, Ilk)
assert isinstance(collateral.adapter, GemJoin)
assert collateral.ilk.name == collateral.adapter.ilk().name
assert our_urn.address == our_address
wrap_eth(mcd, our_address, amount_to_join)
assert collateral.gem.balance_of(our_address) >= amount_to_join
# when
before_join = mcd.vat.gem(collateral.ilk, our_urn.address)
collateral.approve(our_address)
assert collateral.adapter.join(our_address, amount_to_join).transact()
after_join = mcd.vat.gem(collateral.ilk, our_urn.address)
assert collateral.adapter.exit(our_address, amount_to_join).transact()
after_exit = mcd.vat.gem(collateral.ilk, our_urn.address)
# then
assert after_join - before_join == amount_to_join
assert after_exit == before_join
def test_gem_join(self, mcd: DssDeployment):
collateral_bat = mcd.collaterals['BAT-A']
assert isinstance(collateral_bat.adapter, GemJoin)
assert collateral_bat.adapter.dec() == 18
collateral_usdc = mcd.collaterals['USDC-A']
assert isinstance(collateral_usdc.adapter, GemJoin5)
assert collateral_usdc.adapter.dec() == 6
def test_dai(self, mcd, urn):
dai = mcd.vat.dai(urn.address)
assert dai >= Rad(0)
def test_sin(self, mcd, urn):
sin = mcd.vat.sin(urn.address)
assert isinstance(sin, Rad)
assert sin == Rad(0)
def test_debt(self, mcd):
debt = mcd.vat.debt()
assert debt >= Rad(0)
assert debt < mcd.vat.line()
def test_urn(self, urn):
time.sleep(11)
assert urn.ilk is not None
urn_bytes = urn.toBytes()
urn_from_bytes = urn.fromBytes(urn_bytes)
assert urn_from_bytes.address == urn.address
def test_frob_noop(self, mcd, our_address):
# given
collateral = mcd.collaterals['ETH-A']
our_urn = mcd.vat.urn(collateral.ilk, our_address)
# when
assert mcd.vat.frob(collateral.ilk, our_address, Wad(0), Wad(0)).transact()
# then
assert mcd.vat.urn(collateral.ilk, our_address) == our_urn
def test_frob_add_ink(self, mcd, our_address):
# given
collateral = mcd.collaterals['ETH-A']
our_urn = mcd.vat.urn(collateral.ilk, our_address)
# when
wrap_eth(mcd, our_address, Wad(10))
assert collateral.adapter.join(our_address, Wad(10)).transact()
assert mcd.vat.frob(collateral.ilk, our_address, Wad(10), Wad(0)).transact()
# then
assert mcd.vat.urn(collateral.ilk, our_address).ink == our_urn.ink + Wad(10)
# rollback
cleanup_urn(mcd, collateral, our_address)
def test_frob_add_art(self, mcd, our_address: Address):
# given
collateral = mcd.collaterals['ETH-A']
our_urn = mcd.vat.urn(collateral.ilk, our_address)
# when
wrap_eth(mcd, our_address, Wad.from_number(10))
assert collateral.adapter.join(our_address, Wad.from_number(3)).transact()
assert mcd.vat.frob(collateral.ilk, our_address, Wad.from_number(3), Wad.from_number(24)).transact()
# then
assert mcd.vat.urn(collateral.ilk, our_address).art == our_urn.art + Wad.from_number(24)
# rollback
cleanup_urn(mcd, collateral, our_address)
def test_frob_other_account(self, web3, mcd, other_address):
# given
collateral = mcd.collaterals['ETH-A']
collateral.approve(other_address)
mcd.dai_adapter.approve(hope_directly(from_address=other_address), mcd.vat.address)
urn = mcd.vat.urn(collateral.ilk, other_address)
assert urn.address == other_address
# when
wrap_eth(mcd, other_address, Wad.from_number(10))
assert collateral.gem.balance_of(other_address) >= Wad.from_number(10)
assert collateral.gem == collateral.adapter.gem()
collateral.gem.approve(collateral.adapter.address)
assert collateral.adapter.join(other_address, Wad.from_number(3)).transact(from_address=other_address)
assert mcd.vat.frob(collateral.ilk, other_address, Wad.from_number(3), Wad.from_number(20)).transact(from_address=other_address)
# then
assert mcd.vat.urn(collateral.ilk, other_address).art == urn.art + Wad.from_number(20)
# rollback
cleanup_urn(mcd, collateral, other_address)
def test_past_frob(self, mcd, our_address, other_address):
# given
collateral0 = mcd.collaterals['ETH-B']
ilk0 = collateral0.ilk
collateral1 = mcd.collaterals['ETH-C']
ilk1 = collateral1.ilk
try:
# when
wrap_eth(mcd, our_address, Wad.from_number(18))
wrap_eth(mcd, other_address, Wad.from_number(18))
collateral0.approve(our_address)
assert collateral0.adapter.join(our_address, Wad.from_number(9)).transact()
assert mcd.vat.frob(ilk0, our_address, Wad.from_number(3), Wad.from_number(0)).transact()
collateral1.approve(other_address)
assert collateral1.adapter.join(other_address, Wad.from_number(9)).transact(from_address=other_address)
assert mcd.vat.frob(ilk1, other_address, Wad.from_number(9), Wad.from_number(0)).transact(from_address=other_address)
assert mcd.vat.frob(ilk1, other_address, Wad.from_number(-3), Wad.from_number(0)).transact(from_address=other_address)
assert mcd.vat.frob(ilk1, our_address, Wad.from_number(3), Wad.from_number(0),
collateral_owner=other_address, dai_recipient=other_address).transact(
from_address=other_address)
# then
current_block = mcd.web3.eth.blockNumber
from_block = current_block - 6
frobs = mcd.vat.past_frobs(from_block)
assert len(frobs) == 4
assert frobs[0].ilk == ilk0.name
assert frobs[0].urn == our_address
assert frobs[0].dink == Wad.from_number(3)
assert frobs[0].dart == Wad(0)
assert frobs[1].ilk == ilk1.name
assert frobs[1].urn == other_address
assert frobs[1].dink == Wad.from_number(9)
assert frobs[1].dart == Wad(0)
assert frobs[2].ilk == ilk1.name
assert frobs[2].urn == other_address
assert frobs[2].dink == Wad.from_number(-3)
assert frobs[2].dart == Wad(0)
assert frobs[3].urn == our_address
assert frobs[3].collateral_owner == other_address
assert frobs[3].dink == Wad.from_number(3)
assert frobs[3].dart == Wad(0)
assert len(mcd.vat.past_frobs(from_block, ilk=ilk0)) == 1
assert len(mcd.vat.past_frobs(from_block, ilk=ilk1)) == 3
assert len(mcd.vat.past_frobs(from_block, ilk=mcd.collaterals['USDC-A'].ilk)) == 0
finally:
# teardown
cleanup_urn(mcd, collateral0, our_address)
cleanup_urn(mcd, collateral1, other_address)
def test_heal(self, mcd):
assert mcd.vat.heal(Rad(0)).transact()
def test_flux(self, mcd, our_address, other_address):
# given
collateral = mcd.collaterals['ETH-A']
collateral.approve(our_address)
other_balance_before = mcd.vat.gem(collateral.ilk, other_address)
amount = Wad.from_number(3)
wrap_eth(mcd, our_address, amount)
assert collateral.adapter.join(our_address, amount).transact()
# when
assert mcd.vat.flux(collateral.ilk, our_address, other_address, amount).transact()
# then
other_balance_after = mcd.vat.gem(collateral.ilk, other_address)
assert Wad(other_balance_before) + amount == Wad(other_balance_after)
# teardown
cleanup_urn(mcd, collateral, our_address)
def test_move(self, mcd, our_address, other_address):
# given
collateral = mcd.collaterals['ETH-A']
collateral.approve(our_address)
our_urn = mcd.vat.urn(collateral.ilk, our_address)
wrap_eth(mcd, our_address, Wad.from_number(10))
assert collateral.adapter.join(our_address, Wad.from_number(3)).transact()
assert mcd.vat.frob(collateral.ilk, our_address, Wad.from_number(3), Wad.from_number(30)).transact()
other_balance_before = mcd.vat.dai(other_address)
# when
assert mcd.vat.move(our_address, other_address, Rad.from_number(30)).transact()
# then
other_balance_after = mcd.vat.dai(other_address)
assert other_balance_before + Rad.from_number(30) == other_balance_after
# confirm log was emitted and could be parsed
from_block = mcd.web3.eth.blockNumber
logs = mcd.vat.past_logs(from_block)
assert isinstance(logs[0], Vat.LogMove)
logmove: Vat.LogMove = logs[0]
assert logmove.src == our_address
assert logmove.dst == other_address
assert logmove.dart == Rad.from_number(30)
# rollback
cleanup_urn(mcd, collateral, our_address)
def test_fork(self, mcd, our_address, other_address):
# given
collateral = mcd.collaterals['ETH-A']
mcd.vat.hope(our_address).transact(from_address=other_address)
mcd.vat.hope(other_address).transact(from_address=our_address)
our_urn = mcd.vat.urn(collateral.ilk, our_address)
wrap_eth(mcd, our_address, Wad.from_number(6))
assert collateral.adapter.join(our_address, Wad.from_number(6)).transact()
assert mcd.vat.frob(collateral.ilk, our_address, Wad.from_number(6), Wad.from_number(40)).transact()
urn_before = mcd.vat.urn(collateral.ilk, other_address)
# when
assert mcd.vat.fork(collateral.ilk, our_address, other_address, Wad.from_number(3), Wad.from_number(20)).transact()
# then
urn_after = mcd.vat.urn(collateral.ilk, other_address)
assert urn_before.ink + Wad.from_number(3) == urn_after.ink
assert urn_before.art + Wad.from_number(20) == urn_after.art
# confirm log was emitted and could be parsed
from_block = mcd.web3.eth.blockNumber
logs = mcd.vat.past_logs(from_block)
assert isinstance(logs[0], Vat.LogFork)
logfork: Vat.LogFork = logs[0]
assert logfork.ilk == collateral.ilk.name
assert logfork.src == our_address
assert logfork.dst == other_address
assert logfork.dink == Wad.from_number(3)
assert logfork.dart == Wad.from_number(20)
# rollback
cleanup_urn(mcd, collateral, our_address)
class TestCat:
def test_getters(self, mcd):
assert isinstance(mcd.cat.live(), bool)
assert isinstance(mcd.cat.vat, Vat)
assert isinstance(mcd.cat.vow, Vow)
collateral = mcd.collaterals['ETH-C']
assert not collateral.clipper
assert mcd.cat.flipper(collateral.ilk) == collateral.flipper.address
assert mcd.cat.chop(collateral.ilk) == Wad.from_number(1.05)
assert mcd.cat.dunk(collateral.ilk) == Rad.from_number(1000)
assert mcd.cat.box() == Rad.from_number(5000)
class TestDog:
def test_getters(self, mcd):
assert isinstance(mcd.dog.live(), bool)
assert isinstance(mcd.cat.vat, Vat)
assert isinstance(mcd.cat.vow, Vow)
collateral = mcd.collaterals['ETH-B']
assert not collateral.flipper
assert mcd.dog.clipper(collateral.ilk) == collateral.clipper.address
assert mcd.dog.chop(collateral.ilk) == Wad.from_number(1.05)
assert mcd.dog.hole(collateral.ilk) == Rad.from_number(300)
assert mcd.dog.dirt(collateral.ilk) == Rad(0)
assert mcd.dog.dog_hole() == Rad.from_number(5000)
assert mcd.dog.dog_dirt() == Rad(0)
class TestSpotter:
def test_mat(self, mcd):
val = Ray(mcd.collaterals['ETH-A'].pip.read_as_int())
ilk = mcd.vat.ilk('ETH-A')
par = mcd.spotter.par()
mat = mcd.spotter.mat(ilk)
assert mat == (Ray(val * 10 ** 9) / par) / (ilk.spot)
class TestVow:
def test_getters(self, mcd):
assert isinstance(mcd.vow.vat, Vat)
assert isinstance(mcd.vow.live(), bool)
assert isinstance(mcd.vow.flopper(), Address)
assert isinstance(mcd.vow.flopper(), Address)
assert isinstance(mcd.vow.sin(), Rad)
assert isinstance(mcd.vow.sin_of(0), Rad)
assert isinstance(mcd.vow.ash(), Rad)
assert isinstance(mcd.vow.woe(), Rad)
assert isinstance(mcd.vow.wait(), int)
assert isinstance(mcd.vow.dump(), Wad)
assert isinstance(mcd.vow.sump(), Rad)
assert isinstance(mcd.vow.bump(), Rad)
assert isinstance(mcd.vow.hump(), Rad)
def test_empty_flog(self, mcd):
assert mcd.vow.flog(0).transact()
def test_heal(self, mcd):
assert mcd.vow.heal(Rad(0)).transact()
def test_kiss(self, mcd):
assert mcd.vow.kiss(Rad(0)).transact()
class TestJug:
def test_getters(self, mcd, our_address):
c = mcd.collaterals['ETH-A']
assert isinstance(mcd.jug.vat, Vat)
assert isinstance(mcd.jug.vow, Vow)
assert isinstance(mcd.jug.base(), Ray)
assert isinstance(mcd.jug.duty(c.ilk), Ray)
assert isinstance(mcd.jug.rho(c.ilk), int)
assert not mcd.jug.wards(our_address)
def test_drip(self, mcd):
# given
c = mcd.collaterals['ETH-A']
# then
rho_before = mcd.jug.rho(c.ilk)
assert rho_before > 0
assert mcd.jug.drip(c.ilk).transact()
rho_after = mcd.jug.rho(c.ilk)
assert rho_before < rho_after
class TestPot:
def test_getters(self, mcd):
assert isinstance(mcd.pot.pie(), Wad)
assert isinstance(mcd.pot.dsr(), Ray)
assert isinstance(mcd.pot.rho(), datetime)
assert mcd.pot.pie() >= Wad(0)
assert mcd.pot.dsr() > Ray.from_number(1)
# assert datetime.fromtimestamp(0) < mcd.pot.rho() < datetime.utcnow()
def test_drip(self, mcd):
chi_before = mcd.pot.chi()
assert isinstance(chi_before, Ray)
assert mcd.pot.drip().transact()
chi_after = mcd.pot.chi()
if mcd.pot.dsr() == Ray.from_number(1):
assert chi_before == chi_after
else:
assert chi_before < chi_after
class TestOsm:
def test_price(self, web3, mcd):
collateral = mcd.collaterals['ETH-B']
set_collateral_price(mcd, collateral, Wad.from_number(200))
# Note this isn't actually an OSM, but we can still read storage slots
osm = OSM(web3, collateral.pip.address)
raw_price = osm._extract_price(2)
assert isinstance(raw_price, int)
assert Wad.from_number(200) == Wad(raw_price)
class TestMcd:
def test_healthy_cdp(self, mcd, our_address):
collateral = mcd.collaterals['ETH-B']
ilk = collateral.ilk
TestVat.ensure_clean_urn(mcd, collateral, our_address)
initial_dai = mcd.vat.dai(our_address)
wrap_eth(mcd, our_address, Wad.from_number(9))
# Ensure our collateral enters the urn
collateral_balance_before = collateral.gem.balance_of(our_address)
collateral.approve(our_address)
assert collateral.adapter.join(our_address, Wad.from_number(9)).transact()
assert collateral.gem.balance_of(our_address) == collateral_balance_before - Wad.from_number(9)
# Add collateral without generating Dai
frob(mcd, collateral, our_address, dink=Wad.from_number(3), dart=Wad(0))
print(f"After adding collateral: {mcd.vat.urn(ilk, our_address)}")
assert mcd.vat.urn(ilk, our_address).ink == Wad.from_number(3)
assert mcd.vat.urn(ilk, our_address).art == Wad(0)
assert mcd.vat.gem(ilk, our_address) == Wad.from_number(9) - mcd.vat.urn(ilk, our_address).ink
assert mcd.vat.dai(our_address) == initial_dai
# Generate some Dai
frob(mcd, collateral, our_address, dink=Wad(0), dart=Wad.from_number(153))
print(f"After generating dai: {mcd.vat.urn(ilk, our_address)}")
assert mcd.vat.urn(ilk, our_address).ink == Wad.from_number(3)
assert mcd.vat.urn(ilk, our_address).art == Wad.from_number(153)
assert mcd.vat.dai(our_address) == initial_dai + Rad.from_number(153)
# Add collateral and generate some more Dai
frob(mcd, collateral, our_address, dink=Wad.from_number(6), dart=Wad.from_number(180))
print(f"After adding collateral and dai: {mcd.vat.urn(ilk, our_address)}")
assert mcd.vat.urn(ilk, our_address).ink == Wad.from_number(9)
assert mcd.vat.gem(ilk, our_address) == Wad(0)
assert mcd.vat.urn(ilk, our_address).art == Wad.from_number(333)
assert mcd.vat.dai(our_address) == initial_dai + Rad.from_number(333)
# Mint and withdraw our Dai
dai_balance_before = mcd.dai.balance_of(our_address)
mcd.approve_dai(our_address)
assert isinstance(mcd.dai_adapter, DaiJoin)
assert mcd.dai_adapter.exit(our_address, Wad.from_number(333)).transact()
assert mcd.dai.balance_of(our_address) == dai_balance_before + Wad.from_number(333)
assert mcd.vat.dai(our_address) == initial_dai
assert mcd.vat.debt() >= initial_dai + Rad.from_number(333)
# Repay (and burn) our Dai
assert mcd.dai_adapter.join(our_address, Wad.from_number(333)).transact()
assert mcd.dai.balance_of(our_address) == Wad(0)
assert mcd.vat.dai(our_address) == initial_dai + Rad.from_number(333)
wipe = mcd.vat.get_wipe_all_dart(collateral.ilk, our_address)
assert wipe >= Wad.from_number(333)
frob(mcd, collateral, our_address, dink=Wad(0), dart=wipe*-1)
# Withdraw our collateral
frob(mcd, collateral, our_address, dink=Wad.from_number(-9), dart=Wad(0))
assert mcd.vat.gem(ilk, our_address) == Wad.from_number(9)
assert collateral.adapter.exit(our_address, Wad.from_number(9)).transact()
collateral_balance_after = collateral.gem.balance_of(our_address)
assert collateral_balance_before == collateral_balance_after
# Cleanup
cleanup_urn(mcd, collateral, our_address)
@pytest.mark.skip("awaiting change to dss-deploy-scripts allowing faucets to be enabled on local testnet")
def test_faucet(self, mcd, our_address):
token = mcd.collaterals['GUSD-A'].gem
balance_before = token.balance_of(our_address)
assert mcd.faucet.gulp(token.address).transact(from_address=our_address)
balance_after = token.balance_of(our_address)
assert balance_before < balance_after
def test_empty_auctions_collection(self, mcd):
for auction_type, collection in mcd.active_auctions().items():
assert collection is not None
if auction_type in ['flaps', 'flops']:
assert len(collection) == 0
elif auction_type in ['clips', 'flips']:
assert len(collection) > 0
for collateral, collateral_auctions in collection.items():
assert isinstance(collateral, str)
assert collateral_auctions is not None
assert len(collateral_auctions) == 0
================================================
FILE: tests/test_etherdelta.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from mock import Mock
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly
from pymaker.etherdelta import EtherDelta, EtherDeltaApi
from pymaker.numeric import Wad
from pymaker.token import DSToken
from tests.helpers import is_hashable, wait_until_mock_called
PAST_BLOCKS = 100
class TestEtherDelta:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.etherdelta = EtherDelta.deploy(self.web3,
admin=Address('0x1111100000999998888877777666665555544444'),
fee_account=Address('0x8888877777666665555544444111110000099999'),
account_levels_addr=Address('0x0000000000000000000000000000000000000000'),
fee_make=Wad.from_number(0.01),
fee_take=Wad.from_number(0.02),
fee_rebate=Wad.from_number(0.03))
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1.mint(Wad.from_number(100)).transact()
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2.mint(Wad.from_number(100)).transact()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
EtherDelta(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_addresses(self):
# expect
assert self.etherdelta.admin() == Address('0x1111100000999998888877777666665555544444')
assert self.etherdelta.fee_account() == Address('0x8888877777666665555544444111110000099999')
assert self.etherdelta.account_levels_addr() == Address('0x0000000000000000000000000000000000000000')
def test_fees(self):
# expect
assert self.etherdelta.fee_make() == Wad.from_number(0.01)
assert self.etherdelta.fee_take() == Wad.from_number(0.02)
assert self.etherdelta.fee_rebate() == Wad.from_number(0.03)
def test_deposit_and_withdraw_eth(self):
# when
self.etherdelta.deposit(Wad.from_number(2.5)).transact()
# then
assert self.etherdelta.balance_of(self.our_address) == Wad.from_number(2.5)
# when
self.etherdelta.withdraw(Wad.from_number(1.1)).transact()
# then
assert self.etherdelta.balance_of(self.our_address) == Wad.from_number(1.4)
def test_deposit_and_withdraw_token(self):
# given
self.etherdelta.approve([self.token1], directly())
# when
self.etherdelta.deposit_token(self.token1.address, Wad.from_number(1.5)).transact()
# then
assert self.etherdelta.balance_of_token(self.token1.address, self.our_address) == Wad.from_number(1.5)
# when
self.etherdelta.withdraw_token(self.token1.address, Wad.from_number(0.2)).transact()
# then
assert self.etherdelta.balance_of_token(self.token1.address, self.our_address) == Wad.from_number(1.3)
def test_offchain_order_happy_path(self):
# given
self.etherdelta.approve([self.token1, self.token2], directly())
self.etherdelta.deposit_token(self.token1.address, Wad.from_number(10)).transact()
self.etherdelta.deposit_token(self.token2.address, Wad.from_number(10)).transact()
# when
order = self.etherdelta.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(2),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expires=100000000)
# then
assert order.maker == self.our_address
assert order.pay_token == self.token1.address
assert order.pay_amount == Wad.from_number(2)
assert order.buy_token == self.token2.address
assert order.buy_amount == Wad.from_number(4)
assert order.expires == 100000000
# and
assert self.etherdelta.amount_available(order) == Wad.from_number(4)
assert self.etherdelta.amount_filled(order) == Wad.from_number(0)
assert order.remaining_sell_amount == Wad.from_number(2)
assert order.remaining_buy_amount == Wad.from_number(4)
# and
assert self.etherdelta.can_trade(order, Wad.from_number(1.5))
assert not self.etherdelta.can_trade(order, Wad.from_number(5.5))
# when
self.etherdelta.trade(order, Wad.from_number(1.5)).transact()
# then
assert self.etherdelta.amount_available(order) == Wad.from_number(2.5)
assert self.etherdelta.amount_filled(order) == Wad.from_number(1.5)
assert order.remaining_sell_amount == Wad.from_number(1.25)
assert order.remaining_buy_amount == Wad.from_number(2.5)
# when
self.etherdelta.withdraw_token(self.token1.address, Wad.from_number(9.3)).transact()
# then
assert self.etherdelta.amount_available(order) == Wad.from_number(1.4)
assert self.etherdelta.amount_filled(order) == Wad.from_number(1.5)
# when
self.etherdelta.cancel_order(order).transact()
# then
assert self.etherdelta.amount_available(order) == Wad.from_number(0)
assert self.etherdelta.amount_filled(order) == Wad.from_number(4)
def test_no_past_events_on_startup(self):
assert self.etherdelta.past_trade(PAST_BLOCKS) == []
def test_past_take(self):
# given
self.etherdelta.approve([self.token1, self.token2], directly())
self.etherdelta.deposit_token(self.token1.address, Wad.from_number(10)).transact()
self.etherdelta.deposit_token(self.token2.address, Wad.from_number(10)).transact()
# when
order = self.etherdelta.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(2),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expires=100000000)
# and
self.etherdelta.trade(order, Wad.from_number(1.5)).transact()
# then
past_trade = self.etherdelta.past_trade(PAST_BLOCKS)
assert len(past_trade) == 1
assert past_trade[0].maker == self.our_address
assert past_trade[0].taker == self.our_address
assert past_trade[0].pay_token == self.token1.address
assert past_trade[0].buy_token == self.token2.address
assert past_trade[0].take_amount == Wad.from_number(0.75)
assert past_trade[0].give_amount == Wad.from_number(1.5)
assert past_trade[0].raw['blockNumber'] > 0
def test_order_comparison(self):
# given
order1 = self.etherdelta.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(2),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expires=100000000)
# and
order2 = self.etherdelta.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(2),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expires=100000000)
# then
assert order1 == order1
assert order1 != order2 # even if both orders seem to be identical, they will have different
# nonces generated so they are not the same order
def test_order_hashable(self):
# given
order1 = self.etherdelta.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(2),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expires=100000000)
# expect
assert is_hashable(order1)
def test_should_have_printable_representation(self):
assert repr(self.etherdelta) == f"EtherDelta('{self.etherdelta.address}')"
class TestEtherDeltaApi:
def setup_method(self):
self.etherdelta_api = EtherDeltaApi(client_tool_directory='some-dir',
client_tool_command='some command',
api_server='https://127.0.0.1:66666',
number_of_attempts=1,
retry_interval=15,
timeout=90)
def test_should_have_printable_representation(self):
assert repr(self.etherdelta_api) == f"EtherDeltaApi()"
================================================
FILE: tests/test_feed.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.feed import DSValue
class TestDSValue:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.dsvalue = DSValue.deploy(self.web3)
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
DSValue(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_address(self):
assert isinstance(self.dsvalue.address, Address)
def test_no_value_after_deploy(self):
# expect
assert self.dsvalue.has_value() is False
with pytest.raises(Exception):
self.dsvalue.read()
with pytest.raises(Exception):
self.dsvalue.read_as_int()
with pytest.raises(Exception):
self.dsvalue.read_as_hex()
def test_poke(self):
# when
self.dsvalue.poke(bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf4])).transact()
# then
assert self.dsvalue.has_value() is True
assert self.dsvalue.read_as_int() == 500
assert self.dsvalue.read() == bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf4])
def test_poke_with_int(self):
# when
self.dsvalue.poke_with_int(500).transact()
# then
assert self.dsvalue.has_value() is True
assert self.dsvalue.read_as_int() == 500
assert self.dsvalue.read() == bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf4])
def test_void(self):
# given
self.dsvalue.poke_with_int(250).transact()
assert self.dsvalue.has_value() is True
# when
self.dsvalue.void().transact()
# then
assert self.dsvalue.has_value() is False
def test_should_have_printable_representation(self):
assert repr(self.dsvalue) == f"DSValue('{self.dsvalue.address}')"
================================================
FILE: tests/test_gas.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from typing import Optional
from web3 import Web3
from pymaker.gas import DefaultGasPrice, FixedGasPrice, GasPrice, GeometricGasPrice, IncreasingGasPrice, NodeAwareGasPrice
from tests.conftest import web3
class TestGasPrice:
def test_not_implemented(self):
with pytest.raises(Exception):
GasPrice().get_gas_price(0)
def test_gwei(self):
assert GasPrice.GWEI == 1000000000
class TestDefaultGasPrice:
def test_should_always_be_default(self):
# given
default_gas_price = DefaultGasPrice()
# expect
assert default_gas_price.get_gas_price(0) is None
assert default_gas_price.get_gas_price(1) is None
assert default_gas_price.get_gas_price(1000000) is None
class TestNodeAwareGasPrice:
class DumbSampleImplementation(NodeAwareGasPrice):
def get_gas_price(self, time_elapsed: int) -> Optional[int]:
return self.get_node_gas_price() * max(time_elapsed, 1)
class BadImplementation(NodeAwareGasPrice):
pass
def test_retrieve_node_gas_price(self, web3):
strategy = TestNodeAwareGasPrice.DumbSampleImplementation(web3)
assert strategy.get_gas_price(0) > 0
assert strategy.get_gas_price(60) < strategy.get_gas_price(120)
def test_not_implemented(self, web3):
with pytest.raises(NotImplementedError):
NodeAwareGasPrice(web3)
bad = TestNodeAwareGasPrice.BadImplementation(web3)
with pytest.raises(NotImplementedError):
bad.get_gas_price(0)
class TestFixedGasPrice:
def test_gas_price_should_stay_the_same(self):
# given
value = 9000000000
fixed_gas_price = FixedGasPrice(value)
# expect
assert fixed_gas_price.get_gas_price(0) == value
assert fixed_gas_price.get_gas_price(1) == value
assert fixed_gas_price.get_gas_price(2) == value
assert fixed_gas_price.get_gas_price(5) == value
assert fixed_gas_price.get_gas_price(60) == value
assert fixed_gas_price.get_gas_price(120) == value
assert fixed_gas_price.get_gas_price(600) == value
assert fixed_gas_price.get_gas_price(1000000) == value
def test_gas_price_should_be_updated_by_update_gas_price_method(self):
# given
value1 = 9000000000
value2 = 16000000000
# and
fixed_gas_price = FixedGasPrice(value1)
# and
assert fixed_gas_price.get_gas_price(0) == value1
assert fixed_gas_price.get_gas_price(1) == value1
assert fixed_gas_price.get_gas_price(2) == value1
assert fixed_gas_price.get_gas_price(5) == value1
# when
fixed_gas_price.update_gas_price(value2)
# then
assert fixed_gas_price.get_gas_price(60) == value2
assert fixed_gas_price.get_gas_price(120) == value2
assert fixed_gas_price.get_gas_price(600) == value2
class TestIncreasingGasPrice:
def test_gas_price_should_increase_with_time(self):
# given
increasing_gas_price = IncreasingGasPrice(1000, 100, 60, None)
# expect
assert increasing_gas_price.get_gas_price(0) == 1000
assert increasing_gas_price.get_gas_price(1) == 1000
assert increasing_gas_price.get_gas_price(59) == 1000
assert increasing_gas_price.get_gas_price(60) == 1100
assert increasing_gas_price.get_gas_price(119) == 1100
assert increasing_gas_price.get_gas_price(120) == 1200
assert increasing_gas_price.get_gas_price(1200) == 3000
def test_gas_price_should_obey_max_value(self):
# given
increasing_gas_price = IncreasingGasPrice(1000, 100, 60, 2500)
# expect
assert increasing_gas_price.get_gas_price(0) == 1000
assert increasing_gas_price.get_gas_price(1) == 1000
assert increasing_gas_price.get_gas_price(59) == 1000
assert increasing_gas_price.get_gas_price(60) == 1100
assert increasing_gas_price.get_gas_price(119) == 1100
assert increasing_gas_price.get_gas_price(120) == 1200
assert increasing_gas_price.get_gas_price(1200) == 2500
assert increasing_gas_price.get_gas_price(3000) == 2500
assert increasing_gas_price.get_gas_price(1000000) == 2500
def test_should_require_positive_initial_price(self):
with pytest.raises(Exception):
IncreasingGasPrice(0, 1000, 60, None)
with pytest.raises(Exception):
IncreasingGasPrice(-1, 1000, 60, None)
def test_should_require_positive_increase_by_value(self):
with pytest.raises(Exception):
IncreasingGasPrice(1000, 0, 60, None)
with pytest.raises(Exception):
IncreasingGasPrice(1000, -1, 60, None)
def test_should_require_positive_every_secs_value(self):
with pytest.raises(Exception):
IncreasingGasPrice(1000, 100, 0, None)
with pytest.raises(Exception):
IncreasingGasPrice(1000, 100, -1, None)
def test_should_require_positive_max_price_if_provided(self):
with pytest.raises(Exception):
IncreasingGasPrice(1000, 1000, 60, 0)
with pytest.raises(Exception):
IncreasingGasPrice(1000, 1000, 60, -1)
class TestGeometricGasPrice:
def test_gas_price_should_increase_with_time(self):
# given
geometric_gas_price = GeometricGasPrice(100, 10)
# expect
assert geometric_gas_price.get_gas_price(0) == 100
assert geometric_gas_price.get_gas_price(1) == 100
assert geometric_gas_price.get_gas_price(10) == 113
assert geometric_gas_price.get_gas_price(15) == 113
assert geometric_gas_price.get_gas_price(20) == 127
assert geometric_gas_price.get_gas_price(30) == 143
assert geometric_gas_price.get_gas_price(50) == 181
assert geometric_gas_price.get_gas_price(100) == 325
def test_gas_price_should_obey_max_value(self):
# given
geometric_gas_price = GeometricGasPrice(1000, 60, 1.125, 2500)
# expect
assert geometric_gas_price.get_gas_price(0) == 1000
assert geometric_gas_price.get_gas_price(1) == 1000
assert geometric_gas_price.get_gas_price(59) == 1000
assert geometric_gas_price.get_gas_price(60) == 1125
assert geometric_gas_price.get_gas_price(119) == 1125
assert geometric_gas_price.get_gas_price(120) == 1266
assert geometric_gas_price.get_gas_price(1200) == 2500
assert geometric_gas_price.get_gas_price(3000) == 2500
assert geometric_gas_price.get_gas_price(1000000) == 2500
def test_behaves_with_realistic_values(self):
# given
GWEI = 1000000000
geometric_gas_price = GeometricGasPrice(100*GWEI, 10, 1+(0.125*2))
for seconds in [0,1,10,12,30,60]:
print(f"gas price after {seconds} seconds is {geometric_gas_price.get_gas_price(seconds)/GWEI}")
assert round(geometric_gas_price.get_gas_price(0) / GWEI, 1) == 100.0
assert round(geometric_gas_price.get_gas_price(1) / GWEI, 1) == 100.0
assert round(geometric_gas_price.get_gas_price(10) / GWEI, 1) == 125.0
assert round(geometric_gas_price.get_gas_price(12) / GWEI, 1) == 125.0
assert round(geometric_gas_price.get_gas_price(30) / GWEI, 1) == 195.3
assert round(geometric_gas_price.get_gas_price(60) / GWEI, 1) == 381.5
def test_should_require_positive_initial_price(self):
with pytest.raises(AssertionError):
GeometricGasPrice(0, 60)
with pytest.raises(AssertionError):
GeometricGasPrice(-1, 60)
def test_should_require_positive_every_secs_value(self):
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 0)
with pytest.raises(AssertionError):
GeometricGasPrice(1000, -1)
def test_should_require_positive_coefficient(self):
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 60, 0)
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 60, 1)
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 60, -1)
def test_should_require_positive_max_price_if_provided(self):
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 60, 1.125, 0)
with pytest.raises(AssertionError):
GeometricGasPrice(1000, 60, 1.125, -1)
def test_max_price_should_exceed_initial_price(self):
with pytest.raises(AssertionError):
GeometricGasPrice(6000, 30, 2.25, 5000)
================================================
FILE: tests/test_general.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from hexbytes import HexBytes
from web3 import HTTPProvider, Web3
from web3._utils.request import _get_session
from pymaker import Address, Calldata, Contract, Receipt, Transfer, web3_via_http
from pymaker.numeric import Wad
from pymaker.util import eth_balance
from tests.helpers import is_hashable
test_abi = Contract._load_abi(__name__, 'abi/GemMock.abi')
class TestConnect:
def test_connect_to_testchain(self, our_address):
uri = "http://0.0.0.0:8545"
web3 = web3_via_http(uri, 63, 39)
assert isinstance(web3.provider, HTTPProvider)
assert web3.provider._request_kwargs['timeout'] == 63
for adapter in _get_session(uri).adapters.values():
assert adapter._pool_connections == 39
assert adapter._pool_maxsize == 39
assert isinstance(web3, Web3)
assert eth_balance(web3, our_address) > Wad(0)
def test_unsupported_url(self):
with pytest.raises(ValueError):
web3_via_http("wss://0.0.0.0:8545")
class TestAddress:
def test_creation_from_various_representations(self):
# expect
assert Address('0x0000000000111111111100000000001111111111').address == \
'0x0000000000111111111100000000001111111111'
assert Address('0000000000111111111100000000001111111111').address == \
'0x0000000000111111111100000000001111111111'
def test_creation_from_another_address(self):
# given
some_address = Address('0x0000000000111111111100000000001111111111')
# expect
assert Address(some_address).address == some_address.address
def test_should_fail_creation_from_invalid_representation(self):
# expect
with pytest.raises(Exception):
Address('0x000000000011111111110000000000111111111') # too short
# expect
with pytest.raises(Exception):
Address('0x00000000001111111111000000000011111111111') # too long
def test_as_bytes(self):
# expect
assert Address('0x0000011111000001111100000111110000011111').as_bytes() == \
b'\0\0\x01\x11\x11\0\0\x01\x11\x11\0\0\x01\x11\x11\0\0\x01\x11\x11'
def test_string_value(self):
# expect
assert str(Address('0x0000011111000001111100000111110000011111')) == \
'0x0000011111000001111100000111110000011111'
def test_repr(self):
# expect
assert repr(Address('0x0000011111000001111100000111110000011111')) == \
"Address('0x0000011111000001111100000111110000011111')"
def test_should_be_hashable(self):
assert is_hashable(Address('0x0000011111000001111100000111110000011111'))
def test_equality(self):
# given
address1a = Address('0x0000011111000001111100000111110000011111')
address1b = Address('0x0000011111000001111100000111110000011111')
address2 = Address('0x0000011111000001111100000111110000022222')
# expect
assert address1a == address1b
assert address1a != address2
assert address1b != address2
def test_ordering(self):
# given
address1 = Address('0x0000011111000001111100000111110000011111')
address2 = Address('0x0000011111000001111100000111110000022222')
address3 = Address('0x0000011111000001111100000111110000033333')
# expect
assert address1 < address2
assert not address1 > address2
assert address2 > address1
assert not address2 < address1
assert address1 <= address2
assert address2 >= address1
assert address1 < address3
assert address1 <= address3
class TestCalldata:
def test_creation(self):
# expect
assert Calldata('0xa9059cbb').value == '0xa9059cbb'
def test_creation_from_bytes(self):
# expect
assert Calldata(b'\xa9\x05\x9c\xbb').value == '0xa9059cbb'
def test_should_fail_creation_from_invalid_calldata(self):
# expect
with pytest.raises(Exception):
Calldata('a9059cbb') # without `0x`
def test_as_bytes(self):
# expect
assert Calldata('0xa9059cbb').as_bytes() == b'\xa9\x05\x9c\xbb'
def test_string_value(self):
# expect
assert str(Calldata('0xa9059cbb')) == '0xa9059cbb'
def test_repr(self):
# expect
assert repr(Calldata('0xa9059cbb')) == "Calldata('0xa9059cbb')"
def test_should_be_hashable(self):
assert is_hashable(Calldata('0xa9059cbb'))
def test_equality(self):
# given
calldata1a = Calldata('0xa9059cbb')
calldata1b = Calldata('0xa9059cbb')
calldata2 = Calldata('0xa9059ccc')
# expect
assert calldata1a == calldata1b
assert calldata1a != calldata2
assert calldata1b != calldata2
def test_from_signature(self, web3):
# given
calldata1a = Calldata('0xa9059cbb' # function 4byte signature
'00000000000000000000000011223344556600000000000000000000000000ff'
'000000000000000000000000000000000000000000000000000000000000007b')
calldata1b = Calldata.from_signature(web3,
'transfer(address,uint256)',
['0x11223344556600000000000000000000000000ff', 123])
# expect
assert calldata1a == calldata1b
# given
calldata2a = Calldata('0x2b4e4e96' # function 4byte signature
'00000000000000000000000011223344556600000000000000000000000000ff'
'0000000000000000000000000000000000000000000000000000000000000040'
'0000000000000000000000000000000000000000000000000000000000000002'
'000000000000000000000000000000000000000000000000000000000000007b'
'00000000000000000000000000000000000000000000000000000000000001c8')
calldata2b = Calldata.from_signature(web3,
'transfer(address,uint256[])',
['0x11223344556600000000000000000000000000ff', [123, 456]])
# expect
assert calldata2a == calldata2b
def test_from_contract_abi(self, web3):
# given
calldata1a = Calldata('0xa9059cbb' # function 4byte signature
'00000000000000000000000011223344556600000000000000000000000000ff'
'000000000000000000000000000000000000000000000000000000000000007b')
calldata1b = Calldata.from_contract_abi(web3,
'transfer(address,uint256)',
['0x11223344556600000000000000000000000000ff', 123],
test_abi)
# expect
assert calldata1a == calldata1b
class TestReceipt:
@pytest.fixture()
def receipt_success(self) -> dict:
return {'blockHash': '0xef523d31d16592a53826962962bd126d1c66203780a2db59839eee3d3ff7d0b7',
'blockNumber': 3890533,
'contractAddress': None,
'cumulativeGasUsed': 57192,
'gasUsed': 57192,
'logs': [{'address': '0x53eccc9246c1e537d79199d0c7231e425a40f896',
'blockHash': '0xef523d31d16592a53826962962bd126d1c66203780a2db59839eee3d3ff7d0b7',
'blockNumber': 3890533,
'data': '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000',
'logIndex': 0,
'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'),
HexBytes('0x000000000000000000000000375d52588c3f39ee7710290237a95c691d8432e7'),
HexBytes('0x0000000000000000000000000046f01ad360270605e0e5d693484ec3bfe43ba8')],
'transactionHash': '0x8b6851e40d017b2004a54eae3e9e47614398b54bbbaae150eaa889ec36470ec8',
'transactionIndex': 0,
'transactionLogIndex': '0x0',
'type': 'mined'},
{'address': '0x375d52588c3f39ee7710290237a95c691d8432e7',
'blockHash': '0xef523d31d16592a53826962962bd126d1c66203780a2db59839eee3d3ff7d0b7',
'blockNumber': 3890533,
'data': '0x00000000000000000000000000000000000000000000000000000000000000a2',
'logIndex': 1,
'topics': [HexBytes('0xa2c251311b1a7a475913900a2a73dc9789a21b04bc737e050bbc506dd4eb3488')],
'transactionHash': '0x8b6851e40d017b2004a54eae3e9e47614398b54bbbaae150eaa889ec36470ec8',
'transactionIndex': 0,
'transactionLogIndex': '0x1',
'type': 'mined'},
{'address': '0x375d52588c3f39ee7710290237a95c691d8432e7',
'blockHash': '0xef523d31d16592a53826962962bd126d1c66203780a2db59839eee3d3ff7d0b7',
'blockNumber': 3890533,
'data': '0x00000000000000000000000053eccc9246c1e537d79199d0c7231e425a40f896000000000000000000000000228bf3d5be3ee4b80718b89b68069b023c32131e0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000f6d7ac92d746b00000000000000000000000000000000000000000000000000000000000059c17c9c',
'logIndex': 2,
'topics': [HexBytes('0x9577941d28fff863bfbee4694a6a4a56fb09e169619189d2eaa750b5b4819995'),
HexBytes('0x00000000000000000000000000000000000000000000000000000000000000a2'),
HexBytes('0x7188d03e276d4dead4b0c037a93892d986e043a3af3305d7488a731ccaff4b76'),
HexBytes('0x0000000000000000000000000046f01ad360270605e0e5d693484ec3bfe43ba8')],
'transactionHash': '0x8b6851e40d017b2004a54eae3e9e47614398b54bbbaae150eaa889ec36470ec8',
'transactionIndex': 0,
'transactionLogIndex': '0x2',
'type': 'mined'}],
'logsBloom': '0x00000000000000000000000000000000000000000000002002000000000080000000000000800010000000000000000000000000000000000000000000000000000000000000000000000008000020000000000000000000000000040040000000000000000000100000000000000000000000000000000000000030000000000400000000000000000000000400000000000000000000000000000000000040001040000000000000000000000001000400000000000000000000002000000000000002000000100800000000080000080000000100000000000000000000002000000000000000000000000000000000000000000000000000000000000000',
'root': None,
'transactionHash': '0x8b6851e40d017b2004a54eae3e9e47614398b54bbbaae150eaa889ec36470ec8',
'transactionIndex': 0}
@pytest.fixture()
def receipt_failed(self) -> dict:
return {'blockHash': '0x827e0e913f4388318d5c08eff06e200ed1be1cb8b31aa30f932dcf5595c8d81d',
'blockNumber': 3890936,
'contractAddress': None,
'cumulativeGasUsed': 3171658,
'gasUsed': 3100000,
'logs': [],
'logsBloom': '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
'root': None,
'transactionHash': '0x570369e4f70df947e3c4bc08ed9b06c181190a423ee5fcf17db203574e2d5d77',
'transactionIndex': 1}
def test_parsing_receipt(self, receipt_success):
# given
receipt = Receipt(receipt_success)
# expect
assert receipt.transaction_hash == '0x8b6851e40d017b2004a54eae3e9e47614398b54bbbaae150eaa889ec36470ec8'
assert receipt.gas_used == 57192
assert len(receipt.transfers) == 1
assert len(receipt.logs) == 3
assert receipt.transfers[0] == Transfer(token_address=Address('0x53eccc9246c1e537d79199d0c7231e425a40f896'),
from_address=Address('0x375d52588c3f39ee7710290237a95c691d8432e7'),
to_address=Address('0x0046f01ad360270605e0e5d693484ec3bfe43ba8'),
value=Wad.from_number(1))
def test_should_recognize_successful_and_failed_transactions(self, receipt_success, receipt_failed):
# expect
assert Receipt(receipt_success).successful is True
assert Receipt(receipt_failed).successful is False
class TestTransfer:
def test_equality(self):
# given
transfer1a = Transfer(token_address=Address('0x0000011111222223333344444555556666677777'),
from_address=Address('0x0000000000111111111100000000001111111111'),
to_address=Address('0x1111111111000000000011111111110000000000'),
value=Wad.from_number(20))
transfer1b = Transfer(token_address=Address('0x0000011111222223333344444555556666677777'),
from_address=Address('0x0000000000111111111100000000001111111111'),
to_address=Address('0x1111111111000000000011111111110000000000'),
value=Wad.from_number(20))
transfer2 = Transfer(token_address=Address('0x0000011111222223333344444555556666677777'),
from_address=Address('0x0000000000111111111100000000001111111111'),
to_address=Address('0x1111111111000000000011111111112222222222'),
value=Wad.from_number(20))
# expect
assert transfer1a == transfer1b
assert transfer1b == transfer1a
assert transfer1a != transfer2
assert transfer1b != transfer2
assert transfer2 != transfer1a
assert transfer2 != transfer1b
================================================
FILE: tests/test_general2.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import pytest
from mock import MagicMock
from web3 import Web3, HTTPProvider
from pymaker import Address, eth_transfer, get_pending_transactions, RecoveredTransact, TransactStatus, Calldata, Receipt
from pymaker.gas import FixedGasPrice
from pymaker.numeric import Wad
from pymaker.proxy import DSProxy, DSProxyCache
from pymaker.token import DSToken
from pymaker.util import synchronize, eth_balance
class TestTransact:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.second_address = Address(self.web3.eth.accounts[1])
self.third_address = Address(self.web3.eth.accounts[2])
self.token = DSToken.deploy(self.web3, 'ABC')
self.token.mint(Wad(1000000)).transact()
def test_can_only_execute_once(self):
# given
transact = self.token.transfer(self.second_address, Wad(500))
# and
transact.transact()
# expect
with pytest.raises(Exception):
transact.transact()
def test_can_only_execute_once_even_if_tx_failed(self):
# given
transact = self.token.transfer(self.second_address, Wad(2000000)) # more than we minted
# and
try:
transact.transact()
# CAUTION: Note ganache 6.5+ causes a ValueError while older versions fail without exception
except ValueError:
pass
# expect
with pytest.raises(Exception):
transact.transact()
def test_should_update_status_when_finished(self):
# given
transact = self.token.transfer(self.second_address, Wad(500))
assert transact.status == TransactStatus.NEW
# when
transact.transact()
# then
assert transact.status == TransactStatus.FINISHED
def test_should_update_status_to_finished_even_if_tx_failed(self):
# given
transact = self.token.transfer(self.second_address, Wad(2000000)) # more than we minted
assert transact.status == TransactStatus.NEW
# when
try:
transact.transact()
except ValueError:
pass
# then
assert transact.status == TransactStatus.FINISHED
def test_default_gas(self):
# when
receipt = self.token.transfer(self.second_address, Wad(500)).transact()
# then
assert 100000 <= self.web3.eth.getTransaction(receipt.transaction_hash)['gas'] <= 1200000
def test_default_gas_async(self):
# when
receipt = synchronize([self.token.transfer(self.second_address, Wad(500)).transact_async()])[0]
# then
assert 100000 <= self.web3.eth.getTransaction(receipt.transaction_hash)['gas'] <= 1200000
def test_custom_gas(self):
# when
receipt = self.token.transfer(self.second_address, Wad(500)).transact(gas=129995)
# then
assert self.web3.eth.getTransaction(receipt.transaction_hash)['gas'] == 129995
def test_custom_gas_async(self):
# when
receipt = synchronize([self.token.transfer(self.second_address, Wad(500)).transact_async(gas=129995)])[0]
# then
assert self.web3.eth.getTransaction(receipt.transaction_hash)['gas'] == 129995
def test_custom_gas_buffer(self):
# when
receipt = self.token.transfer(self.second_address, Wad(500)).transact(gas_buffer=2500000)
# then
assert self.web3.eth.getTransaction(receipt.transaction_hash)['gas'] > 2500000
def test_gas_and_gas_buffer_not_allowed_at_the_same_time(self):
# expect
with pytest.raises(Exception):
self.token.transfer(self.second_address, Wad(500)).transact(gas=129995, gas_buffer=3000000)
def test_gas_and_gas_buffer_not_allowed_at_the_same_time_async(self):
# expect
with pytest.raises(Exception):
synchronize([self.token.transfer(self.second_address, Wad(500)).transact_async(gas=129995,
gas_buffer=3000000)])
def test_custom_gas_price(self):
# given
gas_price = FixedGasPrice(25000000100)
# when
self.token.transfer(self.second_address, Wad(500)).transact(gas_price=gas_price)
# then
assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == gas_price.gas_price
def test_custom_gas_price_async(self):
# given
gas_price = FixedGasPrice(25000000200)
# when
synchronize([self.token.transfer(self.second_address, Wad(500)).transact_async(gas_price=gas_price)])
# then
assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == gas_price.gas_price
def test_custom_from_address(self):
# given
self.token.transfer(self.second_address, Wad(self.token.balance_of(self.our_address))).transact()
# when
receipt = self.token.transfer(self.our_address, Wad(250)).transact(from_address=self.second_address)
# then
assert Address(self.web3.eth.getTransaction(receipt.transaction_hash)['from']) == self.second_address
def test_name_formatting(self):
# given
transact = self.token.transfer(self.second_address, Wad(123))
# expect
assert transact.name() == f"DSToken('{self.token.address}').transfer('{self.second_address}', 123)"
def test_name_formatting_with_hexstrings(self):
# given
proxy_cache = DSProxyCache.deploy(self.web3)
proxy = DSProxy.deploy(self.web3, proxy_cache.address)
# when
transact = proxy.execute("0x11223344", Calldata("0x55667788"))
# then
assert transact.name() == f"DSProxy('{proxy.address}').execute(bytes,bytes)('0x11223344', '0x55667788')"
def test_eth_transfer(self):
# given
initial_balance = eth_balance(self.web3, self.second_address)
# when
eth_transfer(self.web3, self.second_address, Wad.from_number(1.5)).transact()
# then
assert eth_balance(self.web3, self.second_address) == initial_balance + Wad.from_number(1.5)
def test_eth_transfer_from_other_account(self):
# given
initial_balance_second_address = eth_balance(self.web3, self.second_address)
initial_balance_third_address = eth_balance(self.web3, self.third_address)
# when
eth_transfer(self.web3, self.third_address, Wad.from_number(1.5)).transact(from_address=self.second_address)
# then
assert eth_balance(self.web3, self.second_address) < initial_balance_second_address
assert eth_balance(self.web3, self.third_address) == initial_balance_third_address + Wad.from_number(1.5)
def test_should_raise_exception_on_unknown_kwarg(self):
# expect
with pytest.raises(Exception):
self.token.transfer(self.second_address, Wad(123)).transact(unknown_kwarg="some_value")
# expect
with pytest.raises(Exception):
synchronize([self.token.transfer(self.second_address, Wad(123)).transact_async(unknown_kwarg="some_value")])
class TestTransactReplace:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.second_address = Address(self.web3.eth.accounts[1])
self.third_address = Address(self.web3.eth.accounts[2])
self.token = DSToken.deploy(self.web3, 'ABC')
self.token.mint(Wad(1000000)).transact()
@pytest.mark.asyncio
async def test_transaction_replace(self):
# given
original_send_transaction = self.web3.eth.sendTransaction
original_get_transaction = self.web3.eth.getTransaction
nonce = self.web3.eth.getTransactionCount(self.our_address.address)
# when
self.web3.eth.sendTransaction = MagicMock(return_value='0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd')
self.web3.eth.getTransaction = MagicMock(return_value={'nonce': nonce})
# and
transact_1 = self.token.transfer(self.second_address, Wad(500))
future_receipt_1 = asyncio.ensure_future(transact_1.transact_async(gas_price=FixedGasPrice(100000)))
# and
await asyncio.sleep(2)
# then
assert future_receipt_1.done() is False
assert self.token.balance_of(self.second_address) == Wad(0)
# when
self.web3.eth.sendTransaction = original_send_transaction
self.web3.eth.getTransaction = original_get_transaction
# and
transact_2 = self.token.transfer(self.third_address, Wad(700))
future_receipt_2 = asyncio.ensure_future(transact_2.transact_async(replace=transact_1,
gas_price=FixedGasPrice(150000)))
# and
await asyncio.sleep(10)
# then
assert transact_1.status == TransactStatus.FINISHED
assert future_receipt_1.done()
assert future_receipt_1.result() is None
# and
assert transact_2.status == TransactStatus.FINISHED
assert future_receipt_2.done()
assert isinstance(future_receipt_2.result(), Receipt)
assert future_receipt_2.result().successful is True
# and
assert self.token.balance_of(self.second_address) == Wad(0)
assert self.token.balance_of(self.third_address) == Wad(700)
@pytest.mark.timeout(10)
def test_transaction_replace_of_failed_transaction(self):
# given
original_send_transaction = self.web3.eth.sendTransaction
# when
transact_1 = self.token.transfer(self.second_address, Wad(2000000)) # more than we minted
receipt_1 = None
try:
receipt_1 = transact_1.transact()
except ValueError:
pass
# then
assert transact_1.status == TransactStatus.FINISHED
assert receipt_1 is None
# when
def second_send_transaction(transaction):
# TestRPC doesn't support `sendTransaction` calls with the `nonce` parameter
# (unlike proper Ethereum nodes which handle it very well)
transaction_without_nonce = {key: transaction[key] for key in transaction if key != 'nonce'}
return original_send_transaction(transaction_without_nonce)
self.web3.eth.sendTransaction = MagicMock(side_effect=second_send_transaction)
# when
transact_2 = self.token.transfer(self.second_address, Wad(500))
receipt_2 = transact_2.transact(replace=transact_1)
# then
assert transact_2.status == TransactStatus.FINISHED
assert receipt_2 is not None
assert receipt_2.successful
# and
assert self.token.balance_of(self.second_address) == Wad(500)
class TestTransactRecover:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.token = DSToken.deploy(self.web3, 'ABC')
assert self.token.mint(Wad(100)).transact()
def test_nothing_pending(self):
# given no pending transactions created by prior tests
# then
assert get_pending_transactions(self.web3) == []
@pytest.mark.skip("Ganache and Parity testchains don't seem to simulate pending transactions in the mempool")
@pytest.mark.asyncio
async def test_recover_pending_tx(self, other_address):
# given
low_gas = FixedGasPrice(1)
await self.token.transfer(other_address, Wad(5)).transact_async(gas_price=low_gas)
await asyncio.sleep(0.5)
# when
pending = get_pending_transactions(self.web3)
# and
assert len(pending) == 1
recovered: RecoveredTransact = pending[0]
high_gas = FixedGasPrice(int(1 * FixedGasPrice.GWEI))
recovered.cancel(high_gas)
# then
assert get_pending_transactions(self.web3) == []
================================================
FILE: tests/test_governance.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.auth import DSAuth
from pymaker.governance import DSPause, DSChief
from pymaker.numeric import Wad
from pymaker.deployment import DssDeployment
from datetime import datetime, timedelta
from tests.test_dss import mint_mkr
def mint_approve_lock(mcd: DssDeployment, amount: Wad, address: Address):
prevBalance = mcd.mkr.balance_of(address)
mint_mkr(mcd.mkr, address, amount)
assert mcd.mkr.balance_of(address) == amount + prevBalance
# Lock MKR in DS-Chief
assert mcd.mkr.approve(mcd.ds_chief.address).transact(from_address=address)
assert mcd.ds_chief.lock(amount).transact(from_address=address)
assert mcd.mkr.balance_of(address) == prevBalance
def approve_iou_free_mkr(mcd: DssDeployment, amount: Wad, address: Address):
prevBalance = mcd.mkr.balance_of(address)
iou = mcd.ds_chief.iou()
assert iou.approve(mcd.ds_chief.address).transact(from_address=address)
assert mcd.ds_chief.free(amount).transact(from_address=address)
assert mcd.mkr.balance_of(address) == amount + prevBalance
# Relevant to DS-Chief 1.2
def launch_chief(mcd: DssDeployment, address: Address):
launchAmount = Wad.from_number(80000)
mint_approve_lock(mcd, launchAmount, address)
# Vote on address(0) to activate DSChief.launch()
zero_address = Address("0x0000000000000000000000000000000000000000")
assert mcd.ds_chief.vote_yays([zero_address.address]).transact(from_address=address)
# Launch Ds-Chief (1.2)
assert mcd.ds_chief.launch().transact(from_address=address)
approve_iou_free_mkr(mcd, launchAmount, address)
@pytest.mark.skip(reason="not fully implemented")
class TestDSPause:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
ds_auth = DSAuth.deploy(self.web3)
self.ds_pause = DSPause.deploy(self.web3, 5, self.our_address, ds_auth)
self.plan = DSPause.Plan(usr=self.our_address,
fax=self.web3.toBytes(text='abi.encodeWithSignature("sig()")'),
eta=(datetime.utcnow() + timedelta(seconds=10)))
def test_drop(self):
# assert self.ds_pause.plot(self.plan).transact()
assert self.ds_pause.drop(self.plan).transact()
def test_exec(self):
# assert self.ds_pause.plot(self.plan).transact()
assert self.ds_pause.exec(self.plan).transact()
class TestDSChief:
def test_launch(self, mcd: DssDeployment, our_address: Address):
assert mcd.ds_chief.live() == False
launch_chief(mcd, our_address)
assert mcd.ds_chief.live() == True
def test_scenario(self, mcd: DssDeployment, our_address: Address, other_address: Address):
isinstance(mcd, DssDeployment)
isinstance(our_address, Address)
amount = Wad.from_number(1000)
mint_approve_lock(mcd, amount, our_address)
# Vote for our address
assert mcd.ds_chief.vote_yays([our_address.address]).transact(from_address=our_address)
assert mcd.ds_chief.etch([other_address.address]).transact(from_address=our_address)
# Confirm that etch(our address) != etch(other address)
etches = mcd.ds_chief.past_etch(3)
assert etches[0].slate != etches[-1].slate
assert mcd.ds_chief.get_approvals(our_address.address) == amount
# Lift hat for our address
assert mcd.ds_chief.get_hat() != our_address
assert mcd.ds_chief.lift(our_address).transact(from_address=our_address)
assert mcd.ds_chief.get_hat() == our_address
# Now vote for other address
assert mcd.ds_chief.vote_etch(etches[-1]).transact(from_address=our_address)
assert mcd.ds_chief.lift(other_address).transact(from_address=our_address)
assert mcd.ds_chief.get_hat() == other_address
approve_iou_free_mkr(mcd, amount, our_address)
================================================
FILE: tests/test_keys.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pkg_resources
from web3 import Web3, HTTPProvider
from pymaker import Address, Wad, eth_transfer
from pymaker.keys import register_key_file, register_key
from pymaker.token import DSToken
def test_local_accounts():
# given
# [that address is not recognized by ganache, this way we can be sure it's the local account being used for signing]
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = Address('0x13314e21cd6d343ceb857073f3f6d9368919d1ef').address
# and
keyfile_path = pkg_resources.resource_filename(__name__, "accounts/4_0x13314e21cd6d343ceb857073f3f6d9368919d1ef.json")
passfile_path = pkg_resources.resource_filename(__name__, "accounts/pass")
register_key_file(web3, keyfile_path, passfile_path)
# and
# [as ganache does not know this address, we need to send some ETH to it first]
eth_transfer(web3, Address(web3.eth.defaultAccount), Wad.from_number(100)) \
.transact(from_address=Address(web3.eth.accounts[0]))
# when
# [we deploy some test contract and mint some tokens]
token = DSToken.deploy(web3, 'XYZ')
token.mint(Wad.from_number(150000)).transact()
# then
# [these operations were successful]
assert token.balance_of(Address(web3.eth.defaultAccount)) == Wad.from_number(150000)
def test_local_accounts_register_key():
# given
# [that address is not recognized by ganache, this way we can be sure it's the local account being used for signing]
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = Address('0x13314e21cd6d343ceb857073f3f6d9368919d1ef').address
# and
keyfile_path = pkg_resources.resource_filename(__name__, "accounts/4_0x13314e21cd6d343ceb857073f3f6d9368919d1ef.json")
passfile_path = pkg_resources.resource_filename(__name__, "accounts/pass")
register_key(web3, f"key_file={keyfile_path},pass_file={passfile_path}")
# and
# [as ganache does not know this address, we need to send some ETH to it first]
eth_transfer(web3, Address(web3.eth.defaultAccount), Wad.from_number(100)) \
.transact(from_address=Address(web3.eth.accounts[0]))
# when
# [we deploy some test contract and mint some tokens]
token = DSToken.deploy(web3, 'XYZ')
token.mint(Wad.from_number(150000)).transact()
# then
# [these operations were successful]
assert token.balance_of(Address(web3.eth.defaultAccount)) == Wad.from_number(150000)
def test_multiple_local_accounts():
# given
local_account_1 = Address('0x13314e21cd6d343ceb857073f3f6d9368919d1ef')
local_account_2 = Address('0x176087fea5c41fc370fabbd850521bc4451690ca')
# and
# [that address is not recognized by ganache, this way we can be sure it's the local account being used for signing]
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = local_account_1.address
# and
keyfile_path = pkg_resources.resource_filename(__name__, "accounts/4_0x13314e21cd6d343ceb857073f3f6d9368919d1ef.json")
passfile_path = pkg_resources.resource_filename(__name__, "accounts/pass")
register_key_file(web3, keyfile_path, passfile_path)
# and
keyfile_path = pkg_resources.resource_filename(__name__, "accounts/5_0x176087fea5c41fc370fabbd850521bc4451690ca.json")
passfile_path = pkg_resources.resource_filename(__name__, "accounts/pass")
register_key_file(web3, keyfile_path, passfile_path)
# and
# [as ganache does not know these addresses, we need to send some ETH to it first]
eth_transfer(web3, local_account_1, Wad.from_number(100)).transact(from_address=Address(web3.eth.accounts[0]))
eth_transfer(web3, local_account_2, Wad.from_number(100)).transact(from_address=Address(web3.eth.accounts[0]))
# when
# [we execute some test scenario involving two addresses]
token = DSToken.deploy(web3, 'XYZ')
token.mint(Wad.from_number(150000)).transact()
token.transfer(local_account_2, Wad.from_number(60000)).transact()
token.transfer(local_account_1, Wad.from_number(10000)).transact(from_address=local_account_2)
# then
# [these operations were successful]
assert token.balance_of(local_account_1) == Wad.from_number(100000)
assert token.balance_of(local_account_2) == Wad.from_number(50000)
================================================
FILE: tests/test_lifecycle.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import time
from threading import Event
from unittest.mock import Mock
import pytest
from mock import MagicMock
from web3 import Web3, HTTPProvider
import pymaker
from pymaker import Address
from pymaker.lifecycle import Lifecycle, trigger_event
@pytest.mark.timeout(60)
class TestLifecycle:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
# `test_etherdelta.py` executes before this test file and creates some event filters,
# so we need to clear the list of filter threads as otherwise `Web3Lifecycle` will
# be waiting forever for them to terminate and the test harness will never finish
pymaker.filter_threads = []
def use_web3(self, with_web3: bool):
return self.web3 if with_web3 else None
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_always_exit(self, with_web3):
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)):
pass
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_start_instantly_if_no_initial_delay(self, with_web3):
# given
start_time = int(time.time())
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
pass
# then
end_time = int(time.time())
assert end_time - start_time <= 2
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_obey_initial_delay(self, with_web3):
# given
start_time = int(time.time())
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.initial_delay(5)
# then
end_time = int(time.time())
assert end_time - start_time >= 4
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_check_initial_checks(self, with_web3):
# given
check_1 = Mock(return_value=True)
check_2 = Mock(return_value=True)
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.wait_for(check_1, 5)
lifecycle.wait_for(check_2, 5)
# then
assert check_1.call_count == 1
assert check_2.call_count == 1
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_time_out_initial_checks_even_if_they_constantly_return_false(self, with_web3):
# given
start_time = int(time.time())
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.wait_for(lambda: False, 5)
# then
end_time = int(time.time())
assert end_time - start_time >= 4
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_call_startup_callback(self, with_web3):
# given
startup_mock = MagicMock()
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_startup(startup_mock)
# then
startup_mock.assert_called()
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_fail_to_register_two_startup_callbacks(self, with_web3):
# expect
with pytest.raises(BaseException):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_startup(lambda: 1)
lifecycle.on_startup(lambda: 2)
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_call_shutdown_callback(self, with_web3):
# given
ordering = []
startup_mock = MagicMock(side_effect=lambda: ordering.append('STARTUP'))
shutdown_mock = MagicMock(side_effect=lambda: ordering.append('SHUTDOWN'))
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_startup(startup_mock)
lifecycle.on_shutdown(shutdown_mock)
# then
assert ordering == ['STARTUP', 'SHUTDOWN']
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_fail_to_register_two_shutdown_callbacks(self, with_web3):
# expect
with pytest.raises(BaseException):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_shutdown(lambda: 1)
lifecycle.on_shutdown(lambda: 2)
def test_should_fail_to_register_two_block_callbacks(self):
# expect
with pytest.raises(BaseException):
with Lifecycle(self.web3) as lifecycle:
lifecycle.on_block(lambda: 1)
lifecycle.on_block(lambda: 2)
def test_should_fail_to_register_block_callback_if_no_web3(self):
# expect
with pytest.raises(BaseException):
with Lifecycle() as lifecycle:
lifecycle.on_block(lambda: 1)
@pytest.mark.parametrize('with_web3', [False, True])
def test_every(self, with_web3):
self.counter = 0
def callback():
self.counter = self.counter + 1
if self.counter >= 2:
lifecycle.terminate("Unit test is over")
# given
mock = MagicMock(side_effect=callback)
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.every(1, mock)
# then
assert mock.call_count >= 2
assert lifecycle.terminated_internally
@pytest.mark.parametrize('with_web3', [False, True])
def test_on_event_fires_whenever_event_triggered(self, with_web3):
event = Event()
self.counter = 0
def every_callback():
self.counter = self.counter + 1
trigger_event(event)
if self.counter >= 2:
time.sleep(1)
lifecycle.terminate("Unit test is over")
# given
mock = Mock()
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.every(1, every_callback)
lifecycle.on_event(event, 9999, mock)
# then
assert mock.call_count >= 2
assert lifecycle.terminated_internally
@pytest.mark.parametrize('with_web3', [False, True])
def test_on_event_fires_every_min_frequency_if_event_not_triggered(self, with_web3):
self.counter = 0
def callback():
self.counter = self.counter + 1
if self.counter >= 2:
lifecycle.terminate("Unit test is over")
# given
mock = MagicMock(side_effect=callback)
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_event(Event(), 1, mock)
# then
assert mock.call_count >= 2
assert lifecycle.terminated_internally
@pytest.mark.parametrize('with_web3', [False, True])
def test_every_does_not_start_operating_until_startup_callback_is_finished(self, with_web3):
# given
self.every_triggered = False
def startup_callback():
time.sleep(3)
assert not self.every_triggered
def every_callback():
self.every_triggered = True
lifecycle.terminate("Unit test is over")
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_startup(startup_callback)
lifecycle.every(1, every_callback)
# then
assert self.every_triggered
@pytest.mark.parametrize('with_web3', [False, True])
def test_event_does_not_start_operating_until_startup_callback_is_finished(self, with_web3):
# given
self.event_triggered = False
def startup_callback():
time.sleep(3)
assert not self.event_triggered
def event_callback():
self.event_triggered = True
lifecycle.terminate("Unit test is over")
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_startup(startup_callback)
lifecycle.on_event(Event(), 1, event_callback)
# then
assert self.event_triggered
@pytest.mark.parametrize('with_web3', [False, True])
def test_every_should_not_fire_when_keeper_is_already_terminating(self, with_web3):
# given
self.every_counter = 0
def shutdown_callback():
time.sleep(5)
def every_callback():
self.every_counter = self.every_counter + 1
lifecycle.terminate("Unit test is over")
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.every(1, every_callback)
lifecycle.on_shutdown(shutdown_callback)
# then
assert self.every_counter <= 2
@pytest.mark.parametrize('with_web3', [False, True])
def test_events_should_not_fire_when_keeper_is_already_terminating(self, with_web3):
# given
self.event_counter = 0
def shutdown_callback():
time.sleep(5)
def event_callback():
self.event_counter = self.event_counter + 1
lifecycle.terminate("Unit test is over")
# when
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_event(Event(), 1, event_callback)
lifecycle.on_shutdown(shutdown_callback)
# then
assert self.event_counter <= 2
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_not_call_shutdown_until_every_timer_has_finished(self, with_web3):
# given
self.every1_finished = False
self.every2_finished = False
def shutdown_callback():
assert self.every1_finished
assert self.every2_finished
def every_callback_1():
time.sleep(1)
lifecycle.terminate("Unit test is over")
time.sleep(4)
self.every1_finished = True
def every_callback_2():
time.sleep(2)
self.every2_finished = True
# expect
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.every(1, every_callback_1)
lifecycle.every(1, every_callback_2)
lifecycle.on_shutdown(shutdown_callback) # assertions are in `shutdown_callback`
@pytest.mark.parametrize('with_web3', [False, True])
def test_should_not_call_shutdown_until_every_event_has_finished(self, with_web3):
# given
self.event1_finished = False
self.event2_finished = False
def shutdown_callback():
assert self.event1_finished
assert self.event2_finished
def event_callback_1():
time.sleep(1)
lifecycle.terminate("Unit test is over")
time.sleep(4)
self.event1_finished = True
def event_callback_2():
time.sleep(2)
self.event2_finished = True
# expect
with pytest.raises(SystemExit):
with Lifecycle(self.use_web3(with_web3)) as lifecycle:
lifecycle.on_event(Event(), 1, event_callback_1)
lifecycle.on_event(Event(), 1, event_callback_2)
lifecycle.on_shutdown(shutdown_callback) # assertions are in `shutdown_callback`
================================================
FILE: tests/test_model.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from pymaker import Address, Wad
from pymaker.model import Token
class TestToken:
def setup_class(self):
self.token = Token("COW", Address('0xbeef00000000000000000000000000000000BEEF'), 4)
def test_convert(self):
# two
chain_amount = Wad(20000)
assert self.token.normalize_amount(chain_amount) == Wad.from_number(2)
# three
normalized_amount = Wad.from_number(3)
assert self.token.unnormalize_amount(normalized_amount) == Wad(30000)
def test_min_amount(self):
assert self.token.min_amount == Wad.from_number(0.0001)
assert float(self.token.min_amount) == 0.0001
assert self.token.unnormalize_amount(self.token.min_amount) == Wad(1)
assert Wad.from_number(0.0004) > self.token.min_amount
assert Wad.from_number(0.00005) < self.token.min_amount
assert self.token.unnormalize_amount(Wad.from_number(0.0006)) > self.token.unnormalize_amount(self.token.min_amount)
assert self.token.unnormalize_amount(Wad.from_number(0.00007)) < self.token.unnormalize_amount(self.token.min_amount)
assert self.token.unnormalize_amount(Wad.from_number(0.00008)) == Wad(0)
================================================
FILE: tests/test_numeric.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import math
import pytest
from pymaker.numeric import Wad, Ray, Rad
from tests.helpers import is_hashable
class TestWad:
def test_should_support_negative_values(self):
Wad(-1)
def test_should_support_values_greater_than_uint256(self):
Wad(2**256)
Wad(2**256 + 1)
Wad(2**512)
def test_should_instantiate_from_a_wad(self):
assert Wad(Wad(1)) == Wad(1)
def test_should_instantiate_from_a_ray(self):
assert Wad(Ray(10000000000000001010101010101)) == Wad(10000000000000001010)
assert Wad(Ray(10000000000000001019999999999)) == Wad(10000000000000001019)
def test_should_instantiate_from_an_int(self):
assert Wad(10).value == 10
def test_should_fail_to_instantiate_from_a_float(self):
with pytest.raises(ArithmeticError):
assert Wad(10.5)
def test_should_format_to_string_nicely(self):
assert str(Wad(1)) == "0.000000000000000001"
assert str(Wad(500000000000000000)) == "0.500000000000000000"
assert str(Wad(1500000000000000000)) == "1.500000000000000000"
assert str(Wad(-1500000000000000000)) == "-1.500000000000000000"
assert str(Wad(-500000000000000000)) == "-0.500000000000000000"
assert str(Wad(-1)) == "-0.000000000000000001"
def test_should_have_nice_printable_representation(self):
for wad in [Wad(1), Wad(100), Wad.from_number(2.5), Wad(-1)]:
assert repr(wad) == f"Wad({wad.value})"
def test_add(self):
assert Wad(1) + Wad(2) == Wad(3)
def test_add_should_not_work_with_rays(self):
with pytest.raises(ArithmeticError):
Wad(1) + Ray(2)
def test_add_should_not_work_with_ints(self):
with pytest.raises(ArithmeticError):
Wad(1) + 2
def test_subtract(self):
assert Wad(10) - Wad(2) == Wad(8)
assert Wad(1) - Wad(2) == Wad(-1)
def test_subtract_should_not_work_with_rays(self):
with pytest.raises(ArithmeticError):
Wad(10) - Ray(2)
def test_modulo(self):
assert Wad(10) % Wad(2) == Wad(0)
assert Wad(11) % Wad(5) == Wad(1)
assert Wad(11) % Wad(3) == Wad(2)
def test_modulo_should_not_work_with_rays(self):
with pytest.raises(ArithmeticError):
Wad(10) % Ray(3)
def test_multiply(self):
assert Wad.from_number(2) * Wad.from_number(3) == Wad.from_number(6)
assert Wad.from_number(2) * Wad(3) == Wad(6)
assert Wad.from_number(2.5) * Wad(3) == Wad(7)
assert Wad.from_number(2.99999) * Wad(3) == Wad(8)
def test_multiply_by_ray(self):
assert Wad.from_number(2) * Ray.from_number(3) == Wad.from_number(6)
assert Wad.from_number(2) * Ray(3) == Wad(0)
assert Wad(2) * Ray(499999999999999999999999999) == Wad(0)
assert Wad(2) * Ray(500000000000000000000000000) == Wad(1)
assert Wad(2) * Ray(999999999999999999999999999) == Wad(1)
assert Wad(2) * Ray(1000000000000000000000000000) == Wad(2)
def test_multiply_by_int(self):
assert Wad.from_number(2) * 3 == Wad.from_number(6)
assert Wad.from_number(2) * 1 == Wad.from_number(2)
def test_should_fail_to_multiply_by_float(self):
with pytest.raises(ArithmeticError):
Wad(2) * 3.0
def test_divide(self):
assert Wad.from_number(4) / Wad.from_number(2) == Wad.from_number(2)
assert Wad(4) / Wad.from_number(2) == Wad(2)
assert Wad(3) / Wad.from_number(2) == Wad(1)
assert Wad(39) / Wad.from_number(20) == Wad(1)
assert Wad(40) / Wad.from_number(20) == Wad(2)
assert Wad.from_number(0.2) / Wad.from_number(0.1) == Wad.from_number(2)
def test_should_fail_to_divide_by_rays(self):
with pytest.raises(ArithmeticError):
Wad(4) / Ray(2)
def test_should_fail_to_divide_by_ints(self):
with pytest.raises(ArithmeticError):
Wad(4) / 2
def test_should_support_abs(self):
assert abs(Wad(1000)) == Wad(1000)
assert abs(Wad(0)) == Wad(0)
assert abs(Wad(-1000)) == Wad(1000)
def test_should_compare_wads_with_each_other(self):
assert Wad(1000) == Wad(1000)
assert Wad(1000) != Wad(999)
assert Wad(1000) > Wad(999)
assert Wad(999) < Wad(1000)
assert Wad(999) <= Wad(1000)
assert Wad(1000) <= Wad(1000)
assert Wad(1000) >= Wad(1000)
assert Wad(1000) >= Wad(999)
def test_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
assert Wad(1000) == Ray(1000)
with pytest.raises(ArithmeticError):
assert Wad(1000) != Ray(999)
with pytest.raises(ArithmeticError):
assert Wad(1000) > Ray(999)
with pytest.raises(ArithmeticError):
assert Wad(999) < Ray(1000)
with pytest.raises(ArithmeticError):
assert Wad(999) <= Ray(1000)
with pytest.raises(ArithmeticError):
assert Wad(1000) <= Ray(1000)
with pytest.raises(ArithmeticError):
assert Wad(1000) >= Ray(1000)
with pytest.raises(ArithmeticError):
assert Wad(1000) >= Ray(999)
def test_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
assert Wad(1000) == 100
with pytest.raises(ArithmeticError):
assert Wad(1000) != 999
with pytest.raises(ArithmeticError):
assert Wad(1000) > 999
with pytest.raises(ArithmeticError):
assert Wad(999) < 1000
with pytest.raises(ArithmeticError):
assert Wad(999) <= 1000
with pytest.raises(ArithmeticError):
assert Wad(1000) <= 1000
with pytest.raises(ArithmeticError):
assert Wad(1000) >= 1000
with pytest.raises(ArithmeticError):
assert Wad(1000) >= 999
def test_should_cast_to_int(self):
assert int(Wad.from_number(-4.5)) == -4
assert int(Wad.from_number(0.99)) == 0
assert int(Wad.from_number(1)) == 1
assert int(Wad.from_number(1.0)) == 1
assert int(Wad.from_number(1.5)) == 1
assert int(Wad.from_number(1.9999999999)) == 1
def test_should_cast_to_float(self):
assert float(Wad.from_number(-4.5)) == -4.5
assert float(Wad.from_number(0.99)) == 0.99
assert float(Wad.from_number(1)) == 1.0
assert float(Wad.from_number(1.0)) == 1.0
assert float(Wad.from_number(1.5)) == 1.5
assert float(Wad.from_number(1.9999999999)) == 1.9999999999
def test_should_be_hashable(self):
assert is_hashable(Wad(123))
def test_min_value(self):
assert Wad.min(Wad(10), Wad(20)) == Wad(10)
assert Wad.min(Wad(25), Wad(15)) == Wad(15)
assert Wad.min(Wad(25), Wad(15), Wad(5)) == Wad(5)
def test_min_value_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
Wad.min(Wad(10), Ray(20))
with pytest.raises(ArithmeticError):
Wad.min(Ray(25), Wad(15))
def test_min_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Wad.min(Wad(10), 20)
with pytest.raises(ArithmeticError):
Wad.min(20, Wad(10))
def test_max_value(self):
assert Wad.max(Wad(10), Wad(20)) == Wad(20)
assert Wad.max(Wad(25), Wad(15)) == Wad(25)
assert Wad.max(Wad(25), Wad(15), Wad(40)) == Wad(40)
def test_max_value_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
Wad.max(Wad(10), Ray(20))
with pytest.raises(ArithmeticError):
Wad.max(Wad(25), Ray(15))
def test_max_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Wad.max(Wad(10), 20)
with pytest.raises(ArithmeticError):
Wad.max(15, Wad(25))
def test_round(self):
assert round(Wad.from_number(123.4567), 2) == Wad.from_number(123.46)
assert round(Wad.from_number(123.4567), 0) == Wad.from_number(123.0)
assert round(Wad.from_number(123.4567), -2) == Wad.from_number(100.0)
def test_round_inequality(self):
# should hold for all x, ndigits
x = Wad.from_number(7654.321)
ndigits = 1
round_difference = x - round(x, ndigits)
round_distance = Wad(abs(round_difference.value))
assert round_distance <= Wad.from_number(0.5 * 10**(-ndigits))
def test_square_root(self):
test_std_sqrt = Wad.from_number(math.sqrt(16.5))
test_pymaker_sqrt = Wad.__sqrt__(Wad.from_number(16.5))
assert test_std_sqrt == test_pymaker_sqrt
class TestRay:
def test_should_support_negative_values(self):
Ray(-1)
def test_should_support_values_greater_than_uint256(self):
Ray(2**256)
Ray(2**256 + 1)
Ray(2**512)
def test_should_instantiate_from_a_ray(self):
assert Ray(Ray(1)) == Ray(1)
def test_should_instantiate_from_a_wad(self):
assert Ray(Wad(10000000000000000000)) == Ray(10000000000000000000000000000)
def test_should_instantiate_from_an_int(self):
assert Ray(10).value == 10
def test_should_fail_to_instantiate_from_a_float(self):
with pytest.raises(ArithmeticError):
assert Ray(10.5)
def test_should_format_to_string_nicely(self):
assert str(Ray(1)) == "0.000000000000000000000000001"
assert str(Ray(500000000000000000000000000)) == "0.500000000000000000000000000"
assert str(Ray(1500000000000000000000000000)) == "1.500000000000000000000000000"
assert str(Ray(-1500000000000000000000000000)) == "-1.500000000000000000000000000"
assert str(Ray(-500000000000000000000000000)) == "-0.500000000000000000000000000"
assert str(Ray(-1)) == "-0.000000000000000000000000001"
def test_should_have_nice_printable_representation(self):
for ray in [Ray(1), Ray(100), Ray.from_number(2.5), Ray(-1)]:
assert repr(ray) == f"Ray({ray.value})"
def test_add(self):
assert Ray(1) + Ray(2) == Ray(3)
def test_add_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Ray(1) + Wad(2)
def test_add_should_not_work_with_ints(self):
with pytest.raises(ArithmeticError):
Ray(1) + 2
def test_subtract(self):
assert Ray(10) - Ray(2) == Ray(8)
assert Ray(1) - Ray(2) == Ray(-1)
def test_subtract_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Ray(10) - Wad(2)
def test_modulo(self):
assert Ray(10) % Ray(2) == Ray(0)
assert Ray(11) % Ray(5) == Ray(1)
assert Ray(11) % Ray(3) == Ray(2)
def test_modulo_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Ray(10) % Wad(3)
def test_multiply(self):
assert Ray.from_number(2) * Ray.from_number(3) == Ray.from_number(6)
assert Ray.from_number(2) * Ray(3) == Ray(6)
assert Ray.from_number(2.5) * Ray(3) == Ray(7)
assert Ray.from_number(2.99999) * Ray(3) == Ray(8)
def test_multiply_by_wad(self):
assert Ray.from_number(2) * Wad.from_number(3) == Ray.from_number(6)
assert Ray.from_number(2) * Wad(3) == Ray(6000000000)
assert Ray(2) * Wad(3) == Ray(0)
assert Ray(2) * Wad(999999999999999999) == Ray(1)
assert Ray(2) * Wad(1000000000000000000) == Ray(2)
def test_multiply_by_int(self):
assert Ray.from_number(2) * 3 == Ray.from_number(6)
assert Ray.from_number(2) * 1 == Ray.from_number(2)
def test_should_fail_to_multiply_by_float(self):
with pytest.raises(ArithmeticError):
Ray(2) * 3.0
def test_divide(self):
assert Ray.from_number(4) / Ray.from_number(2) == Ray.from_number(2)
assert Ray(4) / Ray.from_number(2) == Ray(2)
assert Ray(3) / Ray.from_number(2) == Ray(1)
assert Ray(39) / Ray.from_number(20) == Ray(1)
assert Ray(40) / Ray.from_number(20) == Ray(2)
assert Ray.from_number(0.2) / Ray.from_number(0.1) == Ray.from_number(2)
def test_should_fail_to_divide_by_wads(self):
with pytest.raises(ArithmeticError):
Ray(4) / Wad(2)
def test_should_fail_to_divide_by_ints(self):
with pytest.raises(ArithmeticError):
Ray(4) / 2
def test_should_support_abs(self):
assert abs(Ray(1000)) == Ray(1000)
assert abs(Ray(0)) == Ray(0)
assert abs(Ray(-1000)) == Ray(1000)
def test_should_compare_rays_with_each_other(self):
assert Ray(1000) == Ray(1000)
assert Ray(1000) != Ray(999)
assert Ray(1000) > Ray(999)
assert Ray(999) < Ray(1000)
assert Ray(999) <= Ray(1000)
assert Ray(1000) <= Ray(1000)
assert Ray(1000) >= Ray(1000)
assert Ray(1000) >= Ray(999)
def test_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
assert Ray(1000) == Wad(1000)
with pytest.raises(ArithmeticError):
assert Ray(1000) != Wad(999)
with pytest.raises(ArithmeticError):
assert Ray(1000) > Wad(999)
with pytest.raises(ArithmeticError):
assert Ray(999) < Wad(1000)
with pytest.raises(ArithmeticError):
assert Ray(999) <= Wad(1000)
with pytest.raises(ArithmeticError):
assert Ray(1000) <= Wad(1000)
with pytest.raises(ArithmeticError):
assert Ray(1000) >= Wad(1000)
with pytest.raises(ArithmeticError):
assert Ray(1000) >= Wad(999)
def test_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
assert Ray(1000) == 100
with pytest.raises(ArithmeticError):
assert Ray(1000) != 999
with pytest.raises(ArithmeticError):
assert Ray(1000) > 999
with pytest.raises(ArithmeticError):
assert Ray(999) < 1000
with pytest.raises(ArithmeticError):
assert Ray(999) <= 1000
with pytest.raises(ArithmeticError):
assert Ray(1000) <= 1000
with pytest.raises(ArithmeticError):
assert Ray(1000) >= 1000
with pytest.raises(ArithmeticError):
assert Ray(1000) >= 999
def test_should_cast_to_int(self):
assert int(Ray.from_number(-4.5)) == -4
assert int(Ray.from_number(0.99)) == 0
assert int(Ray.from_number(1)) == 1
assert int(Ray.from_number(1.0)) == 1
assert int(Ray.from_number(1.5)) == 1
assert int(Ray.from_number(1.9999999999)) == 1
def test_should_cast_to_float(self):
assert float(Ray.from_number(-4.5)) == -4.5
assert float(Ray.from_number(0.99)) == 0.99
assert float(Ray.from_number(1)) == 1.0
assert float(Ray.from_number(1.0)) == 1.0
assert float(Ray.from_number(1.5)) == 1.5
assert float(Ray.from_number(1.9999999999)) == 1.9999999999
def test_should_be_hashable(self):
assert is_hashable(Ray(123))
def test_min_value(self):
assert Ray.min(Ray(10), Ray(20)) == Ray(10)
assert Ray.min(Ray(25), Ray(15)) == Ray(15)
assert Ray.min(Ray(25), Ray(15), Ray(5)) == Ray(5)
def test_min_value_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
Ray.min(Ray(10), Wad(20))
with pytest.raises(ArithmeticError):
Ray.min(Wad(25), Ray(15))
def test_min_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Ray.min(Ray(10), 20)
with pytest.raises(ArithmeticError):
Ray.min(20, Ray(10))
def test_max_value(self):
assert Ray.max(Ray(10), Ray(20)) == Ray(20)
assert Ray.max(Ray(25), Ray(15)) == Ray(25)
assert Ray.max(Ray(25), Ray(15), Ray(40)) == Ray(40)
def test_max_value_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
Ray.max(Ray(10), Wad(20))
with pytest.raises(ArithmeticError):
Ray.max(Ray(25), Wad(15))
def test_max_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Ray.max(Ray(10), 20)
with pytest.raises(ArithmeticError):
Ray.max(15, Ray(25))
def test_round(self):
assert round(Ray.from_number(123.4567), 2) == Ray.from_number(123.46)
assert round(Ray.from_number(123.4567), 0) == Ray.from_number(123.0)
assert round(Ray.from_number(123.4567), -2) == Ray.from_number(100.0)
def test_square_root(self):
test_std_sqrt = Ray.from_number(math.sqrt(16.5))
test_pymaker_sqrt = Ray.__sqrt__(Ray.from_number(16.5))
assert test_std_sqrt == test_pymaker_sqrt
class TestRad:
def test_should_support_negative_values(self):
Rad(-1)
def test_should_support_values_greater_than_uint256(self):
Rad(2**256)
Rad(2**256 + 1)
Rad(2**512)
def test_should_instantiate_from_a_rad(self):
assert Rad(Rad(1)) == Rad(1)
def test_should_instantiate_from_a_wad(self):
assert Rad(Wad(10000000000000000000)) == Rad.from_number(10)
def test_should_instantiate_from_a_ray(self):
assert Rad(Ray.from_number(10)) == Rad.from_number(10)
def test_should_instantiate_from_an_int(self):
assert Rad(10).value == 10
def test_should_fail_to_instantiate_from_a_float(self):
with pytest.raises(ArithmeticError):
assert Rad(10.5)
def test_should_format_to_string_nicely(self):
assert str(Rad(1)) == "0.000000000000000000000000000000000000000000001"
assert str(Rad(500000000000000000000000000000000000000000000)) == "0.500000000000000000000000000000000000000000000"
assert str(Rad(1500000000000000000000000000000000000000000000)) == "1.500000000000000000000000000000000000000000000"
assert str(Rad(-1500000000000000000000000000000000000000000000)) == "-1.500000000000000000000000000000000000000000000"
assert str(Rad(-500000000000000000000000000000000000000000000)) == "-0.500000000000000000000000000000000000000000000"
assert str(Rad(-1)) == "-0.000000000000000000000000000000000000000000001"
def test_should_have_nice_printable_representation(self):
for ray in [Rad(1), Rad(100), Rad.from_number(2.5), Rad(-1)]:
assert repr(ray) == f"Rad({ray.value})"
def test_add(self):
assert Rad(1) + Rad(2) == Rad(3)
def test_add_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Rad(1) + Wad(2)
def test_add_should_not_work_with_rays(self):
with pytest.raises(ArithmeticError):
Rad(1) + Ray(2)
def test_add_should_not_work_with_ints(self):
with pytest.raises(ArithmeticError):
Rad(1) + 2
def test_subtract(self):
assert Rad(10) - Rad(2) == Rad(8)
assert Rad(1) - Rad(2) == Rad(-1)
def test_subtract_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Rad(10) - Wad(2)
def test_subtract_should_not_work_with_rays(self):
with pytest.raises(ArithmeticError):
Rad(10) - Ray(2)
def test_modulo(self):
assert Rad(10) % Rad(2) == Rad(0)
assert Rad(11) % Rad(5) == Rad(1)
assert Rad(11) % Rad(3) == Rad(2)
def test_modulo_should_not_work_with_wads(self):
with pytest.raises(ArithmeticError):
Rad(10) % Wad(3)
def test_multiply(self):
assert Rad.from_number(2) * Rad.from_number(3) == Rad.from_number(6)
assert Rad.from_number(2) * Rad(3) == Rad(6)
assert Rad.from_number(2.5) * Rad(3) == Rad(7)
assert Rad.from_number(2.99999) * Rad(3) == Rad(8)
def test_multiply_by_wad(self):
assert Rad.from_number(2) * Wad.from_number(3) == Rad.from_number(6)
assert Rad.from_number(2) * Wad(3) == Rad(6000000000000000000000000000)
assert Rad(2) * Wad(3) == Rad(0)
assert Rad(2) * Wad(999999999999999999) == Rad(1)
assert Rad(2) * Wad(1000000000000000000) == Rad(2)
def test_multiply_by_ray(self):
assert Rad.from_number(2) * Ray.from_number(3) == Rad.from_number(6)
assert Rad.from_number(2) * Ray(3) == Rad(6000000000000000000)
assert Rad(2) * Ray(3) == Rad(0)
assert Rad(2) * Ray(999999999999999999999999999) == Rad(1)
assert Rad(2) * Ray(1000000000000000000000000000) == Rad(2)
def test_multiply_by_int(self):
assert Rad.from_number(2) * 3 == Rad.from_number(6)
assert Rad.from_number(2) * 1 == Rad.from_number(2)
def test_should_fail_to_multiply_by_float(self):
with pytest.raises(ArithmeticError):
Rad(2) * 3.0
def test_divide(self):
assert Rad.from_number(4) / Rad.from_number(2) == Rad.from_number(2)
assert Rad(4) / Rad.from_number(2) == Rad(2)
assert Rad(3) / Rad.from_number(2) == Rad(1)
assert Rad(39) / Rad.from_number(20) == Rad(1)
assert Rad(40) / Rad.from_number(20) == Rad(2)
assert Rad.from_number(0.2) / Rad.from_number(0.1) == Rad.from_number(2)
def test_should_fail_to_divide_by_wads(self):
with pytest.raises(ArithmeticError):
Rad(4) / Wad(2)
def test_should_fail_to_divide_by_rays(self):
with pytest.raises(ArithmeticError):
Rad(4) / Ray(2)
def test_should_fail_to_divide_by_ints(self):
with pytest.raises(ArithmeticError):
Rad(4) / 2
def test_should_support_abs(self):
assert abs(Rad(1000)) == Rad(1000)
assert abs(Rad(0)) == Rad(0)
assert abs(Rad(-1000)) == Rad(1000)
def test_should_compare_rays_with_each_other(self):
assert Rad(1000) == Rad(1000)
assert Rad(1000) != Rad(999)
assert Rad(1000) > Rad(999)
assert Rad(999) < Rad(1000)
assert Rad(999) <= Rad(1000)
assert Rad(1000) <= Rad(1000)
assert Rad(1000) >= Rad(1000)
assert Rad(1000) >= Rad(999)
def test_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
assert Rad(1000) == Wad(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) != Wad(999)
with pytest.raises(ArithmeticError):
assert Rad(1000) > Wad(999)
with pytest.raises(ArithmeticError):
assert Rad(999) < Wad(1000)
with pytest.raises(ArithmeticError):
assert Rad(999) <= Wad(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) <= Wad(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) >= Wad(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) >= Wad(999)
def test_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
assert Rad(1000) == Ray(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) != Ray(999)
with pytest.raises(ArithmeticError):
assert Rad(1000) > Ray(999)
with pytest.raises(ArithmeticError):
assert Rad(999) < Ray(1000)
with pytest.raises(ArithmeticError):
assert Rad(999) <= Ray(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) <= Ray(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) >= Ray(1000)
with pytest.raises(ArithmeticError):
assert Rad(1000) >= Ray(999)
def test_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
assert Rad(1000) == 100
with pytest.raises(ArithmeticError):
assert Rad(1000) != 999
with pytest.raises(ArithmeticError):
assert Rad(1000) > 999
with pytest.raises(ArithmeticError):
assert Rad(999) < 1000
with pytest.raises(ArithmeticError):
assert Rad(999) <= 1000
with pytest.raises(ArithmeticError):
assert Rad(1000) <= 1000
with pytest.raises(ArithmeticError):
assert Rad(1000) >= 1000
with pytest.raises(ArithmeticError):
assert Rad(1000) >= 999
def test_should_cast_to_int(self):
assert int(Rad.from_number(-4.5)) == -4
assert int(Rad.from_number(0.99)) == 0
assert int(Rad.from_number(1)) == 1
assert int(Rad.from_number(1.0)) == 1
assert int(Rad.from_number(1.5)) == 1
assert int(Rad.from_number(1.9999999999)) == 1
def test_should_cast_to_float(self):
assert float(Rad.from_number(-4.5)) == -4.5
assert float(Rad.from_number(0.99)) == 0.99
assert float(Rad.from_number(1)) == 1.0
assert float(Rad.from_number(1.0)) == 1.0
assert float(Rad.from_number(1.5)) == 1.5
assert float(Rad.from_number(1.9999999999)) == 1.9999999999
def test_should_be_hashable(self):
assert is_hashable(Rad(123))
def test_min_value(self):
assert Rad.min(Rad(10), Rad(20)) == Rad(10)
assert Rad.min(Rad(25), Rad(15)) == Rad(15)
assert Rad.min(Rad(25), Rad(15), Rad(5)) == Rad(5)
def test_min_value_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
Rad.min(Rad(10), Wad(20))
with pytest.raises(ArithmeticError):
Rad.min(Wad(25), Rad(15))
def test_min_value_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
Rad.min(Rad(10), Ray(20))
with pytest.raises(ArithmeticError):
Rad.min(Ray(25), Rad(15))
def test_min_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Rad.min(Rad(10), 20)
with pytest.raises(ArithmeticError):
Rad.min(20, Rad(10))
def test_max_value(self):
assert Rad.max(Rad(10), Rad(20)) == Rad(20)
assert Rad.max(Rad(25), Rad(15)) == Rad(25)
assert Rad.max(Rad(25), Rad(15), Rad(40)) == Rad(40)
def test_max_value_should_reject_comparison_with_wads(self):
with pytest.raises(ArithmeticError):
Rad.max(Rad(10), Wad(20))
with pytest.raises(ArithmeticError):
Rad.max(Rad(25), Wad(15))
def test_max_value_should_reject_comparison_with_rays(self):
with pytest.raises(ArithmeticError):
Rad.max(Rad(10), Ray(20))
with pytest.raises(ArithmeticError):
Rad.max(Rad(25), Ray(15))
def test_max_value_should_reject_comparison_with_ints(self):
with pytest.raises(ArithmeticError):
Rad.max(Rad(10), 20)
with pytest.raises(ArithmeticError):
Rad.max(15, Rad(25))
def test_round(self):
assert round(Rad.from_number(123.4567), 2) == Rad.from_number(123.46)
assert round(Rad.from_number(123.4567), 0) == Rad.from_number(123.0)
assert round(Rad.from_number(123.4567), -2) == Rad.from_number(100.0)
def test_square_root(self):
test_std_sqrt = Rad.from_number(math.sqrt(16.5))
test_pymaker_sqrt = Rad.__sqrt__(Rad.from_number(16.5))
assert test_std_sqrt == test_pymaker_sqrt
================================================
FILE: tests/test_oasis.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from typing import List
from unittest.mock import Mock
import logging
import pytest
import time
from web3 import HTTPProvider
from web3 import Web3
from pymaker import Address, Wad, Contract, Transact
from pymaker.approval import directly
from pymaker.oasis import SimpleMarket, MatchingMarket, Order
from pymaker.token import DSToken
from pymaker.model import Token
from tests.helpers import wait_until_mock_called, is_hashable, time_travel_by
PAST_BLOCKS = 100
class OasisMockPriceOracle(Contract):
"""A mock price Oracle for deploying Oasis MatchingMarket contract"""
abi = Contract._load_abi(__name__, 'abi/OasisMockPriceOracle.abi')
bin = Contract._load_bin(__name__, 'abi/OasisMockPriceOracle.bin')
def __init__(self, web3: Web3, address: Address):
assert(isinstance(web3, Web3))
assert(isinstance(address, Address))
self.web3 = web3
self.address = address
self._contract = self._get_contract(web3, self.abi, address)
@staticmethod
def deploy(web3: Web3):
return OasisMockPriceOracle(web3=web3, address=Contract._deploy(web3, OasisMockPriceOracle.abi,
OasisMockPriceOracle.bin, []))
def set_price(self, price: Wad):
assert isinstance(price, Wad)
return Transact(self, self.web3, self.abi, self.address, self._contract, 'setPrice', [price.value])
def __eq__(self, other):
return self.address == other.address
def __repr__(self):
return f"OasisMockPriceOracle('{self.address}')"
class GeneralMarketTest:
def setup_method(self):
# reduce logspew
logging.getLogger("web3").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.INFO)
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.price_oracle = OasisMockPriceOracle.deploy(self.web3)
self.price_oracle.set_price(Wad.from_number(10))
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1_tokenclass = Token('AAA', self.token1.address, 18)
self.token1.mint(Wad.from_number(10000)).transact()
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2_tokenclass = Token('BBB', self.token2.address, 18)
self.token2.mint(Wad.from_number(10000)).transact()
self.token3 = DSToken.deploy(self.web3, 'CCC')
self.token3_tokenclass = Token('CCC', self.token3.address, 18)
self.token3.mint(Wad.from_number(10000)).transact()
self.otc = None
def test_approve_and_make_and_getters(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
assert self.otc.get_last_order_id() == 0
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# then
assert self.otc.get_last_order_id() == 1
# and
assert self.otc.get_order(1).order_id == 1
assert self.otc.get_order(1).pay_token == self.token1.address
assert self.otc.get_order(1).pay_amount == Wad.from_number(1)
assert self.otc.get_order(1).buy_token == self.token2.address
assert self.otc.get_order(1).buy_amount == Wad.from_number(2)
assert self.otc.get_order(1).maker == self.our_address
assert self.otc.get_order(1).timestamp != 0
# and
assert self.otc.get_orders() == [self.otc.get_order(1)]
def test_make_returns_new_order_ids(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
# expect
for number in range(1, 10):
receipt = self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
assert receipt.result == number
assert self.otc.get_last_order_id() == number
def test_get_orders_by_pair(self):
if isinstance(self.otc, MatchingMarket):
token1_val = self.token1_tokenclass
token2_val = self.token2_tokenclass
token3_val = self.token3_tokenclass
else:
token1_val = self.token1.address
token2_val = self.token2.address
token3_val = self.token3.address
# given
self.otc.approve([self.token1, self.token2, self.token3], directly())
# when
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(2)).transact()
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(4)).transact()
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(3)).transact()
self.otc.make(token1_val, Wad.from_number(1),
token3_val, Wad.from_number(2)).transact()
self.otc.make(token1_val, Wad.from_number(1),
token3_val, Wad.from_number(4)).transact()
self.otc.make(token1_val, Wad.from_number(1),
token3_val, Wad.from_number(3)).transact()
self.otc.make(token2_val, Wad.from_number(1),
token3_val, Wad.from_number(2)).transact()
self.otc.make(token2_val, Wad.from_number(1),
token3_val, Wad.from_number(4)).transact()
self.otc.make(token2_val, Wad.from_number(1),
token3_val, Wad.from_number(3)).transact()
self.otc.make(token2_val, Wad.from_number(1),
token1_val, Wad.from_number(5)).transact()
self.otc.make(token2_val, Wad.from_number(1),
token1_val, Wad.from_number(6)).transact()
# then
def order_ids(orders: List[Order]) -> List[int]:
return list(map(lambda order: order.order_id, orders))
assert len(self.otc.get_orders()) == 11
assert order_ids(self.otc.get_orders(token1_val, token2_val)) == [1, 2, 3]
assert order_ids(self.otc.get_orders(token1_val, token3_val)) == [4, 5, 6]
assert order_ids(self.otc.get_orders(token2_val, token3_val)) == [7, 8, 9]
assert order_ids(self.otc.get_orders(token2_val, token1_val)) == [10, 11]
# when
self.otc.kill(8).transact()
# then
assert order_ids(self.otc.get_orders(token2_val, token3_val)) == [7, 9]
def test_get_orders_by_maker(self):
if isinstance(self.otc, MatchingMarket):
token1_val = self.token1_tokenclass
token2_val = self.token2_tokenclass
else:
token1_val = self.token1.address
token2_val = self.token2.address
# given
maker1 = self.our_address
maker2 = Address(self.web3.eth.accounts[1])
# and
self.token1.transfer(maker2, Wad.from_number(500)).transact()
# when
self.otc.approve([self.token1], directly())
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(2)).transact()
# and
self.web3.eth.defaultAccount = self.web3.eth.accounts[1]
self.otc.approve([self.token1], directly())
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(2)).transact()
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
# then
assert len(self.otc.get_orders()) == 2
assert len(self.otc.get_orders_by_maker(maker1)) == 1
assert len(self.otc.get_orders_by_maker(maker2)) == 1
# and
assert self.otc.get_orders_by_maker(maker1)[0].maker == maker1
assert self.otc.get_orders_by_maker(maker2)[0].maker == maker2
def test_get_orders_by_block(self):
if isinstance(self.otc, MatchingMarket):
token1_val = self.token1_tokenclass
token2_val = self.token2_tokenclass
# given
maker1 = self.our_address
# when
self.otc.approve([self.token1], directly())
made_order = self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(2)).transact()
block_number = made_order.raw_receipt.blockNumber
# and when
time_travel_by(self.web3, 10)
assert len(self.otc.get_orders_by_maker(maker1)) == 1
self.otc.kill(1).transact(gas=4000000)
assert len(self.otc.get_orders_by_maker(maker1)) == 0
# then
time_travel_by(self.web3, 10)
assert len(self.otc.get_orders(token1_val, token2_val, block_number)) == 1
def test_order_comparison(self):
if isinstance(self.otc, MatchingMarket):
token1_val = self.token1_tokenclass
token2_val = self.token2_tokenclass
else:
token1_val = self.token1.address
token2_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(token1_val, Wad.from_number(1),
token2_val, Wad.from_number(2)).transact()
# and
self.otc.make(token1_val, Wad.from_number(3),
token2_val, Wad.from_number(4)).transact()
# then
assert self.otc.get_last_order_id() == 2
assert self.otc.get_order(1) == self.otc.get_order(1)
assert self.otc.get_order(1) != self.otc.get_order(2)
def test_order_hashable(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# expect
assert is_hashable(self.otc.get_order(1))
def test_take_partial(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# when
self.otc.approve([self.token2], directly())
self.otc.take(1, Wad.from_number(0.25)).transact()
# then
assert self.otc.get_order(1).pay_amount == Wad.from_number(0.75)
assert self.otc.get_order(1).buy_amount == Wad.from_number(1.5)
assert self.otc.get_orders() == [self.otc.get_order(1)]
assert self.otc.get_last_order_id() == 1
def test_remaining_sell_and_buy_amounts(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# and
assert self.otc.get_order(1).remaining_sell_amount == Wad.from_number(1)
assert self.otc.get_order(1).remaining_buy_amount == Wad.from_number(2)
# when
self.otc.approve([self.token2], directly())
self.otc.take(1, Wad.from_number(0.25)).transact()
# then
assert self.otc.get_order(1).remaining_sell_amount == Wad.from_number(0.75)
assert self.otc.get_order(1).remaining_buy_amount == Wad.from_number(1.5)
def test_take_complete(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# when
self.otc.approve([self.token2], directly())
self.otc.take(1, Wad.from_number(1)).transact()
# then
assert self.otc.get_order(1) is None
assert self.otc.get_orders() == []
assert self.otc.get_last_order_id() == 1
def test_kill(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# given
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# when
self.otc.kill(1).transact(gas=4000000)
# then
assert self.otc.get_order(1) is None
assert self.otc.get_orders() == []
assert self.otc.get_last_order_id() == 1
def test_no_past_events_on_startup(self):
assert self.otc.past_make(PAST_BLOCKS) == []
assert self.otc.past_bump(PAST_BLOCKS) == []
assert self.otc.past_take(PAST_BLOCKS) == []
assert self.otc.past_kill(PAST_BLOCKS) == []
def test_past_make(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# then
past_make = self.otc.past_make(PAST_BLOCKS)
assert len(past_make) == 1
assert past_make[0].order_id == 1
assert past_make[0].maker == self.our_address
assert past_make[0].pay_token == self.token1.address
assert past_make[0].pay_amount == Wad.from_number(1)
assert past_make[0].buy_token == self.token2.address
assert past_make[0].buy_amount == Wad.from_number(2)
assert past_make[0].timestamp != 0
assert past_make[0].raw['blockNumber'] > 0
def test_past_bump(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
self.otc.bump(1).transact()
# then
past_bump = self.otc.past_bump(PAST_BLOCKS)
assert len(past_bump) == 1
assert past_bump[0].order_id == 1
assert past_bump[0].maker == self.our_address
assert past_bump[0].pay_token == self.token1.address
assert past_bump[0].pay_amount == Wad.from_number(1)
assert past_bump[0].buy_token == self.token2.address
assert past_bump[0].buy_amount == Wad.from_number(2)
assert past_bump[0].timestamp != 0
assert past_bump[0].raw['blockNumber'] > 0
def test_past_take(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# and
self.otc.approve([self.token2], directly())
self.otc.take(1, Wad.from_number(0.5)).transact()
# then
past_take = self.otc.past_take(PAST_BLOCKS)
assert len(past_take) == 1
assert past_take[0].order_id == 1
assert past_take[0].maker == self.our_address
assert past_take[0].taker == self.our_address
assert past_take[0].pay_token == self.token1.address
assert past_take[0].buy_token == self.token2.address
assert past_take[0].take_amount == Wad.from_number(0.5)
assert past_take[0].give_amount == Wad.from_number(1)
assert past_take[0].timestamp != 0
assert past_take[0].raw['blockNumber'] > 0
def test_past_take_with_filter(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# and
self.otc.approve([self.token2], directly())
self.otc.take(1, Wad.from_number(0.5)).transact()
# then
assert len(self.otc.past_take(PAST_BLOCKS, {'maker': self.our_address.address})) == 1
assert len(self.otc.past_take(PAST_BLOCKS, {'taker': self.our_address.address})) == 1
assert len(self.otc.past_take(PAST_BLOCKS, {'maker': '0x0101010101020202020203030303030404040404'})) == 0
assert len(self.otc.past_take(PAST_BLOCKS, {'taker': '0x0101010101020202020203030303030404040404'})) == 0
def test_past_kill(self):
if isinstance(self.otc, MatchingMarket):
pay_val = self.token1_tokenclass
buy_val = self.token2_tokenclass
else:
pay_val = self.token1.address
buy_val = self.token2.address
# when
self.otc.approve([self.token1], directly())
self.otc.make(pay_val, Wad.from_number(1),
buy_val, Wad.from_number(2)).transact()
# and
self.otc.kill(1).transact()
# then
past_kill = self.otc.past_kill(PAST_BLOCKS)
assert len(past_kill) == 1
assert past_kill[0].order_id == 1
assert past_kill[0].maker == self.our_address
assert past_kill[0].pay_token == self.token1.address
assert past_kill[0].pay_amount == Wad.from_number(1)
assert past_kill[0].buy_token == self.token2.address
assert past_kill[0].buy_amount == Wad.from_number(2)
assert past_kill[0].timestamp != 0
assert past_kill[0].raw['blockNumber'] > 0
class TestSimpleMarket(GeneralMarketTest):
def setup_method(self):
GeneralMarketTest.setup_method(self)
self.otc = SimpleMarket.deploy(self.web3)
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
SimpleMarket(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_should_have_printable_representation(self):
assert repr(self.otc) == f"SimpleMarket('{self.otc.address}')"
class TestMatchingMarket(GeneralMarketTest):
def setup_method(self):
GeneralMarketTest.setup_method(self)
self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), self.price_oracle.address)
self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
self.otc.add_token_pair_whitelist(self.token1.address, self.token3.address).transact()
self.otc.add_token_pair_whitelist(self.token2.address, self.token3.address).transact()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
MatchingMarket(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_simple_matching(self):
# given
self.otc.approve([self.token1, self.token2], directly())
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(2)).transact()
# when
self.otc.make(p_token=self.token2_tokenclass, pay_amount=Wad.from_number(2.5),
b_token=self.token1_tokenclass, buy_amount=Wad.from_number(1)).transact()
# then
assert self.otc.get_order(1) is None
assert self.otc.get_order(2) is None
# and
assert self.otc.get_last_order_id() == 1
def test_advanced_matching(self):
# given
self.otc.approve([self.token1, self.token2], directly())
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(2)).transact()
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(2.2)).transact()
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(1.8)).transact()
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(2.1)).transact()
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(1.9)).transact()
# when
self.otc.make(p_token=self.token2_tokenclass, pay_amount=Wad.from_number(20.1),
b_token=self.token1_tokenclass, buy_amount=Wad.from_number(10)).transact(gas=4000000)
# then
assert self.otc.get_order(1) is None
assert self.otc.get_order(2) is not None
assert self.otc.get_order(3) is None
assert self.otc.get_order(4) is not None
assert self.otc.get_order(5) is None
# and
assert self.otc.get_last_order_id() == 6
# and
assert self.otc.get_order(6).order_id == 6
assert self.otc.get_order(6).pay_token == self.token2.address
assert self.otc.get_order(6).pay_amount == Wad.from_number(14.07)
assert self.otc.get_order(6).buy_token == self.token1.address
assert self.otc.get_order(6).buy_amount == Wad.from_number(7)
assert self.otc.get_order(6).maker == self.our_address
assert self.otc.get_order(6).timestamp != 0
def test_should_have_printable_representation(self):
assert repr(self.otc) == f"MatchingMarket('{self.otc.address}')"
class TestMatchingMarketWithSupportContract(TestMatchingMarket):
def setup_method(self):
GeneralMarketTest.setup_method(self)
support_abi = Contract._load_abi(__name__, '../pymaker/abi/MakerOtcSupportMethods.abi')
support_bin = Contract._load_bin(__name__, '../pymaker/abi/MakerOtcSupportMethods.bin')
support_address = Contract._deploy(self.web3, support_abi, support_bin, [])
self.price_oracle = OasisMockPriceOracle.deploy(self.web3)
self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), self.price_oracle.address, support_address)
self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
self.otc.add_token_pair_whitelist(self.token1.address, self.token3.address).transact()
self.otc.add_token_pair_whitelist(self.token2.address, self.token3.address).transact()
def test_fail_when_no_support_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
MatchingMarket(web3=self.web3,
address=self.otc.address,
support_address=Address('0xdeadadd1e5500000000000000000000000000000'))
class TestMatchingMarketDecimal:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1_tokenclass = Token('AAA', self.token1.address, 18)
self.token1.mint(Wad.from_number(10000)).transact()
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2_tokenclass = Token('BBB', self.token2.address, 6)
self.token2.mint(Wad.from_number(10000)).transact()
support_abi = Contract._load_abi(__name__, '../pymaker/abi/MakerOtcSupportMethods.abi')
support_bin = Contract._load_bin(__name__, '../pymaker/abi/MakerOtcSupportMethods.bin')
support_address = Contract._deploy(self.web3, support_abi, support_bin, [])
price_oracle = OasisMockPriceOracle.deploy(self.web3)
self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address, support_address)
self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
self.otc.approve([self.token1, self.token2], directly())
def test_get_orders(self):
buy_amount_order1 = Wad.from_number(5.124988526145090209)
pay_amount_order1 = Wad.from_number(5.024999999999999500)
buy_amount_order2 = Wad.from_number(5.102550000000000000)
pay_amount_order2 = Wad.from_number(5.000000000000000000)
# given
self.otc.make(p_token=self.token2_tokenclass, pay_amount=self.token2_tokenclass.unnormalize_amount(pay_amount_order1),
b_token=self.token1_tokenclass, buy_amount=buy_amount_order1).transact()
self.otc.make(p_token=self.token1_tokenclass, pay_amount=pay_amount_order2,
b_token=self.token2_tokenclass, buy_amount=self.token2_tokenclass.unnormalize_amount(buy_amount_order2)).transact()
# then
assert self.otc.get_orders(self.token1_tokenclass, self.token2_tokenclass)[0].buy_amount == buy_amount_order2
assert self.token2_tokenclass.unnormalize_amount(self.otc.get_orders(self.token2_tokenclass, self.token1_tokenclass)[0].pay_amount) == self.token2_tokenclass.unnormalize_amount(pay_amount_order1)
class TestMatchingMarketPosition:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1.mint(Wad.from_number(10000)).transact()
self.token1_tokenclass = Token('AAA', self.token1.address, 18)
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2.mint(Wad.from_number(10000)).transact()
self.token2_tokenclass = Token('BBB', self.token2.address, 18)
price_oracle = OasisMockPriceOracle.deploy(self.web3)
self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address)
self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact()
self.otc.approve([self.token1, self.token2], directly())
for amount in [11,55,44,34,36,21,45,51,15]:
self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(amount)).transact()
def test_should_calculate_correct_order_position(self):
# expect
assert self.otc.position(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35)) == 4
@pytest.mark.skip(reason="Works unreliably with ganache-cli")
def test_should_use_correct_order_position_by_default(self):
# when
explicit_position = self.otc.position(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35))
explicit_receipt = self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35),
pos=explicit_position).transact()
explicit_gas_used = explicit_receipt.gas_used
# and
self.setup_method()
implicit_receipt = self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35)).transact()
implicit_gas_used = implicit_receipt.gas_used
# then
assert explicit_gas_used == implicit_gas_used
@pytest.mark.skip(reason="Stopped working as expected after recent `maker-otc` upgrade")
def test_calculated_order_position_should_bring_gas_savings(self):
# when
position = self.otc.position(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35))
gas_used_optimal = self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35),
pos=position).transact().gas_used
# and
self.setup_method()
gas_used_minus_1 = self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35),
pos=position-1).transact().gas_used
# and
self.setup_method()
gas_used_plus_1 = self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1),
b_token=self.token2_tokenclass, buy_amount=Wad.from_number(35),
pos=position+1).transact().gas_used
# then
assert gas_used_optimal < gas_used_minus_1
assert gas_used_optimal < gas_used_plus_1
================================================
FILE: tests/test_proxy.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2018,2019 bargst
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3
from pymaker import Address, Calldata
from pymaker.proxy import DSProxyCache, DSProxy, DSProxyFactory, LogCreated
@pytest.fixture(scope="session")
def web3():
web3 = Web3(Web3.HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = web3.eth.accounts[0]
return web3
@pytest.fixture(scope="session")
def our_address(web3):
return Address(web3.eth.accounts[0])
@pytest.fixture(scope="session")
def other_address(web3):
return Address(web3.eth.accounts[1])
@pytest.fixture(scope="session")
def proxy_cache(web3):
return DSProxyCache.deploy(web3=web3)
@pytest.fixture(scope="session")
def proxy_factory(web3):
return DSProxyFactory.deploy(web3=web3)
@pytest.fixture(scope="session")
def proxy(web3, proxy_cache):
return DSProxy.deploy(web3=web3, cache=proxy_cache.address)
class TestProxyCache:
""" `DSProxyCache` class testing"""
def test_read(self, proxy_cache: DSProxyCache):
assert proxy_cache.read('0x001122') == None
def test_write_invalid(self, proxy_cache: DSProxyCache):
# when
address = proxy_cache.write('0x001122').transact()
# then
assert address is None
assert proxy_cache.read('0x001122') == None
def test_write(self, proxy_cache: DSProxyCache):
# when
proxy_cache.write(DSProxyCache.bin).transact()
# then
assert proxy_cache.read(DSProxyCache.bin) is not None
class TestProxyFactory:
""" `DSProxyFactory` class testing"""
def test_build(self, proxy_factory: DSProxyFactory):
assert proxy_factory.build().transact()
def test_past_build(self, proxy_factory: DSProxyFactory, our_address):
# given
past_build = proxy_factory.past_build(proxy_factory.web3.eth.blockNumber)
past_build_count = len(past_build)
# when
assert proxy_factory.build().transact()
# then
past_build = proxy_factory.past_build(1)
assert past_build
assert len(past_build) == past_build_count + 1
past_build: LogCreated = past_build[0]
assert past_build.owner == our_address
assert past_build.sender == our_address
assert past_build.cache == proxy_factory.cache()
assert proxy_factory.is_proxy(past_build.proxy)
def test_build_for(self, proxy_factory: DSProxyFactory, other_address):
# given
assert proxy_factory.is_proxy(other_address) is False
# when
receipt = proxy_factory.build_for(other_address).transact()
assert receipt
build_event = proxy_factory.log_created(receipt)[0]
# then
assert build_event.owner == other_address
assert proxy_factory.is_proxy(build_event.proxy)
def test_cache(self, proxy_factory: DSProxyFactory, other_address):
assert proxy_factory.cache() is not None
class TestProxy:
""" `DSProxy` class testing"""
def test_execute(self, proxy: DSProxy):
assert proxy.execute(DSProxyFactory.bin, Calldata.from_signature(proxy.web3, "build()", [])).transact()
def test_execute_at(self, proxy: DSProxy):
# given
proxy_cache = DSProxyCache(proxy.web3, proxy.cache())
proxy_cache.write(DSProxyFactory.bin).transact()
new_factory_addr = proxy_cache.read(DSProxyFactory.bin)
assert new_factory_addr
# when
receipt = proxy.execute_at(new_factory_addr, Calldata.from_signature(proxy.web3, "build(address)",
[proxy.address.address])).transact()
assert receipt
build_event = DSProxyFactory.log_created(receipt)[0]
# then
assert build_event.owner == proxy.address
def test_call(self, proxy: DSProxy):
# when
calldata = Calldata.from_signature(proxy.web3, "isProxy(address)", [Address(40*'0').address])
target, response = proxy.call(DSProxyFactory.bin, calldata)
# then
assert target != Address(40*'0')
assert Web3.toInt(response) == 0
def test_call_at(self, proxy: DSProxy):
# given
proxy_cache = DSProxyCache(proxy.web3, proxy.cache())
proxy_cache.write(DSProxyFactory.bin).transact()
new_factory_addr = proxy_cache.read(DSProxyFactory.bin)
receipt = proxy.execute_at(new_factory_addr, Calldata.from_signature(proxy.web3,
"build(address)",
[proxy.address.address])).transact()
log_created: LogCreated = DSProxyFactory.log_created(receipt)[0]
# when
calldata = Calldata.from_signature(proxy.web3, "isProxy(address)", [log_created.proxy.address])
response = proxy.call_at(new_factory_addr, calldata)
# then
assert Web3.toInt(response) == 1
================================================
FILE: tests/test_reloadable_config.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2020 MikeHathaway
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from unittest.mock import MagicMock
from pymaker.reloadable_config import ReloadableConfig
class TestReloadableConfig:
@staticmethod
def write_sample_config(tmpdir):
file = tmpdir.join("sample_config.json")
file.write("""{"a": "b"}""")
return str(file)
@staticmethod
def write_advanced_config(tmpdir, value):
file = tmpdir.join("advanced_config.json")
file.write("""{"a": \"""" + value + """\", "c": self.a}""")
return str(file)
@staticmethod
def write_global_config(tmpdir, val1, val2):
global_file = tmpdir.join("global_config.json")
global_file.write("""{
"firstValue": """ + str(val1) + """,
"secondValue": """ + str(val2) + """
}""")
@staticmethod
def write_importing_config(tmpdir):
file = tmpdir.join("importing_config.json")
file.write("""{
local globals = import "./global_config.json",
"firstValueMultiplied": globals.firstValue * 2,
"secondValueMultiplied": globals.secondValue * 3
}""")
return str(file)
def test_should_read_simple_file(self, tmpdir):
# when
config = ReloadableConfig(self.write_sample_config(tmpdir)).get_config()
# then
assert len(config) == 1
assert config["a"] == "b"
def test_should_read_advanced_file(self, tmpdir):
# when
config = ReloadableConfig(self.write_advanced_config(tmpdir, "b")).get_config()
# then
assert len(config) == 2
assert config["a"] == "b"
assert config["c"] == "b"
def test_should_read_file_again_if_changed(self, tmpdir):
# given
reloadable_config = ReloadableConfig(self.write_advanced_config(tmpdir, "b"))
reloadable_config.logger = MagicMock()
# when
config = reloadable_config.get_config()
# then
assert config["a"] == "b"
# and
# [a log message that the config was loaded gets generated]
assert reloadable_config.logger.info.call_count == 1
# when
self.write_advanced_config(tmpdir, "z")
config = reloadable_config.get_config()
# then
assert config["a"] == "z"
# and
# [a log message that the config was reloaded gets generated]
assert reloadable_config.logger.info.call_count == 2
def test_should_import_other_config_file(self, tmpdir):
# when
self.write_global_config(tmpdir, 17.0, 11.0)
config = ReloadableConfig(self.write_importing_config(tmpdir)).get_config()
# then
assert len(config) == 2
assert config["firstValueMultiplied"] == 34.0
assert config["secondValueMultiplied"] == 33.0
def test_should_reevaluate_if_other_config_file_changed(self, tmpdir):
# given
reloadable_config = ReloadableConfig(self.write_importing_config(tmpdir))
# when
self.write_global_config(tmpdir, 17.0, 11.0)
config = reloadable_config.get_config()
# then
assert len(config) == 2
assert config["firstValueMultiplied"] == 34.0
assert config["secondValueMultiplied"] == 33.0
# when
self.write_global_config(tmpdir, 18.0, 3.0)
config = reloadable_config.get_config()
# then
assert len(config) == 2
assert config["firstValueMultiplied"] == 36.0
assert config["secondValueMultiplied"] == 9.0
================================================
FILE: tests/test_sai.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from pymaker import Address
from pymaker.deployment import Deployment
from pymaker.feed import DSValue
from pymaker.numeric import Wad, Ray
from pymaker.sai import Tub, Tap, Top, Vox
from tests.helpers import time_travel_by
class TestTub:
def test_fail_when_no_contract_under_that_address(self, deployment: Deployment):
# expect
with pytest.raises(Exception):
Tub(web3=deployment.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_tap(self, deployment: Deployment):
assert deployment.tub.tap() == deployment.tap.address
@pytest.mark.skip(reason="flaky test failures due to underlying node issue with evm_mine")
def test_era(self, deployment: Deployment):
# when
era = deployment.tub.era()
deployment.web3.manager.request_blocking("evm_mine", [])
# then
assert era == deployment.web3.eth.getBlock('latest').timestamp
def test_join_and_pie_and_exit(self, deployment: Deployment):
# given
assert deployment.skr.balance_of(deployment.our_address) == Wad(0)
assert deployment.skr.total_supply() == Wad(0)
# when
deployment.tub.join(Wad.from_number(5)).transact()
# then
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(5)
assert deployment.skr.total_supply() == Wad.from_number(5)
assert deployment.tub.pie() == Wad.from_number(5)
# when
deployment.tub.exit(Wad.from_number(4)).transact()
# then
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(1)
assert deployment.skr.total_supply() == Wad.from_number(1)
assert deployment.tub.pie() == Wad.from_number(1)
def test_mold_cap_and_cap(self, deployment: Deployment):
# given
assert deployment.tub.cap() == Wad(0)
# when
deployment.tub.mold_cap(Wad.from_number(150000)).transact()
# then
assert deployment.tub.cap() == Wad.from_number(150000)
def test_mold_tax_and_tax(self, deployment: Deployment):
# given
assert deployment.tub.tax() == Ray.from_number(1)
# when
deployment.tub.mold_tax(Ray(1000000000000000020000000000)).transact()
# then
assert deployment.tub.tax() == Ray(1000000000000000020000000000)
def test_mold_mat_and_mat(self, deployment: Deployment):
# given
assert deployment.tub.mat() == Ray.from_number(1)
# when
deployment.tub.mold_mat(Ray.from_number(1.5)).transact()
# then
assert deployment.tub.mat() == Ray.from_number(1.5)
def test_mold_axe_and_axe(self, deployment: Deployment):
# given
assert deployment.tub.axe() == Ray.from_number(1)
deployment.tub.mold_mat(Ray.from_number(1.5)).transact()
# when
deployment.tub.mold_axe(Ray.from_number(1.2)).transact()
# then
assert deployment.tub.axe() == Ray.from_number(1.2)
def test_sai(self, deployment: Deployment):
assert deployment.tub.sai() == deployment.sai.address
def test_sin(self, deployment: Deployment):
assert deployment.tub.sin() == deployment.sin.address
def test_gem(self, deployment: Deployment):
assert deployment.tub.gem() == deployment.gem.address
def test_skr(self, deployment: Deployment):
assert deployment.tub.skr() == deployment.skr.address
def test_gov(self, deployment: Deployment):
assert deployment.tub.gov() == deployment.gov.address
def test_vox(self, deployment: Deployment):
assert deployment.tub.vox() == deployment.vox.address
def test_pip_and_pep(self, deployment: Deployment):
assert isinstance(deployment.tub.pip(), Address)
assert isinstance(deployment.tub.pep(), Address)
def test_pit(self, deployment: Deployment):
assert isinstance(deployment.tub.pit(), Address)
def test_per(self, deployment: Deployment):
assert deployment.tub.per() == Ray.from_number(1.0)
def test_tag(self, deployment: Deployment):
# when
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250.45).value).transact()
# then
assert deployment.tub.tag() == Ray.from_number(250.45)
def test_drip_and_chi_and_rho(self, deployment: Deployment):
# given
deployment.tub.mold_tax(Ray(1000000000000000020000000000)).transact()
old_chi = deployment.tub.chi()
old_rho = deployment.tub.rho()
# when
time_travel_by(deployment.web3, 1000)
deployment.tub.drip().transact()
# then
assert deployment.tub.chi() > old_chi
assert deployment.tub.rho() > old_rho
def test_open_and_cupi(self, deployment: Deployment):
# when
deployment.tub.open().transact()
# then
assert deployment.tub.cupi() == 1
def test_cups(self, deployment: Deployment):
# when
deployment.tub.open().transact()
# then
assert deployment.tub.cups(1).art == Wad(0)
assert deployment.tub.cups(1).ink == Wad(0)
assert deployment.tub.cups(1).lad == deployment.our_address
def test_not_empty_cups(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250.45).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(3)).transact()
# when
deployment.tub.draw(1, Wad.from_number(50)).transact()
# then
assert deployment.tub.cups(1).art == Wad.from_number(50)
assert deployment.tub.cups(1).ink == Wad.from_number(3)
def test_safe(self, deployment: Deployment):
# given
deployment.tub.mold_mat(Ray.from_number(1.5)).transact()
deployment.tub.mold_axe(Ray.from_number(1.2)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# when
deployment.tub.open().transact()
# then
assert deployment.tub.safe(1)
def test_ink(self, deployment: Deployment):
# when
deployment.tub.open().transact()
# then
assert deployment.tub.ink(1) == Wad(0)
def test_lad(self, deployment: Deployment):
# when
deployment.tub.open().transact()
# then
assert deployment.tub.lad(1) == deployment.our_address
def test_give(self, deployment: Deployment):
# given
deployment.tub.open().transact()
# when
deployment.tub.give(1, Address('0x0101010101020202020203030303030404040404')).transact()
# then
assert deployment.tub.lad(1) == Address('0x0101010101020202020203030303030404040404')
def test_shut(self, deployment: Deployment):
# given
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
deployment.tub.open().transact()
# when
deployment.tub.shut(1).transact()
# then
assert deployment.tub.lad(1) == Address('0x0000000000000000000000000000000000000000')
def test_lock_and_free(self, deployment: Deployment):
# given
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
deployment.tub.open().transact()
deployment.tub.join(Wad.from_number(10)).transact()
# when
print(deployment.tub.cupi())
deployment.tub.lock(1, Wad.from_number(5)).transact()
# then
assert deployment.tub.ink(1) == Wad.from_number(5)
assert deployment.tub.air() == Wad.from_number(5)
# when
deployment.tub.free(1, Wad.from_number(3)).transact()
# then
assert deployment.tub.ink(1) == Wad.from_number(2)
assert deployment.tub.air() == Wad.from_number(2)
def test_draw_and_tab_and_din_and_wipe(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250.45).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(5)).transact()
# when
deployment.tub.draw(1, Wad.from_number(50)).transact()
# then
assert deployment.sai.balance_of(deployment.our_address) == Wad.from_number(50)
assert deployment.tub.tab(1) == Wad.from_number(50)
assert deployment.tub.din() == Wad.from_number(50)
# when
deployment.tub.wipe(1, Wad.from_number(30)).transact()
# then
assert deployment.sai.balance_of(deployment.our_address) == Wad.from_number(20)
assert deployment.tub.tab(1) == Wad.from_number(20)
assert deployment.tub.din() == Wad.from_number(20)
def test_bite_and_safe(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# when
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# then
assert deployment.tub.safe(1)
# when
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(150).value).transact()
# then
assert not deployment.tub.safe(1)
# when
deployment.tub.bite(1).transact()
# then
assert deployment.tub.safe(1)
def test_mold_gap_and_gap(self, deployment: Deployment):
# given
assert deployment.tub.gap() == Wad.from_number(1)
# when
deployment.tub.mold_gap(Wad.from_number(1.05)).transact()
# then
assert deployment.tub.gap() == Wad.from_number(1.05)
def test_bid_and_ask(self, deployment: Deployment):
# when
deployment.tub.mold_gap(Wad.from_number(1.05)).transact()
# then
assert deployment.tub.bid(Wad.from_number(2)) == Wad.from_number(0.95)*Wad.from_number(2)
assert deployment.tub.ask(Wad.from_number(2)) == Wad.from_number(1.05)*Wad.from_number(2)
def test_comparison(self, deployment: Deployment):
# expect
assert deployment.tub == deployment.tub
assert deployment.tub == Tub(web3=deployment.web3, address=deployment.tub.address)
class TestTap:
def test_fail_when_no_contract_under_that_address(self, deployment: Deployment):
# expect
with pytest.raises(Exception):
Tap(web3=deployment.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_tub(self, deployment: Deployment):
assert deployment.tap.tub() == deployment.tub.address
def test_sai(self, deployment: Deployment):
assert deployment.tap.sai() == deployment.sai.address
def test_sin(self, deployment: Deployment):
assert deployment.tap.sin() == deployment.sin.address
def test_skr(self, deployment: Deployment):
assert deployment.tap.skr() == deployment.skr.address
def test_mold_gap_and_gap(self, deployment: Deployment):
# given
assert deployment.tap.gap() == Wad.from_number(1)
# when
deployment.tap.mold_gap(Wad.from_number(1.05)).transact()
# then
assert deployment.tap.gap() == Wad.from_number(1.05)
def test_woe(self, deployment: Deployment):
assert deployment.tap.woe() == Wad(0)
def test_fog(self, deployment: Deployment):
assert deployment.tap.fog() == Wad(0)
def test_joy(self, deployment: Deployment):
assert deployment.tap.joy() == Wad(0)
def test_s2s_and_bid_and_ask(self, deployment: Deployment):
# when
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(500).value).transact()
deployment.tap.mold_gap(Wad.from_number(1.05)).transact()
# then
assert deployment.tap.bid(Wad.from_number(2)) == Wad.from_number(475)*Wad.from_number(2)
assert deployment.tap.s2s() == Ray.from_number(500)
assert deployment.tap.ask(Wad.from_number(2)) == Wad.from_number(525)*Wad.from_number(2)
def test_joy_and_boom(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
deployment.tub.mold_tax(Ray(1000100000000000000000000000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# and
assert deployment.tap.joy() == Wad(0)
# when
time_travel_by(deployment.web3, 3600)
deployment.tub.drip().transact()
# then
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(6)
assert deployment.tap.joy() > Wad(0)
prev_joy = deployment.tap.joy()
# when
deployment.tap.boom(Wad.from_number(1)).transact()
# then
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(5)
assert Wad(0) < deployment.tap.joy() < prev_joy
def test_fog_and_woe_and_bust(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# and
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(150).value).transact()
# when
deployment.tub.bite(1).transact()
# then
assert deployment.tap.fog() == Wad.from_number(4)
assert deployment.tap.woe() == Wad.from_number(1000)
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(6)
assert deployment.sai.balance_of(deployment.our_address) == Wad.from_number(1000)
# when
deployment.tap.bust(Wad.from_number(2)).transact()
assert deployment.tap.fog() == Wad.from_number(2)
assert deployment.tap.woe() == Wad.from_number(700)
assert deployment.skr.balance_of(deployment.our_address) == Wad.from_number(8)
assert deployment.sai.balance_of(deployment.our_address) == Wad.from_number(700)
def test_cash(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# and
gem_before = deployment.gem.balance_of(deployment.our_address)
# when
deployment.top.cage().transact()
deployment.tap.cash(Wad.from_number(500)).transact()
# then
gem_after = deployment.gem.balance_of(deployment.our_address)
assert gem_after - gem_before == Wad.from_number(2)
# when
deployment.tap.cash(Wad.from_number(500)).transact()
# then
gem_after = deployment.gem.balance_of(deployment.our_address)
assert gem_after - gem_before == Wad.from_number(4)
def test_mock(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# and
gem_before = deployment.gem.balance_of(deployment.our_address)
# when
deployment.top.cage().transact()
deployment.tap.cash(Wad.from_number(500)).transact()
deployment.tap.mock(Wad.from_number(250)).transact()
# then
gem_after = deployment.gem.balance_of(deployment.our_address)
assert gem_after - gem_before == Wad.from_number(1)
def test_comparison(self, deployment: Deployment):
# expect
assert deployment.tap == deployment.tap
assert deployment.tap == Tap(web3=deployment.web3, address=deployment.tap.address)
assert deployment.tap != Tap(web3=deployment.web3, address=deployment.tub.address)
class TestTop:
def test_fail_when_no_contract_under_that_address(self, deployment: Deployment):
# expect
with pytest.raises(Exception):
Top(web3=deployment.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_comparison(self, deployment: Deployment):
# expect
assert deployment.top == deployment.top
assert deployment.top == Top(web3=deployment.web3, address=deployment.top.address)
assert deployment.top != Top(web3=deployment.web3, address=deployment.tub.address)
def test_default_fix(self, deployment: Deployment):
# expect
assert deployment.top.fix() == Ray.from_number(0)
def test_cage(self, deployment: Deployment):
# given
deployment.tub.join(Wad.from_number(10)).transact()
deployment.tub.mold_cap(Wad.from_number(100000)).transact()
DSValue(web3=deployment.web3, address=deployment.tub.pip()).poke_with_int(Wad.from_number(250).value).transact()
# and
deployment.tub.open().transact()
deployment.tub.lock(1, Wad.from_number(4)).transact()
deployment.tub.draw(1, Wad.from_number(1000)).transact()
# when
deployment.top.cage().transact()
# then
assert deployment.top.fix() == Ray.from_number(0.004)
class TestVox:
def test_fail_when_no_contract_under_that_address(self, deployment: Deployment):
# expect
with pytest.raises(Exception):
Vox(web3=deployment.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_comparison(self, deployment: Deployment):
# expect
assert deployment.vox == deployment.vox
assert deployment.vox == Vox(web3=deployment.web3, address=deployment.vox.address)
assert deployment.vox != Vox(web3=deployment.web3, address=deployment.top.address)
@pytest.mark.skip(reason="flaky test failures due to underlying node issue with evm_mine")
def test_era(self, deployment: Deployment):
# when
era = deployment.vox.era()
deployment.web3.manager.request_blocking("evm_mine", [])
# then
assert era == deployment.web3.eth.getBlock('latest').timestamp
def test_default_par(self, deployment: Deployment):
# expect
assert deployment.vox.par() == Ray.from_number(1)
================================================
FILE: tests/test_savings.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 grandizzy
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from pymaker import Address
from pymaker.deployment import DssDeployment
from pymaker.dsr import Dsr
from pymaker.numeric import Wad, Ray, Rad
from tests.test_dss import wrap_eth, frob
@pytest.fixture
def dsr(our_address: Address, mcd: DssDeployment) -> Dsr:
return Dsr(mcd, our_address)
@pytest.mark.dependency()
def test_proxy(dsr):
assert dsr.has_proxy() is False
dsr.build_proxy().transact()
assert dsr.has_proxy() is True
@pytest.mark.dependency(depends=['test_proxy'])
def test_join_and_exit(dsr):
proxy = dsr.get_proxy()
assert dsr.get_balance(proxy.address) == Wad.from_number(0)
mcd = dsr.mcd
# create a vault
collateral = mcd.collaterals['ETH-C']
wrap_eth(mcd, dsr.owner, Wad.from_number(2))
collateral.approve(dsr.owner)
assert collateral.adapter.join(dsr.owner, Wad.from_number(2)).transact(from_address=dsr.owner)
frob(mcd, collateral, dsr.owner, dink=Wad.from_number(2), dart=Wad(0))
dart = Wad.from_number(100)
frob(mcd, collateral, dsr.owner, dink=Wad(0), dart=dart)
# mint and withdraw all the Dai
mcd.approve_dai(dsr.owner)
assert mcd.dai_adapter.exit(dsr.owner, dart).transact(from_address=dsr.owner)
assert mcd.dai.balance_of(dsr.owner) == dart
initial_dai_balance = mcd.dai.balance_of(dsr.owner)
assert initial_dai_balance >= Wad.from_number(100)
assert dsr.get_balance(proxy.address) == Wad.from_number(0)
# approve Proxy to use 100 DAI from account
mcd.dai.approve(proxy.address, Wad.from_number(100)).transact(from_address=dsr.owner)
# join 100 DAI in DSR
assert dsr.join(Wad.from_number(100), proxy).transact(from_address=dsr.owner)
assert mcd.dai.balance_of(dsr.owner) == initial_dai_balance - Wad.from_number(100)
assert round(dsr.get_balance(proxy.address)) == Wad.from_number(100)
assert mcd.pot.drip().transact()
# exit 33 DAI from DSR
assert dsr.exit(Wad.from_number(33), proxy).transact(from_address=dsr.owner)
assert round(mcd.dai.balance_of(dsr.owner)) == round(initial_dai_balance) - Wad.from_number(100) + Wad.from_number(33)
assert round(dsr.get_balance(proxy.address)) == Wad.from_number(67)
assert mcd.pot.drip().transact()
# exit remaining DAI from DSR and join to vat
assert dsr.exit_all(proxy).transact(from_address=dsr.owner)
assert round(mcd.dai.balance_of(dsr.owner)) == round(initial_dai_balance)
assert dsr.get_balance(proxy.address) == Wad.from_number(0)
assert mcd.dai_adapter.join(dsr.owner, mcd.dai.balance_of(dsr.owner)).transact(from_address=dsr.owner)
# repay the vault
assert collateral.ilk.dust == Rad(0)
wipe: Wad = mcd.vat.get_wipe_all_dart(collateral.ilk, dsr.owner)
frob(mcd, collateral, dsr.owner, dink=Wad(0), dart=wipe*-1)
================================================
FILE: tests/test_shutdown.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2019 EdNoepel
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from datetime import datetime, timedelta
from pymaker import Address
from pymaker.approval import directly
from pymaker.deployment import Collateral, DssDeployment
from pymaker.numeric import Wad, Ray, Rad
from pymaker.shutdown import ShutdownModule, End
from tests.helpers import time_travel_by
from tests.test_auctions import create_surplus
from tests.test_dss import mint_mkr, wrap_eth, frob
def open_cdp(mcd: DssDeployment, collateral: Collateral, address: Address):
assert isinstance(mcd, DssDeployment)
assert isinstance(collateral, Collateral)
assert isinstance(address, Address)
collateral.approve(address)
wrap_eth(mcd, address, Wad.from_number(10))
assert collateral.adapter.join(address, Wad.from_number(10)).transact(from_address=address)
frob(mcd, collateral, address, Wad.from_number(10), Wad.from_number(20))
assert mcd.vat.debt() >= Rad(Wad.from_number(20))
assert mcd.vat.dai(address) >= Rad.from_number(10)
def create_flap_auction(mcd: DssDeployment, deployment_address: Address, our_address: Address):
assert isinstance(mcd, DssDeployment)
assert isinstance(deployment_address, Address)
assert isinstance(our_address, Address)
flapper = mcd.flapper
create_surplus(mcd, flapper, deployment_address)
joy = mcd.vat.dai(mcd.vow.address)
assert joy > mcd.vat.sin(mcd.vow.address) + mcd.vow.bump() + mcd.vow.hump()
assert (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() == Rad(0)
assert mcd.vow.flap().transact()
mint_mkr(mcd.mkr, our_address, Wad.from_number(10))
flapper.approve(mcd.mkr.address, directly(from_address=our_address))
bid = Wad.from_number(0.001)
assert mcd.mkr.balance_of(our_address) > bid
assert flapper.tend(flapper.kicks(), mcd.vow.bump(), bid).transact(from_address=our_address)
nobody = Address("0x0000000000000000000000000000000000000000")
class TestShutdownModule:
"""This test must be run after other MCD tests because it will leave the testchain `cage`d."""
def test_init(self, mcd, deployment_address, our_address):
assert mcd.esm is not None
assert isinstance(mcd.esm, ShutdownModule)
assert isinstance(mcd.esm.address, Address)
assert mcd.esm.sum() == Wad(0)
assert mcd.esm.min() > Wad(0)
assert mcd.end.live()
joy = mcd.vat.dai(mcd.vow.address)
awe = mcd.vat.sin(mcd.vow.address)
# If `test_shutdown.py` is run in isolation, create a flap auction to exercise `yank`
if joy == Rad(0) and awe == Rad(0):
create_flap_auction(mcd, deployment_address, our_address)
def test_join(self, mcd, our_address):
assert mcd.mkr.approve(mcd.esm.address).transact()
# This should have no effect yet succeed regardless
assert mcd.esm.join(Wad(0)).transact()
assert mcd.esm.sum() == Wad(0)
assert mcd.esm.sum_of(our_address) == Wad(0)
# Ensure the appropriate amount of MKR can be joined
mint_mkr(mcd.mkr, our_address, mcd.esm.min())
assert mcd.esm.join(mcd.esm.min()).transact()
assert mcd.esm.sum() == mcd.esm.min()
# Joining extra MKR should succeed yet have no effect
mint_mkr(mcd.mkr, our_address, Wad(153))
assert mcd.esm.join(Wad(153)).transact()
assert mcd.esm.sum() == mcd.esm.min() + Wad(153)
assert mcd.esm.sum_of(our_address) == mcd.esm.sum()
def test_fire(self, mcd, our_address):
open_cdp(mcd, mcd.collaterals['ETH-A'], our_address)
assert mcd.end.live()
assert mcd.esm.fire().transact()
assert not mcd.end.live()
class TestEnd:
"""This test must be run after TestShutdownModule, which calls `esm.fire`."""
def test_init(self, mcd):
assert mcd.end is not None
assert isinstance(mcd.end, End)
assert isinstance(mcd.esm.address, Address)
def test_getters(self, mcd):
assert not mcd.end.live()
assert datetime.utcnow() - timedelta(minutes=5) < mcd.end.when() < datetime.utcnow()
assert mcd.end.wait() >= 0
assert mcd.end.debt() >= Rad(0)
for collateral in mcd.collaterals.values():
ilk = collateral.ilk
assert mcd.end.tag(ilk) == Ray(0)
assert mcd.end.gap(ilk) == Wad(0)
assert mcd.end.art(ilk) == Wad(0)
assert mcd.end.fix(ilk) == Ray(0)
def test_cage(self, mcd):
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
assert mcd.end.cage(ilk).transact()
assert mcd.end.art(ilk) > Wad(0)
assert mcd.end.tag(ilk) > Ray(0)
def test_yank(self, mcd):
last_flap = mcd.flapper.bids(mcd.flapper.kicks())
last_flop = mcd.flopper.bids(mcd.flopper.kicks())
if last_flap.end > 0 and last_flap.guy is not nobody:
auction = mcd.flapper
elif last_flop.end > 0 and last_flop.guy is not nobody:
auction = mcd.flopper
else:
auction = None
if auction:
print(f"active {auction} auction: {auction.bids(auction.kicks())}")
assert not auction.live()
kick = auction.kicks()
assert auction.yank(kick).transact()
assert auction.bids(kick).guy == nobody
def test_skim(self, mcd, our_address):
ilk = mcd.collaterals['ETH-A'].ilk
urn = mcd.vat.urn(ilk, our_address)
owe = Ray(urn.art) * mcd.vat.ilk(ilk.name).rate * mcd.end.tag(ilk)
assert owe > Ray(0)
wad = min(Ray(urn.ink), owe)
print(f"owe={owe} wad={wad}")
assert mcd.end.skim(ilk, our_address).transact()
assert mcd.vat.urn(ilk, our_address).art == Wad(0)
assert mcd.vat.urn(ilk, our_address).ink > Wad(0)
assert mcd.vat.sin(mcd.vow.address) > Rad(0)
assert mcd.vat.debt() > Rad(0)
assert mcd.vat.vice() > Rad(0)
@pytest.mark.skip(reason="have to heal system before calling thaw")
def test_close_cdp(self, web3, mcd, our_address):
collateral = mcd.collaterals['ETH-A']
ilk = collateral.ilk
assert mcd.end.free(ilk).transact()
assert mcd.vat.urn(ilk, our_address).ink == Wad(0)
assert mcd.vat.gem(ilk, our_address) > Wad(0)
assert collateral.adapter.exit(our_address, mcd.vat.gem(ilk, our_address)).transact()
assert mcd.end.wait() == 5
time_travel_by(web3, 5)
assert mcd.end.thaw().transact()
assert mcd.end.flow(ilk).transact()
assert mcd.end.fix(ilk) > Ray(0)
@pytest.mark.skip(reason="unable to add dai to the `bag`")
def test_pack(self, mcd, our_address):
assert mcd.end.bag(our_address) == Wad(0)
assert mcd.end.debt() > Rad(0)
assert mcd.dai.approve(mcd.end.address).transact()
assert mcd.vat.dai(our_address) >= Rad.from_number(10)
# FIXME: `pack` fails, possibly because we're passing 0 to `vat.flux`
assert mcd.end.pack(Wad.from_number(10)).transact()
assert mcd.end.bag(our_address) == Wad.from_number(10)
================================================
FILE: tests/test_sign.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pkg_resources
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.keys import register_key_file
from pymaker.sign import eth_sign
def test_signing():
# given
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = web3.eth.accounts[0]
# and
text = "abc"
msg = bytes(text, 'utf-8')
# expect
assert eth_sign(msg, web3).startswith("0x")
def test_signing_with_key_and_rpc_should_return_same_result():
# given
web3 = Web3(HTTPProvider("http://localhost:8555"))
web3.eth.defaultAccount = web3.eth.accounts[0]
assert Address(web3.eth.defaultAccount) == Address('0x9596c16d7bf9323265c2f2e22f43e6c80eb3d943')
# and
text = "abc"
msg = bytes(text, 'utf-8')
rpc_signature = eth_sign(msg, web3)
# when
keyfile_path = pkg_resources.resource_filename(__name__, "accounts/0_0x9596c16d7bf9323265c2f2e22f43e6c80eb3d943.json")
passfile_path = pkg_resources.resource_filename(__name__, "accounts/pass")
register_key_file(web3, keyfile_path, passfile_path)
# and
# [we do this in order to make sure that the message was signed using the local key]
# [with `request_blocking` set to `None` any http requests will basically fail]
web3.manager.request_blocking = None
# and
local_signature = eth_sign(msg, web3)
# then
assert rpc_signature == local_signature
================================================
FILE: tests/test_token.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from pymaker import Address
from pymaker.numeric import Wad
from pymaker.util import synchronize
from web3 import HTTPProvider
from web3 import Web3
from pymaker.token import DSToken, DSEthToken, ERC20Token
class TestERC20Token:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.second_address = Address(self.web3.eth.accounts[1])
self.third_address = Address(self.web3.eth.accounts[2])
self.token = DSToken.deploy(self.web3, 'ABC')
self.token.mint(Wad(1000000)).transact()
def test_fail_when_no_token_with_that_address(self):
with pytest.raises(Exception):
ERC20Token(web3=self.web3, address=Address('0x0123456789012345678901234567890123456789'))
def test_symbol_for_dstoken_which_returns_bytes32(self):
assert self.token.symbol() == 'ABC'
def test_total_supply(self):
assert self.token.total_supply() == Wad(1000000)
def test_balance_of(self):
assert self.token.balance_of(self.our_address) == Wad(1000000)
assert self.token.balance_of(self.second_address) == Wad(0)
def test_balance_at_block(self):
""" This test relies on ganache creating a new block for every transaction by default """
starting_block = int(self.web3.eth.getBlock('latest')['number'])
# check balance before minting
assert self.token.balance_at_block(self.our_address, starting_block - 1) == Wad(0)
# check balance after minting
assert self.token.balance_at_block(self.our_address, starting_block) == Wad(1000000)
self.token.transfer(self.second_address, Wad(500)).transact()
# check balance after additional transfer
assert self.token.balance_at_block(self.our_address, starting_block + 1) == Wad(999500)
assert self.token.balance_at_block(self.our_address, starting_block) == Wad(1000000)
def test_transfer(self):
# when
receipt = self.token.transfer(self.second_address, Wad(500)).transact()
# then
assert receipt is not None
assert self.token.balance_of(self.our_address) == Wad(999500)
assert self.token.balance_of(self.second_address) == Wad(500)
def test_transfer_async(self):
# when
receipt = synchronize([self.token.transfer(self.second_address, Wad(750)).transact_async()])
# then
assert receipt is not None
assert self.token.balance_of(self.our_address) == Wad(999250)
assert self.token.balance_of(self.second_address) == Wad(750)
def test_transfer_failed(self):
# when
receipt = self.token.transfer(self.second_address, Wad(5000000)).transact()
# then
assert receipt is None
assert self.token.balance_of(self.our_address) == Wad(1000000)
assert self.token.balance_of(self.second_address) == Wad(0)
def test_transfer_failed_async(self):
# when
receipt = synchronize([self.token.transfer(self.second_address, Wad(5000000)).transact_async()])[0]
# then
assert receipt is None
assert self.token.balance_of(self.our_address) == Wad(1000000)
assert self.token.balance_of(self.second_address) == Wad(0)
def test_transfer_out_of_gas(self):
# when
with pytest.raises(Exception):
self.token.transfer(self.second_address, Wad(500)).transact(gas=26000)
# then
assert self.token.balance_of(self.our_address) == Wad(1000000)
assert self.token.balance_of(self.second_address) == Wad(0)
def test_transfer_out_of_gas_async(self):
# when
with pytest.raises(Exception):
synchronize([self.token.transfer(self.second_address, Wad(500)).transact_async(gas=26000)])[0]
# then
assert self.token.balance_of(self.our_address) == Wad(1000000)
assert self.token.balance_of(self.second_address) == Wad(0)
def test_transfer_generates_transfer(self):
# when
receipt = self.token.transfer(self.second_address, Wad(500)).transact()
# then
assert len(receipt.transfers) == 1
assert receipt.transfers[0].token_address == self.token.address
assert receipt.transfers[0].from_address == self.our_address
assert receipt.transfers[0].to_address == self.second_address
assert receipt.transfers[0].value == Wad(500)
def test_transfer_from(self):
# given
self.token.approve(self.second_address).transact()
# when
self.token.transfer_from(self.our_address, self.third_address, Wad(500)).transact(from_address=self.second_address)
# then
assert self.token.balance_of(self.third_address) == Wad(500)
def test_allowance_of(self):
assert self.token.allowance_of(self.our_address, self.second_address) == Wad(0)
def test_approve(self):
# when
self.token.approve(self.second_address, Wad(2000)).transact()
# then
assert self.token.allowance_of(self.our_address, self.second_address) == Wad(2000)
def test_equals(self):
# given
token1 = DSToken.deploy(self.web3, 'ABC')
token2 = DSToken.deploy(self.web3, 'DEF')
token2b = ERC20Token(web3=self.web3, address=token2.address)
# expect
assert token1 == token1
assert token2 == token2b
assert not token1 == token2
assert not token1 == token2b
def test_should_have_printable_representation(self):
erc20token = ERC20Token(web3=self.web3, address=self.token.address)
assert repr(erc20token) == f"ERC20Token('{erc20token.address}')"
class TestDSToken:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.second_address = Address(self.web3.eth.accounts[1])
self.dstoken = DSToken.deploy(self.web3, 'ABC')
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
DSToken(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_authority(self):
# given
some_address = Address('0x4545454545676767676789898989890101010101')
# when
self.dstoken.set_authority(some_address).transact()
# then
assert self.dstoken.authority() == some_address
def test_mint(self):
# when
self.dstoken.mint(Wad(100000)).transact()
# then
assert self.dstoken.balance_of(self.our_address) == Wad(100000)
def test_mint_to_other_address(self):
# when
self.dstoken.mint_to(self.second_address, Wad(100000)).transact()
# then
assert self.dstoken.balance_of(self.second_address) == Wad(100000)
def test_mint_generates_transfer(self):
# when
receipt = self.dstoken.mint(Wad(100000)).transact()
# then
assert len(receipt.transfers) == 1
assert receipt.transfers[0].token_address == self.dstoken.address
assert receipt.transfers[0].from_address == Address('0x0000000000000000000000000000000000000000')
assert receipt.transfers[0].to_address == self.our_address
assert receipt.transfers[0].value == Wad(100000)
def test_burn(self):
# given
self.dstoken.mint(Wad(100000)).transact()
# when
self.dstoken.burn(Wad(40000)).transact()
# then
assert self.dstoken.balance_of(self.our_address) == Wad(60000)
def test_burn_from_other_address(self):
# given
self.dstoken.mint_to(self.second_address, Wad(100000)).transact()
# when
self.dstoken.approve(self.our_address).transact(from_address=self.second_address)
self.dstoken.burn_from(self.second_address, Wad(40000)).transact()
# then
assert self.dstoken.balance_of(self.second_address) == Wad(60000)
def test_burn_generates_transfer(self):
# given
self.dstoken.mint(Wad(100000)).transact()
# when
receipt = self.dstoken.burn(Wad(40000)).transact()
# then
assert len(receipt.transfers) == 1
assert receipt.transfers[0].token_address == self.dstoken.address
assert receipt.transfers[0].from_address == self.our_address
assert receipt.transfers[0].to_address == Address('0x0000000000000000000000000000000000000000')
assert receipt.transfers[0].value == Wad(40000)
def test_should_have_printable_representation(self):
assert repr(self.dstoken) == f"DSToken('{self.dstoken.address}')"
class TestDSEthToken:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.dsethtoken = DSEthToken.deploy(self.web3)
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
DSEthToken(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_deposit(self):
# when
self.dsethtoken.deposit(Wad(100000)).transact()
# then
assert self.dsethtoken.balance_of(self.our_address) == Wad(100000)
def test_withdraw(self):
# given
self.dsethtoken.deposit(Wad(100000)).transact()
# when
self.dsethtoken.withdraw(Wad(40000)).transact()
# then
assert self.dsethtoken.balance_of(self.our_address) == Wad(60000)
def test_should_have_printable_representation(self):
assert repr(self.dsethtoken) == f"DSEthToken('{self.dsethtoken.address}')"
================================================
FILE: tests/test_transactional.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly
from pymaker.numeric import Wad
from pymaker.token import DSToken
from pymaker.transactional import TxManager
class TestTxManager:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.other_address = Address(self.web3.eth.accounts[1])
self.tx = TxManager.deploy(self.web3)
self.token1 = DSToken.deploy(self.web3, 'ABC')
self.token1.mint(Wad.from_number(1000000)).transact()
self.token2 = DSToken.deploy(self.web3, 'DEF')
self.token2.mint(Wad.from_number(1000000)).transact()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
TxManager(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_owner(self):
assert self.tx.owner() == self.our_address
def test_approve(self):
# given
assert self.token1.allowance_of(self.our_address, self.tx.address) == Wad(0)
assert self.token2.allowance_of(self.our_address, self.tx.address) == Wad(0)
# when
self.tx.approve([self.token1, self.token2], directly())
# then
assert self.token1.allowance_of(self.our_address, self.tx.address) == Wad(2**256-1)
assert self.token2.allowance_of(self.our_address, self.tx.address) == Wad(2**256-1)
def test_execute_zero_calls(self):
# given
self.tx.approve([self.token1], directly())
# when
res = self.tx.execute([self.token1.address], []).transact()
# then
assert res.successful
def test_execute_one_call(self):
# given
self.tx.approve([self.token1], directly())
# when
res = self.tx.execute([self.token1.address],
[self.token1.transfer(self.other_address, Wad.from_number(500)).invocation()]).transact()
# then
assert res.successful
assert self.token1.balance_of(self.our_address) == Wad.from_number(999500)
assert self.token1.balance_of(self.other_address) == Wad.from_number(500)
def test_execute_one_call_fails_if_no_approval(self):
# given
# [no approval]
# when
res = self.tx.execute([self.token1.address],
[self.token1.transfer(self.other_address, Wad.from_number(500)).invocation()]).transact()
# then
assert res is None
assert self.token1.balance_of(self.our_address) == Wad.from_number(1000000)
assert self.token1.balance_of(self.other_address) == Wad.from_number(0)
def test_execute_multiple_calls_with_multiple_tokens(self):
# given
self.tx.approve([self.token1, self.token2], directly())
# when
res = self.tx.execute([self.token1.address, self.token2.address],
[self.token1.transfer(self.other_address, Wad.from_number(500)).invocation(),
self.token1.transfer(self.other_address, Wad.from_number(200)).invocation(),
self.token2.transfer(self.other_address, Wad.from_number(150)).invocation()]).transact()
# then
assert res.successful
assert self.token1.balance_of(self.our_address) == Wad.from_number(999300)
assert self.token1.balance_of(self.other_address) == Wad.from_number(700)
assert self.token2.balance_of(self.our_address) == Wad.from_number(999850)
assert self.token2.balance_of(self.other_address) == Wad.from_number(150)
def test_should_have_printable_representation(self):
assert repr(self.tx) == f"TxManager('{self.tx.address}')"
================================================
FILE: tests/test_util.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import asyncio
import time
from unittest.mock import Mock, call
import pytest
from web3 import Web3
from pymaker import Address
from pymaker.util import synchronize, int_to_bytes32, bytes_to_int, bytes_to_hexstring, hexstring_to_bytes, \
AsyncCallback, chain
async def async_return(result):
return result
async def async_exception():
await asyncio.sleep(0.1)
raise Exception("Exception to be passed further down")
def mocked_web3(block_0_hash: str) -> Web3:
def side_effect(block_number):
if block_number == 0:
return {'hash': block_0_hash}
else:
raise Exception("Unknown block number queried")
web3 = Mock(Web3)
web3.eth = Mock()
web3.eth.getBlock = Mock(side_effect=side_effect)
return web3
def test_chain_should_recognize_ethlive():
# given
web3 = mocked_web3(block_0_hash="0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
# expect
assert chain(web3) == "ethlive"
def test_chain_should_recognize_kovan():
# given
web3 = mocked_web3(block_0_hash="0xa3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9")
# expect
assert chain(web3) == "kovan"
def test_chain_should_recognize_ropsten():
# given
web3 = mocked_web3(block_0_hash="0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d")
# expect
assert chain(web3) == "ropsten"
def test_chain_should_recognize_morden():
# given
web3 = mocked_web3(block_0_hash="0x0cd786a2425d16f152c658316c423e6ce1181e15c3295826d7c9904cba9ce303")
# expect
assert chain(web3) == "morden"
def test_chain_should_report_unknown_chains_as_unknown():
# given
web3 = mocked_web3(block_0_hash="0x0000000000011111111222222333333333333555555555555666666444444333")
# expect
assert chain(web3) == "unknown"
def mocked_web3_transaction_count(address: Address, latest: int, pending: int) -> Web3:
def side_effect(param_address, param_mode):
assert param_address == address.address
if param_mode == 'latest':
return latest
elif param_mode == 'pending':
return pending
else:
raise Exception("Unknown mode")
web3 = Mock(Web3)
web3.eth = Mock()
web3.eth.getTransactionCount = Mock(side_effect=side_effect)
return web3
def test_synchronize_should_return_empty_list_for_no_futures():
assert synchronize([]) == []
def test_synchronize_should_return_results_of_all_async_calls():
assert synchronize([async_return(1)]) == [1]
assert synchronize([async_return(1), async_return(2)]) == [1, 2]
assert synchronize([async_return(1), async_return(2), async_return(3)]) == [1, 2, 3]
def test_synchronize_should_pass_exceptions():
with pytest.raises(Exception):
synchronize([async_return(1), async_exception(), async_return(3)])
def test_int_to_bytes32():
assert int_to_bytes32(0) == bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
assert int_to_bytes32(1) == bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])
assert int_to_bytes32(512) == bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00])
assert int_to_bytes32(2**256-1) == bytes([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
def test_bytes_to_int():
assert bytes_to_int(bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) == 0
assert bytes_to_int(bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])) == 1
assert bytes_to_int(bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01])) == 257
assert bytes_to_int(bytes([0x00])) == 0
assert bytes_to_int(bytes([0x01, 0x01])) == 257
assert bytes_to_int(bytes([0x00, 0x01, 0x01])) == 257
assert bytes_to_int(bytes([0x00, 0x00, 0x01, 0x01])) == 257
assert bytes_to_int(bytes([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])) == 2**256-1
def test_bytes_to_int_from_string():
assert bytes_to_int('\x00') == 0
assert bytes_to_int('\x01') == 1
assert bytes_to_int('\x01\x01') == 257
assert bytes_to_int('\x00\x01\x01') == 257
assert bytes_to_int('\x00\x00\x01\x01') == 257
def test_bytes_to_int_from_int_should_fail():
with pytest.raises(AssertionError):
bytes_to_int(0)
def test_bytes_to_hexstring():
assert bytes_to_hexstring(bytes([0x00])) == '0x00'
assert bytes_to_hexstring(bytes([0x01, 0x02, 0x03])) == '0x010203'
assert bytes_to_hexstring(bytes([0xff, 0xff])) == '0xffff'
def test_hexstring_to_bytes():
assert hexstring_to_bytes('0x00') == bytes([0x00])
assert hexstring_to_bytes('0x010203') == bytes([0x01, 0x02, 0x03])
assert hexstring_to_bytes('0xffff') == bytes([0xff, 0xff])
class TestAsyncCallback:
@pytest.fixture
def callbacks(self):
class Callbacks:
counter = 0
def short_running_callback(self):
self.counter += 1
def long_running_callback(self):
time.sleep(1)
self.counter += 1
return Callbacks()
def test_should_call_callback(self, callbacks):
# given
async_callback = AsyncCallback(callbacks.short_running_callback)
# when
result = async_callback.trigger()
# then
assert result
assert callbacks.counter == 1
def test_should_not_call_callback_if_previous_one_is_still_running(self, callbacks):
# given
async_callback = AsyncCallback(callbacks.long_running_callback)
# when
result1 = async_callback.trigger()
result2 = async_callback.trigger()
# and
time.sleep(2)
# then
assert result1
assert not result2
assert callbacks.counter == 1
def test_should_call_callback_again_if_previous_one_is_finished(self, callbacks):
# given
async_callback = AsyncCallback(callbacks.long_running_callback)
# when
result1 = async_callback.trigger()
time.sleep(2)
# and
result2 = async_callback.trigger()
time.sleep(2)
# then
assert result1
assert result2
assert callbacks.counter == 2
def test_should_wait_for_the_callback_to_finish(self, callbacks):
# given
async_callback = AsyncCallback(callbacks.long_running_callback)
async_callback.trigger()
assert callbacks.counter == 0
# when
async_callback.wait()
# then
assert callbacks.counter == 1
def test_should_call_on_start_and_on_finish_before_and_after_the_callback(self, callbacks):
# given
mock = Mock()
on_start = mock.on_start
callback = mock.callback
on_finish = mock.on_finish
# when
async_callback = AsyncCallback(callback)
async_callback.trigger(on_start, on_finish)
# then
assert mock.mock_calls == [call.on_start(), call.callback(), call.on_finish()]
================================================
FILE: tests/test_vault.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import pytest
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.vault import DSVault
class TestDSVault:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.dsvault = DSVault.deploy(self.web3)
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
DSVault(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_authority(self):
# given
some_address = Address('0x0000000000111111111122222222223333333333')
# when
self.dsvault.set_authority(some_address).transact()
# then
assert self.dsvault.authority() == some_address
def test_should_have_printable_representation(self):
assert repr(self.dsvault) == f"DSVault('{self.dsvault.address}')"
================================================
FILE: tests/test_zrx.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import json
import pkg_resources
import pytest
from mock import Mock
from web3 import Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly
from pymaker.deployment import deploy_contract
from pymaker.numeric import Wad
from pymaker.token import DSToken, ERC20Token
from pymaker.zrx import ZrxExchange, Order, ZrxRelayerApi
from tests.helpers import is_hashable, wait_until_mock_called
PAST_BLOCKS = 100
class TestZrx:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract(self.web3, 'ZRXToken'))
self.token_transfer_proxy_address = deploy_contract(self.web3, 'TokenTransferProxy')
self.exchange = ZrxExchange.deploy(self.web3, self.zrx_token.address, self.token_transfer_proxy_address)
token_proxy_abi = json.loads(pkg_resources.resource_string('pymaker.deployment', f'abi/TokenTransferProxy.abi'))
self.web3.eth.contract(abi=token_proxy_abi)\
(address=self.token_transfer_proxy_address.address).functions.addAuthorizedAddress(
self.exchange.address.address).transact()
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1.mint(Wad.from_number(100)).transact()
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2.mint(Wad.from_number(100)).transact()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
ZrxExchange(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_correct_deployment(self):
# expect
assert self.exchange is not None
assert self.exchange.address is not None
assert self.exchange.zrx_token() == self.zrx_token.address
assert self.exchange.token_transfer_proxy() == self.token_transfer_proxy_address
def test_approval(self):
# given
assert self.token1.allowance_of(self.our_address, self.token_transfer_proxy_address) == Wad(0)
assert self.zrx_token.allowance_of(self.our_address, self.token_transfer_proxy_address) == Wad(0)
# when
self.exchange.approve([self.token1], directly())
# then
assert self.token1.allowance_of(self.our_address, self.token_transfer_proxy_address) > Wad(0)
assert self.zrx_token.allowance_of(self.our_address, self.token_transfer_proxy_address) > Wad(0)
def test_create_order(self):
# when
order = self.exchange.create_order(pay_token=Address("0x0202020202020202020202020202020202020202"),
pay_amount=Wad.from_number(100),
buy_token=Address("0x0101010101010101010101010101010101010101"),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# then
assert order.maker == Address(self.web3.eth.defaultAccount)
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_token == Address("0x0202020202020202020202020202020202020202")
assert order.pay_amount == Wad.from_number(100)
assert order.buy_token == Address("0x0101010101010101010101010101010101010101")
assert order.buy_amount == Wad.from_number(2.5)
assert order.salt >= 0
assert order.expiration == 1763920792
assert order.exchange_contract_address == self.exchange.address
# and
# [fees should be zero by default]
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.fee_recipient == Address("0x0000000000000000000000000000000000000000")
def test_get_order_hash(self):
# given
order = self.exchange.create_order(pay_token=Address("0x0202020202020202020202020202020202020202"),
pay_amount=Wad.from_number(100),
buy_token=Address("0x0101010101010101010101010101010101010101"),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# when
order_hash = self.exchange.get_order_hash(order)
# then
assert order_hash.startswith('0x')
assert len(order_hash) == 66
def test_sign_order(self):
# given
order = self.exchange.create_order(pay_token=Address("0x0202020202020202020202020202020202020202"),
pay_amount=Wad.from_number(100),
buy_token=Address("0x0101010101010101010101010101010101010101"),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# when
signed_order = self.exchange.sign_order(order)
# then
assert signed_order.ec_signature_r.startswith('0x')
assert len(signed_order.ec_signature_r) == 66
assert signed_order.ec_signature_s.startswith('0x')
assert len(signed_order.ec_signature_s) == 66
assert signed_order.ec_signature_v in [27, 28]
def test_cancel_order(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(10),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad(0)
# when
self.exchange.cancel_order(signed_order).transact()
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad.from_number(4)
def test_fill_order(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(10),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad(0)
# when
self.exchange.fill_order(signed_order, Wad.from_number(3.5)).transact()
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad.from_number(3.5)
def test_remaining_buy_amount_and_remaining_sell_amount(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(10),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert signed_order.remaining_sell_amount == Wad.from_number(10)
assert signed_order.remaining_buy_amount == Wad.from_number(4)
# when
self.exchange.fill_order(signed_order, Wad.from_number(3.5)).transact()
# then
assert signed_order.remaining_sell_amount == Wad.from_number(1.25)
assert signed_order.remaining_buy_amount == Wad.from_number(0.5)
def test_past_fill(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(10),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
self.exchange.fill_order(self.exchange.sign_order(order), Wad.from_number(3)).transact()
# then
past_fill = self.exchange.past_fill(PAST_BLOCKS)
assert len(past_fill) == 1
assert past_fill[0].maker == self.our_address
assert past_fill[0].taker == self.our_address
assert past_fill[0].fee_recipient == Address("0x0000000000000000000000000000000000000000")
assert past_fill[0].pay_token == self.token1.address
assert past_fill[0].buy_token == self.token2.address
assert past_fill[0].filled_pay_amount == Wad.from_number(7.5)
assert past_fill[0].filled_buy_amount == Wad.from_number(3)
assert past_fill[0].paid_maker_fee == Wad.from_number(0)
assert past_fill[0].paid_taker_fee == Wad.from_number(0)
assert past_fill[0].tokens.startswith('0x')
assert past_fill[0].order_hash == self.exchange.get_order_hash(self.exchange.sign_order(order))
assert past_fill[0].raw['blockNumber'] > 0
def test_past_cancel(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_token=self.token1.address, pay_amount=Wad.from_number(10),
buy_token=self.token2.address, buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
self.exchange.cancel_order(self.exchange.sign_order(order)).transact()
# then
past_cancel = self.exchange.past_cancel(PAST_BLOCKS)
assert len(past_cancel) == 1
assert past_cancel[0].maker == self.our_address
assert past_cancel[0].fee_recipient == Address("0x0000000000000000000000000000000000000000")
assert past_cancel[0].pay_token == self.token1.address
assert past_cancel[0].cancelled_pay_amount == Wad.from_number(10)
assert past_cancel[0].buy_token == self.token2.address
assert past_cancel[0].cancelled_buy_amount == Wad.from_number(4)
assert past_cancel[0].tokens.startswith('0x')
assert past_cancel[0].order_hash == self.exchange.get_order_hash(self.exchange.sign_order(order))
assert past_cancel[0].raw['blockNumber'] > 0
def test_should_have_printable_representation(self):
assert repr(self.exchange) == f"ZrxExchange('{self.exchange.address}')"
class TestOrder:
def test_should_be_comparable(self):
# given
order1 = Order(exchange=None,
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_token=Address("0x323b5d4c32345ced77393b3530b1eed0f346429d"),
pay_amount=Wad(10000000000000000),
buy_token=Address("0xef7fff64389b814a946f3e92105513705ca6b990"),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
ec_signature_r="0xf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc3",
ec_signature_s="0x15baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab964",
ec_signature_v=28)
order2 = Order(exchange=None,
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_token=Address("0x323b5d4c32345ced77393b3530b1eed0f346429d"),
pay_amount=Wad(10000000000000000),
buy_token=Address("0xef7fff64389b814a946f3e92105513705ca6b990"),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
ec_signature_r="0xf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc3",
ec_signature_s="0x15baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab964",
ec_signature_v=28)
# expect
assert order1 == order2
# when
order2.maker_fee = Wad.from_number(124)
# then
assert order1 != order2
# when
order1.maker_fee = Wad.from_number(124)
# then
assert order1 == order2
def test_should_be_hashable(self):
# given
order = Order(exchange=None,
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_token=Address("0x323b5d4c32345ced77393b3530b1eed0f346429d"),
pay_amount=Wad(10000000000000000),
buy_token=Address("0xef7fff64389b814a946f3e92105513705ca6b990"),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
ec_signature_r="0xf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc3",
ec_signature_s="0x15baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab964",
ec_signature_v=28)
# expect
assert is_hashable(order)
def test_parse_signed_json_order(self):
# given
json_order = json.loads("""{
"orderHash": "0x02266a4887256fdf16b47ca13e3f2cca76f93724842f3f7ddf55d92fb6601b6f",
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"maker": "0x0046cac6668bef45b517a1b816a762f4f8add2a9",
"taker": "0x0000000000000000000000000000000000000000",
"makerTokenAddress": "0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4",
"takerTokenAddress": "0x2956356cd2a2bf3202f771f50d3d14a367b48070",
"feeRecipient": "0xa258b39954cef5cb142fd567a46cddb31a670124",
"makerTokenAmount": "11000000000000000000",
"takerTokenAmount": "30800000000000000",
"makerFee": "0",
"takerFee": "0",
"expirationUnixTimestampSec": "1511988904",
"salt": "50626048444772008084444062440502087868712695090943879708059561407114509847312",
"ecSignature": {
"r": "0xf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc3",
"s": "0x15baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab964",
"v": 28
}
}""")
# when
order = Order.from_json(None, json_order)
# then
assert order.exchange_contract_address == Address("0x12459c951127e0c374ff9105dda097662a027093")
assert order.maker == Address("0x0046cac6668bef45b517a1b816a762f4f8add2a9")
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_token == Address("0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4")
assert order.buy_token == Address("0x2956356cd2a2bf3202f771f50d3d14a367b48070")
assert order.fee_recipient == Address("0xa258b39954cef5cb142fd567a46cddb31a670124")
assert order.pay_amount == Wad.from_number(11)
assert order.buy_amount == Wad.from_number(0.0308)
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.expiration == 1511988904
assert order.salt == 50626048444772008084444062440502087868712695090943879708059561407114509847312
assert order.ec_signature_r == "0xf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc3"
assert order.ec_signature_s == "0x15baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab964"
assert order.ec_signature_v == 28
def test_parse_unsigned_json_order(self):
# given
json_order = json.loads("""{
"orderHash": "0x02266a4887256fdf16b47ca13e3f2cca76f93724842f3f7ddf55d92fb6601b6f",
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"maker": "0x0046cac6668bef45b517a1b816a762f4f8add2a9",
"taker": "0x0000000000000000000000000000000000000000",
"makerTokenAddress": "0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4",
"takerTokenAddress": "0x2956356cd2a2bf3202f771f50d3d14a367b48070",
"feeRecipient": "0xa258b39954cef5cb142fd567a46cddb31a670124",
"makerTokenAmount": "11000000000000000000",
"takerTokenAmount": "30800000000000000",
"makerFee": "0",
"takerFee": "0",
"expirationUnixTimestampSec": "1511988904",
"salt": "50626048444772008084444062440502087868712695090943879708059561407114509847312"
}""")
# when
order = Order.from_json(None, json_order)
# then
assert order.exchange_contract_address == Address("0x12459c951127e0c374ff9105dda097662a027093")
assert order.maker == Address("0x0046cac6668bef45b517a1b816a762f4f8add2a9")
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_token == Address("0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4")
assert order.buy_token == Address("0x2956356cd2a2bf3202f771f50d3d14a367b48070")
assert order.fee_recipient == Address("0xa258b39954cef5cb142fd567a46cddb31a670124")
assert order.pay_amount == Wad.from_number(11)
assert order.buy_amount == Wad.from_number(0.0308)
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.expiration == 1511988904
assert order.salt == 50626048444772008084444062440502087868712695090943879708059561407114509847312
assert order.ec_signature_r is None
assert order.ec_signature_s is None
assert order.ec_signature_v is None
def test_serialize_order_to_json_without_fees(self):
# given
order = Order(exchange=None,
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_token=Address("0x323b5d4c32345ced77393b3530b1eed0f346429d"),
pay_amount=Wad(10000000000000000),
buy_token=Address("0xef7fff64389b814a946f3e92105513705ca6b990"),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
ec_signature_r="0xde21c90d3db3abdc8bdc5fafb1f5432a1dede4d621508e7d96fb2ebc15d7eb2f",
ec_signature_s="0x74f3cb421f75727b78ae98157ddce6a77b46c8714f5848d70f6da083527e1719",
ec_signature_v=28)
# when
json_order = order.to_json_without_fees()
# then
assert json_order == json.loads("""{
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
"taker": "0x0000000000000000000000000000000000000000",
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
"makerTokenAmount": "10000000000000000",
"takerTokenAmount": "20000000000000000",
"expirationUnixTimestampSec": "42",
"salt": "67006738228878699843088602623665307406148487219438534730168799356281242528500"
}""")
def test_serialize_order_to_json(self):
# given
order = Order(exchange=None,
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_token=Address("0x323b5d4c32345ced77393b3530b1eed0f346429d"),
pay_amount=Wad(10000000000000000),
buy_token=Address("0xef7fff64389b814a946f3e92105513705ca6b990"),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
ec_signature_r="0xde21c90d3db3abdc8bdc5fafb1f5432a1dede4d621508e7d96fb2ebc15d7eb2f",
ec_signature_s="0x74f3cb421f75727b78ae98157ddce6a77b46c8714f5848d70f6da083527e1719",
ec_signature_v=28)
# when
json_order = order.to_json()
# then
assert json_order == json.loads("""{
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
"taker": "0x0000000000000000000000000000000000000000",
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
"feeRecipient": "0x6666666666666666666666666666666666666666",
"makerTokenAmount": "10000000000000000",
"takerTokenAmount": "20000000000000000",
"makerFee": "123000000000000000000",
"takerFee": "456000000000000000000",
"expirationUnixTimestampSec": "42",
"salt": "67006738228878699843088602623665307406148487219438534730168799356281242528500",
"ecSignature": {
"r": "0xde21c90d3db3abdc8bdc5fafb1f5432a1dede4d621508e7d96fb2ebc15d7eb2f",
"s": "0x74f3cb421f75727b78ae98157ddce6a77b46c8714f5848d70f6da083527e1719",
"v": 28
}
}""")
================================================
FILE: tests/test_zrxv2.py
================================================
# This file is part of Maker Keeper Framework.
#
# Copyright (C) 2017-2018 reverendus
#
# This program is free software: you can redistribute it and/or modify
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import json
import pkg_resources
import pytest
from eth_abi import encode_single
from mock import Mock
from web3 import EthereumTesterProvider, Web3, HTTPProvider
from pymaker import Address
from pymaker.approval import directly
from pymaker.deployment import deploy_contract
from pymaker.numeric import Wad
from pymaker.token import DSToken, ERC20Token
from pymaker.util import bytes_to_hexstring
from pymaker.zrxv2 import ZrxExchangeV2, Order, ZrxRelayerApiV2, ERC20Asset
from tests.helpers import is_hashable, wait_until_mock_called
PAST_BLOCKS = 100
class TestZrxV2:
def setup_method(self):
self.web3 = Web3(HTTPProvider("http://localhost:8555"))
self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
self.our_address = Address(self.web3.eth.defaultAccount)
self.zrx_token = ERC20Token(web3=self.web3, address=deploy_contract(self.web3, 'ZRXToken'))
self.asset_proxy = deploy_contract(self.web3, 'ExchangeV2-ERC20Proxy')
self.exchange = ZrxExchangeV2.deploy(self.web3, None) #"0xf47261b0" + self.zrx_token.address.address - unused yet
self.exchange._contract.functions.registerAssetProxy(self.asset_proxy.address).transact()
token_proxy_abi = json.loads(pkg_resources.resource_string('pymaker.deployment', f'abi/ExchangeV2-ERC20Proxy.abi'))
asset_proxy_contract = self.web3.eth.contract(abi=token_proxy_abi)(address=self.asset_proxy.address)
asset_proxy_contract.functions.addAuthorizedAddress(self.exchange.address.address).transact()
self.token1 = DSToken.deploy(self.web3, 'AAA')
self.token1.mint(Wad.from_number(100)).transact()
self.token2 = DSToken.deploy(self.web3, 'BBB')
self.token2.mint(Wad.from_number(100)).transact()
def test_fail_when_no_contract_under_that_address(self):
# expect
with pytest.raises(Exception):
ZrxExchangeV2(web3=self.web3, address=Address('0xdeadadd1e5500000000000000000000000000000'))
def test_correct_deployment(self):
# expect
assert self.exchange is not None
assert self.exchange.address is not None
assert self.exchange.zrx_asset() == "0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498"
assert self.exchange.zrx_token() == Address("0xe41d2489571d322189246dafa5ebde1f4699f498")
assert self.exchange.asset_transfer_proxy(ERC20Asset.ID) == self.asset_proxy
def test_approval(self):
# given
assert self.token1.allowance_of(self.our_address, self.asset_proxy) == Wad(0)
assert self.zrx_token.allowance_of(self.our_address, self.asset_proxy) == Wad(0)
# when
self.exchange.approve([self.token1], directly())
# then
assert self.token1.allowance_of(self.our_address, self.asset_proxy) > Wad(0)
#TODO commented out until we figure out how to handle the 0x token
# assert self.zrx_token.allowance_of(self.our_address, self.asset_proxy) > Wad(0)
def test_create_order(self):
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(Address("0x0202020202020202020202020202020202020202")),
pay_amount=Wad.from_number(100),
buy_asset=ERC20Asset(Address("0x0101010101010101010101010101010101010101")),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# then
assert order.maker == Address(self.web3.eth.defaultAccount)
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_asset == ERC20Asset(Address("0x0202020202020202020202020202020202020202"))
assert order.pay_amount == Wad.from_number(100)
assert order.buy_asset == ERC20Asset(Address("0x0101010101010101010101010101010101010101"))
assert order.buy_amount == Wad.from_number(2.5)
assert order.salt >= 0
assert order.expiration == 1763920792
assert order.exchange_contract_address == self.exchange.address
# and
# [fees should be zero by default]
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.fee_recipient == Address("0x0000000000000000000000000000000000000000")
def test_get_order_hash(self):
# given
order = self.exchange.create_order(pay_asset=ERC20Asset(Address("0x0202020202020202020202020202020202020202")),
pay_amount=Wad.from_number(100),
buy_asset=ERC20Asset(Address("0x0101010101010101010101010101010101010101")),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# when
order_hash = self.exchange.get_order_hash(order)
# then
assert order_hash.startswith('0x')
assert len(order_hash) == 66
def test_sign_order(self):
# given
order = self.exchange.create_order(pay_asset=ERC20Asset(Address("0x0202020202020202020202020202020202020202")),
pay_amount=Wad.from_number(100),
buy_asset=ERC20Asset(Address("0x0101010101010101010101010101010101010101")),
buy_amount=Wad.from_number(2.5), expiration=1763920792)
# when
signed_order = self.exchange.sign_order(order)
# then
assert signed_order.signature.startswith('0x')
assert signed_order.signature.endswith('03')
assert len(signed_order.signature) == 134
def test_cancel_order(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(self.token1.address), pay_amount=Wad.from_number(10),
buy_asset=ERC20Asset(self.token2.address), buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad(0)
# when
self.exchange.cancel_order(signed_order).transact()
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad.from_number(4)
def test_fill_order(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(self.token1.address), pay_amount=Wad.from_number(10),
buy_asset=ERC20Asset(self.token2.address), buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad(0)
# when
self.exchange.fill_order(signed_order, Wad.from_number(3.5)).transact()
# then
assert self.exchange.get_unavailable_buy_amount(signed_order) == Wad.from_number(3.5)
def test_remaining_buy_amount_and_remaining_sell_amount(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(self.token1.address), pay_amount=Wad.from_number(10),
buy_asset=ERC20Asset(self.token2.address), buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
signed_order = self.exchange.sign_order(order)
# then
assert signed_order.remaining_sell_amount == Wad.from_number(10)
assert signed_order.remaining_buy_amount == Wad.from_number(4)
# when
self.exchange.fill_order(signed_order, Wad.from_number(3.5)).transact()
# then
assert signed_order.remaining_sell_amount == Wad.from_number(1.25)
assert signed_order.remaining_buy_amount == Wad.from_number(0.5)
def test_past_fill(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(self.token1.address), pay_amount=Wad.from_number(10),
buy_asset=ERC20Asset(self.token2.address), buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
self.exchange.fill_order(self.exchange.sign_order(order), Wad.from_number(3)).transact()
# then
past_fill = self.exchange.past_fill(PAST_BLOCKS)
assert len(past_fill) == 1
assert past_fill[0].sender == self.our_address
assert past_fill[0].maker == self.our_address
assert past_fill[0].taker == self.our_address
assert past_fill[0].fee_recipient == Address("0x0000000000000000000000000000000000000000")
assert past_fill[0].pay_asset == ERC20Asset(self.token1.address)
assert past_fill[0].buy_asset == ERC20Asset(self.token2.address)
assert past_fill[0].filled_pay_amount == Wad.from_number(7.5)
assert past_fill[0].filled_buy_amount == Wad.from_number(3)
assert past_fill[0].paid_maker_fee == Wad.from_number(0)
assert past_fill[0].paid_taker_fee == Wad.from_number(0)
assert past_fill[0].order_hash == self.exchange.get_order_hash(self.exchange.sign_order(order))
assert past_fill[0].raw['blockNumber'] > 0
def test_past_cancel(self):
# given
self.exchange.approve([self.token1, self.token2], directly())
# when
order = self.exchange.create_order(pay_asset=ERC20Asset(self.token1.address), pay_amount=Wad.from_number(10),
buy_asset=ERC20Asset(self.token2.address), buy_amount=Wad.from_number(4),
expiration=1763920792)
# and
self.exchange.cancel_order(self.exchange.sign_order(order)).transact()
# then
past_cancel = self.exchange.past_cancel(PAST_BLOCKS)
assert len(past_cancel) == 1
assert past_cancel[0].maker == self.our_address
assert past_cancel[0].fee_recipient == Address("0x0000000000000000000000000000000000000000")
assert past_cancel[0].sender == self.our_address
assert past_cancel[0].pay_asset == ERC20Asset(self.token1.address)
assert past_cancel[0].buy_asset == ERC20Asset(self.token2.address)
assert past_cancel[0].order_hash == self.exchange.get_order_hash(self.exchange.sign_order(order))
assert past_cancel[0].raw['blockNumber'] > 0
def test_should_have_printable_representation(self):
assert repr(self.exchange) == f"ZrxExchangeV2('{self.exchange.address}')"
class TestOrder:
def test_should_be_comparable(self):
# given
order1 = Order(exchange=None,
sender=Address("0x0000000000000000000000000000000000000000"),
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_asset=ERC20Asset(Address("0x323b5d4c32345ced77393b3530b1eed0f346429d")),
pay_amount=Wad(10000000000000000),
buy_asset=ERC20Asset(Address("0xef7fff64389b814a946f3e92105513705ca6b990")),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
signature="0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403")
order2 = Order(exchange=None,
sender=Address("0x0000000000000000000000000000000000000000"),
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_asset=ERC20Asset(Address("0x323b5d4c32345ced77393b3530b1eed0f346429d")),
pay_amount=Wad(10000000000000000),
buy_asset=ERC20Asset(Address("0xef7fff64389b814a946f3e92105513705ca6b990")),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
signature="0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403")
# expect
assert order1 == order2
# when
order2.maker_fee = Wad.from_number(124)
# then
assert order1 != order2
# when
order1.maker_fee = Wad.from_number(124)
# then
assert order1 == order2
def test_should_be_hashable(self):
# given
order = Order(exchange=None,
sender=Address("0x0000000000000000000000000000000000000000"),
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_asset=ERC20Asset(Address("0x323b5d4c32345ced77393b3530b1eed0f346429d")),
pay_amount=Wad(10000000000000000),
buy_asset=ERC20Asset(Address("0xef7fff64389b814a946f3e92105513705ca6b990")),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
signature="0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403")
# expect
assert is_hashable(order)
def test_parse_signed_json_order(self):
# given
json_order = json.loads("""{
"orderHash": "0x02266a4887256fdf16b47ca13e3f2cca76f93724842f3f7ddf55d92fb6601b6f",
"exchangeAddress": "0x12459C951127e0c374FF9105DdA097662A027093",
"senderAddress": "0x0000000000000000000000000000000000000000",
"makerAddress": "0x0046cac6668bef45b517a1b816a762f4f8add2a9",
"takerAddress": "0x0000000000000000000000000000000000000000",
"makerAssetData": "0xf47261b059adcf176ed2f6788a41b8ea4c4904518e62b6a4",
"takerAssetData": "0xf47261b02956356cd2a2bf3202f771f50d3d14a367b48070",
"feeRecipientAddress": "0xa258b39954cef5cb142fd567a46cddb31a670124",
"makerAssetAmount": "11000000000000000000",
"takerAssetAmount": "30800000000000000",
"makerFee": "0",
"takerFee": "0",
"expirationTimeSeconds": "1511988904",
"salt": "50626048444772008084444062440502087868712695090943879708059561407114509847312",
"signature": "0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403"
}""")
# when
order = Order.from_json(None, json_order)
# then
assert order.exchange_contract_address == Address("0x12459c951127e0c374ff9105dda097662a027093")
assert order.sender == Address("0x0000000000000000000000000000000000000000")
assert order.maker == Address("0x0046cac6668bef45b517a1b816a762f4f8add2a9")
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_asset == ERC20Asset(Address("0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4"))
assert order.buy_asset == ERC20Asset(Address("0x2956356cd2a2bf3202f771f50d3d14a367b48070"))
assert order.fee_recipient == Address("0xa258b39954cef5cb142fd567a46cddb31a670124")
assert order.pay_amount == Wad.from_number(11)
assert order.buy_amount == Wad.from_number(0.0308)
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.expiration == 1511988904
assert order.salt == 50626048444772008084444062440502087868712695090943879708059561407114509847312
assert order.signature == "0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403"
def test_parse_unsigned_json_order(self):
# given
json_order = json.loads("""{
"orderHash": "0x02266a4887256fdf16b47ca13e3f2cca76f93724842f3f7ddf55d92fb6601b6f",
"exchangeAddress": "0x12459C951127e0c374FF9105DdA097662A027093",
"senderAddress": "0x0000000000000000000000000000000000000000",
"makerAddress": "0x0046cac6668bef45b517a1b816a762f4f8add2a9",
"takerAddress": "0x0000000000000000000000000000000000000000",
"makerAssetData": "0xf47261b059adcf176ed2f6788a41b8ea4c4904518e62b6a4",
"takerAssetData": "0xf47261b02956356cd2a2bf3202f771f50d3d14a367b48070",
"feeRecipientAddress": "0xa258b39954cef5cb142fd567a46cddb31a670124",
"makerAssetAmount": "11000000000000000000",
"takerAssetAmount": "30800000000000000",
"makerFee": "0",
"takerFee": "0",
"expirationTimeSeconds": "1511988904",
"salt": "50626048444772008084444062440502087868712695090943879708059561407114509847312"
}""")
# when
order = Order.from_json(None, json_order)
# then
assert order.exchange_contract_address == Address("0x12459c951127e0c374ff9105dda097662a027093")
assert order.maker == Address("0x0046cac6668bef45b517a1b816a762f4f8add2a9")
assert order.taker == Address("0x0000000000000000000000000000000000000000")
assert order.pay_asset == ERC20Asset(Address("0x59adcf176ed2f6788a41b8ea4c4904518e62b6a4"))
assert order.buy_asset == ERC20Asset(Address("0x2956356cd2a2bf3202f771f50d3d14a367b48070"))
assert order.fee_recipient == Address("0xa258b39954cef5cb142fd567a46cddb31a670124")
assert order.pay_amount == Wad.from_number(11)
assert order.buy_amount == Wad.from_number(0.0308)
assert order.maker_fee == Wad.from_number(0)
assert order.taker_fee == Wad.from_number(0)
assert order.expiration == 1511988904
assert order.salt == 50626048444772008084444062440502087868712695090943879708059561407114509847312
assert order.signature is None
def test_serialize_order_to_json_without_fees(self):
# given
order = Order(exchange=None,
sender=Address("0x0000000000000000000000000000000000000000"),
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_asset=ERC20Asset(Address("0x323b5d4c32345ced77393b3530b1eed0f346429d")),
pay_amount=Wad(10000000000000000),
buy_asset=ERC20Asset(Address("0xef7fff64389b814a946f3e92105513705ca6b990")),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459C951127e0c374FF9105DdA097662A027093"),
signature="0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403")
# when
json_order = order.to_json_without_fees()
# then
assert json_order == json.loads("""{
"exchangeAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"makerAddress": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
"takerAddress": "0x0000000000000000000000000000000000000000",
"makerAssetData": "0xf47261b0000000000000000000000000323b5d4c32345ced77393b3530b1eed0f346429d",
"takerAssetData": "0xf47261b0000000000000000000000000ef7fff64389b814a946f3e92105513705ca6b990",
"makerAssetAmount": "10000000000000000",
"takerAssetAmount": "20000000000000000",
"expirationTimeSeconds": "42"
}""")
def test_serialize_order_to_json(self):
# given
order = Order(exchange=None,
sender=Address("0x0000000000000000000000000000000000000000"),
maker=Address("0x9e56625509c2f60af937f23b7b532600390e8c8b"),
taker=Address("0x0000000000000000000000000000000000000000"),
maker_fee=Wad.from_number(123),
taker_fee=Wad.from_number(456),
pay_asset=ERC20Asset(Address("0x323b5d4c32345ced77393b3530b1eed0f346429d")),
pay_amount=Wad(10000000000000000),
buy_asset=ERC20Asset(Address("0xef7fff64389b814a946f3e92105513705ca6b990")),
buy_amount=Wad(20000000000000000),
salt=67006738228878699843088602623665307406148487219438534730168799356281242528500,
fee_recipient=Address('0x6666666666666666666666666666666666666666'),
expiration=42,
exchange_contract_address=Address("0x12459c951127e0c374ff9105dda097662a027093"),
signature="0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403")
# when
json_order = order.to_json()
# then
assert json_order == json.loads("""{
"exchangeAddress": "0x12459c951127e0c374ff9105dda097662a027093",
"senderAddress": "0x0000000000000000000000000000000000000000",
"makerAddress": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
"takerAddress": "0x0000000000000000000000000000000000000000",
"makerAssetData": "0xf47261b0000000000000000000000000323b5d4c32345ced77393b3530b1eed0f346429d",
"takerAssetData": "0xf47261b0000000000000000000000000ef7fff64389b814a946f3e92105513705ca6b990",
"makerAssetAmount": "10000000000000000",
"takerAssetAmount": "20000000000000000",
"feeRecipientAddress": "0x6666666666666666666666666666666666666666",
"makerFee": "123000000000000000000",
"takerFee": "456000000000000000000",
"expirationTimeSeconds": "42",
"salt": "67006738228878699843088602623665307406148487219438534730168799356281242528500",
"signature": "0x1bf9f6a3b67b52d40c16387df2cd6283bbdbfc174577743645dd6f4bd828c7dbc315baf69f6c3cc8ac0f62c89264d73accf1ae165cce5d6e2a0b6325c6e4bab96403"
}""")
================================================
FILE: utils/etherdelta-client/.gitignore
================================================
node_modules/
================================================
FILE: utils/etherdelta-client/main.js
================================================
/*!
* This file is part of Maker Keeper Framework.
*
* Copyright (C) 2017-2018 reverendus
*
* This program is free software: you can redistribute it and/or modify
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
var args = require('minimist')(process.argv.slice(2));
const order = args['_'].join(" ");
const url = args['url'];
const retryInterval = args['retry-interval'];
const timeout = args['timeout'];
function publishOrder() {
socket.emit('message', JSON.parse(order));
console.log('Order sent');
}
console.log("Sending order '" + order + "' to " + url);
const io = require('socket.io-client');
const socket = io.connect(url, { transports: ['websocket'] });
socket.on('connect', () => {
console.log("Connected to socket");
publishOrder();
});
socket.on('messageResult', (messageResult) => {
console.log("Response received: ", messageResult);
if (messageResult[0] === 'Added/updated order.') {
console.log("Order placed successfully");
socket.disconnect();
setTimeout(() => process.exit(0), 2500);
}
else {
console.log("Order placement failed");
setTimeout(publishOrder, retryInterval*1000);
}
});
socket.on('disconnect', () => {
console.log('Disconnected from socket');
});
socket.on('reconnect', () => {
console.log('Reconnected to socket');
});
setTimeout(() => {
console.log('Timed out');
process.exit(-1);
}, timeout*1000);
================================================
FILE: utils/etherdelta-client/package.json
================================================
{
"name": "etherdelta-client",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "AGPL",
"dependencies": {
"minimist": "^1.2.0",
"socket.io-client": "^2.0.4"
}
}