├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── erc20token ├── __init__.py ├── exceptions.py ├── provider.py ├── sdk.py ├── utils.py └── version.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── sha3.py.alt └── test ├── conftest.py ├── test_sdk.py └── truffle_env ├── contracts ├── Migrations.sol └── TestToken.sol ├── migrations └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── scripts ├── testrpc.sh └── truffle.sh └── truffle.js /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = erc20token/* 4 | omit = test/* 5 | 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | raise AssertionError 10 | raise NotImplementedError 11 | if __name__ == .__main__.: 12 | ignore_errors = True 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | .idea 4 | .coverage 5 | *.pyc 6 | *.log 7 | MANIFEST 8 | .cache/ 9 | docs/build 10 | dist/ 11 | virtualenv/ 12 | test/truffle_env/build/ 13 | test/truffle_env/node_modules/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.6" 6 | 7 | cache: 8 | - pip 9 | - yarn 10 | 11 | before_install: 12 | # downgrade setuptools to work around the 'tests_require' error 13 | - pip install setuptools==37.0.0 14 | - . $HOME/.nvm/nvm.sh 15 | - nvm install stable 16 | - nvm use stable 17 | - npm install 18 | 19 | install: 20 | - make init 21 | 22 | script: 23 | - make test 24 | 25 | #after_success: 26 | - codecov --token=9c6e620b-d46a-4049-b501-4f61655ec695 27 | 28 | notifications: 29 | email: false -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinecosystem/erc20token-sdk-python/eb23b0fb61dda6757cc4bc7b38a57de78ae09886/CHANGES.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in the project. We look forward to your contribution. In order to make the process as fast 4 | and streamlined as possible, here is a set of guidelines we recommend you follow. 5 | 6 | ## Reporting Issues 7 | First of all, please be sure to check our documentation and [issue archive](https://github.com/kinfoundation/erc20token-sdk-python/issues) 8 | to find out if your issue has already been addressed, or is currently being looked at. 9 | 10 | To start a discussion around a bug or a feature, [open a new issue](https://github.com/kinfoundation/erc20token-sdk-python/issues/new). 11 | When opening an issue, please provide the following information: 12 | 13 | - SDK version and Python version 14 | - OS version 15 | - The issue you are encountering including a stacktrace if applicable 16 | - Steps or a code snippet to reproduce the issue 17 | 18 | For feature requests it is encouraged to include sample code highlighting a use case for the new feature. 19 | 20 | ## Use Github Pull Requests 21 | 22 | All potential code changes should be submitted as pull requests on Github. A pull request should only include 23 | commits directly applicable to its change (e.g. a pull request that adds a new feature should not include PEP8 changes in 24 | an unrelated area of the code). Please check the following guidelines for submitting a new pull request. 25 | 26 | - Ensure that nothing has been broken by your changes by running the test suite. You can do so by running 27 | `make test` in the project root. 28 | - Write clear, self-contained commits. Your commit message should be concise and describe the nature of the change. 29 | - Rebase proactively. Your pull request should be up to date against the current master branch. 30 | - Add tests. All non-trivial changes should include full test coverage. Make sure there are relevant tests to 31 | ensure the code you added continues to work as the project evolves. 32 | - Add docs. This usually applies to new features rather than bug fixes, but new behavior should always be documented. 33 | - Follow the coding style described below. 34 | - Ask questions. If you are confused about something pertaining to the project, feel free to communicate with us. 35 | 36 | ### Code Style 37 | 38 | Code *must* be compliant with [PEP 8](https://www.python.org/dev/peps/pep-0008/). Use the latest version of 39 | [PEP8](https://pypi.python.org/pypi/pep8) or [flake8](https://pypi.python.org/pypi/flake8) to catch issues. 40 | 41 | Git commit messages should include [a summary and proper line wrapping](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 42 | 43 | ## Development 44 | If you are looking to contribute to this SDK, here are the steps to get you started. 45 | 46 | 1. Fork this project 47 | 2. Setup your Python [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs): 48 | ```bash 49 | $ mkvirtualenv erc20-sdk-python 50 | $ workon erc20-sdk-python 51 | ``` 52 | 3. Setup `pip` and `npm` dependencies: 53 | ```bash 54 | $ make init 55 | ``` 56 | 4. Work on code 57 | 5. Test your code locally 58 | ```bash 59 | $ make test 60 | ``` 61 | The `make test` flow is as follows: 62 | - runs `testrpc` with predefined accounts pre-filled with Ether. 63 | - runs `truffle deploy --reset` to compile and deploy your contract. This will aso add some tokens to the first account. 64 | - runs `pytest` to test your code 65 | 66 | 6. Test your code with live testnet (Ropsten). This will also run an additional concurrency test. 67 | ```bash 68 | $ make test-ropsten 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include erc20token *.py 3 | 4 | global-exclude .DS_Store 5 | recursive-exclude test * 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # default target does nothing 3 | .DEFAULT_GOAL: default 4 | default: ; 5 | 6 | export PATH := ./test/truffle_env/node_modules/.bin:$(PATH) 7 | 8 | init: 9 | pip install -r requirements.txt 10 | pip install -r requirements-dev.txt 11 | cd ./test/truffle_env && npm install 12 | .PHONY: init 13 | 14 | test-ropsten: 15 | python -m pytest -v -rs --ropsten --cov=erc20token -s -x test 16 | .PHONY: test 17 | 18 | test: truffle 19 | python -m pytest -v -rs --cov=erc20token -s -x test 20 | .PHONY: test 21 | 22 | truffle: testrpc truffle-clean 23 | cd ./test/truffle_env && npm run-script truffle 24 | .PHONY: truffle 25 | 26 | truffle-clean: 27 | cd ./test/truffle_env && rm -f *.log token_contract_address.txt 28 | 29 | testrpc: 30 | cd ./test/truffle_env && npm run-script testrpc 31 | .PHONY: testrpc 32 | 33 | wheel: 34 | python setup.py bdist_wheel 35 | .PHONY: wheel 36 | 37 | pypi: 38 | twine upload dist/* 39 | .PHONY: pypi 40 | 41 | clean: truffle-clean 42 | rm -f .coverage 43 | find . -name \*.pyc -delete 44 | .PHONY: clean 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ERC20 Token Python SDK 3 | [![Build Status](https://travis-ci.org/kinfoundation/erc20token-sdk-python.svg)](https://travis-ci.org/kinfoundation/erc20token-sdk-python) [![Coverage Status](https://codecov.io/gh/kinfoundation/erc20token-sdk-python/branch/master/graph/badge.svg?token=dOvV9K8oFe)](https://codecov.io/gh/kinfoundation/erc20token-sdk-python) 4 | 5 | ## Disclaimer 6 | 7 | The SDK is still in beta. No warranties are given, use on your own discretion. 8 | 9 | ## Requirements. 10 | 11 | Make sure you have Python 2 >=2.7.9. 12 | 13 | ## Installation 14 | 15 | ```sh 16 | pip install erc20token 17 | ``` 18 | 19 | ### Installation in Google App Engine Python Standard Environment 20 | [GAE Python Standard environment](https://cloud.google.com/appengine/docs/standard/) executes Python 21 | application code using a pre-loaded Python interpreter in a safe sandboxed environment. The interpreter cannot 22 | load Python services with C code; it is a "pure" Python environment. However, the required 23 | [web3 package](https://pypi.python.org/pypi/web3/) requires other packages that are natively implemented, namely 24 | [pysha3](https://pypi.python.org/pypi/pysha3) and [cytoolz](https://pypi.python.org/pypi/cytoolz). 25 | In order to overcome this limitation, do the following: 26 | 1. Replace the `sha3.py` installed by pysha3 with the [attached sha3.py](sha3.py.alt). 27 | 2. Replace the installed `cytoolz` package with the `toolz` package. 28 | 29 | You will still not be able to use the functions `monitor_ether_transactions` and `monitor_token_transactions` 30 | because they launch a thread, and GAE Standard applications cannot spawn threads. 31 | 32 | 33 | ## Usage 34 | 35 | ### Initialization 36 | 37 | To initialize the SDK, you need to provide the following parameters: 38 | - [JSON-RPC API](https://github.com/ethereum/wiki/wiki/JSON-RPC) endpoint URI of your Ethereum node 39 | (for example, http://mainnet.infura.io) 40 | - The address of your token contract 41 | - The ABI of your token contract as json 42 | - (optionally) either your private key, or a keyfile+password 43 | - (optionally) gas price in Gwei 44 | - (optionally) constant gas limit for your transactions 45 | 46 | **NOTE**: if you do not provide a private key or a keyfile, you will NOT be able to use the following functions: 47 | `get_address`, `get_ether_balance`, `get_token_balance`, `send_ether`, `send_tokens`. 48 | 49 | 50 | ```python 51 | import erc20token 52 | import json 53 | 54 | # Init SDK without a private key (for generic blockchain queries) 55 | token_sdk = erc20token.SDK(provider_endpoint_uri='http://localhost:8545', 56 | contract_address='0x04f72aa40046c5fb3b143aaba3ab64d1a82410a7', 57 | contract_abi=json.loads(contract_abi)) 58 | 59 | # Init SDK with a private key 60 | token_sdk = erc20token.SDK(provider_endpoint_uri='http://localhost:8545', 61 | private_key='a60baaa34ed125af0570a3df7d4cd3e80dd5dc5070680573f8de0ecfc1957575', 62 | contract_address='0x04f72aa40046c5fb3b143aaba3ab64d1a82410a7', 63 | contract_abi=json.loads(contract_abi)) 64 | 65 | # Init SDK with a keyfile 66 | # First, create a keyfile from my private key 67 | erc20token.create_keyfile('a60baaa34ed125af0570a3df7d4cd3e80dd5dc5070680573f8de0ecfc1957575', 68 | 'my password', 'keyfile.json') 69 | # Then, init SDK with the keyfile 70 | token_sdk = erc20token.SDK(provider_endpoint_uri='http://localhost:8545', 71 | keyfile='keyfile.json', password='my password', 72 | contract_address='0x04f72aa40046c5fb3b143aaba3ab64d1a82410a7', 73 | contract_abi=json.loads(contract_abi)) 74 | 75 | # Init SDK with custom gas parameters 76 | token_sdk = erc20token.SDK(provider_endpoint_uri='http://localhost:8545', 77 | private_key='a60baaa34ed125af0570a3df7d4cd3e80dd5dc5070680573f8de0ecfc1957575', 78 | contract_address='0x04f72aa40046c5fb3b143aaba3ab64d1a82410a7', 79 | contract_abi=json.loads(contract_abi), 80 | gas_price=10, gas_limit=50000) 81 | ```` 82 | For more examples, see the [SDK test file](test/test_sdk.py). The file also contains pre-defined values for testing 83 | with testrpc and Ropsten. 84 | 85 | 86 | ### Get Wallet Details 87 | ```python 88 | # Get my public address. The address is derived from the private key the SDK was inited with. 89 | address = token_sdk.get_address() 90 | ``` 91 | 92 | ### Get the Number of Issued Tokens 93 | ```python 94 | # Get total supply of tokens 95 | total_supply = token_sdk.get_token_total_supply() 96 | ``` 97 | 98 | ### Getting Account Balance 99 | ```python 100 | # Get Ether balance of my account 101 | eth_balance = token_sdk.get_ether_balance() 102 | 103 | # Get token balance of my account 104 | token_balance = token_sdk.get_token_balance() 105 | 106 | # Get Ether balance of some address 107 | eth_balance = token_sdk.get_address_ether_balance('address') 108 | 109 | # Get token balance of some address 110 | token_balance = token_sdk.get_address_token_balance('address') 111 | ``` 112 | 113 | ### Sending Coin 114 | You can send Ether or tokens: 115 | ```python 116 | # Send Ether from my account to some address. The amount is in Ether. 117 | tx_id = token_sdk.send_ether('address', 10) 118 | 119 | # Send tokens from my account to some address. The amount is in tokens. 120 | tx_id = token_sdk.send_tokens('address', 10) 121 | ``` 122 | If you do not have enough Ether, `send_ether` will raise an exception. 123 | However, if you do not have enough tokens, `send_tokens` will finish successfully. The transaction will end up as 124 | FAILED on the blockchain, consuming all your gas. 125 | 126 | ### Getting Transaction Data 127 | ```python 128 | # Get transaction status 129 | tx_status = token_sdk.get_transaction_status(tx_id) 130 | # Returns one of: 131 | # erc20token.TransactionStatus.UNKNOWN = 0 132 | # erc20token.TransactionStatus.PENDING = 1 133 | # erc20token.TransactionStatus.SUCCESS = 2 134 | # erc20token.TransactionStatus.FAIL = 3 135 | 136 | # Get transaction details 137 | tx_data = token_sdk.get_transaction_data(tx_id) 138 | # Returns an erc20token.TransactionData object containing the following fields: 139 | # from_address - the address this transaction was sent from 140 | # to_address - the address this transaction was sent to. For token transactions, this is the decoded recipient address. 141 | # ether_amount - the amount of transferred Ether. 0 for token transactions. 142 | # token_amount - the amount of transferred tokens. 0 for Ether transactions. 143 | # status - the transaction status, see above. 144 | # num_confirmations - the number of confirmations for this transaction: 145 | # -1 if transaction is not found 146 | # 0 if transaction is pending 147 | # >0 if transaction is confirmed 148 | ``` 149 | 150 | ### Transaction Monitoring 151 | 152 | You can monitor Ether and token transactions, either from some address or to some address, or both. Provide a 153 | callback to the monitoring function, to be called when the transaction status changes. 154 | NOTE: PENDING status can be received several times, it means the transaction changes blocks. 155 | 156 | ```python 157 | # Setup monitoring callback 158 | tx_statuses = {} 159 | def mycallback(tx_id, status, from_address, to_address, amount): 160 | tx_statuses[tx_id] = status 161 | 162 | # Monitor token transactions from me 163 | token_sdk.monitor_token_transactions(mycallback, from_address=token_sdk.get_address()) 164 | 165 | # Send tokens 166 | tx_id = token_sdk.send_tokens('to address', 10) 167 | 168 | # In a short while, the transaction enters the pending queue 169 | for wait in range(0, 5000): 170 | if tx_statuses[tx_id] > erc20token.TransactionStatus.UNKNOWN: 171 | break 172 | sleep(0.001) 173 | assert tx_statuses[tx_id] >= erc20token.TransactionStatus.PENDING 174 | 175 | # Wait until transaction is confirmed 176 | for wait in range(0, 90): 177 | if tx_statuses[tx_id] > erc20token.TransactionStatus.PENDING: 178 | break 179 | sleep(1) 180 | assert tx_statuses[tx_id] == erc20token.TransactionStatus.SUCCESS 181 | ``` 182 | 183 | **NOTE**: if you are using a public Ethereum node (for example, http://mainnet.infura.io), it will probably have 184 | some of the [JSON-RPC API](https://github.com/ethereum/wiki/wiki/JSON-RPC) disabled to prevent abuse. Usually, it 185 | means that filter-related calls are blocked, so the SDK functions `monitor_ether_transactions` and 186 | `monitor_token_transactions` will not work. As a workaround, you can create your own transaction monitor using 187 | the function `get_transaction_status` or `get_transaction_data`. 188 | 189 | ## Limitations 190 | 191 | ### Ethereum Node 192 | 193 | The SDK requires that some of the features in [JSON-RPC API](https://github.com/ethereum/wiki/wiki/JSON-RPC) 194 | implementation of Ethereum node work correctly: specifically handling filters and pending transactions. Due to a very 195 | dynamic state of development of Ethereum nodes, the support for these features is not yet solid and varies from 196 | vendor to vendor and from version to version. After some experimentation, we settled on the 197 | [Parity Ethereum Node](https://www.parity.io/), version **v1.8.3-beta**. 198 | 199 | If you are running several Ethereum nodes behind a load balancer, you should enable 200 | [connection stickiness](https://stackoverflow.com/questions/10494431/sticky-and-non-sticky-sessions) on the 201 | load balancer: The SDK keeps a state (running filters) on the node it is using and stickiness ensures that requests 202 | will reach the same node. In addition, sending a transaction to one node will not make it immediately visible on 203 | another node, so stickiness ensures consistent transaction-state when polling on nodes. 204 | 205 | ### GAE Standard 206 | 207 | As was mentioned earlier, you will not be able to use the functions `monitor_ether_transactions` and 208 | `monitor_token_transactions`, because they launch a thread, and GAE Standard applications cannot spawn threads. 209 | 210 | ### SDK Limitations 211 | 212 | 1. The SDK only support tokens with 18 decimals, which is the most common number of decimal places. When using tokens 213 | with a different number of decimals, you will need to make your own conversions. 214 | 2. The SDK supports only a limited subset of [ERC20 Token Standard](https://theethereum.wiki/w/index.php/ERC20_Token_Standard), 215 | namely `totalSupply`, `transfer` and `balanceOf` functions. Additional functionality will be added as needed. 216 | Your PRs are welcome! 217 | 3. The SDK initialization with keyfile and password is currently supported only in Python 2.7. 218 | 219 | ## Roadmap 220 | 221 | - Use a default ERC20 ABI if contract ABI is not provided. 222 | - Use [web3.py v.4](https://github.com/ethereum/web3.py). Currently v.3.16.x is used. 223 | - Change the license to MIT after removing GPL'ed packages. 224 | - Add the rest of ERC20 methods if needed. 225 | - Add support to [ERC223 `transfer` method](https://github.com/ethereum/EIPs/issues/223). 226 | - Support various token `decimals`. 227 | - Retrieve contract ABI using [Etherscan Contract API](https://etherscan.io/apis#contracts). 228 | - Get current USD/BTC/ETH prices using [Coinmarketcap API](https://coinmarketcap.com/api/). 229 | 230 | ## License 231 | The code is currently released under [GPLv2 license](LICENSE) due to some GPL-licensed packages it uses. In the 232 | future, we will make an effort to use a less restrictive license. 233 | 234 | ## Contributing 235 | See [CONTRIBUTING.md](CONTRIBUTING.md) for SDK contributing guidelines. 236 | 237 | -------------------------------------------------------------------------------- /erc20token/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | # Copyright (C) 2017 Kin Foundation 4 | 5 | from .exceptions import SdkConfigurationError, SdkNotConfiguredError 6 | from .sdk import TransactionStatus, TransactionData, SDK 7 | from .utils import create_keyfile, load_keyfile 8 | from .version import __version__ 9 | -------------------------------------------------------------------------------- /erc20token/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | # Copyright (C) 2017 Kin Foundation 4 | 5 | 6 | class SdkError(Exception): 7 | """Base class for all SDK errors.""" 8 | 9 | 10 | class SdkConfigurationError(SdkError): 11 | """Cannot create SDK instance due to misconfiguration""" 12 | pass 13 | 14 | 15 | class SdkNotConfiguredError(SdkError): 16 | """Cannot call some SDK functions that need extra configuration details""" 17 | pass 18 | 19 | -------------------------------------------------------------------------------- /erc20token/provider.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | # Copyright (C) 2017 Kin Foundation 4 | 5 | import backoff 6 | import requests 7 | from web3 import HTTPProvider 8 | from web3.utils.compat import make_post_request 9 | 10 | 11 | class RetryHTTPProvider(HTTPProvider): 12 | """RetryHTTPProvider is a custom HTTPProvider that retries failed http requests.""" 13 | 14 | def __init__(self, endpoint_uri, request_kwargs=None): 15 | super(RetryHTTPProvider, self).__init__(endpoint_uri, request_kwargs) 16 | 17 | def make_request(self, method, params): 18 | """overrides the parent method to replace `make_post_request` with custom implementation""" 19 | request_data = self.encode_rpc_request(method, params) 20 | raw_response = self.retriable_post_request(request_data) # instead of make_post_request 21 | response = self.decode_rpc_response(raw_response) 22 | return response 23 | 24 | @backoff.on_exception( 25 | lambda: backoff.expo(factor=0.2), 26 | requests.exceptions.RequestException, 27 | max_tries=4, 28 | giveup=lambda e: e.response is not None and 400 <= e.response.status_code < 500 29 | ) 30 | def retriable_post_request(self, request_data): 31 | return make_post_request( 32 | self.endpoint_uri, 33 | request_data, 34 | **self.get_request_kwargs() 35 | ) 36 | -------------------------------------------------------------------------------- /erc20token/sdk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | # Copyright (C) 2017 Kin Foundation 4 | 5 | 6 | import threading 7 | from time import sleep 8 | 9 | from eth_abi import decode_abi 10 | from eth_keys import keys 11 | from eth_keys.exceptions import ValidationError 12 | from eth_utils import ( 13 | encode_hex, 14 | function_signature_to_4byte_selector 15 | ) 16 | from ethereum.transactions import Transaction 17 | 18 | import rlp 19 | from web3 import Web3 20 | from web3.utils.encoding import ( 21 | hexstr_if_str, 22 | to_bytes, 23 | to_hex, 24 | ) 25 | from web3.utils.transactions import get_buffered_gas_estimate 26 | from web3.utils.validation import ( 27 | validate_abi, 28 | validate_address, 29 | ) 30 | 31 | from .exceptions import ( 32 | SdkConfigurationError, 33 | SdkNotConfiguredError, 34 | ) 35 | from .provider import RetryHTTPProvider 36 | from .utils import load_keyfile 37 | 38 | import logging 39 | logger = logging.getLogger(__name__) 40 | 41 | # ERC20 contract consts. 42 | ERC20_TRANSFER_ABI_PREFIX = encode_hex(function_signature_to_4byte_selector('transfer(address, uint256)')) 43 | 44 | # default gas configuration. 45 | DEFAULT_GAS_PER_TX = 60000 46 | DEFAULT_GAS_PRICE = 10 * 10 ** 9 # 10 Gwei 47 | 48 | # default request retry configuration (linear backoff). 49 | RETRY_ATTEMPTS = 3 50 | RETRY_DELAY = 0.3 51 | 52 | 53 | class TransactionStatus: 54 | """Transaction status enumerator.""" 55 | UNKNOWN = 0 56 | PENDING = 1 57 | SUCCESS = 2 58 | FAIL = 3 59 | 60 | 61 | class TransactionData(object): 62 | """Token transaction data holder""" 63 | from_address = None 64 | to_address = None 65 | ether_amount = 0 66 | token_amount = 0 67 | status = TransactionStatus.UNKNOWN 68 | num_confirmations = -1 69 | 70 | 71 | class SDK(object): 72 | """ 73 | This class is the primary interface to the ERC20 Token Python SDK. 74 | It maintains a connection context with an Ethereum JSON-RPC node and hides most of the specifics 75 | of dealing with Ethereum JSON-RPC API. 76 | """ 77 | 78 | def __init__(self, keyfile='', password='', private_key='', provider='', provider_endpoint_uri='', 79 | contract_address='', contract_abi={}, gas_price=None, gas_limit=None): 80 | """Create a new instance of the Token SDK. 81 | 82 | The SDK needs a JSON-RPC provider, contract definitions and (optionally) a wallet private key. 83 | 84 | The user may pass either a provider or a provider endpoint URI, in which case a default 85 | :class:`web3:providers:HTTPProvider` will be created. If neither private_key nor keyfile+password 86 | are provided, the SDK can still be used in "anonymous" mode, with only the following functions available: 87 | - get_address_ether_balance 88 | - get_transaction_status 89 | - get_transaction_data 90 | - monitor_ether_transactions 91 | 92 | :param str private_key: a private key to initialize the wallet with. If either private key or keyfile 93 | are not provided, the wallet will not be initialized and methods needing the wallet will raise exception. 94 | 95 | :param str keyfile: the path to the keyfile to initialize the wallet with. You will also need to supply 96 | a password for this keyfile. 97 | 98 | :param str password: a password for the keyfile. 99 | 100 | :param provider: JSON-RPC provider to work with. If not given, a default `web3:providers:HTTPProvider` 101 | is used, inited with provider_endpoint_uri. 102 | :type provider: :class:`web3:providers:BaseProvider` 103 | 104 | :param str provider_endpoint_uri: a URI to use with a default HTTPProvider. 105 | 106 | :param str contract_address: the address of the token contract. 107 | 108 | :param list contract_abi: The contract ABI json. 109 | 110 | :param number gas_price: The price of gas in Gwei. 111 | 112 | :param number gas_limit: Transaction gas limit. 113 | 114 | :returns: An instance of the SDK. 115 | :rtype: :class:`~erc20token.SDK` 116 | 117 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError` if some of the configuration 118 | parameters are invalid. 119 | """ 120 | 121 | if not provider and not provider_endpoint_uri: 122 | raise SdkConfigurationError('either provider or provider endpoint must be provided') 123 | 124 | try: 125 | validate_address(contract_address) 126 | except ValueError as ve: 127 | raise SdkConfigurationError('invalid token contract address: ' + str(ve)) 128 | 129 | try: 130 | validate_abi(contract_abi) 131 | except Exception as e: 132 | raise SdkConfigurationError('invalid token contract abi: ' + str(e)) 133 | 134 | if gas_price and not (isinstance(gas_price, int) or isinstance(gas_price, float)): 135 | raise SdkConfigurationError('gas price must be either integer of float') 136 | 137 | if gas_limit and not isinstance(gas_limit, int): 138 | raise SdkConfigurationError('gas limit must be integer') 139 | 140 | if provider: 141 | self.web3 = Web3(provider) 142 | else: 143 | self.web3 = Web3(RetryHTTPProvider(provider_endpoint_uri)) 144 | if not self.web3.isConnected(): 145 | raise SdkConfigurationError('cannot connect to provider endpoint') 146 | 147 | self.token_contract = self.web3.eth.contract(contract_address, abi=contract_abi) 148 | self.private_key = None 149 | self.address = None 150 | 151 | if keyfile: 152 | try: 153 | self.private_key = load_keyfile(keyfile, password) 154 | except Exception as e: 155 | raise SdkConfigurationError('cannot load keyfile: ' + str(e)) 156 | elif private_key: 157 | self.private_key = private_key 158 | 159 | if self.private_key: 160 | try: 161 | private_key_bytes = hexstr_if_str(to_bytes, self.private_key) 162 | pk = keys.PrivateKey(private_key_bytes) 163 | self.address = self.web3.eth.defaultAccount = pk.public_key.to_checksum_address() 164 | except ValidationError as e: 165 | raise SdkConfigurationError('cannot load private key: ' + str(e)) 166 | 167 | # init transaction manager 168 | self._tx_manager = TransactionManager(self.web3, self.private_key, self.address, self.token_contract, 169 | gas_price, gas_limit) 170 | 171 | # monitoring filter manager 172 | self._filter_mgr = FilterManager(self.web3) 173 | 174 | def __del__(self): 175 | """The destructor is used to remove filter subscriptions, if any.""" 176 | if hasattr(self, '_filter_mgr') and self._filter_mgr: 177 | self._filter_mgr.remove_filters() 178 | 179 | def get_address(self): 180 | """Get public address of the SDK wallet. 181 | The wallet is configured by a private key supplied during SDK initialization. 182 | 183 | :returns: public address of the wallet. 184 | :rtype: str 185 | 186 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError`: if the SDK was not configured with a private key. 187 | """ 188 | if not self.address: 189 | raise SdkNotConfiguredError('private key not configured') 190 | return self.address 191 | 192 | def get_ether_balance(self): 193 | """Get Ether balance of the SDK wallet. 194 | The wallet is configured by a private key supplied in during SDK initialization. 195 | 196 | :returns: : the balance in Ether of the internal wallet. 197 | :rtype: Decimal 198 | 199 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError`: if the SDK was not configured with a private key. 200 | """ 201 | if not self.address: 202 | raise SdkNotConfiguredError('private key not configured') 203 | return self.web3.fromWei(self.web3.eth.getBalance(self.address), 'ether') 204 | 205 | def get_token_balance(self): 206 | """Get token balance of the SDK wallet. 207 | The wallet is configured by a private key supplied in during SDK initialization. 208 | 209 | :returns: : the balance in tokens of the internal wallet. 210 | :rtype: Decimal 211 | 212 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError`: if the SDK was not configured with a private key. 213 | """ 214 | if not self.address: 215 | raise SdkNotConfiguredError('private key not configured') 216 | return self.web3.fromWei(self.token_contract.call().balanceOf(self.address), 'ether') 217 | 218 | def get_address_ether_balance(self, address): 219 | """Get Ether balance of a public address. 220 | 221 | :param: str address: a public address to query. 222 | 223 | :returns: the balance in Ether of the provided address. 224 | :rtype: Decimal 225 | 226 | :raises: ValueError: if the supplied address has a wrong format. 227 | """ 228 | validate_address(address) 229 | return self.web3.fromWei(self.web3.eth.getBalance(address), 'ether') 230 | 231 | def get_address_token_balance(self, address): 232 | """Get token balance of a public address. 233 | 234 | :param: str address: a public address to query. 235 | 236 | :returns: : the balance in tokens of the provided address. 237 | :rtype: Decimal 238 | 239 | :raises: ValueError: if the supplied address has a wrong format. 240 | """ 241 | validate_address(address) 242 | return self.web3.fromWei(self.token_contract.call().balanceOf(address), 'ether') 243 | 244 | def get_token_total_supply(self): 245 | """Get total number of tokens issued. 246 | 247 | :return: total supply of tokens 248 | :rtype: Decimal 249 | """ 250 | return self.web3.fromWei(self.token_contract.call().totalSupply(), 'ether') 251 | 252 | def send_ether(self, address, amount): 253 | """Send Ether from my wallet to address. 254 | 255 | :param str address: the address to send Ether to. 256 | 257 | :param Decimal amount: the amount of Ether to transfer. 258 | 259 | :return: transaction id (hash) 260 | :rtype: str 261 | 262 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError`: if the SDK was not configured with a private key. 263 | :raises: ValueError: if the amount is not positive. 264 | :raises: ValueError: if the address has a wrong format. 265 | :raises: ValueError: if the nonce is incorrect. 266 | :raises: ValueError: if insufficient funds for for gas * gas_price + value. 267 | """ 268 | if not self.address: 269 | raise SdkNotConfiguredError('private key not configured') 270 | validate_address(address) 271 | if amount <= 0: 272 | raise ValueError('amount must be positive') 273 | return self._tx_manager.send_transaction(address, amount) 274 | 275 | def send_tokens(self, address, amount): 276 | """Send tokens from my wallet to address. 277 | 278 | :param str address: the address to send tokens to. 279 | 280 | :param Decimal amount: the amount of tokens to transfer. 281 | 282 | :returns: transaction id (hash) 283 | :rtype: str 284 | 285 | :raises: :class:`~erc20token.exceptions.SdkConfigurationError`: if the SDK was not configured with a private key. 286 | :raises: ValueError: if the amount is not positive. 287 | :raises: ValueError: if the address has a wrong format. 288 | :raises: ValueError: if the nonce is incorrect. 289 | :raises: ValueError: if insufficient funds for for gas * gas_price. 290 | """ 291 | if not self.address: 292 | raise SdkNotConfiguredError('private key not configured') 293 | validate_address(address) 294 | if amount <= 0: 295 | raise ValueError('amount must be positive') 296 | hex_data = self.token_contract._encode_transaction_data('transfer', args=(address, self.web3.toWei(amount, 'ether'))) 297 | data = hexstr_if_str(to_bytes, hex_data) 298 | return self._tx_manager.send_transaction(self.token_contract.address, 0, data) 299 | 300 | def get_transaction_status(self, tx_id): 301 | """Get the transaction status for the provided transaction id. 302 | 303 | :param str tx_id: transaction id (hash). 304 | 305 | :returns: transaction status. 306 | :rtype: :class:`~erc20token.TransactionStatus` 307 | """ 308 | tx = self.web3.eth.getTransaction(tx_id) 309 | if not tx: 310 | return TransactionStatus.UNKNOWN 311 | return self._get_tx_status(tx) 312 | 313 | def get_transaction_data(self, tx_id): 314 | """Gets transaction data for the provided transaction id. 315 | 316 | :param str tx_id: transaction id (hash) 317 | :return: transaction data 318 | :rtype: :class:`~erc20token.TransactionData` 319 | """ 320 | tx_data = TransactionData() 321 | tx = self.web3.eth.getTransaction(tx_id) 322 | if not tx: 323 | return tx_data 324 | tx_data.from_address = tx['from'] 325 | tx_data.to_address = tx['to'] 326 | tx_data.ether_amount = self.web3.fromWei(tx['value'], 'ether') 327 | tx_data.status = self._get_tx_status(tx) 328 | if not tx.get('blockNumber'): 329 | tx_data.num_confirmations = 0 330 | else: 331 | tx_block_number = int(tx['blockNumber']) 332 | cur_block_number = int(self.web3.eth.blockNumber) 333 | tx_data.num_confirmations = cur_block_number - tx_block_number + 1 334 | tx_input = tx.get('input') 335 | if tx_input and (tx_input.lower().startswith(ERC20_TRANSFER_ABI_PREFIX.lower())): 336 | to, amount = decode_abi(['uint256', 'uint256'], tx_input[len(ERC20_TRANSFER_ABI_PREFIX):]) 337 | tx_data.to_address = to_hex(to) 338 | tx_data.token_amount = self.web3.fromWei(amount, 'ether') 339 | return tx_data 340 | 341 | def monitor_ether_transactions(self, callback_fn, from_address=None, to_address=None): 342 | """Monitors Ether transactions and calls back on transactions matching the supplied filter. 343 | 344 | :param callback_fn: the callback function with the signature `func(tx_id, status, from_address, to_address, amount)` 345 | 346 | :param str from_address: the transactions must originate from this address. If not provided, 347 | all addresses will match. 348 | 349 | :param str to_address: the transactions must be sent to this address. If not provided, 350 | all addresses will match. 351 | """ 352 | filter_args = self._get_filter_args(from_address, to_address) 353 | 354 | def check_and_callback(tx, status): 355 | if tx.get('input') and not (tx['input'] == '0x' or tx['input'] == '0x0'): # contract transaction, skip it 356 | return 357 | if (('from' in filter_args and tx['from'].lower() == filter_args['from'].lower() and 358 | ('to' not in filter_args or tx['to'].lower() == filter_args['to'].lower())) or 359 | ('to' in filter_args and tx['to'].lower() == filter_args['to'].lower())): 360 | callback_fn(tx['hash'], status, tx['from'], tx['to'], self.web3.fromWei(tx['value'], 'ether')) 361 | 362 | def pending_tx_callback_adapter_fn(tx_id): 363 | tx = self.web3.eth.getTransaction(tx_id) 364 | if not tx: # probably invalid and removed from tx pool 365 | return 366 | check_and_callback(tx, TransactionStatus.PENDING) 367 | 368 | def new_block_callback_adapter_fn(block_id): 369 | block = self.web3.eth.getBlock(block_id, True) 370 | for tx in block['transactions']: 371 | check_and_callback(tx, TransactionStatus.SUCCESS) # TODO: number of block confirmations 372 | 373 | # start monitoring pending and latest transactions 374 | self._filter_mgr.add_filter('pending', pending_tx_callback_adapter_fn) 375 | self._filter_mgr.add_filter('latest', new_block_callback_adapter_fn) 376 | 377 | def monitor_token_transactions(self, callback_fn, from_address=None, to_address=None): 378 | """Monitors token transactions and calls back on transactions matching the supplied filter. 379 | 380 | :param callback_fn: the callback function with the signature `func(tx_id, status, from_address, to_address, amount)` 381 | 382 | :param str from_address: the transactions must originate from this address. If not provided, 383 | all addresses will match. 384 | 385 | :param str to_address: the transactions must be sent to this address. If not provided, 386 | all addresses will match. Note that token transactions are always sent to the contract, and the real 387 | recipient is found in transaction data. This function will decode the data and return the correct 388 | recipient address. 389 | """ 390 | filter_args = self._get_filter_args(from_address, to_address) 391 | 392 | def pending_tx_callback_adapter_fn(tx_id): 393 | tx = self.web3.eth.getTransaction(tx_id) 394 | if not tx: # probably invalid and removed from tx pool 395 | return 396 | ok, tx_from, tx_to, amount = self._check_parse_contract_tx(tx, filter_args) 397 | if ok: 398 | callback_fn(tx['hash'], TransactionStatus.PENDING, tx_from, tx_to, amount) 399 | 400 | def new_block_callback_adapter_fn(block_id): 401 | block = self.web3.eth.getBlock(block_id, True) 402 | for tx in block['transactions']: 403 | ok, tx_from, tx_to, amount = self._check_parse_contract_tx(tx, filter_args) 404 | if ok: 405 | status = self._get_tx_status(tx) 406 | callback_fn(tx['hash'], status, tx_from, tx_to, amount) 407 | 408 | # start monitoring pending and latest transactions 409 | self._filter_mgr.add_filter('pending', pending_tx_callback_adapter_fn) 410 | self._filter_mgr.add_filter('latest', new_block_callback_adapter_fn) 411 | 412 | # helpers 413 | 414 | def _get_tx_status(self, tx): 415 | """Determines transaction status. 416 | 417 | :param dict tx: transaction object 418 | 419 | :returns: the status of this transaction. 420 | :rtype: :class:`~erc20token.TransactionStatus` 421 | """ 422 | if not tx.get('blockNumber'): 423 | return TransactionStatus.PENDING 424 | 425 | # transaction is mined 426 | tx_receipt = self.web3.eth.getTransactionReceipt(tx['hash']) 427 | 428 | # Byzantium fork introduced a status field 429 | tx_status = tx_receipt.get('status') 430 | if tx_status == '0x1' or tx_status == 1: 431 | return TransactionStatus.SUCCESS 432 | if tx_status == '0x0' or tx_status == 0: 433 | return TransactionStatus.FAIL 434 | 435 | # pre-Byzantium, no status field 436 | # failed transaction usually consumes all the gas 437 | if tx_receipt.get('gasUsed') < tx.get('gas'): 438 | return TransactionStatus.SUCCESS 439 | # WARNING: there can be cases when gasUsed == gas for successful transactions! 440 | # We give our transactions extra gas, so it should not happen. 441 | return TransactionStatus.FAIL 442 | 443 | def _check_parse_contract_tx(self, tx, filter_args): 444 | """Parse contract transaction and check whether it matches the supplied filter. 445 | If the transaction matches the filter, the first returned value will be True, and the rest will be 446 | correctly filled. If there is no match, the first returned value is False and the rest are empty. 447 | 448 | :param dict tx: transaction object 449 | 450 | :param dict filter_args: a filter that contains fields 'to', 'from' or both. 451 | 452 | :returns: matching status, from address, to address, token amount 453 | :rtype: tuple 454 | """ 455 | if not tx.get('to') or tx['to'].lower() != self.token_contract.address.lower(): # must be sent to our contract 456 | return False, '', '', 0 457 | tx_input = tx.get('input') 458 | if not tx_input or tx_input == '0x': # not a contract transaction 459 | return False, '', '', 0 460 | if not tx_input.lower().startswith(ERC20_TRANSFER_ABI_PREFIX.lower()): 461 | # only interested in calls to 'transfer' method 462 | return False, '', '', 0 463 | 464 | to, amount = decode_abi(['uint256', 'uint256'], tx_input[len(ERC20_TRANSFER_ABI_PREFIX):]) 465 | to = to_hex(to) 466 | amount = self.web3.fromWei(amount, 'ether') 467 | if (('from' in filter_args and tx['from'].lower() == filter_args['from'].lower() and 468 | ('to' not in filter_args or to.lower() == filter_args['to'].lower())) or 469 | ('to' in filter_args and to.lower() == filter_args['to'].lower())): 470 | return True, tx['from'], to, amount 471 | return False, '', '', 0 472 | 473 | @staticmethod 474 | def _get_filter_args(from_address, to_address): 475 | if not from_address and not to_address: 476 | raise ValueError('either from_address or to_address or both must be provided') 477 | filter_args = {} 478 | if from_address: 479 | validate_address(from_address) 480 | filter_args['from'] = from_address 481 | if to_address: 482 | validate_address(to_address) 483 | filter_args['to'] = to_address 484 | return filter_args 485 | 486 | 487 | class TransactionManager(object): 488 | """TransactionManager handles sending of raw transactions. 489 | Due to the requirement that nonce number be continuous, we need to serialize concurrent transactions 490 | and centralize nonce calculation. 491 | """ 492 | 493 | def __init__(self, web3, private_key, address, token_contract, gas_price, gas_limit): 494 | self.web3 = web3 495 | self.private_key = private_key 496 | self.address = address 497 | self.token_contract = token_contract 498 | self.local_nonce = self.web3.eth.getTransactionCount(self.address) 499 | self.gas_limit = gas_limit 500 | self.lock = threading.Lock() 501 | 502 | if gas_price: 503 | self.gas_price = int(gas_price * 10**9) # gas_price is in Gwei, convert it to wei 504 | else: 505 | self.gas_price = self.web3.eth.gasPrice or DEFAULT_GAS_PRICE 506 | 507 | def send_transaction(self, address, amount, data=b''): 508 | """Send transaction with retry. 509 | Submitting a raw transaction can result in a nonce collision error. In this case, the submission is 510 | retried with a new nonce. 511 | 512 | :param str address: the target address. 513 | 514 | :param Decimal amount: the amount of Ether to send. 515 | 516 | :param data: binary data to put into transaction data field. 517 | 518 | :returns: transaction id (hash) 519 | :rtype: str 520 | """ 521 | with self.lock: 522 | attempts = 0 523 | while True: 524 | try: 525 | remote_nonce = self.web3.eth.getTransactionCount(self.address, 'pending') 526 | nonce = max(self.local_nonce, remote_nonce) 527 | value = self.web3.toWei(amount, 'ether') 528 | tx = Transaction( 529 | nonce=nonce, 530 | gasprice=self.gas_price, 531 | startgas=self.estimate_tx_gas({'to': address, 'from': self.address, 'value': value, 'data': data}), 532 | to=address, 533 | value=value, 534 | data=data, 535 | ) 536 | signed_tx = tx.sign(self.private_key) 537 | raw_tx_hex = self.web3.toHex(rlp.encode(signed_tx)) 538 | tx_id = self.web3.eth.sendRawTransaction(raw_tx_hex) 539 | # send successful, increment nonce. 540 | self.local_nonce = nonce + 1 541 | return tx_id 542 | except ValueError as ve: 543 | if 'message' in ve.args[0]: 544 | err_msg = ve.args[0]['message'] 545 | if ('nonce too low' in err_msg 546 | or 'another transaction with same nonce' in err_msg 547 | or "the tx doesn't have the correct nonce" in err_msg) \ 548 | and attempts < RETRY_ATTEMPTS: 549 | logging.warning('transaction nonce error, retrying') 550 | attempts += 1 551 | sleep(RETRY_DELAY) # TODO: exponential backoff, configurable retry? 552 | continue 553 | raise 554 | 555 | def estimate_tx_gas(self, tx): 556 | """Estimate transaction gas. 557 | If there is a predefined limit, return it. 558 | Otherwise ask the API to estimate gas and add a buffer for safety. 559 | 560 | :param dict tx: sample transaction to estimate gas for. 561 | :return: estimated gas, or default gas if estimate has failed. 562 | :rtype: int 563 | """ 564 | if self.gas_limit: 565 | return self.gas_limit 566 | gas_buffer = 10000 if tx.get('data') else 5000 567 | try: 568 | if tx['data']: 569 | tx['data'] = encode_hex(tx['data']) 570 | return get_buffered_gas_estimate(self.web3, tx, gas_buffer=gas_buffer) 571 | except Exception as e: 572 | logging.warning('cannot estimate gas for transaction: ' + str(e)) 573 | return DEFAULT_GAS_PER_TX 574 | 575 | 576 | class FilterManager(object): 577 | """FilterManager encapsulates transaction filters management. 578 | Currently, its main purpose is to override the `web3.eth.filter._run` worker function that does not handle 579 | exceptions and crashes the polling thread whenever an exception occurs. 580 | """ 581 | def __init__(self, web3): 582 | self.web3 = web3 583 | self.filters = {} 584 | super(FilterManager, self).__init__() 585 | 586 | def add_filter(self, filter_params, *callbacks): 587 | """Setup a new filter or add a callback if the filter already exists. 588 | After registering of a new filter, its worker function is overriden with our custom one. 589 | 590 | :param filter_params: parameters to pass to `web3.eth.filter` 591 | 592 | :param callbacks: callback function to add 593 | 594 | :returns: filter_id 595 | :rtype: str 596 | """ 597 | filter_key = hash(filter_params) 598 | if filter_key not in self.filters: 599 | new_filter = self.web3.eth.filter(filter_params) 600 | # WARNING: ugly hack to replace thread worker 601 | new_filter._Thread__target = self._run_filter(filter_params, new_filter) 602 | new_filter.callbacks.extend(callbacks) 603 | self.filters[filter_key] = new_filter 604 | new_filter.start() 605 | sleep(0) 606 | else: 607 | self.filters[filter_key].callbacks.extend(callbacks) 608 | return self.filters[filter_key].filter_id 609 | 610 | def _run_filter(self, filter_params, filtr): 611 | if filtr.stopped: 612 | raise ValueError("Cannot restart a Filter") 613 | filtr.running = True 614 | 615 | def _runner(): 616 | """Our custom filter worker""" 617 | while filtr.running: 618 | try: 619 | changes = self.web3.eth.getFilterChanges(filtr.filter_id) 620 | if changes: 621 | for entry in changes: 622 | for callback_fn in filtr.callbacks: 623 | if filtr.is_valid_entry(entry): 624 | callback_fn(filtr.format_entry(entry)) 625 | sleep(1) # TODO: configurable? 626 | except ValueError as ve: 627 | if 'message' in ve.args[0] and ve.args[0]['message'] == 'filter not found': 628 | logging.warning('filter {} has expired, recreating'.format(filtr.filter_id)) 629 | new_filter = self.web3.eth.filter(filter_params) 630 | filtr.filter_id = new_filter.filter_id 631 | continue 632 | logging.exception(ve) 633 | except Exception as e: 634 | logging.exception(e) 635 | return _runner 636 | 637 | def remove_filters(self): 638 | """Unregister our filters from the node. Not mandatory, as filters will time out anyway.""" 639 | for key, filtr in self.filters.items(): 640 | filtr.stop_watching(0.1) 641 | self.filters.pop(key, None) 642 | -------------------------------------------------------------------------------- /erc20token/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | 3 | # Copyright (C) 2017 Kin Foundation 4 | 5 | import json 6 | import os 7 | import sys 8 | 9 | import logging 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def create_keyfile(private_key, password, filename): 14 | """Creates a wallet keyfile. 15 | See https://github.com/ethereum/go-ethereum/wiki/Passphrase-protected-key-store-spec 16 | 17 | :param str private_key: private key 18 | 19 | :param str password: keyfile password 20 | 21 | :param str filename: keyfile path 22 | 23 | :raises: NotImplementedError: when using Python 3 24 | """ 25 | if sys.version_info.major >= 3: 26 | raise NotImplementedError('keyfile creation is only supported in python2') 27 | from ethereum.tools import keys 28 | keyfile_json = keys.make_keystore_json(private_key.encode(), password, kdf='scrypt') 29 | try: 30 | oldumask = os.umask(0) 31 | fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) 32 | with os.fdopen(fd, "w") as f: 33 | json.dump(keyfile_json, f) 34 | except IOError as e: 35 | logger.exception(e) 36 | finally: 37 | os.umask(oldumask) 38 | 39 | 40 | def load_keyfile(keyfile, password): 41 | """Loads a private key from the given keyfile. 42 | 43 | :param str keyfile: keyfile path 44 | 45 | :param str password: keyfile password 46 | 47 | :returns: private key 48 | :rtype: str 49 | 50 | :raises: NotImplementedError: when using Python 3 51 | :raises: IOError: if the file is not found 52 | :raises: ValueError: if the keyfile format is invalid 53 | """ 54 | if sys.version_info.major >= 3: 55 | raise NotImplementedError('keyfile usage is only supported in python2') 56 | with open(keyfile, 'r') as f: 57 | keystore = json.load(f) 58 | from ethereum.tools import keys as keys 59 | if not keys.check_keystore_json(keystore): 60 | raise ValueError('invalid keyfile format') 61 | return keys.decode_keystore_json(keystore, password) 62 | 63 | -------------------------------------------------------------------------------- /erc20token/version.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.1.8" 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | apipkg>=1.4 2 | codecov>=2.0.9 3 | coverage>=4.4.2 4 | execnet>=1.5.0 5 | pytest>=3.2.5 6 | pytest-cov>=2.5.1 7 | pytest-forked>=0.2 8 | pytest-xdist>=1.20.1 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto>=0.23.0 2 | backoff>=1.4.3 3 | certifi>=2017.11.5 4 | cffi>=1.11.2 5 | chardet>=3.0.4 6 | coincurve>=6.0.0 7 | cytoolz>=0.8.2 8 | ethereum>=2.2.0 9 | ethereum-abi-utils==0.4.4 10 | ethereum-keys==0.1.0a7 11 | ethereum-tester==0.1.0b2 12 | ethereum-utils==0.6.0 13 | future>=0.16.0 14 | idna>=2.6 15 | pbkdf2>=1.3 16 | py>=1.5.2 17 | py-ecc>=1.4.2 18 | pycparser>=2.18 19 | pycryptodome>=3.4.7 20 | pyethash>=0.1.27 21 | pylru>=1.0.9 22 | pysha3>=1.0.2 23 | PyYAML>=3.12 24 | repoze.lru>=0.7 25 | requests>=2.18.4 26 | rlp>=0.6.0 27 | scrypt>=0.8.0 28 | semantic-version>=2.6.0 29 | toolz>=0.8.2 30 | urllib3>=1.22 31 | web3>=3.16.3 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3. 3 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | exec(open("erc20token/version.py").read()) 6 | 7 | with open('requirements.txt') as f: 8 | requires = [line.strip() for line in f if line.strip()] 9 | with open('requirements-dev.txt') as f: 10 | tests_requires = [line.strip() for line in f if line.strip()] 11 | 12 | setup( 13 | name='erc20token', 14 | version=__version__, 15 | description='ERC20 token SDK for Python', 16 | author='Kin Foundation', 17 | author_email='david.bolshoy@kik.com', 18 | maintainer='David Bolshoy', 19 | maintainer_email='david.bolshoy@kik.com', 20 | url='https://github.com/kinfoundation/erc20token-sdk-python', 21 | license='GPLv2', 22 | packages=["erc20token"], 23 | long_description=open("README.md").read(), 24 | keywords=["ethereum", "erc20", "blockchain", "cryptocurrency"], 25 | classifiers=[ 26 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 27 | 'Intended Audience :: Developers', 28 | 'Development Status :: 4 - Beta', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | ], 34 | install_requires=requires, 35 | tests_require=tests_requires, 36 | python_requires='>=2.7', 37 | ) 38 | -------------------------------------------------------------------------------- /sha3.py.alt: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Implementation by the Keccak, Keyak and Ketje Teams, namely, Guido Bertoni, 3 | # Joan Daemen, Michaël Peeters, Gilles Van Assche and Ronny Van Keer, hereby 4 | # denoted as "the implementer". 5 | # 6 | # For more information, feedback or questions, please refer to our websites: 7 | # http://keccak.noekeon.org/ 8 | # http://keyak.noekeon.org/ 9 | # http://ketje.noekeon.org/ 10 | # 11 | # To the extent possible under law, the implementer has waived all copyright 12 | # and related or neighboring rights to the source code in this file. 13 | # http://creativecommons.org/publicdomain/zero/1.0/ 14 | 15 | def ROL64(a, n): 16 | return ((a >> (64-(n%64))) + (a << (n%64))) % (1 << 64) 17 | 18 | def KeccakF1600onLanes(lanes): 19 | R = 1 20 | for round in range(24): 21 | # θ 22 | C = [lanes[x][0] ^ lanes[x][1] ^ lanes[x][2] ^ lanes[x][3] ^ lanes[x][4] for x in range(5)] 23 | D = [C[(x+4)%5] ^ ROL64(C[(x+1)%5], 1) for x in range(5)] 24 | lanes = [[lanes[x][y]^D[x] for y in range(5)] for x in range(5)] 25 | # ρ and π 26 | (x, y) = (1, 0) 27 | current = lanes[x][y] 28 | for t in range(24): 29 | (x, y) = (y, (2*x+3*y)%5) 30 | (current, lanes[x][y]) = (lanes[x][y], ROL64(current, (t+1)*(t+2)//2)) 31 | # χ 32 | for y in range(5): 33 | T = [lanes[x][y] for x in range(5)] 34 | for x in range(5): 35 | lanes[x][y] = T[x] ^((~T[(x+1)%5]) & T[(x+2)%5]) 36 | # ι 37 | for j in range(7): 38 | R = ((R << 1) ^ ((R >> 7)*0x71)) % 256 39 | if (R & 2): 40 | lanes[0][0] = lanes[0][0] ^ (1 << ((1<> (8*i)) % 256 for i in range(8)) 48 | 49 | def KeccakF1600(state): 50 | lanes = [[load64(state[8*(x+5*y):8*(x+5*y)+8]) for y in range(5)] for x in range(5)] 51 | lanes = KeccakF1600onLanes(lanes) 52 | state = bytearray(200) 53 | for x in range(5): 54 | for y in range(5): 55 | state[8*(x+5*y):8*(x+5*y)+8] = store64(lanes[x][y]) 56 | return state 57 | 58 | def Keccak(rate, capacity, inputBytes, delimitedSuffix, outputByteLen): 59 | outputBytes = bytearray() 60 | state = bytearray([0 for i in range(200)]) 61 | rateInBytes = rate//8 62 | blockSize = 0 63 | if (((rate + capacity) != 1600) or ((rate % 8) != 0)): 64 | return 65 | inputOffset = 0 66 | # === Absorb all the input blocks === 67 | while(inputOffset < len(inputBytes)): 68 | blockSize = min(len(inputBytes)-inputOffset, rateInBytes) 69 | for i in range(blockSize): 70 | state[i] = state[i] ^ ord(inputBytes[i+inputOffset]) 71 | inputOffset = inputOffset + blockSize 72 | if (blockSize == rateInBytes): 73 | state = KeccakF1600(state) 74 | blockSize = 0 75 | # === Do the padding and switch to the squeezing phase === 76 | state[blockSize] = state[blockSize] ^ delimitedSuffix 77 | if (((delimitedSuffix & 0x80) != 0) and (blockSize == (rateInBytes-1))): 78 | state = KeccakF1600(state) 79 | state[rateInBytes-1] = state[rateInBytes-1] ^ 0x80 80 | state = KeccakF1600(state) 81 | # === Squeeze out all the output blocks === 82 | while(outputByteLen > 0): 83 | blockSize = min(outputByteLen, rateInBytes) 84 | outputBytes = outputBytes + state[0:blockSize] 85 | outputByteLen = outputByteLen - blockSize 86 | if (outputByteLen > 0): 87 | state = KeccakF1600(state) 88 | return outputBytes 89 | 90 | 91 | def keccak_256(input): 92 | 93 | class Wrapper: 94 | def __init__(self, input): 95 | self.input = input 96 | 97 | def digest(self): 98 | return Keccak(1088, 512, input, 0x01, 256/8) 99 | 100 | return Wrapper(input) 101 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption("--ropsten", action="store_true", default=False, help="whether testing on Ropsten instead of local testrpc") 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def ropsten(request): 10 | return request.config.getoption("--ropsten") 11 | -------------------------------------------------------------------------------- /test/test_sdk.py: -------------------------------------------------------------------------------- 1 | 2 | from decimal import Decimal 3 | import json 4 | import os 5 | import pytest 6 | import sys 7 | import threading 8 | from time import sleep 9 | 10 | import erc20token 11 | 12 | # Ropsten configuration. 13 | # the following address is set up in Ropsten and is pre-filled with ether and tokens. 14 | ROPSTEN_ADDRESS = '0x4921a61F2733d9Cf265e13865820d7eb435DcBB2' 15 | ROPSTEN_PRIVATE_KEY = 'dd5c6c0e12667d0563bc951e6eee5994cc786b6ce6a2192fd17b9d2bc810a25d' 16 | ROPSTEN_CONTRACT = '0xEF2Fcc998847DB203DEa15fC49d0872C7614910C' 17 | ROPSTEN_CONTRACT_ABI = json.loads('[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newOwnerCandidate","type":"address"}],"name":"requestOwnershipTransfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"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":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"issueTokens","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"acceptOwnership","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":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"newOwnerCandidate","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"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"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_by","type":"address"},{"indexed":true,"name":"_to","type":"address"}],"name":"OwnershipRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"}],"name":"OwnershipTransferred","type":"event"}]') # noqa: E501 18 | ROPSTEN_PROVIDER_ENDPOINT = 'http://159.89.240.246:8545' 19 | 20 | # TestRpc configuration 21 | # the following address is set up in TestRpc and is pre-filled with ether and tokens. 22 | TESTRPC_ADDRESS = '0x8B455Ab06C6F7ffaD9fDbA11776E2115f1DE14BD' 23 | TESTRPC_PRIVATE_KEY = '0x11c98b8fa69354b26b5db98148a5bc4ef2ebae8187f651b82409f6cefc9bb0b8' 24 | TESTRPC_CONTRACT_FILE = './test/truffle_env/token_contract_address.txt' 25 | TESTRPC_ABI_FILE = './test/truffle_env/build/contracts/TestToken.json' 26 | TESTRPC_PROVIDER_ENDPOINT = 'http://localhost:8545' 27 | 28 | TEST_KEYFILE = './test/test-keyfile.json' 29 | TEST_PASSWORD = 'password' 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | def testnet(ropsten): 34 | class Struct: 35 | """Handy variable holder""" 36 | def __init__(self, **entries): self.__dict__.update(entries) 37 | 38 | # if running on Ropsten, return predefined constants. 39 | if ropsten: 40 | return Struct(type='ropsten', address=ROPSTEN_ADDRESS, private_key=ROPSTEN_PRIVATE_KEY, 41 | contract_address=ROPSTEN_CONTRACT, contract_abi=ROPSTEN_CONTRACT_ABI, 42 | provider_endpoint_uri=ROPSTEN_PROVIDER_ENDPOINT) 43 | 44 | # using testrpc, needs truffle build environment. 45 | # testrpc contract address is set up during truffle deploy, and is passed in a file. 46 | contract_file = open(TESTRPC_CONTRACT_FILE) 47 | TESTRPC_CONTRACT = contract_file.read().strip() 48 | if not TESTRPC_CONTRACT: 49 | raise ValueError('contract address file {} is empty'.format(TESTRPC_CONTRACT_FILE)) 50 | 51 | abi_file = open(TESTRPC_ABI_FILE).read() 52 | TESTRPC_CONTRACT_ABI = json.loads(abi_file)['abi'] 53 | 54 | return Struct(type='testrpc', address=TESTRPC_ADDRESS, private_key=TESTRPC_PRIVATE_KEY, 55 | contract_address=TESTRPC_CONTRACT, contract_abi=TESTRPC_CONTRACT_ABI, 56 | provider_endpoint_uri=TESTRPC_PROVIDER_ENDPOINT) 57 | 58 | 59 | def test_create_fail_empty_endpoint(): 60 | with pytest.raises(erc20token.SdkConfigurationError, 61 | match='either provider or provider endpoint must be provided'): 62 | erc20token.SDK() 63 | with pytest.raises(erc20token.SdkConfigurationError, 64 | match='either provider or provider endpoint must be provided'): 65 | erc20token.SDK(provider_endpoint_uri='') 66 | 67 | 68 | def test_create_fail_invalid_contract_address(testnet): 69 | with pytest.raises(erc20token.SdkConfigurationError, 70 | match="invalid token contract address: '' is not an address"): 71 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri) 72 | with pytest.raises(erc20token.SdkConfigurationError, 73 | match="invalid token contract address: '0xbad' is not an address"): 74 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address='0xbad') 75 | with pytest.raises(erc20token.SdkConfigurationError, 76 | match="invalid token contract address: '0x4c6527c2BEB032D46cfe0648072cAb641cA0aA81' " 77 | "has an invalid EIP55 checksum"): 78 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, 79 | contract_address='0x4c6527c2BEB032D46cfe0648072cAb641cA0aA81') 80 | 81 | 82 | def test_create_fail_invalid_abi(testnet): 83 | with pytest.raises(erc20token.SdkConfigurationError, match="invalid token contract abi: 'abi' is not a list"): 84 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address) 85 | with pytest.raises(erc20token.SdkConfigurationError, match="invalid token contract abi: 'abi' is not a list"): 86 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 87 | contract_abi={}) 88 | with pytest.raises(erc20token.SdkConfigurationError, match="invalid token contract abi: 'abi' is not a list"): 89 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 90 | contract_abi='bad') 91 | with pytest.raises(erc20token.SdkConfigurationError, match="invalid token contract abi: The elements of 'abi' " 92 | "are not all dictionaries"): 93 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 94 | contract_abi=['bad']) 95 | 96 | 97 | def test_create_invalid_gas_params(testnet): 98 | with pytest.raises(erc20token.SdkConfigurationError, match='gas price must be either integer of float'): 99 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 100 | contract_abi=testnet.contract_abi, gas_price='bad') 101 | with pytest.raises(erc20token.SdkConfigurationError, match='gas price must be either integer of float'): 102 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 103 | contract_abi=testnet.contract_abi, gas_price='0x123') 104 | with pytest.raises(erc20token.SdkConfigurationError, match='gas limit must be integer'): 105 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 106 | contract_abi=testnet.contract_abi, gas_price=10, gas_limit='bad') 107 | 108 | 109 | def test_create_fail_bad_endpoint(testnet): 110 | with pytest.raises(erc20token.SdkConfigurationError, match='cannot connect to provider endpoint'): 111 | erc20token.SDK(provider_endpoint_uri='bad', contract_address=testnet.address, contract_abi=testnet.contract_abi) 112 | 113 | 114 | def test_create_fail_bad_private_key(testnet): 115 | with pytest.raises(erc20token.SdkConfigurationError, match='cannot load private key: Unexpected private key format.' 116 | ' Must be length 32 byte string'): 117 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 118 | contract_abi=testnet.contract_abi, private_key='bad') 119 | 120 | 121 | @pytest.mark.skipif(sys.version_info.major >= 3, reason="not yet supported in python 3") 122 | def test_create_fail_keyfile(testnet): 123 | # file missing 124 | with pytest.raises(erc20token.SdkConfigurationError, 125 | match="cannot load keyfile: \[Errno 2\] No such file or directory: 'missing.json'"): 126 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 127 | contract_abi=testnet.contract_abi, keyfile='missing.json') 128 | 129 | # not json 130 | with open(TEST_KEYFILE, 'w+') as f: 131 | f.write('not json') 132 | with pytest.raises(erc20token.SdkConfigurationError, match="cannot load keyfile: No JSON object could be decoded"): 133 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 134 | contract_abi=testnet.contract_abi, keyfile=TEST_KEYFILE) 135 | 136 | # json, but invalid format 137 | with open(TEST_KEYFILE, 'w+') as f: 138 | f.write('[]') 139 | with pytest.raises(erc20token.SdkConfigurationError, match="cannot load keyfile: invalid keyfile format"): 140 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 141 | contract_abi=testnet.contract_abi, keyfile=TEST_KEYFILE) 142 | os.remove(TEST_KEYFILE) 143 | 144 | # good keyfile, wrong password 145 | erc20token.create_keyfile(testnet.private_key, TEST_PASSWORD, TEST_KEYFILE) 146 | with pytest.raises(erc20token.SdkConfigurationError, match='cannot load keyfile: MAC mismatch. Password incorrect?'): 147 | erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 148 | contract_abi=testnet.contract_abi, keyfile=TEST_KEYFILE, password='wrong') 149 | os.remove(TEST_KEYFILE) 150 | 151 | 152 | def test_sdk_not_configured(testnet): 153 | sdk = erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 154 | contract_abi=testnet.contract_abi) 155 | with pytest.raises(erc20token.SdkNotConfiguredError, match='private key not configured'): 156 | sdk.get_address() 157 | with pytest.raises(erc20token.SdkNotConfiguredError, match='private key not configured'): 158 | sdk.get_ether_balance() 159 | with pytest.raises(erc20token.SdkNotConfiguredError, match='private key not configured'): 160 | sdk.get_token_balance() 161 | with pytest.raises(erc20token.SdkNotConfiguredError, match='private key not configured'): 162 | sdk.send_ether('address', 100) 163 | with pytest.raises(erc20token.SdkNotConfiguredError, match='private key not configured'): 164 | sdk.send_tokens('address', 100) 165 | 166 | 167 | def test_create_with_private_key(testnet): 168 | sdk = erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 169 | contract_abi=testnet.contract_abi, private_key=testnet.private_key) 170 | assert sdk 171 | assert sdk.web3 172 | assert sdk.token_contract 173 | assert sdk.private_key == testnet.private_key 174 | assert sdk.get_address() == testnet.address 175 | 176 | 177 | @pytest.mark.skipif(sys.version_info.major >= 3, reason="not yet supported in python 3") 178 | def test_create_with_keyfile(testnet): 179 | erc20token.create_keyfile(testnet.private_key, TEST_PASSWORD, TEST_KEYFILE) 180 | sdk = erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 181 | contract_abi=testnet.contract_abi, keyfile=TEST_KEYFILE, password=TEST_PASSWORD) 182 | assert sdk 183 | assert sdk.web3 184 | assert sdk.token_contract 185 | assert sdk.private_key == testnet.private_key 186 | assert sdk.get_address() == testnet.address 187 | os.remove(TEST_KEYFILE) 188 | 189 | 190 | def test_create_with_gas_params(testnet): 191 | sdk = erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, contract_address=testnet.address, 192 | contract_abi=testnet.contract_abi, private_key=testnet.private_key, 193 | gas_price=10.1, gas_limit=10000) 194 | assert sdk 195 | assert sdk._tx_manager.gas_price == 10100000000 196 | assert sdk._tx_manager.gas_limit == 10000 197 | 198 | 199 | @pytest.fixture(scope='session') 200 | def test_sdk(testnet): 201 | sdk = erc20token.SDK(provider_endpoint_uri=testnet.provider_endpoint_uri, private_key=testnet.private_key, 202 | contract_address=testnet.contract_address, contract_abi=testnet.contract_abi) 203 | assert sdk 204 | assert sdk.web3 205 | assert sdk.token_contract 206 | assert sdk.private_key == testnet.private_key 207 | assert sdk.get_address() == testnet.address 208 | assert sdk._tx_manager.gas_price == sdk.web3.eth.gasPrice 209 | return sdk 210 | 211 | 212 | def test_get_address(test_sdk, testnet): 213 | assert test_sdk.get_address() == testnet.address 214 | 215 | 216 | def test_get_ether_balance(test_sdk): 217 | balance = test_sdk.get_ether_balance() 218 | assert balance > 0 219 | 220 | 221 | def test_get_address_ether_balance(test_sdk, testnet): 222 | with pytest.raises(ValueError, match="'0xBAD' is not an address"): 223 | test_sdk.get_address_ether_balance('0xBAD') 224 | balance = test_sdk.get_address_ether_balance(testnet.address) 225 | assert balance > 0 226 | 227 | 228 | def test_get_token_balance(test_sdk): 229 | balance = test_sdk.get_token_balance() 230 | assert balance > 0 231 | 232 | 233 | def test_get_address_token_balance(test_sdk, testnet): 234 | with pytest.raises(ValueError, match="'0xBAD' is not an address"): 235 | test_sdk.get_address_token_balance('0xBAD') 236 | balance = test_sdk.get_address_token_balance(testnet.address) 237 | assert balance > 0 238 | 239 | 240 | def test_get_token_total_supply(test_sdk, testnet): 241 | total_supply = test_sdk.get_token_total_supply() 242 | if testnet.type == 'testrpc': 243 | assert total_supply == 1000 244 | else: 245 | assert total_supply > 1000000000 246 | 247 | 248 | def test_send_ether_fail(test_sdk, testnet): 249 | with pytest.raises(ValueError, match='amount must be positive'): 250 | test_sdk.send_ether(testnet.address, 0) 251 | with pytest.raises(ValueError, match="'0xBAD' is not an address"): 252 | test_sdk.send_ether('0xBAD', 1) 253 | if testnet.type == 'ropsten': 254 | msg_match = 'Insufficient funds. The account you tried to send transaction from does not have enough funds' 255 | else: 256 | msg_match = "Error: sender doesn't have enough funds to send tx" 257 | with pytest.raises(ValueError, match=msg_match): 258 | test_sdk.send_ether(testnet.address, 100) 259 | 260 | 261 | def test_send_tokens_fail(test_sdk, testnet): 262 | with pytest.raises(ValueError, match='amount must be positive'): 263 | test_sdk.send_tokens(testnet.address, 0) 264 | with pytest.raises(ValueError, match="'0xBAD' is not an address"): 265 | test_sdk.send_tokens('0xBAD', 1) 266 | 267 | # NOTE: sending more tokens than available will not cause immediate exception like with ether, 268 | # but will result in failed onchain transaction 269 | 270 | 271 | def test_get_transaction_status(test_sdk, testnet): 272 | # unknown transaction 273 | tx_status = test_sdk.get_transaction_status('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') 274 | assert tx_status == erc20token.TransactionStatus.UNKNOWN 275 | 276 | # test with real transactions on Ropsten. 277 | # NOTE: depending on the Ropsten node configuration, these transactions can be already pruned. 278 | if testnet.type == 'ropsten': 279 | # successful ether transfer 280 | tx_status = test_sdk.get_transaction_status('0x86d51e5547b714232d39e86e86295c20e0241f38d9b828c080cc1ec561f34daf') 281 | assert tx_status == erc20token.TransactionStatus.SUCCESS 282 | # successful token transfer 283 | tx_status = test_sdk.get_transaction_status('0xb5101d58c1e51271837b5343e606b751512882e4f4b175b2f6dae68b7a42d4ab') 284 | assert tx_status == erc20token.TransactionStatus.SUCCESS 285 | # failed token transfer 286 | tx_status = test_sdk.get_transaction_status('0x7a3f2c843a04f6050258863dbea3fec3651b107baa5419e43adb6118478da36b') 287 | assert tx_status == erc20token.TransactionStatus.FAIL 288 | 289 | 290 | def test_get_transaction_data(test_sdk, testnet): 291 | tx_data = test_sdk.get_transaction_data('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') 292 | assert tx_data.status == erc20token.TransactionStatus.UNKNOWN 293 | 294 | # some tests for Ropsten only 295 | if testnet.type == 'ropsten': 296 | # check ether transaction 297 | tx_id = '0x94d0ca03b3e5c132d3821c6fa416d74bab7e77969d5293a0df73ae7e15b46d7f' 298 | tx_data = test_sdk.get_transaction_data(tx_id) 299 | assert tx_data.status == erc20token.TransactionStatus.SUCCESS 300 | assert tx_data.to_address.lower() == testnet.address.lower() 301 | assert tx_data.to_address.lower() == testnet.address.lower() 302 | assert tx_data.ether_amount == Decimal('0.001') 303 | assert tx_data.token_amount == 0 304 | # check the number of confirmations 305 | tx = test_sdk.web3.eth.getTransaction(tx_id) 306 | assert tx and tx.get('blockNumber') 307 | tx_block_number = int(tx['blockNumber']) 308 | cur_block_number = int(test_sdk.web3.eth.blockNumber) 309 | calc_confirmations = cur_block_number - tx_block_number + 1 310 | tx_data = test_sdk.get_transaction_data(tx_id) 311 | assert tx_data.num_confirmations == calc_confirmations or tx_data.num_confirmations == calc_confirmations + 1 312 | 313 | 314 | def monitor_ether_transactions(test_sdk, testnet): 315 | tx_statuses = {} 316 | 317 | def my_callback(tx_id, status, from_address, to_address, amount): 318 | if tx_id not in tx_statuses: # not mine, skip it 319 | return 320 | assert from_address.lower() == testnet.address.lower() 321 | assert to_address.lower() == testnet.address.lower() 322 | assert amount == Decimal('0.001') 323 | tx_statuses[tx_id] = status 324 | 325 | with pytest.raises(ValueError, match='either from_address or to_address or both must be provided'): 326 | test_sdk.monitor_ether_transactions(my_callback) 327 | 328 | # start monitoring ether transactions 329 | test_sdk.monitor_ether_transactions(my_callback, from_address=testnet.address) 330 | 331 | # successful ether transfer 332 | tx_id = test_sdk.send_ether(testnet.address, Decimal('0.001')) 333 | tx_statuses[tx_id] = erc20token.TransactionStatus.UNKNOWN 334 | 335 | for wait in range(0, 30000): 336 | if tx_statuses[tx_id] > erc20token.TransactionStatus.UNKNOWN: 337 | break 338 | sleep(0.001) 339 | assert tx_statuses[tx_id] >= erc20token.TransactionStatus.PENDING 340 | tx_data = test_sdk.get_transaction_data(tx_id) 341 | assert tx_data.status >= erc20token.TransactionStatus.PENDING 342 | assert tx_data.from_address.lower() == testnet.address.lower() 343 | assert tx_data.to_address.lower() == testnet.address.lower() 344 | assert tx_data.ether_amount == Decimal('0.001') 345 | assert tx_data.token_amount == 0 346 | assert tx_data.num_confirmations >= 0 347 | 348 | for wait in range(0, 90): 349 | if tx_statuses[tx_id] > erc20token.TransactionStatus.PENDING: 350 | break 351 | sleep(1) 352 | assert tx_statuses[tx_id] == erc20token.TransactionStatus.SUCCESS 353 | tx_data = test_sdk.get_transaction_data(tx_id) 354 | assert tx_data.num_confirmations >= 1 355 | 356 | 357 | def monitor_token_transactions(test_sdk, testnet): 358 | tx_statuses = {} 359 | 360 | def my_callback(tx_id, status, from_address, to_address, amount): 361 | if tx_id not in tx_statuses: # not mine, skip it 362 | return 363 | assert from_address.lower() == testnet.address.lower() 364 | assert to_address.lower() == testnet.address.lower() 365 | tx_statuses[tx_id] = status 366 | 367 | with pytest.raises(ValueError, match='either from_address or to_address or both must be provided'): 368 | test_sdk.monitor_token_transactions(my_callback) 369 | 370 | # start monitoring token transactions from my address 371 | test_sdk.monitor_token_transactions(my_callback, to_address=testnet.address) 372 | 373 | # transfer more than available. 374 | # NOTE: with a standard ethereum node (geth, parity, etc), this will result in a failed onchain transaction. 375 | # With testrpc, this results in ValueError exception instead. 376 | if testnet.type == 'testrpc': 377 | with pytest.raises(ValueError, match='VM Exception while processing transaction: invalid opcode'): 378 | test_sdk.send_tokens(testnet.address, 10000000) 379 | else: 380 | tx_id = test_sdk.send_tokens(testnet.address, 10000000) 381 | tx_statuses[tx_id] = erc20token.TransactionStatus.UNKNOWN 382 | 383 | for wait in range(0, 30000): 384 | if tx_statuses[tx_id] > erc20token.TransactionStatus.UNKNOWN: 385 | break 386 | sleep(0.001) 387 | assert tx_statuses[tx_id] == erc20token.TransactionStatus.PENDING 388 | 389 | for wait in range(0, 90): 390 | if tx_statuses[tx_id] > erc20token.TransactionStatus.PENDING: 391 | break 392 | sleep(1) 393 | assert tx_statuses[tx_id] == erc20token.TransactionStatus.FAIL 394 | 395 | # successful token transfer 396 | tx_id = test_sdk.send_tokens(testnet.address, 10) 397 | tx_statuses[tx_id] = erc20token.TransactionStatus.UNKNOWN 398 | 399 | # wait for transaction status change 400 | for wait in range(0, 30000): 401 | if tx_statuses[tx_id] > erc20token.TransactionStatus.UNKNOWN: 402 | break 403 | sleep(0.001) 404 | assert tx_statuses[tx_id] >= erc20token.TransactionStatus.PENDING 405 | tx_data = test_sdk.get_transaction_data(tx_id) 406 | assert tx_data.status >= erc20token.TransactionStatus.PENDING 407 | assert tx_data.from_address.lower() == testnet.address.lower() 408 | assert tx_data.to_address.lower() == testnet.address.lower() 409 | assert tx_data.ether_amount == 0 410 | assert tx_data.token_amount == 10 411 | assert tx_data.num_confirmations >= 0 412 | 413 | # test transaction status 414 | tx_status = test_sdk.get_transaction_status(tx_id) 415 | assert tx_status >= erc20token.TransactionStatus.PENDING 416 | 417 | # wait for transaction status change 418 | for wait in range(0, 90): 419 | if tx_statuses[tx_id] > erc20token.TransactionStatus.PENDING: 420 | break 421 | sleep(1) 422 | assert tx_statuses[tx_id] == erc20token.TransactionStatus.SUCCESS 423 | tx_status = test_sdk.get_transaction_status(tx_id) 424 | assert tx_status == erc20token.TransactionStatus.SUCCESS 425 | tx_data = test_sdk.get_transaction_data(tx_id) 426 | assert tx_data.num_confirmations >= 1 427 | 428 | 429 | def test_monitor_ether_transactions(test_sdk, testnet): 430 | if testnet.type == 'ropsten': 431 | pytest.skip("test is skipped in ropsten in favor of concurrent test") 432 | else: 433 | monitor_ether_transactions(test_sdk, testnet) 434 | 435 | 436 | def test_monitor_token_transactions(test_sdk, testnet): 437 | if testnet.type == 'ropsten': 438 | pytest.skip("test is skipped in ropsten in favor of concurrent test") 439 | else: 440 | monitor_token_transactions(test_sdk, testnet) 441 | 442 | 443 | def test_parallel_transactions(test_sdk, testnet): 444 | if testnet.type == 'testrpc': 445 | pytest.skip("concurrent test is skipped in testrpc") 446 | return 447 | t1 = threading.Thread(target=monitor_ether_transactions, args=(test_sdk, testnet)) 448 | t2 = threading.Thread(target=monitor_token_transactions, args=(test_sdk, testnet)) 449 | t1.daemon = True 450 | t2.daemon = True 451 | t1.start() 452 | t2.start() 453 | # below is a workaround for handling KeyboardInterrupt 454 | while True: 455 | t1.join(1) 456 | if not t1.isAlive(): 457 | break 458 | while True: 459 | t2.join(1) 460 | if not t2.isAlive(): 461 | break 462 | -------------------------------------------------------------------------------- /test/truffle_env/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.15; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/truffle_env/contracts/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.16; 2 | 3 | /// @title Ownable 4 | /// @dev The Ownable contract has an owner address, and provides basic authorization control functions, this simplifies 5 | /// and the implementation of "user permissions". 6 | contract Ownable { 7 | address public owner; 8 | address public newOwnerCandidate; 9 | 10 | event OwnershipRequested(address indexed _by, address indexed _to); 11 | event OwnershipTransferred(address indexed _from, address indexed _to); 12 | 13 | /// @dev The Ownable constructor sets the original `owner` of the contract to the sender 14 | /// account. 15 | function Ownable() { 16 | owner = msg.sender; 17 | } 18 | 19 | /// @dev Reverts if called by any account other than the owner. 20 | modifier onlyOwner() { 21 | if (msg.sender != owner) { 22 | revert(); 23 | } 24 | 25 | _; 26 | } 27 | 28 | modifier onlyOwnerCandidate() { 29 | if (msg.sender != newOwnerCandidate) { 30 | revert(); 31 | } 32 | 33 | _; 34 | } 35 | 36 | /// @dev Proposes to transfer control of the contract to a newOwnerCandidate. 37 | /// @param _newOwnerCandidate address The address to transfer ownership to. 38 | function requestOwnershipTransfer(address _newOwnerCandidate) external onlyOwner { 39 | require(_newOwnerCandidate != address(0)); 40 | 41 | newOwnerCandidate = _newOwnerCandidate; 42 | 43 | OwnershipRequested(msg.sender, newOwnerCandidate); 44 | } 45 | 46 | /// @dev Accept ownership transfer. This method needs to be called by the perviously proposed owner. 47 | function acceptOwnership() external onlyOwnerCandidate { 48 | owner = newOwnerCandidate; 49 | newOwnerCandidate = address(0); 50 | 51 | OwnershipTransferred(owner, newOwnerCandidate); 52 | } 53 | } 54 | 55 | /// @title Math operations with safety checks 56 | library SafeMath { 57 | function mul(uint256 a, uint256 b) internal returns (uint256) { 58 | uint256 c = a * b; 59 | assert(a == 0 || c / a == b); 60 | return c; 61 | } 62 | 63 | function div(uint256 a, uint256 b) internal returns (uint256) { 64 | // assert(b > 0); // Solidity automatically throws when dividing by 0 65 | uint256 c = a / b; 66 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 67 | return c; 68 | } 69 | 70 | function sub(uint256 a, uint256 b) internal returns (uint256) { 71 | assert(b <= a); 72 | return a - b; 73 | } 74 | 75 | function add(uint256 a, uint256 b) internal returns (uint256) { 76 | uint256 c = a + b; 77 | assert(c >= a); 78 | return c; 79 | } 80 | 81 | function max64(uint64 a, uint64 b) internal constant returns (uint64) { 82 | return a >= b ? a : b; 83 | } 84 | 85 | function min64(uint64 a, uint64 b) internal constant returns (uint64) { 86 | return a < b ? a : b; 87 | } 88 | 89 | function max256(uint256 a, uint256 b) internal constant returns (uint256) { 90 | return a >= b ? a : b; 91 | } 92 | 93 | function min256(uint256 a, uint256 b) internal constant returns (uint256) { 94 | return a < b ? a : b; 95 | } 96 | } 97 | 98 | /// @title ERC20 test token contract. 99 | contract TestToken is Ownable { 100 | using SafeMath for uint256; 101 | 102 | mapping (address => uint256) balances; 103 | mapping (address => mapping (address => uint256)) allowed; 104 | uint256 public totalSupply; 105 | 106 | string constant public name = "Test Token"; 107 | string constant public symbol = "TTT"; 108 | uint8 constant public decimals = 18; 109 | 110 | event Transfer(address indexed from, address indexed to, uint256 value); 111 | event Approval(address indexed owner, address indexed spender, uint256 value); 112 | 113 | /// @dev Issue test tokens to an account. 114 | /// @param _to address The address which will receive the funds. 115 | /// @param _value uint256 The amount of tokens to be issued. 116 | function issueTokens(address _to, uint256 _value) public onlyOwner { 117 | balances[_to] = balances[_to].add(_value); 118 | totalSupply = totalSupply.add(_value); 119 | 120 | Transfer(0, _to, _value); 121 | } 122 | 123 | /// @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 124 | /// @param _spender address The address which will spend the funds. 125 | /// @param _value uint256 The amount of tokens to be spent. 126 | function approve(address _spender, uint256 _value) public returns (bool) { 127 | // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 128 | if ((_value != 0) && (allowed[msg.sender][_spender] != 0)) { 129 | revert(); 130 | } 131 | 132 | allowed[msg.sender][_spender] = _value; 133 | 134 | Approval(msg.sender, _spender, _value); 135 | 136 | return true; 137 | } 138 | 139 | /// @dev Function to check the amount of tokens that an owner allowed to a spender. 140 | /// @param _owner address The address which owns the funds. 141 | /// @param _spender address The address which will spend the funds. 142 | /// @return uint256 specifying the amount of tokens still available for the spender. 143 | function allowance(address _owner, address _spender) constant returns (uint256 remaining) { 144 | return allowed[_owner][_spender]; 145 | } 146 | 147 | 148 | /// @dev Gets the balance of the specified address. 149 | /// @param _owner address The address to query the the balance of. 150 | /// @return uint256 representing the amount owned by the passed address. 151 | function balanceOf(address _owner) constant returns (uint256 balance) { 152 | return balances[_owner]; 153 | } 154 | 155 | /// @dev transfer token to a specified address. 156 | /// @param _to address The address to transfer to. 157 | /// @param _value uint256 The amount to be transferred. 158 | function transfer(address _to, uint256 _value) public returns (bool) { 159 | balances[msg.sender] = balances[msg.sender].sub(_value); 160 | balances[_to] = balances[_to].add(_value); 161 | 162 | Transfer(msg.sender, _to, _value); 163 | 164 | return true; 165 | } 166 | 167 | /// @dev Transfer tokens from one address to another. 168 | /// @param _from address The address which you want to send tokens from. 169 | /// @param _to address The address which you want to transfer to. 170 | /// @param _value uint256 the amount of tokens to be transferred. 171 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 172 | var _allowance = allowed[_from][msg.sender]; 173 | 174 | balances[_from] = balances[_from].sub(_value); 175 | balances[_to] = balances[_to].add(_value); 176 | 177 | allowed[_from][msg.sender] = _allowance.sub(_value); 178 | 179 | Transfer(_from, _to, _value); 180 | 181 | return true; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/truffle_env/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | let Migrations = artifacts.require('./Migrations.sol'); 3 | let TestToken = artifacts.require('./TestToken.sol'); 4 | 5 | module.exports = (deployer, network, accounts) => { 6 | deployer.deploy(Migrations); 7 | deployer.deploy(TestToken).then(async() => { 8 | instance = await TestToken.deployed() 9 | console.log(`TestToken contract deployed at ${instance.address}`); 10 | 11 | // give tokens to the testing account 12 | let numTokens = 1000; 13 | ok = await instance.issueTokens(accounts[0], web3.toWei(numTokens, "ether")); 14 | assert.ok(ok); 15 | 16 | // check resulting balance 17 | let balanceWei = (await instance.balanceOf(accounts[0])).toNumber(); 18 | assert.equal(web3.fromWei(balanceWei, "ether"), numTokens); 19 | console.log(`Assigned ${numTokens} tokens to account ${accounts[0]} ...`); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/truffle_env/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truffle_env", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "truffle.js", 6 | "scripts": { 7 | "testrpc": "scripts/testrpc.sh", 8 | "truffle": "scripts/truffle.sh" 9 | }, 10 | "dependencies": { 11 | "babel-polyfill": "^6.26.0", 12 | "babel-preset-es2015": "^6.24.1", 13 | "babel-preset-stage-2": "^6.24.1", 14 | "babel-preset-stage-3": "^6.24.1", 15 | "babel-register": "^6.26.0", 16 | "ethereumjs-testrpc": "^6.0.1", 17 | "truffle": "^4.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/truffle_env/scripts/testrpc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Executes cleanup function at script exit. 4 | #trap cleanup EXIT 5 | 6 | # Test if testrpc is running on port $1. 7 | # Result is in $? 8 | testrpc_running() { 9 | nc -z localhost $1 10 | } 11 | 12 | # Kills testrpc process with its PID in $testrpc_pid. 13 | cleanup() { 14 | echo "cleaning up" 15 | # Kill the testrpc instance that we started (if we started one). 16 | if [ -n "$testrpc_pid" ]; then 17 | kill -9 $testrpc_pid 18 | fi 19 | } 20 | 21 | balance=100000000000000000000 22 | 23 | if testrpc_running 8545; then 24 | echo "Using existing testrpc instance" 25 | else 26 | echo "Starting testrpc instance on port 8545" 27 | testrpc --account=0x11c98b8fa69354b26b5db98148a5bc4ef2ebae8187f651b82409f6cefc9bb0b8,"$balance" \ 28 | --account=0xc5db67b3865454f6d129ec83204e845a2822d9fe5338ff46fe4c126859e1357e,"$balance" \ 29 | -u 0 -u 1 -p 8545 --gasLimit 0xfffffffffff > testrpc.log 2>&1 & testrpc_pid=$! 30 | fi 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/truffle_env/scripts/truffle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | truffle deploy --reset > truffle.log 2>&1 4 | grep -Po 'contract deployed at \K(.*)$$' truffle.log > token_contract_address.txt 5 | -------------------------------------------------------------------------------- /test/truffle_env/truffle.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: 'localhost', 8 | port: 8545, 9 | network_id: '*' // Match any network id 10 | }, 11 | }, 12 | }; 13 | --------------------------------------------------------------------------------