From dfa80898e1faea2cee92ebec6fe04873381bd40f Mon Sep 17 00:00:00 2001 From: Jared Rice Sr Date: Mon, 15 Oct 2018 03:03:06 -0500 Subject: [PATCH] Pushed to 0.2.0-Madison --- .DS_Store | Bin 0 -> 10244 bytes .bandit.yml | 84 + .circleci/config.yml | 137 + .coveragerc | 20 + .flake8 | 16 + .gitignore | 67 + .pylintrc | 378 ++ .pyup.yml | 11 + .travis.yml | 72 + CHANGELOG.rst | 538 +++ LICENSE.txt | 22 + MANIFEST.in | 26 + Makefile | 46 + README.rst | 178 + appveyor.yml | 103 + benchmarks/README.rst | 46 + benchmarks/asv.conf.json | 84 + benchmarks/benchmarks/__init__.py | 0 benchmarks/benchmarks/bench_account.py | 55 + benchmarks/benchmarks/bench_ecdsa.py | 120 + benchmarks/benchmarks/bench_transaction.py | 420 +++ docs/Makefile | 192 + docs/_static/beem-icon.png | Bin 0 -> 1732 bytes docs/_static/beem-icon_bw.png | Bin 0 -> 997 bytes docs/_static/beem-logo.png | Bin 0 -> 73601 bytes docs/_static/beem-logo.svg | 1 + docs/_static/beem-logo_2.png | Bin 0 -> 58281 bytes docs/_static/beem-logo_2.svg | 1 + docs/_static/beem-logo_bw.png | Bin 0 -> 27709 bytes docs/_static/beem-logo_bw.svg | 1 + docs/apidefinitions.rst | 793 ++++ docs/beem.account.rst | 7 + docs/beem.aes.rst | 7 + docs/beem.amount.rst | 7 + docs/beem.asciichart.rst | 7 + docs/beem.asset.rst | 7 + docs/beem.block.rst | 7 + docs/beem.blockchain.rst | 7 + docs/beem.blockchainobject.rst | 7 + docs/beem.comment.rst | 7 + docs/beem.conveyor.rst | 7 + docs/beem.discussions.rst | 7 + docs/beem.exceptions.rst | 7 + docs/beem.imageuploader.rst | 7 + docs/beem.instance.rst | 7 + docs/beem.market.rst | 7 + docs/beem.memo.rst | 7 + docs/beem.message.rst | 7 + docs/beem.nodelist.rst | 7 + docs/beem.notify.rst | 7 + docs/beem.price.rst | 7 + docs/beem.rc.rst | 7 + docs/beem.snapshot.rst | 7 + docs/beem.steem.rst | 7 + docs/beem.steemconnect.rst | 7 + docs/beem.storage.rst | 7 + docs/beem.transactionbuilder.rst | 7 + docs/beem.utils.rst | 7 + docs/beem.vote.rst | 7 + docs/beem.wallet.rst | 7 + docs/beem.witness.rst | 8 + docs/beemapi.exceptions.rst | 7 + docs/beemapi.graphenenerpc.rst | 15 + docs/beemapi.node.rst | 7 + docs/beemapi.steemnoderpc.rst | 7 + docs/beemapi.websocket.rst | 27 + docs/beembase.memo.rst | 7 + docs/beembase.objects.rst | 7 + docs/beembase.objecttypes.rst | 7 + docs/beembase.operationids.rst | 8 + docs/beembase.operations.rst | 7 + docs/beembase.signedtransactions.rst | 7 + docs/beembase.transactions.rst | 7 + docs/beemgraphenebase.account.rst | 7 + docs/beemgraphenebase.base58.rst | 7 + docs/beemgraphenebase.bip38.rst | 7 + docs/beemgraphenebase.ecdsasig.rst | 7 + docs/beemgraphenebase.objects.rst | 7 + docs/beemgraphenebase.objecttypes.rst | 7 + docs/beemgraphenebase.operations.rst | 7 + docs/beemgraphenebase.signedtransactions.rst | 7 + docs/cli.rst | 179 + docs/conf.py | 286 ++ docs/configuration.rst | 175 + docs/contribute.rst | 56 + docs/index.rst | 134 + docs/indices.rst | 5 + docs/installation.rst | 92 + docs/make.bat | 263 ++ docs/modules.rst | 77 + docs/quickstart.rst | 206 ++ docs/requirements.txt | 19 + docs/support.rst | 7 + docs/tutorials.rst | 319 ++ dpay-onedir.spec | 64 + dpay-onefile.spec | 57 + dpay.ico | Bin 0 -> 4286 bytes dpaycli/__init__.py | 27 + dpaycli/account.py | 3065 ++++++++++++++++ dpaycli/aes.py | 54 + dpaycli/amount.py | 359 ++ dpaycli/asciichart.py | 271 ++ dpaycli/asset.py | 85 + dpaycli/block.py | 375 ++ dpaycli/blockchain.py | 940 +++++ dpaycli/blockchainobject.py | 224 ++ dpaycli/cli.py | 3189 +++++++++++++++++ dpaycli/comment.py | 811 +++++ dpaycli/constants.py | 49 + dpaycli/conveyor.py | 316 ++ dpaycli/discussions.py | 677 ++++ dpaycli/dpay.py | 1948 ++++++++++ dpaycli/dpayid.py | 300 ++ dpaycli/exceptions.py | 157 + dpaycli/imageuploader.py | 73 + dpaycli/instance.py | 65 + dpaycli/market.py | 836 +++++ dpaycli/memo.py | 268 ++ dpaycli/message.py | 154 + dpaycli/nodelist.py | 159 + dpaycli/notify.py | 89 + dpaycli/price.py | 521 +++ dpaycli/profile.py | 66 + dpaycli/rc.py | 187 + dpaycli/snapshot.py | 531 +++ dpaycli/storage.py | 676 ++++ dpaycli/transactionbuilder.py | 498 +++ dpaycli/utils.py | 290 ++ dpaycli/version.py | 2 + dpaycli/vote.py | 409 +++ dpaycli/wallet.py | 688 ++++ dpaycli/witness.py | 431 +++ dpaycliapi/__init__.py | 10 + dpaycliapi/dpaynoderpc.py | 187 + dpaycliapi/exceptions.py | 104 + dpaycliapi/graphenerpc.py | 477 +++ dpaycliapi/node.py | 171 + dpaycliapi/rpcutils.py | 82 + dpaycliapi/version.py | 2 + dpaycliapi/websocket.py | 300 ++ dpayclibase/__init__.py | 11 + dpayclibase/memo.py | 232 ++ dpayclibase/objects.py | 310 ++ dpayclibase/objecttypes.py | 25 + dpayclibase/operationids.py | 105 + dpayclibase/operations.py | 823 +++++ dpayclibase/signedtransactions.py | 48 + dpayclibase/transactions.py | 23 + dpayclibase/version.py | 2 + dpaycligrapheneapi/__init__.py | 4 + dpaycligraphenebase/__init__.py | 21 + dpaycligraphenebase/account.py | 447 +++ dpaycligraphenebase/base58.py | 212 ++ dpaycligraphenebase/bip38.py | 133 + dpaycligraphenebase/chains.py | 48 + dpaycligraphenebase/dictionary.py | 2 + dpaycligraphenebase/ecdsasig.py | 303 ++ dpaycligraphenebase/objects.py | 137 + dpaycligraphenebase/objecttypes.py | 6 + dpaycligraphenebase/operationids.py | 3 + dpaycligraphenebase/operations.py | 31 + dpaycligraphenebase/py23.py | 42 + dpaycligraphenebase/signedtransactions.py | 212 ++ dpaycligraphenebase/types.py | 416 +++ dpaycligraphenebase/version.py | 2 + .../account_curation_per_week_and_1k_sp.py | 39 + examples/account_rep_over_time.py | 38 + examples/account_sp_over_time.py | 46 + examples/account_vp_over_time.py | 38 + examples/accout_reputation_by_SP.py | 42 + examples/benchmark_beem.py | 90 + examples/benchmark_nodes.py | 96 + examples/benchmark_nodes2.py | 206 ++ examples/cache_performance.py | 64 + .../compare_transactions_speed_with_steem.py | 128 + examples/compare_with_steem_python_account.py | 95 + examples/hf20_testnet.py | 33 + examples/login_app/app.py | 38 + examples/memory_profiler1.py | 49 + examples/memory_profiler2.py | 52 + examples/next_witness_block_coundown.py | 54 + examples/op_on_testnet.py | 76 + examples/post_to_html.py | 80 + examples/post_to_md.py | 51 + examples/print_appbase_calls.py | 47 + examples/print_comments.py | 26 + examples/print_votes.py | 29 + examples/print_votes_notify.py | 71 + examples/stream_threading_performance.py | 64 + examples/using_custom_chain.py | 37 + examples/using_steem_offline.py | 47 + examples/waitForRecharge.py | 33 + examples/watching_the_watchers.py | 154 + examples/write_blocks_to_file.py | 76 + package-linux.sh | 26 + package-osx.sh | 28 + pytest.ini | 3 + requirements-test.txt | 32 + setup.cfg | 9 + setup.py | 107 + tests/__init__.py | 1 + tests/dpaycli/__init__.py | 1 + tests/dpaycli/test_account.py | 517 +++ tests/dpaycli/test_aes.py | 58 + tests/dpaycli/test_amount.py | 262 ++ tests/dpaycli/test_asciichart.py | 40 + tests/dpaycli/test_asset.py | 87 + tests/dpaycli/test_base_objects.py | 44 + tests/dpaycli/test_block.py | 147 + tests/dpaycli/test_blockchain.py | 254 ++ tests/dpaycli/test_blockchain_batch.py | 90 + tests/dpaycli/test_blockchain_threading.py | 100 + tests/dpaycli/test_cli.py | 506 +++ tests/dpaycli/test_comment.py | 245 ++ tests/dpaycli/test_connection.py | 50 + tests/dpaycli/test_constants.py | 92 + tests/dpaycli/test_conveyor.py | 40 + tests/dpaycli/test_discussions.py | 140 + tests/dpaycli/test_instance.py | 358 ++ tests/dpaycli/test_market.py | 180 + tests/dpaycli/test_message.py | 85 + tests/dpaycli/test_nodelist.py | 36 + tests/dpaycli/test_objectcache.py | 77 + tests/dpaycli/test_price.py | 135 + tests/dpaycli/test_profile.py | 35 + tests/dpaycli/test_steem.py | 523 +++ tests/dpaycli/test_steemconnect.py | 91 + tests/dpaycli/test_storage.py | 78 + tests/dpaycli/test_testnet.py | 644 ++++ tests/dpaycli/test_txbuffers.py | 64 + tests/dpaycli/test_utils.py | 104 + tests/dpaycli/test_vote.py | 135 + tests/dpaycli/test_wallet.py | 168 + tests/dpaycli/test_witness.py | 152 + tests/dpaycliapi/__init__.py | 1 + tests/dpaycliapi/test_node.py | 56 + tests/dpaycliapi/test_rpcutils.py | 85 + tests/dpaycliapi/test_steemnoderpc.py | 200 ++ tests/dpaycliapi/test_websocket.py | 39 + tests/dpayclibase/__init__.py | 1 + tests/dpayclibase/test_memo.py | 151 + tests/dpayclibase/test_objects.py | 37 + tests/dpayclibase/test_operations.py | 45 + tests/dpayclibase/test_transactions.py | 1155 ++++++ tests/dpaycligraphene/__init__.py | 1 + tests/dpaycligraphene/test_account.py | 223 ++ tests/dpaycligraphene/test_base58.py | 113 + tests/dpaycligraphene/test_bip38.py | 30 + tests/dpaycligraphene/test_ecdsa.py | 94 + tests/dpaycligraphene/test_key_format.py | 72 + tests/dpaycligraphene/test_objects.py | 31 + tests/dpaycligraphene/test_py23.py | 124 + tests/dpaycligraphene/test_types.py | 181 + tox.ini | 153 + util/appveyor/build.cmd | 21 + util/dpay.ico | Bin 0 -> 4286 bytes util/travis_osx_install.sh | 59 + 257 files changed, 39966 insertions(+) create mode 100644 .DS_Store create mode 100755 .bandit.yml create mode 100755 .circleci/config.yml create mode 100755 .coveragerc create mode 100755 .flake8 create mode 100755 .gitignore create mode 100755 .pylintrc create mode 100755 .pyup.yml create mode 100755 .travis.yml create mode 100755 CHANGELOG.rst create mode 100755 LICENSE.txt create mode 100755 MANIFEST.in create mode 100755 Makefile create mode 100755 README.rst create mode 100755 appveyor.yml create mode 100755 benchmarks/README.rst create mode 100755 benchmarks/asv.conf.json create mode 100755 benchmarks/benchmarks/__init__.py create mode 100755 benchmarks/benchmarks/bench_account.py create mode 100755 benchmarks/benchmarks/bench_ecdsa.py create mode 100755 benchmarks/benchmarks/bench_transaction.py create mode 100755 docs/Makefile create mode 100755 docs/_static/beem-icon.png create mode 100755 docs/_static/beem-icon_bw.png create mode 100755 docs/_static/beem-logo.png create mode 100755 docs/_static/beem-logo.svg create mode 100755 docs/_static/beem-logo_2.png create mode 100755 docs/_static/beem-logo_2.svg create mode 100755 docs/_static/beem-logo_bw.png create mode 100755 docs/_static/beem-logo_bw.svg create mode 100755 docs/apidefinitions.rst create mode 100755 docs/beem.account.rst create mode 100755 docs/beem.aes.rst create mode 100755 docs/beem.amount.rst create mode 100755 docs/beem.asciichart.rst create mode 100755 docs/beem.asset.rst create mode 100755 docs/beem.block.rst create mode 100755 docs/beem.blockchain.rst create mode 100755 docs/beem.blockchainobject.rst create mode 100755 docs/beem.comment.rst create mode 100755 docs/beem.conveyor.rst create mode 100755 docs/beem.discussions.rst create mode 100755 docs/beem.exceptions.rst create mode 100755 docs/beem.imageuploader.rst create mode 100755 docs/beem.instance.rst create mode 100755 docs/beem.market.rst create mode 100755 docs/beem.memo.rst create mode 100755 docs/beem.message.rst create mode 100755 docs/beem.nodelist.rst create mode 100755 docs/beem.notify.rst create mode 100755 docs/beem.price.rst create mode 100755 docs/beem.rc.rst create mode 100755 docs/beem.snapshot.rst create mode 100755 docs/beem.steem.rst create mode 100755 docs/beem.steemconnect.rst create mode 100755 docs/beem.storage.rst create mode 100755 docs/beem.transactionbuilder.rst create mode 100755 docs/beem.utils.rst create mode 100755 docs/beem.vote.rst create mode 100755 docs/beem.wallet.rst create mode 100755 docs/beem.witness.rst create mode 100755 docs/beemapi.exceptions.rst create mode 100755 docs/beemapi.graphenenerpc.rst create mode 100755 docs/beemapi.node.rst create mode 100755 docs/beemapi.steemnoderpc.rst create mode 100755 docs/beemapi.websocket.rst create mode 100755 docs/beembase.memo.rst create mode 100755 docs/beembase.objects.rst create mode 100755 docs/beembase.objecttypes.rst create mode 100755 docs/beembase.operationids.rst create mode 100755 docs/beembase.operations.rst create mode 100755 docs/beembase.signedtransactions.rst create mode 100755 docs/beembase.transactions.rst create mode 100755 docs/beemgraphenebase.account.rst create mode 100755 docs/beemgraphenebase.base58.rst create mode 100755 docs/beemgraphenebase.bip38.rst create mode 100755 docs/beemgraphenebase.ecdsasig.rst create mode 100755 docs/beemgraphenebase.objects.rst create mode 100755 docs/beemgraphenebase.objecttypes.rst create mode 100755 docs/beemgraphenebase.operations.rst create mode 100755 docs/beemgraphenebase.signedtransactions.rst create mode 100755 docs/cli.rst create mode 100755 docs/conf.py create mode 100755 docs/configuration.rst create mode 100755 docs/contribute.rst create mode 100755 docs/index.rst create mode 100755 docs/indices.rst create mode 100755 docs/installation.rst create mode 100755 docs/make.bat create mode 100755 docs/modules.rst create mode 100755 docs/quickstart.rst create mode 100755 docs/requirements.txt create mode 100755 docs/support.rst create mode 100755 docs/tutorials.rst create mode 100755 dpay-onedir.spec create mode 100755 dpay-onefile.spec create mode 100644 dpay.ico create mode 100755 dpaycli/__init__.py create mode 100755 dpaycli/account.py create mode 100755 dpaycli/aes.py create mode 100755 dpaycli/amount.py create mode 100755 dpaycli/asciichart.py create mode 100755 dpaycli/asset.py create mode 100755 dpaycli/block.py create mode 100755 dpaycli/blockchain.py create mode 100755 dpaycli/blockchainobject.py create mode 100755 dpaycli/cli.py create mode 100755 dpaycli/comment.py create mode 100755 dpaycli/constants.py create mode 100755 dpaycli/conveyor.py create mode 100755 dpaycli/discussions.py create mode 100755 dpaycli/dpay.py create mode 100755 dpaycli/dpayid.py create mode 100755 dpaycli/exceptions.py create mode 100755 dpaycli/imageuploader.py create mode 100755 dpaycli/instance.py create mode 100755 dpaycli/market.py create mode 100755 dpaycli/memo.py create mode 100755 dpaycli/message.py create mode 100755 dpaycli/nodelist.py create mode 100755 dpaycli/notify.py create mode 100755 dpaycli/price.py create mode 100755 dpaycli/profile.py create mode 100755 dpaycli/rc.py create mode 100755 dpaycli/snapshot.py create mode 100755 dpaycli/storage.py create mode 100755 dpaycli/transactionbuilder.py create mode 100755 dpaycli/utils.py create mode 100755 dpaycli/version.py create mode 100755 dpaycli/vote.py create mode 100755 dpaycli/wallet.py create mode 100755 dpaycli/witness.py create mode 100755 dpaycliapi/__init__.py create mode 100755 dpaycliapi/dpaynoderpc.py create mode 100755 dpaycliapi/exceptions.py create mode 100755 dpaycliapi/graphenerpc.py create mode 100755 dpaycliapi/node.py create mode 100755 dpaycliapi/rpcutils.py create mode 100755 dpaycliapi/version.py create mode 100755 dpaycliapi/websocket.py create mode 100755 dpayclibase/__init__.py create mode 100755 dpayclibase/memo.py create mode 100755 dpayclibase/objects.py create mode 100755 dpayclibase/objecttypes.py create mode 100755 dpayclibase/operationids.py create mode 100755 dpayclibase/operations.py create mode 100755 dpayclibase/signedtransactions.py create mode 100755 dpayclibase/transactions.py create mode 100755 dpayclibase/version.py create mode 100755 dpaycligrapheneapi/__init__.py create mode 100755 dpaycligraphenebase/__init__.py create mode 100755 dpaycligraphenebase/account.py create mode 100755 dpaycligraphenebase/base58.py create mode 100755 dpaycligraphenebase/bip38.py create mode 100755 dpaycligraphenebase/chains.py create mode 100755 dpaycligraphenebase/dictionary.py create mode 100755 dpaycligraphenebase/ecdsasig.py create mode 100755 dpaycligraphenebase/objects.py create mode 100755 dpaycligraphenebase/objecttypes.py create mode 100755 dpaycligraphenebase/operationids.py create mode 100755 dpaycligraphenebase/operations.py create mode 100755 dpaycligraphenebase/py23.py create mode 100755 dpaycligraphenebase/signedtransactions.py create mode 100755 dpaycligraphenebase/types.py create mode 100755 dpaycligraphenebase/version.py create mode 100755 examples/account_curation_per_week_and_1k_sp.py create mode 100755 examples/account_rep_over_time.py create mode 100755 examples/account_sp_over_time.py create mode 100755 examples/account_vp_over_time.py create mode 100755 examples/accout_reputation_by_SP.py create mode 100755 examples/benchmark_beem.py create mode 100755 examples/benchmark_nodes.py create mode 100755 examples/benchmark_nodes2.py create mode 100755 examples/cache_performance.py create mode 100755 examples/compare_transactions_speed_with_steem.py create mode 100755 examples/compare_with_steem_python_account.py create mode 100755 examples/hf20_testnet.py create mode 100755 examples/login_app/app.py create mode 100755 examples/memory_profiler1.py create mode 100755 examples/memory_profiler2.py create mode 100755 examples/next_witness_block_coundown.py create mode 100755 examples/op_on_testnet.py create mode 100755 examples/post_to_html.py create mode 100755 examples/post_to_md.py create mode 100755 examples/print_appbase_calls.py create mode 100755 examples/print_comments.py create mode 100755 examples/print_votes.py create mode 100755 examples/print_votes_notify.py create mode 100755 examples/stream_threading_performance.py create mode 100755 examples/using_custom_chain.py create mode 100755 examples/using_steem_offline.py create mode 100755 examples/waitForRecharge.py create mode 100755 examples/watching_the_watchers.py create mode 100755 examples/write_blocks_to_file.py create mode 100755 package-linux.sh create mode 100755 package-osx.sh create mode 100755 pytest.ini create mode 100755 requirements-test.txt create mode 100755 setup.cfg create mode 100755 setup.py create mode 100755 tests/__init__.py create mode 100755 tests/dpaycli/__init__.py create mode 100755 tests/dpaycli/test_account.py create mode 100755 tests/dpaycli/test_aes.py create mode 100755 tests/dpaycli/test_amount.py create mode 100755 tests/dpaycli/test_asciichart.py create mode 100755 tests/dpaycli/test_asset.py create mode 100755 tests/dpaycli/test_base_objects.py create mode 100755 tests/dpaycli/test_block.py create mode 100755 tests/dpaycli/test_blockchain.py create mode 100755 tests/dpaycli/test_blockchain_batch.py create mode 100755 tests/dpaycli/test_blockchain_threading.py create mode 100755 tests/dpaycli/test_cli.py create mode 100755 tests/dpaycli/test_comment.py create mode 100755 tests/dpaycli/test_connection.py create mode 100755 tests/dpaycli/test_constants.py create mode 100755 tests/dpaycli/test_conveyor.py create mode 100755 tests/dpaycli/test_discussions.py create mode 100755 tests/dpaycli/test_instance.py create mode 100755 tests/dpaycli/test_market.py create mode 100755 tests/dpaycli/test_message.py create mode 100755 tests/dpaycli/test_nodelist.py create mode 100755 tests/dpaycli/test_objectcache.py create mode 100755 tests/dpaycli/test_price.py create mode 100755 tests/dpaycli/test_profile.py create mode 100755 tests/dpaycli/test_steem.py create mode 100755 tests/dpaycli/test_steemconnect.py create mode 100755 tests/dpaycli/test_storage.py create mode 100755 tests/dpaycli/test_testnet.py create mode 100755 tests/dpaycli/test_txbuffers.py create mode 100755 tests/dpaycli/test_utils.py create mode 100755 tests/dpaycli/test_vote.py create mode 100755 tests/dpaycli/test_wallet.py create mode 100755 tests/dpaycli/test_witness.py create mode 100755 tests/dpaycliapi/__init__.py create mode 100755 tests/dpaycliapi/test_node.py create mode 100755 tests/dpaycliapi/test_rpcutils.py create mode 100755 tests/dpaycliapi/test_steemnoderpc.py create mode 100755 tests/dpaycliapi/test_websocket.py create mode 100755 tests/dpayclibase/__init__.py create mode 100755 tests/dpayclibase/test_memo.py create mode 100755 tests/dpayclibase/test_objects.py create mode 100755 tests/dpayclibase/test_operations.py create mode 100755 tests/dpayclibase/test_transactions.py create mode 100755 tests/dpaycligraphene/__init__.py create mode 100755 tests/dpaycligraphene/test_account.py create mode 100755 tests/dpaycligraphene/test_base58.py create mode 100755 tests/dpaycligraphene/test_bip38.py create mode 100755 tests/dpaycligraphene/test_ecdsa.py create mode 100755 tests/dpaycligraphene/test_key_format.py create mode 100755 tests/dpaycligraphene/test_objects.py create mode 100755 tests/dpaycligraphene/test_py23.py create mode 100755 tests/dpaycligraphene/test_types.py create mode 100755 tox.ini create mode 100755 util/appveyor/build.cmd create mode 100644 util/dpay.ico create mode 100755 util/travis_osx_install.sh diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d1e094809e0d89df8580ac0665c3852007dcd81e GIT binary patch literal 10244 zcmeHM-EI;=6h2F-U1+QLm!@}{7?WO50v1gZ(}dDiCGjuP=!Ke=pDmEWZe^i}Si_C+ z6{v8MD22Q?7a%p@iA?H}O-)Kbs-zfD3@8Q^1BwB~z`w`<_H0i2tSeQi7*Gr-1_lgp zd@zt%W<1$;} z-<2+$G#5^qXIb-3D4MVioFUXn%eYdNiUGyIFavycpQ8dD;CC1}zc;9!tJ|ew-7Z3h zm(^9|rEemkMiiwY5EhlFN)5Epqf5$mAJhxQ`!%z(6FawI zTdhLPGMfTiE1OVx^q^*y?M~KiTV+SIlXpMpdPI-x#1?wJbUJRN(?@Zmm%4l<6*pE_ zSB{P%`ohK3T5h+|eb#$^^x~KaLKyPG6`gXQ2jy$jd|Vvnn^xVnDhxrMrwzJ6>vWs0 z(-z&NJCF(R>rhK#?IpDRa}uU9*d5mUGEVP5<#xb>#4NwJ;jxmRh6)ltG3Xt@r@OQa zeG7ihyzCCsdZDFWq?8SJTuGRM9sb4Ef{ zFbf>7sv`E7qbw6g9e*2Vf78auo`{I#Z$*d2r?A(6^*ZX34{5v>{ra<5xjC%lEv(}_ z$^^B5msc>aOg~DeTd=W@_C1dY@*_OeuRjZYjubh%1{-3ogJQO3~Yq{u;#G!|2v&6QgA& zN5n$mqkhbJXqogx>^EF%AX~8LV6;E2kC>A%d#=Bw2rwovZS*{WwYW!>AV2K2n(p&j zJ%GS?C4G2kK!1ABBiXC4F9tMcplJ)HdA_7I$37U)4x`U4%&fGRYw)_!dcOVOBz}XMlW%pw9p5{O`Nzo`m!NcPdvv761SM literal 0 HcmV?d00001 diff --git a/.bandit.yml b/.bandit.yml new file mode 100755 index 0000000..ea868e2 --- /dev/null +++ b/.bandit.yml @@ -0,0 +1,84 @@ +tests: +skips: +- B404 # Ignore warnings about importing subprocess +- B603 # Ignore warnings about calling subprocess.Popen without shell=True +- B607 # Ignore warnings about calling subprocess.Popen without a full path to executable + +### (optional) plugin settings - some test plugins require configuration data +### that may be given here, per-plugin. All bandit test plugins have a built in +### set of sensible defaults and these will be used if no configuration is +### provided. It is not necessary to provide settings for every (or any) plugin +### if the defaults are acceptable. + +any_other_function_with_shell_equals_true: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +execute_with_run_as_root_equals_true: + function_names: [ceilometer.utils.execute, cinder.utils.execute, neutron.agent.linux.utils.execute, + nova.utils.execute, nova.utils.trycmd] +hardcoded_tmp_directory: + tmp_dirs: [/tmp, /var/tmp, /dev/shm] +linux_commands_wildcard_injection: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +password_config_option_not_marked_secret: + function_names: [oslo.config.cfg.StrOpt, oslo_config.cfg.StrOpt] +ssl_with_bad_defaults: + bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3, + PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD] +ssl_with_bad_version: + bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3, + PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD] +start_process_with_a_shell: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +start_process_with_no_shell: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +start_process_with_partial_path: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +subprocess_popen_with_shell_equals_true: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +subprocess_without_shell_equals_true: + no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, + os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, + os.spawnvp, os.spawnvpe, os.startfile] + shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, + popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] + subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, + utils.execute, utils.execute_with_timeout] +try_except_continue: {check_typed_exception: false} +try_except_pass: {check_typed_exception: false} + diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100755 index 0000000..ae7ff19 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,137 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build-python3.6: + docker: + # specify the version you desire here + # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` + - image: circleci/python:3.6.4 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements-test.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: + name: Setup Code Climate test-reporter + command: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + - run: + name: install dependencies + command: | + sudo python -m pip install -r requirements-test.txt + + # run tests! + # this example uses Django's built-in test-runner + # other common Python testing frameworks include pytest and nose + # https://pytest.org + # https://nose.readthedocs.io + - run: + name: run tests + command: | + ./cc-test-reporter before-build + tox -e py36 + ./cc-test-reporter after-build --exit-code $? + + # - deploy: + # name: Push coverage + # command: | + # if [ "${CIRCLE_BRANCH}" == "master" ]; then + # tox -e upload_coverage + # fi + + build-python2.7: + docker: + - image: circleci/python:2.7.13 + working_directory: ~/repo + steps: + - checkout + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements-test.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: + name: install dependencies + command: | + sudo python -m pip install --upgrade -r requirements-test.txt + + - run: + name: run tests + command: | + tox -e py27 + + build-python3.4: + docker: + - image: circleci/python:3.4.8 + working_directory: ~/repo + steps: + - checkout + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements-test.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: + name: install dependencies + command: | + sudo python -m pip install --upgrade -r requirements-test.txt + + - run: + name: run tests + command: | + tox -e py34 + + build-python3.5: + docker: + - image: circleci/python:3.5.5 + working_directory: ~/repo + steps: + - checkout + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements-test.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: + name: install dependencies + command: | + sudo python -m pip install --upgrade -r requirements-test.txt + - run: + name: run tests + command: | + tox -e py35 + +workflows: + version: 2 + build: + jobs: + - build-python3.6 + - build-python2.7: + requires: + - build-python3.6 + - build-python3.4: + requires: + - build-python2.7 + - build-python3.5: + requires: + - build-python3.4 + diff --git a/.coveragerc b/.coveragerc new file mode 100755 index 0000000..1901b1a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,20 @@ +[run] +branch = True +source = + dpaycli/ + dpayclibase/ + dpaycliapi/ + dpaycligraphenebase/ +omit = + */.eggs/* + */.tox/* + +[report] +omit = + */.eggs/* + */.tox/* + +[paths] +source = + .tox/*/lib/python*/site-packages/dpaycli + .tox/pypy/site-packages/dpaycli diff --git a/.flake8 b/.flake8 new file mode 100755 index 0000000..393dc2f --- /dev/null +++ b/.flake8 @@ -0,0 +1,16 @@ +[flake8] +ignore = + # indentation is not a multiple of four, + E111,E114, + # visually indented line with same indent as next logical line, + E129,E501,F401,E722, E122 +exclude = + .git, + .eggs, + __pycache__, + docs/conf.py, + old, + build, + dist +max-line-length=80 +max-complexity = 50 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..41fdc26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +.idea/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.coverage +coverage.xml +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +.pytest_cache/ +*.egg +*.eggs + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ +docs/html + +# PyBuilder +target/ + +# Configuration Files +*config.py + +# Vim temp files +*.swp +.ropeproject/ +*/.ropeproject/ diff --git a/.pylintrc b/.pylintrc new file mode 100755 index 0000000..f778dd4 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,378 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,.git,flake8.egg-info + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence=INFERENCE_FAILURE + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). This supports can work +# with qualified names. +ignored-classes= + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=20 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.pyup.yml b/.pyup.yml new file mode 100755 index 0000000..df91590 --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,11 @@ +# autogenerated pyup.io config file +# see https://pyup.io/docs/configuration/ for all available options + +update: all + +# update schedule +# default: empty +# allowed: "every day", "every week", .. +schedule: "every week" + +pin: False diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..1ca27cf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,72 @@ +# After changing this file, check it on: +# http://lint.travis-ci.org/ +language: python +sudo: false + +matrix: + include: + - os: linux + python: 3.6 + env: + - TOXENV=pylint + - os: linux + python: 3.6 + env: + - TOXENV=flake8 + #- os: linux + # python: 3.6 + # env: + # - TOXENV=bandit + - os: linux + python: 3.6 + env: + - TOXENV=readme + - os: linux + python: 2.7 + env: + - TOXENV=short + - os: linux + python: 3.4 + env: + - TOXENV=short + - os: linux + python: 3.5 + env: + - TOXENV=short + - os: linux + python: 3.6 + env: + - TOXENV=py36short + - BUILD_LINUX=yes + - os: osx + osx_image: xcode9.3 + language: objective-c + env: + - TRAVIS_PYTHON_VERSION=3.6 + - TOXENV=short + +cache: pip + +before_install: + - uname -a + - df -h + - ulimit -a + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then source util/travis_osx_install.sh; fi + - ccache -s + - which python; python --version + - pip install --upgrade pip + - pip install --upgrade wheel + # Set numpy version first, other packages link against it + - pip install six nose coverage codecov tox-travis pytest pytest-cov coveralls codacy-coverage parameterized secp256k1 cryptography scrypt + - pip install pycryptodomex pyyaml appdirs pylibscrypt + - pip install ecdsa requests future websocket-client pytz six Click events prettytable + +script: + - tox + +after_success: + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash util/package-osx.sh; fi + - if [[ "$BUILD_LINUX" == "yes" ]]; then bash util/package-linux.sh; fi + - coveralls + # - codecov + # - python-codacy-coverage -r coverage.xml diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100755 index 0000000..ed6b319 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,538 @@ +Changelog +========= +0.20.7 +------ +* Fix issue #97 `get_discussions()` does not finish if discussions are empty by espoem +* Fix issue #99 DivisionByZero Error in Account.get_rc_manabar() by crokkon +* Add claimaccount to dpay and some improvements for dpay.bbd_symbol +* newaccount adapted for HF20 and can be used to create claimed account +* Correct operationids for WLS +* Improve dpay.refresh_data() reading +* Set network prefix in Signed_Transaction and Operation for using the correct operationids +* Fix test_block unit test + +0.20.6 +------ +* fix issue #93 - Wrong input parameters for `Discussions_by_author_before_date` in Docstring and `get_discussions` by espoem +* Add support for whaleshares (WLS) and Financial Transparency Gateway (EFTG) +* Using generic asset symbols by crokkon +* Bug fixes for python 2.7 +* Fix for witness update + +0.20.5 +------ +* fix get_effective_vesting_shares() + +0.20.4 +------ +* get_effective_vesting_shares() added to calculated max_mana correctly +* dict key words adapted to dpayd for get_manabar() and get_rc_manabar() +* Voting mana fixed for 0 BP accounts +* comment_benefactor_reward adapted for snapshot +* Custom_json RC costs added to print_info + +0.20.3 +------ +* add RC class to calculate RC costs of operations +* add comment, vote, transfer RC costs in account.print_info() and dpay power +* Shows number of possible comments, votes, tranfers with available RCs in account.print_info() and dpay power +* get_rc_cost was added to dpay to calculation RC costs from resource count +* bug regarding new amount format in witness update fixed (also for dpay witnessenable and witnessdisable) + +0.20.2 +------ +* estimated_mana is now capped by estimated_max +* print_info fixed() +* get_api_methods() and get_apis() added to DPay + +0.20.1 +------ +* Improved get_rc_manabar(), get_manabar() output +* get_voting_power() fixed again +* print_info for account improved +* get_manabar_recharge_time_str(), get_manabar_recharge_timedelta() and get_manabar_recharge_time() added +* greatchain.dpaynodes.com added to nodelist + +0.20.0 +------ +* Fully supporting hf20 +* add get_resource_params(), get_resource_pool(), claim_account(), create_claimed_account() to DPay +* fix 30x fee for create_account +* add find_rc_accounts() to Blockchain +* get_rc(), get_rc_manabar(), get_manabar() added to Account +* get_voting_power() fixed + +0.19.57 +-------- +* last hf19 release +* working witness_set_properties operation +* witness_set_properties() added to dpay +* dpay witnessproperties added +* dpay pricefeed uses witnessproperties when witness wif is provided + +0.19.56 +------- +* adding methods to claim and create discounted accouts (PR #84) by crokkon +* Make vote rshare calculations HF20 ready (PR #85) by flugschwein +* Issue #80 fixed +* Fix some Warnings +* Blockchain.stream() improved for appbase format +* All unit tests are fixed and non-appbase related tests were removed + +0.19.55 +------- +* Issue #72 fixed by crokkon +* Improved Docu by jrswab +* Add get_vote_pct_for_BBD, bbd_to_vote_pct and bbd_to_rshares by flugschwein +* dpayclibase/objects: fix serialization of appbase trx by crokkon +* Fix many documentation errors (based on error messages when building) by flugschwein +* Appbase detection fixed +* Unit tests fixed + +0.19.54 +------- +* Issue #69 fixed +* bug in batched streaming + cli fixed +* Nodelist updated +* unit tests improved +* Add last_current_block_num parameter to wait_for_and_get_block for reducing the number of api calls +* not_broadcasted_vote parameter added for improving vote calculation accuracy thanks to flugschwein + +0.19.53 +------- +* Add userdata and featureflags to dpay +* greatchain.dpaynodes.com and greatchain.dpays.io removed from Nodelist +* bug fixed in allow and disallow for CLI +* Issue #52 closed thanks to crokkon +* Issue #64 fixed +* Issue #66 fixed thanks to flugschwein + +0.19.52 +------- +* appbase.buildtime.io node added +* history is made ready for appbase +* account refresh fixed +* fix ops_statistics for new appase nodes + +0.19.51 +------- +* Add missing trx_num to streamed block operation +* Add d.tube format to resolve_authorperm +* disable_chain_detection added to graphenerpc (for testing hivemind e.g.) +* set_next_node_on_empty_reply added to some appbase rpc calls + +0.19.50 +------- +* Class to access DPayit Conveyor instances added by crokkon +* Option added to loed custom chains into the DPay object + +0.19.49 +------- +* add get_parent() to comment +* fix for dpay reward +* fix #46 (used power calculation may treat downvotes incorrectly) by crokkon +* fix #49 (discussions: set dpay inst. as keyword argument) by crokkon +* Fix issue #51 (Discussions.get_discussions("blog", ...) returns the same two comments over and over) +* Fix #52 discussions.Replies_by_last_update() by crokkon +* Some bug fixes for Discussions +* Fix #54 (discussions may fail to handle empty responses correctly) by crokkon +* Snapshot improved +* Unit tests fixed +* Examples account_vp_over_time, account_reputation_by_SP +* Spelling errors fix by crokkon +* Adding account methods for feed, blog, comments and replies by crokkon +* Fix #57 (DPayID expects double quotes in JSON) +* Improved handling of "Client returned invalid format. Expected JSON!" erros + +0.19.48 +------- +* Fix issue #45 (upvote() and downvote() of a pending post/comment without vote did not work) +* fix Amount for condenser broadcast ops on appbase nodes (fixes transfer broadcast for example) +* Added get_all_replies() to Comment for fetching all replies to a post +* bemepy claimreward improved +* Amount handling in Account improved +* upvote and downvote in dpay fixed +* update_vote and build_vp_arrays added to AccountSnapshot for showing vote power history +* account_vp_over_time added to examples + +0.19.47 +------- +* Some bug fixes +* Unit tests using testnet fixed +* dpaycli.snapshot improved +* Example account_bp_over_time added +* Example account_curation_per_week_and_1k_sp added +* Add block_number check to wait_for_and_get_block + +0.19.46 +------- +* Force refresh of chain_params on node switch +* Replace recursive call in _get_followers +* Nodelist updated and bitcoiner.me node disabled +* First testing version of dpaycli.snapshot with example added (thanks to crokkon for his example) + +0.19.45 +------- +* Add RLock to ObjectCache (ObjectCache is threadsafe now) +* Fix Blockchain Version comparison +* Add support for RPC Nodes below 0.19.5 +* Add Example for measuring objectcache performance + +0.19.44 +------- +* Fix start and datetime in history_reverse +* add lazy option to all Discussion classes +* VIT and SMT testnet added to chains +* estimate_virtual_op_num improved by crokkon (fixes issue #36) + +0.19.43 +------- +* Fix minimal version in known_chains from 0.0.0 to 0.19.5 + +0.19.42 +------- +* improve parse_body for post() +* Add conversion of datetime objects to timestamp in get_dpay_per_mvest +* Fix dpaycli for dpay update 0.19.5 and 0.19.10 + +0.19.41 +------- +* Issue #34 fixed thanks to crokkon +* "Bad or missing upstream response" is handled +* Use thread_num - 1 instances for blocks with threading +* Fix missing repsonses in market +* add parse_body to post() (thanks to crokkon) +* Examples added to all Discussions classes +* Discussions added for fetch more than 100 posts + +0.19.40 +------- +* Improvement of blocks/stream with threading (issue #32 fixed) +* Remove 5 tag limit +* Empty answer fixed for discussions +* Add fallback to condenser api for appbase nodes + +0.19.39 +------- +* get_feed_entries, get_blog_authors, get_savings_withdrawals, get_escrow, verify_account_authority, get_expiring_vesting_delegations, get_vesting_delegations, get_tags_used_by_author added to Account +* get_account_reputations, get_account_count added to Blockchain +* Replies_by_last_update, Trending_tags, Discussions_by_author_before_date +* ImageUploader class added +* Score calculation improved in update_nodes +* apidefinitions added to docs, which includes a complete condenser API call list. + +0.19.38 +------- +* Bug fixes +* Bool variables for DPayID link creation fixed +* Account handling in dpaycli.account is improved +* json_metadata property added to dpaycli.account +* missing addTzInfo added to dpaycli.blockchain +* json_metadata update for comment edit improved +* use_stored_data option added to dpay.info() +* poloniex removed and huobi and ubpit added to dpay_btc_ticker() +* Add timeout to websocket connections +* Documentation improved by crokkon +* "time", "reputation" and "rshares" are parsed from string in all vote objects and inside all active_votes from a comment object +* lazy and full properly passed +* "votes", "virtual_last_update", "virtual_position", "virtual_scheduled_time", + "created", "last_bbd_exchange_update", "hardfork_time_vote" are properly casted in all witness objects +* "time" and "expiration" are parsed to a datetime object inside all block objects +* The json() function returns the original not parsed json dict. It is available for Account, Block, BlockHeader, Comment, Vote and Witness +* json_transactions and json_operations added to Block, for returning all dates as string +* Issues #27 and #28 fixed (thanks to crokkon for reporting) +* Thread and Worker class for blockchain.blocks(threading=True) + +0.19.37 +------- +* Bug fixes +* Fix handling of empty json_metadata +* Prepare broadcasting in new appbase format +* Condenser API handling improved +* Condenser API forced for Broadcast operation on appbase-nodes + +0.19.36 +------- +* Several bug fixes +* Account features + some fixes and refactorings by crokkon +* blockchain.awaitTxConfirmation() fix timeout by crokkon +* dpay updatenodes added, this command can be used to update the nodes list +* NodeList.update_nodes() added, this command reads the metadata from fullnodeupdate, which contain newest nodes information +* add option wss and https for NodeList.get_nodes +* updatenodes is used in all tests +* add witnessenable, witnessdisable, witnessfeed and witness +* time_diff_est and block_diff_est added to witness for next block producing estimation +* btc_usd_ticker, dpay_btc_ticker, dpay_usd_implied and _weighted_average added to Market +* dpay witnesses uses the proxy name when set +* dpay keygen added, for creating a witness signing key +* dpay parsewif improved + +0.19.35 +------- +* Several bug fixes (including issue #18 and #20) +* fix get_config and get_blockchain_version +* fix get_network + +0.19.34 +------- +* Several bug fixes (including issue #17) +* missing dpay_instance fixed +* update_account_profile fixed +* update_account_metadata added + +0.19.33 +------- +* Several bug fixes (including issue #13 and #16) +* dpayid v2 integration added +* token storage added to wallet +* add setToken, clear_local_token, encrypt_token, decrypt_token, + addToken, getTokenForAccountName, removeTokenFromPublicName, getPublicNames added to the wallet class +* url_from_tx add to dpayid for creating a URL from any operation +* login demo add added +* add -l option to dpay for creating URL from any operation +* add -s option to dpay for broadcasting via dpayid +* addtoken, deltoken and listtoken added to dpay + +0.19.32 +------- +* bug fix and improvements for dpay curation + +0.19.31 +------- +* datetime.date is also supported +* dpay curation improved +* owner key is used, when provided and when no other permission is given +* active key is used, when provided and when no posting key is given (post, vote, ...) +* MissingKeyError is raised when a wrong key is set by DPay(keys=[]) + +0.19.30 +------- +* get_replies() for comments added +* Account_witness_proxy added +* Custom added +* Custom_binary added +* Prove_authority added +* Limit_order_create2 added +* Request_account_recovery added +* Recover_account added +* Escrow_transfer added +* Escrow_dispute added +* Escrow_release added +* Escrow_approve added +* Decline_voting_rights added +* Export option for votes and curation command under dpay added +* getOwnerKeysForAccount, getActiveKeysForAccount, getPostingKeysForAccount added +* Node Class and Nodelist added + +0.19.29 +------- +* Several bug fixes +* CLI improved +* wait_for_and_get_block refactoring (Thanks to crokkon) +* Bug fix for blockchain.stream(), raw_ops added +* Fix and improve estimate_virtual_op_num +* Support for New Appbase Operations format + +0.19.28 +------- +* Improve rewards command in dpay +* estimate_virtual_op_num improved and small bug fixed +* BBD value in Comment always converted to Amount +* accuracy renamed to stop_diff +* Doku of estimate_virtual_op_num improved +* Unit test for estimate_virtual_op_num added +* dpay rewards command renamed to pending +* new dpay command: rewards shows now the received rewards + +0.19.27 +------- +* Block have only_ops and only_virtual_ops as parameter +* transactions and operations property added to Block +* entryId changed to start_entry_id in get_feed, get_blog_entries and get_blog +* estimate_virtual_op_num() added to Account, can be used to fastly get account op numbers from dates or blocknumbers +* history and history_reverse uses estimate_virtual_op_num() +* blockchain.ops() is obsolete +* only_ops and only_virtual_ops added to blockchain.get_current_block(), blockchain.blocks() and blockchain.stream() +* reward, curation, verify added to cli +* new curation functions added to the Comment class +* Signed_Transaction.verify() fixed, by trying all recover_parameter from 0 to 3 +* get_potential_signatures, get_transaction_hex and get_required_signatures added to Transactionbuilder +* KeyNotFound is replaced by MissingKeyError and KeyNotFound is removed + +0.19.26 +------- +* Several small bugs fixed +* cache which stores blockchainobjects is now autocleaned +* requests.session is now a shared instance +* websocket will be created again for each DPay instance +* A node benchmark which uses threads added to examples +* Documentation improved +* Optional threading added to dpay pingnode (use --threading with --sort) + +0.19.25 +------- +* bug fix release + +0.19.24 +------- +* AsciiChart for dpay: pricehistory, tradehistory and orderbook +* Sort nodes regarding their ping times (dpay ping --sort --remove) +* currentnode and nextnode skip not working nodes +* Memory consumption fer requests and websocket reduced when creating more instances of dpay +* trade_history added to market +* Issue #4 fixed +* DPay(use_condenser=True) activates condenser_api calls for 19.4 nodes + +0.19.23 +------- +* new function for dpay added: power, follower, following, muter, muting, mute, nextnode, pingnode, currentnode +* support for read-only systems added +* more unit tests +* Several improvements and bug fixes + +0.19.22 +------- +* dpay (command line tool) improved and all missing functions which are available in dpay are added +* new functions to dpay added: witnesses, walletinfo, openorders, orderbook and claimreward +* unit tests for cli added + +0.19.21 +------- +* Transactionbuilder and Wallet improved +* Accounts with more than one authority can be used for signing +* Examples added +* reconstruct_tx added to sign and addSigningInformation +* proposer from Transactionbuilder removed, as it had no function +* rshares_to_vote_pct added + +0.19.20 +------- +* serveral bug fixes and improvements +* coverage improved +* rpc improvements +* Native appbase support for broadcasting transactions added +* Native appbase support for Transfer added + +0.19.19 +------- +* serveral bug fixes and improvements +* coverage improved +* dpay.get_blockchain_version added +* post and comment_options moved from dpaycli.commment to dpaycli.dpay +* wait_for_and_get_block improved +* num_retries handling improved +* block_numbers can be set as start and stop in account.history and account.history_reverse, when use_block_num=True (default) + +0.19.18 +------- +* bug fix release + +0.19.17 +------- +* GOLOS chain added +* Huge speed improvements for all sign/verify operations (around 200%) when secp256k1 can not be installed and cryptography is installed +* benchmark added +* Example for speed comparison with dpay-python added +* Several bug fixes and improvements + +0.19.16 +------- +* rename wallet.purge() and wallet.purgeWallet() to wallet.wipe() +* Handle internal node errors +* Account class improved +* Several improvements + +0.19.15 +------- +* bugfixes for testnet operations +* refactoring + +0.19.14 +------- +* batched api calls possible +* Threading added for websockets +* bug fixes + +0.19.13 +------- +* dpaycli is now in the beta state, as now 270 unit tests exists +* unit tests added for appbase +* bug fixes for appbase-api calls + +0.19.12 +------- +* bug fix release for condenser_api + +0.19.11 +------- +* dpaycli is appbase ready +* more examples added +* print_appbase_calls added +* https nodes can be used + +0.19.10 +------- +* Memo encryption/decryption fixed + +0.19.9 +------ +* CLI tool improved +* bug fixes +* more unittests + +0.19.8 +------ +* bug fixes +* CLI tool added +* dpaycli added to conda-forge +* more unittests + +0.19.7 +------ +* works on python 2.7 +* can be installed besides dpay-python +* graphenelib included +* unit tests added +* comment and account improved +* timezone added +* Delete_comment added + +0.19.6 +------ +* Small bug-fix + +0.19.5 +------ +* Market fixed +* Account, Comment, Discussion and Witness class improved +* Bug fixes + +0.19.4 +------ +* New library name is now dpaycli +* Upstream fixes from https://github.com/xeroc/python-bitshares +* Improved Docu + +0.19.3 +------ +* Add Comment/Post +* Add Witness +* Several bugfixes +* Added all transactions that are supported from dpay-python +* New library name planned: dpaycli + +0.19.2 +------ +* Notify and websocket fixed +* Several fixes + +0.19.1 +------ +* Imported from https://github.com/xeroc/python-bitshares +* Replaced all BitShares by DPay +* Flake8 fixed +* Unit tests are working +* renamed to dpaycli +* Docs fixed +* Signing fixed +* dpay-python: Account, Amount, Asset, Block, Blockchain, Instance, Memo, Message, Notify, Price, DPay, Transactionbuilder, Vote, Witness are working diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..2f26102 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Fabian Schuh + 2018 Holger Nahrstaedt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 0000000..bcbe4d8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +include setup.py +include README.rst +include LICENSE.txt +include *.txt +include MANIFEST.in + +# All source files +recursive-include dpaycli * +recursive-include dpayclibase * +recursive-include dpaycliapi * +# All documentation +recursive-include docs * +# recursive-include demo * + + +# Add build and testing tools +include tox.ini +recursive-include util * + +# Exclude what we don't want to include +prune build +prune doc/build +prune */__pycache__ + +global-exclude *.py[cod] *.egg *.egg-info +global-exclude *~ *.bak *.swp \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..932c63c --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +.PHONY: clean-pyc clean-build docs + +clean: clean-build clean-pyc + +clean-build: + rm -fr build/ + rm -fr dist/ + rm -fr *.egg-info + rm -fr __pycache__/ .eggs/ .cache/ .tox/ + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + +lint: + flake8 dpaycliapi/ dpayclibase/ dpaycli/ + +test: + python3 setup.py test + +build: + python3 setup.py build + +install: build + python3 setup.py install + +install-user: build + python3 setup.py install --user + +git: + git push --all + git push --tags + +check: + python3 setup.py check + +dist: + python3 setup.py sdist upload -r pypi + python3 setup.py bdist_wheel upload + +docs: + sphinx-apidoc -d 6 -e -f -o docs . *.py tests + make -C docs clean html + +release: clean check dist git diff --git a/README.rst b/README.rst new file mode 100755 index 0000000..f6492c2 --- /dev/null +++ b/README.rst @@ -0,0 +1,178 @@ +dpaycli - Unofficial Python Library for DPay +=============================================== + +dpaycli is an unofficial python library for dpay, which is created new from scratch from `python-bitshares`_ +The library name is derived from a beam machine, similar to the analogy between dpay and steam. dpaycli includes `python-graphenelib`_. + +.. image:: https://img.shields.io/pypi/v/dpaycli.svg + :target: https://pypi.python.org/pypi/dpaycli/ + :alt: Latest Version + +.. image:: https://img.shields.io/pypi/pyversions/dpaycli.svg + :target: https://pypi.python.org/pypi/dpaycli/ + :alt: Python Versions + + +.. image:: https://anaconda.org/conda-forge/dpaycli/badges/version.svg + :target: https://anaconda.org/conda-forge/dpaycli + + +.. image:: https://anaconda.org/conda-forge/dpaycli/badges/downloads.svg + :target: https://anaconda.org/conda-forge/dpaycli + + +Current build status +-------------------- + +.. image:: https://travis-ci.org/holgern/dpaycli.svg?branch=master + :target: https://travis-ci.org/holgern/dpaycli + +.. image:: https://ci.appveyor.com/api/projects/status/ig8oqp8bt2fmr09a?svg=true + :target: https://ci.appveyor.com/project/holger80/dpaycli + +.. image:: https://circleci.com/gh/holgern/dpaycli.svg?style=svg + :target: https://circleci.com/gh/holgern/dpaycli + +.. image:: https://readthedocs.org/projects/dpaycli/badge/?version=latest + :target: http://dpaycli.readthedocs.org/en/latest/?badge=latest + +.. image:: https://api.codacy.com/project/badge/Grade/e5476faf97df4c658697b8e7a7efebd7 + :target: https://www.codacy.com/app/holgern/dpaycli?utm_source=github.com&utm_medium=referral&utm_content=holgern/dpaycli&utm_campaign=Badge_Grade + +.. image:: https://pyup.io/repos/github/holgern/dpaycli/shield.svg + :target: https://pyup.io/repos/github/holgern/dpaycli/ + :alt: Updates + +.. image:: https://api.codeclimate.com/v1/badges/e7bdb5b4aa7ab160a780/test_coverage + :target: https://codeclimate.com/github/holgern/dpaycli/test_coverage + :alt: Test Coverage + +Support & Documentation +======================= +You may find help in the `dpaycli-discord-channel`_. The discord channel can also be used to discuss things about dpaycli. + +A complete library documentation is available at `dpaycli.readthedocs.io`_. + +Advantages over the official dpay-python library +================================================= + +* High unit test coverage +* Support for websocket nodes +* Native support for new Appbase calls +* Node error handling and automatic node switching +* Usage of pycryptodomex instead of the outdated pycrypto +* Complete documentation of dpay and all classes including all functions +* dpayid integration +* Works on read-only systems +* Own BlockchainObject class with cache +* Contains all broadcast operations +* Estimation of virtual account operation index from date or block number +* the command line tool dpay uses click and has more commands +* DPayNodeRPC can be used to execute even not implemented RPC-Calls +* More complete implemention + +Installation +============ +The minimal working python version is 2.7.x. or 3.4.x + +dpaycli can be installed parallel to python-dpay. + +For Debian and Ubuntu, please ensure that the following packages are installed: + +.. code:: bash + + sudo apt-get install build-essential libssl-dev python-dev + +For Fedora and RHEL-derivatives, please ensure that the following packages are installed: + +.. code:: bash + + sudo yum install gcc openssl-devel python-devel + +For OSX, please do the following:: + + brew install openssl + export CFLAGS="-I$(brew --prefix openssl)/include $CFLAGS" + export LDFLAGS="-L$(brew --prefix openssl)/lib $LDFLAGS" + +For Termux on Android, please install the following packages: + +.. code:: bash + + pkg install clang openssl-dev python-dev + +Signing and Verify can be fasten (200 %) by installing cryptography: + +.. code:: bash + + pip install -U cryptography + +Install or update dpaycli by pip:: + + pip install -U dpaycli + +You can install dpaycli from this repository if you want the latest +but possibly non-compiling version:: + + git clone https://github.com/holgern/dpaycli.git + cd dpaycli + python setup.py build + + python setup.py install --user + +Run tests after install:: + + pytest + + +Installing dpaycli with conda-forge +-------------------------------- + +Installing dpaycli from the conda-forge channel can be achieved by adding conda-forge to your channels with:: + + conda config --add channels conda-forge + +Once the conda-forge channel has been enabled, dpaycli can be installed with:: + + conda install dpaycli + +Signing and Verify can be fasten (200 %) by installing cryptography:: + + conda install cryptography + +dpaycli can be updated by:: + + conda update dpaycli + +CLI tool dpay +--------------- +A command line tool is available. The help output shows the available commands: + + dpay --help + +Stand alone version of CLI tool dpay +-------------------------------------- +With the help of pyinstaller, a stand alone version of dpay was created for Windows, OSX and linux. +Each version has just to be unpacked and can be used in any terminal. The packed directories +can be found under release. Each release has a hash sum, which is created directly in the build-server +before transmitting the packed file. Please check the hash-sum after downloading. + +Changelog +========= +Can be found in CHANGELOG.rst. + +License +======= +This library is licensed under the MIT License. + +Acknowledgements +================ +`python-bitshares`_ and `python-graphenelib`_ were created by Fabian Schuh (xeroc). + + +.. _python-graphenelib: https://github.com/xeroc/python-graphenelib +.. _python-bitshares: https://github.com/xeroc/python-bitshares +.. _Python: http://python.org +.. _Anaconda: https://www.continuum.io +.. _dpaycli.readthedocs.io: http://dpaycli.readthedocs.io/en/latest/ +.. _dpaycli-discord-channel: https://discord.gg/4HM592V diff --git a/appveyor.yml b/appveyor.yml new file mode 100755 index 0000000..0f4de6d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,103 @@ +# Based on https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor.yml +version: '{build}' + +environment: + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script intepreter + # See: http://stackoverflow.com/a/13751649/163740 + WITH_COMPILER: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_compiler.cmd" + + matrix: + - PYTHON: "C:\\Python36-x64" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda36-x64 + COMM_PY: "py36" + + +install: +- ps: | # set env vars for versioning + $env:COMM_TAG = $(git describe --tags $(git rev-list --tags --max-count=1)) + $env:COMM_COUNT = $(git rev-list --count HEAD) + $env:COMM_HASH = $env:APPVEYOR_REPO_COMMIT.Substring(0,8) + + if ($env:APPVEYOR_PULL_REQUEST_NUMBER) { + $env:BUILD = "dpay-{0}-{1}-{2}_win64.zip" -f $env:COMM_TAG, $env:COMM_HASH, $env:COMM_PY + $env:BUILD2 = "dpay-onefile-{0}-{1}-{2}_win64.zip" -f $env:COMM_TAG, $env:COMM_HASH, $env:COMM_PY + $env:AVVER = "{0}-{1}" -f $env:COMM_TAG.TrimStart("v"), $env:COMM_HASH + } + else { + $env:BUILD = "dpay-{0}-{1}-{2}-{3}_win64.zip" -f $env:COMM_TAG, $env:COMM_COUNT, $env:COMM_HASH, $env:COMM_PY + $env:BUILD2 = "dpay-onefile-{0}-{1}-{2}-{3}_win64.zip" -f $env:COMM_TAG, $env:COMM_COUNT, $env:COMM_HASH, $env:COMM_PY + $env:AVVER = "{0}-{1}" -f $env:COMM_TAG.TrimStart("v"), $env:COMM_COUNT + } + +- ps: | # used for experimental build warnings for pr builds + $env:BRANCH = "{0}/{1}/#{2}" -f $env:APPVEYOR_REPO_NAME, ` + $env:APPVEYOR_REPO_BRANCH, $env:APPVEYOR_PULL_REQUEST_NUMBER + $env:BRANCH = $env:BRANCH -replace "/#$" + +#- set "PATH=%PYTHON%;%PYTHON%\\Scripts;%PYTHON%\\Tools\\Scripts;%PATH%" +- cmd: set "PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" +#- set VCINSTALLDIR="C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC" +- cmd: call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" +- cmd: set CL=-FI"%VCINSTALLDIR%\INCLUDE\stdint.h" +- cmd: conda config --set always_yes yes --set changeps1 no +- cmd: conda config --add channels conda-forge +- cmd: conda config --add channels wheeler-microfluidics +- cmd: conda update -q conda +- cmd: conda info -a +- cmd: conda install --yes conda-build setuptools pip pytest-pylint parameterized cryptography +- cmd: conda install --yes pycryptodomex scrypt pyyaml pytest pytest-mock coverage mock appdirs pylibscrypt +- cmd: conda install --yes ecdsa requests future websocket-client pytz six Click events prettytable pyinstaller + + +build_script: + # Build the compiled extension +- cmd: python setup.py build +- cmd: python setup.py install --user + +test_script: +# Run the project tests +- cmd: py.test tests/dpayclibase +- cmd: py.test tests/dpaycligraphene + +after_test: + # If tests are successful, create binary packages for the project. +- cmd: pyinstaller dpay-onedir.spec +- cmd: pyinstaller dpay-onefile.spec + +# package artifacts +- cmd: copy /Y C:\OpenSSL-Win64\bin\libeay32.dll dist\dpay +- cmd: copy /Y C:\OpenSSL-Win64\bin\ssleay32.dll dist\dpay +# - cmd: 7z a -�mx9 dpay.zip %APPVEYOR_BUILD_FOLDER%\dist\dpay +#- ps: 7z a -m0=LZMA2 -mx9 $env:BUILD .\dist\dpay +- ps: 7z a $env:BUILD .\dist\dpay +- ps: 7z a $env:BUILD2 .\dist\dpay.exe + +- ps: | # generate sha256 hashes + (get-filehash $env:BUILD -algorithm SHA256).Hash | out-file ("{0}.sha256" -f $env:BUILD) -encoding ascii + type ("{0}.sha256" -f $env:BUILD) + (get-filehash $env:BUILD2 -algorithm SHA256).Hash | out-file ("{0}.sha256" -f $env:BUILD2) -encoding ascii + type ("{0}.sha256" -f $env:BUILD2) + +#(get-filehash dpay.zip -algorithm SHA256).Hash | out-file "dpay.zip.sha256" -encoding ascii + +artifacts: + # Archive the generated packages in the ci.appveyor.com build report. +- path: $(BUILD) + name: dpay +- path: $(BUILD).sha256 + name: dpay sha256 hash +- path: $(BUILD2) + name: dpay onefile +- path: $(BUILD2).sha256 + name: dpay onefile sha256 hash +#- path: dpay.zip +# name: dpay_zip +#- path: dpay.zip.sha256 +# name: dpay_zip sha256 hash + +on_finish: +- ps: | # update appveyor build version, done last to prevent webhook breakage + update-appveyorbuild -version $env:AVVER \ No newline at end of file diff --git a/benchmarks/README.rst b/benchmarks/README.rst new file mode 100755 index 0000000..d949372 --- /dev/null +++ b/benchmarks/README.rst @@ -0,0 +1,46 @@ +.. -*- rst -*- + +=============== +dpaycli benchmarks +=============== + +Benchmarking dpaycli with Airspeed Velocity. + + +Usage +----- + +Airspeed Velocity manages building and Python virtualenvs (or conda +environments) by itself, unless told otherwise. + +First navigate to the benchmarks subfolder of the repository. + + cd benchmarks + +To run all benchmarks once against the current build of dpaycli:: + + asv run --python=same --quick + +To run all benchmarks more than once against the current build of dpaycli:: + + asv run --python=same + +The following notation (tag followed by ^!) can be used to run only on a +specific tag or commit. (In this case, a python version for the virtualenv +must be provided) + + asv run --python=3.6 --quick v0.19.16^! + +To record the results use: + + asv publish + +And to see the results via a web broweser, run: + + asv preview + +More on how to use ``asv`` can be found in `ASV documentation`_ +Command-line help is available as usual via ``asv --help`` and +``asv run --help``. + +.. _ASV documentation: https://asv.readthedocs.io/ diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json new file mode 100755 index 0000000..088b34f --- /dev/null +++ b/benchmarks/asv.conf.json @@ -0,0 +1,84 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "dpaycli", + + // The project's homepage + "project_url": "https://github.com/holgern/dpaycli", + + // The URL or local path of the source code repository for the + // project being benchmarked + //"repo": "https://github.com/holgern/dpaycli.git", + "repo": "..", + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "tip" (for mercurial). + "branches": ["master"], // for git + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "conda", + + + // the base URL to show a commit for the project. + "show_commit_url": "http://github.com/holgern/dpaycli/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + // "pythons": ["2.7", "3.4"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list indicates to just test against the default (latest) + // version. + "matrix": { + "future": [], + "ecdsa": [], + "requests": [], + "websocket-client": [], + "appdirs": [], + "Events": [], + "pylibscrypt": [], + "pycryptodomex": [], + "pytz": [], + "Click": [], + "prettytable": [], + }, + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + // "env_dir": "env", + + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": "results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": "html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache wheels of the recent builds in each + // environment, making them faster to install next time. This is + // number of builds to keep, per environment. + "wheel_cache_size": 2 +} diff --git a/benchmarks/benchmarks/__init__.py b/benchmarks/benchmarks/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/benchmarks/benchmarks/bench_account.py b/benchmarks/benchmarks/bench_account.py new file mode 100755 index 0000000..78731aa --- /dev/null +++ b/benchmarks/benchmarks/bench_account.py @@ -0,0 +1,55 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from dpaycligraphenebase.base58 import Base58 +from dpaycligraphenebase.account import BrainKey, Address, PublicKey, PrivateKey, PasswordKey + + +class Benchmark(object): + goal_time = 1 + + +class Account(Benchmark): + def setup(self): + self.b = BrainKey("COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO") + + def time_B85hexgetb58_btc(self): + format(Base58("02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49"), "WIF") + + def time_B85hexgetb58(self): + format(Base58("02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49"), "BTS") + + def time_Address(self): + format(Address("BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi", prefix="BTS"), "BTS") + + def time_PubKey(self): + format(PublicKey("BTS6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL", prefix="BTS").address, "BTS") + + def time_btsprivkey(self): + format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").address, "BTS") + + def time_btcprivkey(self): + format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").uncompressed.address, "BTC") + + def time_PublicKey(self): + str(PublicKey("BTS6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL", prefix="BTS")) + + def time_Privatekey(self): + str(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq")) + + def time_BrainKey(self): + str(BrainKey("COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO").get_private()) + + def time_BrainKey_normalize(self): + BrainKey("COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO").get_brainkey() + + def time_BrainKey_sequences(self): + p = self.b.next_sequence().get_private() + + def time_PasswordKey(self): + pwd = "Aang7foN3oz1Ungai2qua5toh3map8ladei1eem2ohsh2shuo8aeji9Thoseo7ah" + format(PasswordKey("xeroc", pwd, "posting").get_public(), "DWB") + \ No newline at end of file diff --git a/benchmarks/benchmarks/bench_ecdsa.py b/benchmarks/benchmarks/bench_ecdsa.py new file mode 100755 index 0000000..b19f8ca --- /dev/null +++ b/benchmarks/benchmarks/bench_ecdsa.py @@ -0,0 +1,120 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import hashlib +import ecdsa +from binascii import hexlify, unhexlify +from dpaycligraphenebase.account import PrivateKey, PublicKey, Address +import dpaycligraphenebase.ecdsasig as ecda +from dpaycligraphenebase.py23 import py23_bytes + + +class Benchmark(object): + goal_time = 10 + + +class ECDSA(Benchmark): + def setup(self): + ecda.SECP256K1_MODULE = "ecdsa" + + def time_sign(self): + wif = "5J4KCbg1G3my9b9hCaQXnHSm6vrwW9xQTJS6ZciW2Kek7cCkCEk" + message = '576b2c99564392ed50e36c80654224953fdf8b5259528a1a4342c19be2da9b133c44429ac2be4d5dd588ec28e97015c34db80b7e8d8915e023c2501acd3eafe0' + signature = ecda.sign_message(message, wif) + message = 'foo' + signature = ecda.sign_message(message, wif) + message = 'This is a short Message' + signature = ecda.sign_message(message, wif) + message = '1234567890' + signature = ecda.sign_message(message, wif) + + + def time_verify(self): + message = '576b2c99564392ed50e36c80654224953fdf8b5259528a1a4342c19be2da9b133c44429ac2be4d5dd588ec28e97015c34db80b7e8d8915e023c2501acd3eafe0' + signature = b' S\xef\x14x\x06\xeb\xba\xc5\xf9\x0e\xac\x02pL\xbeLO;\x1d"$\xd7\xfc\x07\xfb\x9c\x08\xc5b^\x1e\xec\x19\xb1y\x11\np\xec(\xc9\xf3\xfd\x1f~\xe3\x99\xe8\xc98]\xd3\x951m${\x82\x0f[(\xa9\x90#' + pubkey = ecda.verify_message(message, signature) + signature = b' W\x83\xe5w\x8f\x07\x19EV\xba\x9d\x90\x9f\xfd \x81&\x0f\xa1L\xa00zK0\x08\xf78/\x9d\x0c\x06JFx[*Z\xfe\xd1F\x8d\x9f \x19\xad\xd9\xc9\xbf\xd3\x1br\xdd\x8e\x8ei\xf8\xd2\xf40\xad\xc6\x9c\xe5' + message = 'foo' + pubkey = ecda.verify_message(message, signature) + signature = b'\x1f9\xb6_\x85\xbdr7\\\xb2N\xfb~\x82\xb7E\x80\xf1M\xa4EP=\x8elJ\x1d[t\xab%v~a\xb7\xdbS\x86;~N\xd2!\xf1k=\xb6tMm-\xf1\xd9\xfc\xf3`\xbf\xd5)\x1b\xb3N\x92u/' + message = 'This is a short Message' + pubkey = ecda.verify_message(message, signature) + message = '1234567890' + signature = b' 7\x82\xe2\xad\xdc\xdb]~\xd6\xa8J\xdc\xa5\xf4\x13/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-bitshares.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-bitshares.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/python-bitshares" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-bitshares" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/beem-icon.png b/docs/_static/beem-icon.png new file mode 100755 index 0000000000000000000000000000000000000000..d1d388644e29aad4d8cd024a735f429567c5fb49 GIT binary patch literal 1732 zcmV;#20QtQP)T_YD|XfC)+!zMuleUyOM$W~;X%bP zZQ)Vj@g>`Ec~iJi$t356kA*u-xuEd1tEUS5;Fe27kZ>5?5K^1HwDC&^2$BHmyb zlLgXbu;~zWQL3x5YND$$vMjs(|Jzqf>+3C44}_uO!Es*Nz_$UP>yGoj?>L_a;Jt^l zTroet2mP<2uNSq}U8q}ylk=jM+Je(HXu30!X7XF|%JgU&JRd<#0vax%dbZHBpK}gx zA-*DCbphKu&@SE?|J$> z@45Fq?|BL59ND31ytmKi8XO)P@0Y!C=)dk(t$UGaxgA45Dg=*L)Z&)1xO7m1TH#kuD>f1NDtKU5~aq7%S=5HegrX z1RiSS8jrV$*6oyWTyN8sK9gA;z?a8yrXIKNB6VQSH6R?Bz(AkBl*Rr6S9L9Ie91ye zvmd6ef3f}rxV-TB!8kZaRKe9&tg1Q%Z2(|^25@i<&;SOOK>j*vSSA>kbER3=+JTyC zAxAEYD)H+z)V+$*S=fCDz5oC;wP01Hi^|8+$c#+q#+OO>I1I}KX+Bq)X?GucdQPrp z@cKsVeH%Rvyz_BV9Y90|XojMo+(nct#JmFl70aRC(uE?#l%LhWAH=barM|~%?LucF3*6l z43iNTf5(aQxYL7}lFl<1z#_F!C2%k6r(oJjjcYiMfe=h0hVw~f%qgcEE ztCwe>`bXdmm5=Y+v>j@vHuMVjy_ZcL09U)9FVj_(qqT$8RMVYX(Rc--h%M_NiI7A{ z5)2|l5t4`mEoXcVWqkkl-Lf)?KCJ~l<<3Q^pXRL9 z4=4QmV+8e_S4FnP$n^ZNW{L}y2;$TqDNTRW6K zr~2sXak*OwHA~h_$>-Frd;#sl1D*kw@+hxHksa>N4>WZ+6-ggk_8)QfyB>0EiH5rS zZ+)aH?*9kV2d|OQ#Np2!4VT@THkEJA*kE71qvL?8c+*xgkB-0m;cIR8u32H9S7sH2 z2}Vc1=cdPb(Ppo(&s_Up%gZ|a21nJaErVU%p4l_ z9QKX|ZWot4`S`5mdeNLwC#yjUj{ZPrw<{b9M&+PDqJqH81+=KJz+$zU%{HrTHl?|4 a-2VXYKI;g)N0PSy0000G1>1mvXyaArU!bjEEeMJth=rB4qK$^{?zmc}KWgNkwA9XTly$E+~9w>d`X4nw%{2x5-*4Fb>vEVP~?2qcyCgw2ZL4 z99Je?P%8H^U(z1WE(B%mmDz(%fGPYqnPGx)d<-%w-bX(}49Jc;`QG3Bd2*7gh*w!h zW0fQ+@_0loNcFg^3DM&+(yW{P|JzqRF+5bR-tLE{XTx~!1KT!~iiYuZWEiDmXn2Cl zY&Ac32qPb&7pt1<0PLG^epS_?yKu1ujz=}3MSi6Qnbt%C-cF$-3|%+Sx?JvA<(!iT z2yMt$UBli!xFz1c@&hfaXUy4efR6wG010qNS#tmY3ljhU3ljkVnw%H_00Id~L_t(Y ziS3oo%PCP9$I)##lphO8Y-P(vGg+aKL}}I*nuUc@Qf7;##zM(vwn`QvE0GdOEJilw zUzo^7OikUHyU-n8Tr+R?THNwooH|eE`F@_~Ih~GD5Cp|LrFyIT2k-&>6aA2Xbhq0r zmrJA3h#-hcCA%^~hFYx_48vkdE|-&&b&)inR4O3|0zr@n005j$Cz$~05m6MCPN#7k zXBdX(d4)p3vg~j;JfF`rP5*qRWHNc}Sh9p#trm;LNRnKwR%WyLl9fuOipAn%HBP6q zUauz-iEoTZ5Cn!{ve~R!trm??6eS4aX~)4}aJ${g1so0s9LIG!-K~X2qnS)5(P;F} zIur^m7K=NFKLU=&W4T;57!0=-4u>P3&yPkU48v~YKA&$ooj&z>0$^E|AP8~X7y14E z*=$CVWH=mFDwSf~>-CPu;}4)YPH@5Js!_sFyMLqh5Hvk zx7$S!1OV`OZx)NC)9Hv?x7*$C_j#V@IPSH(APCnNpUGqbfdGo4kAVGtZ@1gUZM9mZ zD9Yt>#pChE!*@~&zyIW3uh(w3H5v^7005xXYQ+@*;Pd$+k;rDV5i^_3W~EZ0C`y7L z*`uhtmt``U=JUB^*0lpkaU54D6xQpt*=&Yk7{f5VUjK~|y-EN7kM;q40DrGv#s$|z TG9WnI00000NkvXXu0mjfmVw7J literal 0 HcmV?d00001 diff --git a/docs/_static/beem-logo.png b/docs/_static/beem-logo.png new file mode 100755 index 0000000000000000000000000000000000000000..62cc7dc4a8109baf189016aaf49c8ca3a4998d6c GIT binary patch literal 73601 zcmeEtWm}s;({?BnZE*kc+LpfrXJH$xkB_Giw2|(^d!>iJ74QnHq--vy82X zk*S%uo4t{eo2;^dn}q?7A(@aMGQSHiK)}k#QJ=)c%F^0_*F}KrKXiG4@$1J-WF-Ij zilc=9nYxTTiHME85eWw)2O~3?ATkNRy`eF$qUiVk{xR@QfXvj<(UzBq2@D1^g4q~t z>`j5bruzCAC{pDauI*(84IWK5yu4z$v|e#->^r76r6H9WCg4*DB?aIs4RxJJz6=eO!P{z` z$$aBoV+!Xmus%AOI0!E;oFz2NE70g>%g9IYhXTc#+He*rU32L0r^A20qe&!Cql(KE zwG%$zX(z<|Tse4I>-cou<*RsI2;$C^xok%`Xvm4?8mas|MNc3yf~93ld%qy^;Q?Jh zcVZb~yaj1b4Tqrx1y6JQzWCdfxI!sLW8c=9Gx7+^xEJhuse;UeTOL=Rvp|m-j-z9d z%5)oTD9je6NvQ%4vp;}LBwO(x4j>Q;*6R}nl$!nl1R?=Rh<;UeNk3X~^)OUP;X2z( zcQ2zY$)|l>eT0uVdPDnW3*`+X^*VJZFZV$)cg0rQ=x|x_))Ct&cV%SmR);VCo6s6g zzKA0Wu5tIGsf9*KMTvq$_c7NOmlFY}IBVY%*}cM2uQfDc!F@@JbmWXuU4dJkF$@ih zS8oFO6vza&zYahk7c<0@{|qQVU4*ZLY&eo8*w;Zk2oC)E>VxUCj`})~e}ldZ^E!|O z`SHEJenI+Ti|{(2hWS4t|EH7xFPzNlGXVfXjhkJxOBe20zpVjJhr7e_1l_eo_y<7D&=byaT0CvN^o0jW7=zu^~ zf$ z#F3UGBQ@PQqitX<-6sV-6%OgbzRFB@1CGgGRT4ACsJUytypA25jMvK3l*U~VUq?UZ z#DIF9-hVGNvKF+4j~+8Q8C#*qIxlX}QS^h^I}uXbOvSZ_4au37l=!M4Ss-zy__;wl z%~W!hgEDpm;Du}89mKwXLHA>Bl(ZwKge}HY=j^2j1foCyz_Y$SIkn&^khdbRCGaQ~C}z@R zDNUd$VQbK`cKtQ+%zUFKoG!QyGBLHrY|iO8O6QgKP*=to_1C6CC|b8+?FFQA^4g-W(Gju5tnDvh?`*oz4bC zCifG`x!B!qc1>dtCKa~Y>^f1{?qjOdkg$1Vk+6A1p95Y~$mr_s=$Ba3K(pS)`9y<; zpw!@_5ho<}Nw}ruUmQ*ja+mK%yy?_sxGYei1p|_N1-x?0u%*}B$%eLj zVC!v!TmhQ~3hA>4&2GN=Xi{M|V*sB1FG8ktFvEV!;AXh2gS$f8I1|S{Puv6z`tq-y z>-F6UJ=DTZROPsJnLnJLe^N7#3vie8!d=0Z?Qe74z&*~~wj4y6wC8;SO{M1X8~MQnTzQnF0qb zt+gqE)S*LHLVC*n)FZ*Yu;+-PLhlv!$>AmQj~@76y#8h-we7X3oIM&grPKHG^-Z%U zDXr%#LNV2TTxwVkgBz@dYRD#v>#X>GYBpGDI`6@jGk@@S9w;BiLz) zRE<33jVy~lq~2+&IBcnyt@x;G0M$g0tztPP7|u~BBpAIk>}MLjE zB7_bR61;oqf9$gV{Qh?7PD~VsFaC+c<+T2{x<%i^Y~+^4QdITve9SY&8xSHffbjV8 znCcXhMpVH@D;HB!KO2@EhgAmihanzD2LvhGIU4VTk{ATBqD$~OYi_2}03~FtH;sP_ zFNa~DcQBL9`+KQtoQd?74Eay0xN}3YOz~+-OpbFGmA?%~;{GDdgwR-}#X)h?tg3|n#nSwzN&kx5N$iX|r-UmEbnu(nnd`p$)dXVaZc-(xo;JO$5e z^*$LZm0F&a>AS;4E(B=@A@j_9l_)t3XEk0rC)B|8DG{Hv&MM@g_xd=aug;kOuS}A0 zdJGw)7em?v`=vQ0eNeF0=-fu0M6G04e<&q|Q^j%&i1No2OHi7ZmL^&Us9R(V8SxC7 z)Uqusr0nm9l$3;&3@CH*<;35LTv9H`D_3uQ<*yn#~>}|7eRqc^YSkD)S(qo)|5uu5vD5+qK0rVet{?KwD znoF4yOclowve;ylFms<>ahN}awO?`$@VbAc?q=>`PMa`h!>lTfDM(47Qr-O={f^xp;C+~_(#1jel(xBZe zlk=@MkhjSNTZPOfwnW~vL^eD$H1q;vR=NTv`Z=2LD73=f$G*M?r7h2?sr_|@->V;0 zd^vB|iGDKGhi$&dusLNuk)BhwB2eAK^+}HMt%n5vRB-~VysvdLm@2AiDuf5*FadzE zDU4m%FUJi$<@g1-6mS6(%)sOYA4M8GCUU(Mxfi+8j7lVolTC zJGLtLayy;gj^IGGgOXzrH^iHiwK(e}$*$3=E(GjDlU{?fU4DQnki+Eek}SEPXPuwnVEXw2>(cf0r{X%w!&!R-T$x+L*~ohKz>h z{WRv~5M0grbeJZ7Ete0kHOFG2jdQw<7LfuTrp23X@8a-9C9qjLXoI}-rMdGkSpP@2 zFmJzHqhi9@VA*LW3#t+Y_Q;)zT$zK=jcTY^)`#?zffymW@oBA*-l@%KCvP>=svf_v zlT~hUNK8hPNT7T6ug^yLG$V@cn)c6&iS-tXNnj4L5+=d&nZGVCXX7U^IV)QRo|5}| z#2&VA(Re33NofM_7A-P2NQJu&_}03fy!Sq54X?{yHd5$JE!JNQ-xw7wIje9B0sHya zI7zP{xxd#CC2(cB7OA~e5o%(u(F9>{9hr7?F^G#~Nu)1(%8w zYJKkF_`J9a@G@I&T~hL+#o!}dgi?P*( z`#l%vywri0W@O4&vmX_%2&O-(Z;UFBG+b5N8a&U5xj`TiTLd2p6MNd1v#n<|0Zwj~ zgsi#G81#*4hPdgS{ljNsHrv5#^k02p#dva4YK0gihFT8h%B{2^4F$FBsIcD?n}(&$ z4Nd3H)-xw!+5?1V-`S~Y*oS1S?Z*%=jBvZ!=(TxoCN!pWJ!N22G>WN~>S0AL%xJ9D zbvW!S$dvb?_%gf)Rm}tIp&ru0lz{6ktfyvU0US<|8$w@rGM0z*Ey`QBWo@g4#BQ4i9ZeWKjs$%;SoNFBNuxl z9^3RkH#llp*{zmVhS@BSw|Q9&rOrf5i0=>uDI;k$GV@niWrK(Q^`rLLp%NfS>{cee z8;+Y;?LdU)#Sbp8@r{M?+1|BESP6T3UfR*k>O}w6#hUDX>m%t4gpiH?o#m0JcdM<< z-mDp0qi|ERn;B}v1Sk5|w@Zmw=BxN@r{<>kERI960y;~i~fUK7L={=-fe zBEOj4-B^7jT+E3#IN0fmQ+0KF2f4k3($b#tM%~kUhfruu(aCK?f~HsLQw_Txa4g0u zZFo6{5#E7}-UIOhcS)Jo`*hbAftax07=<7qbV=^H*2ypKvCZ@(D|o;}CpG&tW9YL= zl|x2wY1z>hS3{GOeUBxMX}e zLJzf!3mprSW!Y+VO>>IMATT1enSFV| zsG*{_^ixu0dvWu#ifJZ{Pj?_7ABqj<;i}zOr$8D)4bL$LCfwq9JK9t4Pa2%4FeIWt z^UWADT9+WTuEk@#EG%gLT2Yh%gO6K4nH z%I079pLJ<2Sle-h|D}rVT{tDpa+EEp9V-i=*UGtS?SCeDC|GS{q1bJuBC)q+ zce6>QRy*s*)z7fWax^BkFWrx~s~Ks8RNl^fF=}>uRwc{0HmD~YX{8};FdQCmFhO1& z3$ZH4bWE1IP&YIEC3sNVhgTV;_s#Zt({6>%#3hlv!vnLXd6Lj^_Jr&wV#d@qY0#pa40 zq|7&vw>rN*B7Nn}RBt~>zWs|uC+ukoBtt;@+L`03Z}MQmb#;Sg%wv_jMLyyxf&I@P zs7=wBxxx02oj;XxH_cI$3-Q!m9?dx_t8KV^q82ErK5lpClnFm3AKmnY9B7~n*^daw zWPP}Q&t~ZCcDE!H9PDp;$lGdhU62@YC<|gn06f1<%b?_m$$RQ$yLJovUnon!_u9kw zuz!+Lg5P2xi9#bAt2J6`KSZY&2|S;9GECZP7=8{_5n*@wiOV^juCH)3!NK`=)zNFNOxCKW=>s)Y%D!($dt=l)L5ltf_PuDm z3%U~z*TF-qOF2_krPce4^807WX&dzpsDR3O1v)V{h@S9uBFxN1-utk%tH_8QKZdr7 z8q57}SjfW>8%#*36_k)9HDro{&^`kj?#9C-wZ?_w8j+O2(a4+QcB|}L7MkVt9%|B1 zm<%~)=Au~FrKac7wp+R_tCa@Lq>Q!d3x>%*H)uYRuT@F!>FK)5sYACc7h}Jde&5gB z9jQ-=AXcjr1OgF*3o)m9r&S*9z04J&FpV4ma-W{q6r%aY!^V=B^4%&GL0Jx$4{`TC zQGAHj8rP8=XHm?xc@aai?PVSJbyY;DAo{m}D7Lm}LNN~u?>(p3BAW|soU;}6V#D`$ zt_63-+VZ{-d$7~$fQ=vTD-TOU3?6Be6HBHmPBOSgrJXKC``2-;zP8LCo#5@>lf$Jx z3Z^`TX$ zWUYCBlUQ*XQY8eJz4@Bm6Wj`=Kio&aE?D{o5BEH*6E#

X{#G{Tz0&c}9${q?aHkH^vjfzxT*r4x{4yg0+uX)#Rqxo@^Ps1y z#~&l`fkc+O0a_!aTF04}LAW0MFUB{>9jtqbTX@HcSL9GY?v0*4@mF{Km?|ce$@$zv zg48khJielyxGK*nN00!--2#Mu+G*+D=TVMlIh&02;4i)(@zRl`eV9#Z5rl?9z9O#7 z60{%1;!s)sM!Ver#WVH|Tpc&t<-d4+?ZJrVPa0V2K0wQ5`Mi3+R%jCu>_48UJ=J{U z=`>P_dVdb;Q~*jO@*xeUnfJSBk9|}ZU5w5bjeV}fVLV&((kKxrCU{|kTv=;g(5$*8 z&fZBFf|8VylT5-nz4 zV=BY7%6pNfDHa?$(Al=_U{Y?yvurU`?|jiGewoe72$$(>KV^gm=A8c4?fL;^ByqR_ zeS~kV9NTjc{ac%M$F>{ccs9bR`w<0q<$iQRy}QOh5LeC;abF+QIpGH||5XudbVlf4 zsl&o;bx7!*>E0u3fxHhZ@7{oHv)umLX@ZVX)AdiUVR95)wg=C><7wt$FFY|TBw>4Q zWh5}}SELJ?S7N`l?tMdHMVPHD))yWC>c!Y{f@#pKEY#7_-Ct%TE}XPfuw<;?z#S{n z?&P@+Ix8LvJsC49vp*||vj0fS9W6r+i<+e?XZ>#D;P_VFnyLLTeu&%8CB?cbnADh_ z&1$pk78~b;mFEP^IjGw;Aq27ecjKEua*UP0&EJ;cv*_Yp#k^P-&%a_BW!bR~Dt?ooQ7L7rJZ~XYaq~L!T^nOP0 zTzPcR(ZLJVT^YfU+NJ`LZ5obUJ2`ljqC1`}jMzXBVHaV9qa{v{7ohFK!{h0MWr`RW zV~H$OxR<9);HDHfIZ3ie45l}oY9N5SHrt@>0(A~X!&ib@(p5y=Y0wjmn zcSX*-egot3`$!2CnxjcBwJUVfI;jcVvke%q435OF2LF6 zY()CxXyN?X!?}Oh2UD1@g0P7jcfMvbt8CC_v6;w5EVY%>BbT>YFdaDF7kdZ5?w!n7 zlkT(i%O#(2e-?+n&V-El9-+-HMb^^+p^enBW!b~Jdt&8}k7#?qX>1oY*Bs)(e-R`Tst;EtqJg`Bw#jpJO&t8SC zMXtD548~fYeMwyRY+|9b6&*@eqZx){%6{8e z;TBY)aE?etB-(u&E*h6{`0|P26hr|B=$2sEc*fh?`(KEOiQm%naKH13-QLg+Hg$8# zoC{44g^g+R-TU%4D=Z+2f@u7;#^M%8wnTqXArA3#yWwTtA3C*J=!Af3AQ2 zA~Cs0c+}zb`^6Z!?k_gDe0as*-0OLOX@HIapO?}42b9rL?XCI)pa97wB~6nK=(F$s zJB4f&&((Zq2fnBnBpnr@iU`KT(Iy+mmg2*zb|xr04VNcz{Mnr6vxr%pTS zw%Tg(yFC#>Rr1Kq)z3?nqxs+zUGG{aj(H&i>?l==atY<0B(U`zKHnndbkk|SbJ%O5K5_Hy3D7hT=Owevv^dD(WKl*u-=O9}qnueH&AHycRWa)E|Z7!8%8 zQKjkPT^N9TegUD8nx*!~wKs&P$!N*a7A1ATs<*Pk_&v3 zqTpTBv!b z+`6trHroK<0L2fzf%}O{N-y_E!!vb?R6Z`_)nSCf`lhRh>m=Ptg4r6F?7F5=>9t7j zG1=2>*uuf&BD{*$OMQ&x+M>F;eA6G7T<0xQBAUEk3SQaL@VXm1PdgD$qnq~F^8Hq+ z=SM|WzJtJI({bkHl=J%|NHSirxhvy^Vpn!*aQ7EsN!LlzO1uo-2eYuAukH5W zYTmd45spe2-U4A)d3TfhlUd{M`__Z)k{kxY?2A{3AZbj2D`Zxj$EMrhKV0ata=89l z@;fdPRo470_&6~daabnugA)}0R$%cmFC<5EWGW1-?l0tQyvj3TJQsl;Gh6>sL~AGM zLWfZN8u&M+QlWcAC447on_&}$0RrT4z4Eb+rXk;VDtMWPJb-Y)KNyTtu4?~LyqH1 zrF6H!L-eQNUMF!GGVedk0<|#%dzY#Hj#DHB4=4+t(ul)ETM?yHi-vN2c{{DDyRiBY zeDP}+3hCpl9Hlh`OzR?LOJ`oyH*G2Ilu++%v>oO>qo8TPu&W4@eI!#v^l9fC8>ecVMS@F=QqL;?D8jSziQSE7=^uIL zx9KXrnlflnaWw2n4W>L8tl@%C@r6O1xr=jq9``R1I~S{7N|`VHgsh6xnfD)_up#W$ zT56s+K-i;iQ6E_xeXk}#Hwr`p@``!~=e-IrV@uliR1ar8$5SEG1Qd$}&<-*WNbmez zREzGz;J>SIGO}_wylJ3&tsz)EjyZ0TM!t_1np|2YxT1>FZ9@n0CsD2Kuv)>(S2?B) zdo&y-wM$>&8EIWa7WBknUFvg(X4JV&oW2JI!7BjA@Y01dDlaUYnfBt*v%=!qmf;hB zV=^8^+Xtc~C39q#)vxd6>Q#FFhIv|meDY{i9i$Hz%Pi?6MGIRqp2`B+z6Ei&I)I7I z$Oa3DGyXk(FGfM2<^pgYrq5Xhxyv$u?S5N8Zbqg+5cFF@eR8K;hPysZaklz!GwV@Q zlRk7B6+~KAx-#DYy%g^CALmDmeKc&#N@Urm+BLvdO`v9+l;}Z-1ndNy%tt4sc=s@8)==C?%qDY*%g}BuBPuHF|FGzKyEy zDJGa5siRKZZ&C!(AHIfrvj{T?=f~HnaC>Y)ndzyoYE~j)N+FINC3@|go%+5+Lt}DA zqpp5l_fDCQQs_-*tYQXthv&+g#1JPHuT&2G{$gTx9_hXHpTn0aMe0fRiICZWg z1liIgxF7;;NGqGeUl zK^tq1GFZIq>2C)FeEFd{^A|GhKvRZLI&2tjb_H@p*bqI2b_Dn7!D*F99_O$ntn-% z0`gOJ3Nq4vTc&-n*cz+Umpo`dnxR;GJ@b=3W0Z{Tjsp8P4a71MZ8n)Eqnf5YOB z4)=eW+|*sx?iZ%xG%s~<28m;KTX5GlRr39JY2&da&F-N?2cw=={6_(RiI7Rt)Syl( z$1H94I%We-$ZXtUl`P05_o6)3VObxcuGZ!bO#ti_0NA4(?Y5fOv=}5l3Kl49+3h7) z)NS*eY>)BmCXR#Kns+x*;x-uZLF>$jAFX~ksN|cckc^}q?Gcbu#dFC^Jt$;~V?~_4 zl*S#SI&FX`kO09+1ui|H2gGt|`^X-j$J9wqZj}At&T$MgZ9Qf`&UoJxUFTGM0}jGTk@pKzR_-N#sEDK zxjt0g$ff{M)1CKTTBI_PkyX^V10|*2uPuugvZfcPsuTr=ZtE7KN0I1r@8pGnjbP+7 z)a&+8NZjE%7~f!c!S_jw24{@t=Ef33+e_kj!9>px=-$c+tU&v2AxAF5&plK(B7cUQ ze}?=~*tGmIu3cC4M0p;-&Fj?xG4iCTm#4M3=C-+dnt+RgLqG92i8EtEJ|y1Dy1Q1U zt}`(ZwbCDRjr&H{#qZmMoqt>1?&2C8n?k$XecRxLO~_YXboDDfX214j5X6*ur>`y@ znlt1pd+=sQr`{G_>;fAx1yj-ddi8d)n-sJ;gYb8tcos9NG&N7z{tv{Re^s1G>&oai zi$nz_Qv0PxM+%5D2_ZDoDzLA&Abh$T_kEYkZ*iMSR-714+rAKupFp(i;==PR$;^Kz z={_loyfZKaWiNj#ottKQ#5S88O#8h<8zwZaqnR_O@{e{%C6OfKBeWrI1?%PyOd|U$ z$?2&*uPrgNCHp$uNQ<>-FTx`Y1OlGRe$2cs@ zeUGcq7GReTNUR)f*oRZSI3*nyUdEuN3B4pc_wuyCg2;<*Z@W$XT6+f`0ZDxX0@%jg z1DeUrb&K&SO$?Z4o;a3d7{>Q$!9f>a=q24nJnSt9G0ar`{0Ci4IM)-pYn)*)g7Cwz zisV;bAy-Z7gG~8ZwsNvY0owBAZ{o9@6vl1G-f++AmgFjOIE4 z0_jJq%1j-U<^SHj+;8h<%uiH|ArY*IG#Nc(^d?LF_*AO-0nvQ}B!vMmma~uV2!VEg zgwA8n1?ci`y0QwpVTA>ZpC`8*U=5Uh;LHaaE@}*h4wAf6%kV22;uHCQ(Qz{;xf^R4 znK8ukrJX$DP0ALR!K;S?5JvT#y&`^LiyLvB^LT6KMZ;Pt!rz)6}e9q7XQ!4~ewrF3Fp(6v@Ks|j@rL^0*Haf6uHN6{q&z4fHi)T!P z<}?73LIs2znwi>z{%iT+5RYjBhQ?E9ld?ll&j(5ri*Yh;bUP{;|GtF<&Wwab@zuO$ zYseBE+oj?WEETSu-=m}2{t4qGJwRqxr!og)29NZ{pM?>v`AoC~cSTvQ9n+j0Y zOjw6BaO)`g=%FsW?z}I3<$l`?IG8#QnckigH4f}3oN;`m!}?0cZ)+T)lVPVtmN``M zHtR;r*V&l{`}8}-SGvRn;+IR&CJT6wdX2w|66DE7LjBU33*jd^2A-G;Z@zOcYd5zn zjt^oN;X1|hRp7$O^V^F0OG@R7&#mZ-v5fZ#E@ZL79ygyQ>KcHhI#}ET*necDBwHUG zN=nok1utRTZ!K>V2Bh=}YcY3X0-Y{rKO0V+E1qQ{nYQ3{rS58lO$#XGzC{buP^>xG zjkKsFF9NGGrnv@VSg#zJj0TdRX9(R%n^Xc&OWM_=EdSGn$}tzT%G9U_uCbup zS1-ia1l20~P{0C`MLl#enY7+Ms@cPK2Q~}zYAx+NMAvP{o-7S$%ME*~!8^JPQu1cGM6p3z+@3+1gNl*4<;=1QW z9A%06(9fVw(&AD+s#I<%SJG)4>{;!afJ{6bRPY? z71m<-()8-Fo3rkhW2cVv`xnJxWQrOWLxIg`r>U62YCZpI)hNlmQa~Woj5>VGV>NTY zTNO+7CRUaT27^jtNyuc#gzWOz9xVavAfna4cX}w9J+EXL4O1!$PDZ_P0#8wOufB}l z)u3Jh_9iG)UvZ6DoKpu9VV49a2vf`(s%0960(p(8qciUqXAvFtFCPGgb{|K_YOs`q zX$Cxx6{$~`DZJHN_v0{xv+5>Vu7n9u^E_b7zfrLXmiaLr+GNIHLj<;c+|gZVbJa5z#_d@68;Bi zYAejckzoa{A7+Zbg1&k-|v{X(I;@ga! z9yz!3zz>!kHVtm_7%lU3-(m|BH?DU4Yb#|=LIKI^m@mDAil%3I-Qx-h%^Y!W3Sq{e z^K!Yirg@SM@4TT0jygbNcmB{Wrm!tvnVyx{3h+?_F8}fu8XEt*?MI6Va#&E6k70g! z;W5))EaloyjU|~Ca%Or(qreVXc5f}FFCT%%xihWjxMk@=ZkAqr5lS5I*`vtKF&&Su zGpPyO)=0qhBNeVT9#waKZ+@n(%ZbwMBG-caw57zX7LMNqZ|H$y9*8WrDAVC&F&!<^ zjH@rNWw0h6KYeR;ngsep_)1~j6rIB=OOaR7SOmk&!8&k1j`W2&;^-f^J{8Gz02BtC zxcLwybvy`EO7vRta}c}d1q5(j_E&h=@ase{egyV}e?rTP&D_G8?@H?EM+4q@tS4wF z({e$(<>yeUTmXRl1A}*4AKQdwZ9|!DqFMQrn!BF2)kL9lDb==k??CdarD>H_WbX^X zF>+O?vnwOBpUb~_U3}+SBfJQO!F}t;2i)=a4V`d$%f`~HY}eGPjW{A^iv?Vjs`*Q` zri*pr2Ll?+j6Qc6LJ0c|^Hjq6RGsqN1D=)K`QCxMYY&5q@=8G$$|#iJe78gD!k4s^ z5DQ$VN{y0lKguoL$@sARdXuc@V6?G}D^;KMFPrQ0NQtC`k`8IE@UEH3!)mlD@~a>@ zxe;>zbe}3ZN0{lk6$78>YPIZaED}$f^LgW)-hh;e6sB(M$TntH2FhA5-%hT}l6SE1 z-4IU7_DjKY55E5X`K-l*Q_bTB&01;)>#_gTnkqL+M*%-fUyc^kNmgCP`S)OLeFQ#x zb5vPS=U3Tj;j=d0!-W!8P}@(RJm3t9b`A>Tbh+Y6#LihcZf%Sg?S0T!CKu6mQzx!2 z^%hhteV&LGD~(7i>l;65k&L8+v14+yd?zQdC#=oXIRP{?Cmb%#VH1cU=8OcWDUBjU zA~)pIG2EZu=19PU^r|Jw7+0SJr^rQo--~zA6|6cm@ug-pu>d|n@D1@qJ|}Z&!`h1; z9KGiWRhLlm1See2UK$XV&&MHy(EUmS8#StDv>d6(7^@vtR?_3C0_K+0lbhi8SiS-{ z4nn>z&)RKA`AyD_^mUP4@&dxsijq@Sw(ZXJt(g!+LSNHxytVs*k_deui@8MGY+z5b z^~6#C0xzcmcb!_;Hb}DWp5=DIiZRNK{N=);YGh@4B_Z~-gIgo1GgBGlGd_QgWiU|N z&(t`UCxHlDez3>sO?fJCro#1%0EKm-y2F6NWN034>Ukn}6{%&&<^{}nP42P+c^{8$ z{_vE@zo{z>q51Twz*ql(FhAvE>WBiP+&w?dH{n<-1$Idlh0=<`LSBvL@Rr4rf*5jS zZh7R=v^8CE8{B+pxx9O9xzO!tSy7FVU8T9dsX|1nKVc$)7KCE%#^tS}LnTpR#A8}1 zNgRI8geQr&%e0qJ@L%RPn2Ci8^zWl4))B9-luki11rlb1!;|CGGKDm{*}}dTs5r>m z+~f?Q^Vroim^r~RGc4vd1%c#0aA&B@jwN}`J~N#1tFvI~nN{V|GIhHi=vn^-Cd+vlvprr-zMl`}?+A4E5$Fu0hsC-Pt8bBBatK7LT$-OsG7OQm{P z&yeGueK=*B3#?Nt){<0sK|ek!WLPre|F%GI{cijWFwB5TST%^WI4?n#CsNgMQgf`R zVYlZl>P?&Upo9!qx56p%X}9<1(mOM2hdnB1vKsTzGV{krAj(x$$=d}X+wU0{d5g!b z_0bhvIJ2hkoIu$+&>seGr~O;hi%C*V9n){F#%q^~ZGs7c9^g&&{ClbT?eIKVQXhg$ zZT(f96i!L3w8Zc-#Olv`bl^L`O0<${yxeFVXfW2{jkVt>@$I=({u6X)9E8YyR|^8t zsJB2XMuM=#!@&EutPb7_p!Hdmjf4DzDgP>?xnOot;*r7YE7;(cI(H3E`8+P6tdDRXEdi+r3ard}0$ zW0~I0j;>Uk1%KX1$xKd-P34V^O9!^Nmw=-wr%(dt~0+jPtDtW3*X+ zng4n14TQf{%&o#@S?GfFZpw0HUu(dC?)Nl7jMsGqHPa`DjQ=UW)gTHP*T9d>r+v?J z#5Z%*0ZCTN?`>R40mIUM4iycD#Is0UW0GQALCoAQ)>_BCU+f7vTA9G|Yq$}?;4j%< zs@|Pt6bRAR_!->aK+sn|qC%yj`TKVh^Ki$DVxPD2agFGQ6*g(zwk1L}99KrJMjNwD z;DvF^yrnpuGUT5Nr&jE&1ZASocGf3b(KX6`m|O&kcR&8oVeNeL#Wv{TcG}^jb_sj$ zosCbp5Mt~tn`NU9@)v+yPtSLAk^hgZ zw+yPQ3AR9ScXti$kl?Nd2@oW>ySuxG;2t!%2G`*30fM^*cRe_7^WA%2y{h;7RGpcg z-P66+>Yf=u_)Mi~hrlISnt0)QzO+${`K4tPRwGI8w}XAtU2t>4J!)z-q<=)Njn!v_ zmvubt7pKx+oGQ(~^RGq&I)x>pot@_uTKz||3Ij{n)Vh_oe@yuJr2^w_p+jv_T6nAe zfBo5JXAjv&2_++m`aSHzOzufkdQ{Gv4 z9EuU=AZUmlugGLxg>*TbQj&_O(#OaCxlO;#Wb^l$?|4MHHA~ir&FQ}Z$%D|)X`o5k z>}+U3plO$$1)w61e_Z$t$Y9O#aE=F4+gn(&C^2Ye@!!*Y)g6) zrZcfz{xzNFsq5YJD)QU=Ey@QW(VA9f`&MM!(>z3!z$uwg zfTnX=^4xD{E{26pbzvdaiQi|sDFq}*zyRd7Qiu4a)4X(rMPzNLK{_kn;HI-&RRhdB zmE2(2HojCrHW4@C2OpIvc-J4-s*Ri2b=f07r7>6Yk||pxmRfhoc(^LUF3|L-PHzP= zY9?GLBQ1CM`FBj8S-X2sW2Vo~3SXcbDl*-{%AFU}1}c0_r^QcX(vA`FQpaqQN^7hj zry$_S3gai*{%c!~6hB?uRx?xeMF7|-ATTL6`TJEM&Fi(G^vPyF!+F>T_@1wnOXx8$ zf0-d{J=I@s&2Bm?JX@a@7CxjWBrCX-JtEJaXsStr!c~ygSia<18#h-x6{L@b%2aK{ zd#~zP9Tqzz#{}R402mE_k4ntT`&kQ+M1-8KaaD?FO5Cxx!={37zAJj&9eTneyYQl| zff4+6+g!&wT#x-4O5n?0HXDld)*2ozYZf4J+2%d@9+b{gT~iWQcgO@AC6H)90*5QI z7JWJl-I!wfL07IXm8_GN`;e!S{UywQ2ruO`zOnvhr(&N%^UXN;P3k(6{ZUX($ySQ( zNq%KPX7ZeBh=#aNjZ*OI+-2R-hisqa21#dk9(=ur$MWgvY1Olv?62GTHEAf0yuc1e zL5(?hB3Ivw)Ujd+0|=`pA!{E!jh-2e^(?VLL?po`AxYk`eO{0eU?kD!u#OC@gg;-n z<<~kI?9BiV9mTWpTsZ0Gy1pyC8c^>Hp*51#u=xUd!|NfH!wD8h&Lg(>NTFgF$qHc! z<;Gs+o4Ov_|A`%`QLv6y7)jwNkbZK#|biv`GABz&R-XJnFMe`$ye9_S_fVKJUHIw6czY z>QY(r(bW79zAHbI`>!7Qbf<9Podh?#_~a?o^CE>Ul?oV-igRZ5A-{qUX8bfkPM#qp zsgq;15sHKi#)FsM{y!}nm&qE7Ol=oT%7?8!hh3dYndC(RZ=ziF zM%Be1KTj&NDuwhJRYNoCf4G#EQh%<+$dHLh??r7MMVOAYY2)&U@5RHT7Xq;;+gG)j zDN2KBQD7L)+NDO^Dw{hn*G_RiTP@!}yj*AiV9DE@cyEv-Vcp-`-(oQ;@H^cvQ)Z{w zX%#E038mYNHCY=LU9-z9jy$f3n^U7|{7pcKk-n^gy{`BFLHtm`2c)Qlig@^G{e@=eGQ2zP0S}o%seC6QM;&5GtJ`?yuGb7)` zX~GPJhz_8M`0E^^`&{|vz58BvPYKP>!@Oji3HOv0qERYHG~z_$xc z7akZZF&C<~c`DbS8}i}Gwjr>|JiG4K}N|I(FYSkGNB)K`a4|Q=w7(&EA!izB#Qjr8==DDUR|bc zN`TT_IURSPA#RW5Ho1E0w^95NPz_n*bQo%ymAjq&>V1LO3N~V-Xk=?h-~fn;uJDPQ z=m2)zV3nWHBqjS(Msh02{5!r@nXc27A1_ZA==&7g>wp6{}cV);IAhH zgaU)@Fh4Fid!F7;^iKenTjV)@MRYs>0)fJ+FAri~k+QifgvUol7O#&4;I@?}-g5<} zlL2uAZ}@{Hy9T!>BmdttWqGlwZc~Geu7k1-_B4&3RNFIP?&M%lVyCL?75XD+D7C>~ z>qSy3_@2ePe*$8%0Yd}rr+S9j6etp?@!D|bnl_p9)nji$Icz87f>e@SzW!@$uvW=R z|M z9KLzI02H6T%o?^`>@MG;>$IQ^0tgO%PDs~viluGvAYG}$MSNFHM;O2)_*V|Lku9~J zo+qsVybZ`De}@T#I#Z+_o3=l_K>DRd@)iX9U{w)%c znYLmtSo{%f`}%%(<*8o+x%~^5oLm_westA-&X4!sE>t_pfm1{x_@2w!RQLL;n&N5C zNSv+{9TmeYuv565eWbh}y^an=8rYU||<2 z>)6wamq=n_f$a`KRq^;S5vbm$yj3>1>aM8coriCi^=Kmtx%ezb!Fp-|KJ{DrqcwYX zVk+Ilg67bb?$njv3Ou^WYt_}?U^arsV4eEO5e^ZJLJaD|V8(vA&!m4qmJTckZ-7w& zj8_vNk6fYL(tL)e$qTOVyL;shf`DYHJ`q;Kk`%9S7uq3$SQxQYkTV+}BuYwL`$Do9 z_VbaT-L1>aJOBQWyzeDqYVCI@!2)ixx#L#dj2Jr#gpsEXne^NnW+2XJd}`It*|s`& zs7~jk(UWieA0e4n)Ax#%`;*wTZ;p~nsThi;$7=0GplZyLR@*{0E{GE{XXsN6wQtGD zk+y^S?nMF!PAg;dN}G{IuKZVha{ods5M!(Ia%{U|pg=>gYp;h5#KzBMh9%263?jwb zo#mKFcoZWVlJM)|+`gnEF}% z_x#!EL3D1j4a0sFu`%KY)#2Uj*bpNKYDWm^E62L)(Hs3zoQd1d$3lo0{RiU1Qd@A`)mld6CbQBmSFw?K5Ao%p1WigE6 z6rrrdb}{LPXzTRLI&^+XlpZTL{Voz?iZ?o1#O5_?#gOZUVyFZHeA+A0ap0wF7VrLl z%W~1sUcnt%c2_8*)_5dX{8P=VD%EHe4cVhO(rcG_7g@5#<@B*aMTMsXO8J9OIAY+j zDL3aXlAIedxYPb*z4Mn>sd(Ke2kkz0GSDua3)C}5?keql6IBjf z<0j4zraeZS04c0_Ql;Co%Dn zNz!wV26-;uon;9YRNdRi@+G+C9HW;~#>9+-W*WGaz#E+vhxpjkPmf;!?S{<(9`haBL7Cgw+(hK+e_}%u}7IPcHjC<(>RpWpa`NsgCdgRg2p~~W1j-NAag-EzLi2Xw+M39DF<6j(Km3(_!mT49KEQ}l z|iN8HB4 zurXrNjOSIG=Y~M4XC<-*yO&Mmeic`u)xRrHA%;~oIC*vaC;r}BIb#NHLb>cBEFW@F z7Y%(kRZ(C@ygEfW|KrlKn&nIDUY+{vL}f>;pYbgbf9cU=xrp~4jz_8XD5|no!>yi* zg39n?M#dCzVKDkN_1hXT|7z_^fcJBshp6!F=PMu^0xR`}K?ARxHWyU!y^I$3wcF`LaV zCgVPxd^6U*KMv?UAV?g$E7Ic8?V4Lem=pacVJ`Im&Eg*rHA#N=&?js#e2nbnCsxg~ zc?l;-)-|-lBPiT=K^U$i+y^O33se`?fOnfUI>PMy$Tpn z0ek!MzO9ufa`bTVzz2jof7aW}1PxyHKm8<6D5_*I?FZ@Jqp-ynSgAnB)7zVEC9E&_ z*?U+0gWa1Rg!){S4PKGjU$oi){H{L2%r^IeuD10M7G$N4Z}>_(HgV!}9FQHx+bc>t z{}6ISMkaQvd+XvywnuLtCozIz(Q%O4Eovt)#a*SJio7VfAJ?swdbKG+9JJb%Y(Vm0 zUduR6%Q)y@PHg(?XZFFEnqNj4aT*he_Kf0<;(wb$eknG5mD3*eJn$d!*;9zg*5R01 z>AoxuqciBQuiiv9=v|6xPT7&$N~ynpc?)+S8Mr*}ZTR|O(QROkn3r$5`!Z;PoHcb^ z9XY}YJ-@!!4e5qK*8^_mYNw}RCTT+S$H(h%T{DsKEE{5@Y0vi;Ozx5uwh zJ#Xs352C>N08xz(CfRHgt`Edm{1iP?(BfTCi@m>ne>Zb*+B+=eDuek;`%m&1$im4D zitCz*?Bv*l;2<{Fma6k)smeySEZP`x!NJ(Gv}z{|f_&6JaE5k9PuK~le z@Qu5XCqgC5f#rZMN}O<`yM_T~)#uG|%Z^@B(cEj3frkxLRyR1jdLoQ6UGA@}70yMa zjD&q%ON<*B;@*CCkX>}EM0#~Um0F6pG6~s=VjosEe%Enyv2xzyQQz`FdPS2f3lEx)A&h1qw4Ah;xSj zpwlBKA^j?d@l<(sRanV>UWSvS-1+_b^i1pE$QBAuQStH1$j=eQ7bEvaH5nNT3Qcfn zrN+$id`9|n<0q`Mpf(B}7&F(w2oOPdksie@Ue%cj4Z?fMfCs=@Zw+(Nh4fnZPF?An#gJUx4QtRt{4SoH4DrvFLs&BP)k!-| z5K7uOgb#I-W0FSl)p}0^z1xoX*DoBBax8ymfggB}&)6jyQ!?20Tu%D}a8I7MG~Sd6 zUMc%TaK8EG{_th~^jaU*>ssi-K;9gjH=%YFt;Ktec&3w3vw^_?4X${uJ>o%TQE5FQ zRWC3~Hqd%8Bq-*1M@0QmC8v^Y>+`e9ZfJame+d(z{JDco^?&-HZ=RGMhqf(*oY6O- z=GQ}?R<`(v+?`4)EgbKcpq~$oTz{rrg;bc>eBSjA6KH5wIICb@C8bOG(HJcx6!9wN zy)mr#Y3efMHN;|nVm;~ekoIzZjJ^~CCQ1`dQe4xoFvkc?Udz2$`8+&EmLe!y9*@47 znI&%-SUfX{MWUTK-JRm;s^9hX{r2vuq)2sSjY6Q++#n=G*NuPn5JUfe^W z4vq(#{#YXBRl|eV!`4=&mYpN&-EF1h3CIdYe$mmDI6A|ed)BP)KqsJ=07b*53`!1< zLQMtyv2yT~2=dOv8;*E*z7JWpkk8`r^%Jy*H+ca1t0+a$nP% z6bACOC=7(=XlH;@RPWLB)%;HDZ zACe*m7asl0X)LsG`%0fQ`hL;NX+_@K?oYf`lg(9eJnIN}ysAD=NBBH85lNf8LcEH~ zQ@v7sk@}|ohM(4%9hz(O&(?$xs{Cs&##BeS3jG+TU=cPDtXdG?C(G_k_yAQi?5h!o zQ>fUm5WbEX$>S6x;No+}7wD2UuiYXaS%nY|H?9mc8MH)+Rn$djd5 zOVi(MZ9W8RU)qF!MYeE9YD~;CtwO@^2Nbg3I+t!`Ae)=fr!Xb3h;)Qv_X&w`@jMm9 zf|Lq6PgEXLQrK%Adr9MiLd-8y!Z7`#s2ag;vY@h1FFdv2i)t|tiYhulXp6UsxS(pd zjx#d00QHKUnPvNYoh>bOz?2(d?F$T)rHvSWn|7KdVnH==UnVAsp(TXJ`B0TnD3phA zt>UuIUn23sjuBdpKLhMSQO-r{t+}E@K1mt@;iV2Ea9iVvlq%+YwGCFqIh*!}hvmu0 z>u^3%MIV#XHG6T>0&mEFH5OroQvT9T)!R@7&X5#RGN}nOJv(S;mJ;m6zC%7}DT3dn zuCLN;Wm8PQlrg5v&XzGYJ41ZqEY=YqNfu-3@ijDCkb^)1+_y$9B2fM(D?uSXHEtL= zb6RHT=Y7BBfnCi+Tjv$6H?&UbM#|Q)ma|#q%y5i;aWmRjtis^^tP_U9dg`iVm}$0V z+3-~l6!L3)x@wVrIv~&xWONkW=8gv^it&g?9Y#foBU|8##k@+m&<$W1v_ZNAHu20x z;B4Dc(y1+&)#|X05OxIZY@GDB4Nnev|G^?sd-9N|2aXPRp1}9{_|t!j3-w0@E8vrE zLy?K-sw=av8ShlDQ-2GMV)5ax_z|0COSMpna}Tlqb)r&QC*_#+J^DE3Sh$1Z2)-Q( z;T>F)W>Kwnc62s+upLI3A>C$~YjC|-w9k9qhTmsPNZm^{JJ5DNr7IXSv5g=cj^*YM z?6+ZdfqzGzI`hwe12ip?NqlSu!>a2Is zO1HliB7NOB4mUaq0!SoDRu@V(l-6oiIq1k(l4YYvIpWT-HTySLc>f*cjr-Q)pFu@i zn*mYuCe+^jzF10<^*BDS|YLLwT&{R0Z3G)v&vZe@OoFI62=8bn~z+(rcr&y-%hG1U<)fZx$5z&8&BQA3-d%z!-zfK>TTwhe=8)m!q;vq3|tNvDPL?|Jq4r zZbpx%94hy!^gX6Q1Hl5@Fr&peCO1vZ2!HUP)!i@w|AXa*VpWpSk?{~-L+94>mumcu zFSewT76SasPLuv8YmrqG>TLP}s4&qnJI68{5=CRjww$WJJ*fLGfLjA#CT^#H%Y<e2%;kQOWCQcvt`Q;ta*h$En6uUDG-e%rE?)=CZC`0~{W5c;{w~{bQVbJdj zrFZ+V^t2K{;m;>quJmh{3?90nsj0s@fE(i>{^lb)w0wH)O-umsp{=bgUIHTyR>#^; zYxY629T8izxWjUS1NX9{`Knh&7(}tJhny8OgESX9306?jtkC>(XhsC7Dv1;*R#r)b z!Bb<$qScz2XQJkQj^}#39+;~27*M2ki?b1V{xHt~)a~=BwL{R~#~aS%;=SUfEpal( z;VD^&G+n?j^2RGL@hlaN5Hf1G*FI;!D+$SqRY!~Q!W9mXr}!bS|8h8?XHfC>>)+f@ zL>MUl024y6xbUvc?~RK)9)ca`gig}%=U6JOijGhSF@*T4q*`VPIhx9`%~!QS()Xwr zc%25)=*{JKE!%_0P1EQY1JpSM2_PSwv9>>8c(-DpZD07GY(xg>^9r0=n3HYzLhU30 zCh*vcG0=r*(b%u{M!MW0c|g40p{J_6(Hu;54oMex6!@4wMm|4pgtrQ7OR-H0#QHn|-b@Q`fOVRoJ(b8})qG?e(T?AU|k6F#U+lZ1FoF3SwFSHJITn5-_{ zQsNOJ$h{6#4h-+HdKTnCBxX6t*t-M4zD}N+)x&5;(pZS{e-{(r|~#Id1d5 zzq9{xXr|VL35emh9B;WJ)tSRH@a;@|q+B8)3&5kB+&MzkZTH;xMtG=kFp5!@Pr9ZWrT^memsk=VQAwDPw5~qT|{G4*k(@#RjH+b8ViBd zXyZH9o< zyt7zLgeLglQ#q{^=T6;6Z$2;^S)3(0d`E&%jQP?hNk+`fa0`pE;>l{yhFg>Sh{{nd zh`E#aDWBhDR@PA-LZNbMtdK~XHyAtYz;|qRaf%j=7e9P(Z#

ODgwKFEXVD6(d{>132eTw-|7ZQ7hA8I(oN^sdUzo*Op9r$Lhpv3>c1CRnl`THA z5Vni7UI!p={oCi*TCuNiu@vy|30R;i*kh#r76`?-u%$PH`32>=)aMzfGfVB#f>cL5}^_XSvRRuaDDslY?4FHZiR zZc4RxexDb+)#X~);K$J_f?L`uYHVbF6pSDI^*SXAkX3g+{%4<^I1+s@h1t_d#wc}B zV%-rm1rB0u8ZzL*CR&~kD4j1Tdpo>tWa(KmkbE)8>_nKhy_Ne@G&5ozb1Q3BLDspBF(1CX%qGmXraRoh`D99dzYvTi7=oy5# zaC_|^7SK_Q<~ot4n*K~|KoIQtd_e+q%)fLTY29U*g`B{WqCNYE5?`5>aK44{e3DBq zoAsqkvm$-=wb)=oIP~DpXRL#k#xL`Npuh-CmcuuKn)|HRv7ys>pcajy8Uh$-JxG_g zqEeRez`5pw*@T27H> zIU!0iW=5KEMH=1=J-WZd-na{p{ff&j&;HvpvP0ViV^7pD?vXL5g$`~2MRyP~8%Mo6g z9LPXrwzR2#XnpM*&X?Z=)~6_e(WnapGE*=i3e}(rDOk)7Bn2Ss4yykAF;wi{Pu>;m zg8&>P&BYF?@hfFEa_w##g-{MrAddAhtv?;t1Q$mbb==N8ilRYxr51*V>UvI)KFTLf zgp^X03$_MC<EN91&qD3g7$6@WN+Kjo@p)G6G^6knQ1=;nqiyhhKW=tBqq1K$9`T0{hP)SC zoQ-#EuETh?ar>02K4Q8Zrb{(-8FU8yHiEsn<0|R4b67p6ZyiCXw`ZR?)D$vH-HO=T zdgl6TTsi`94gK#l3X}3wpt27*Vw$OW3tu5Q3yT^B5;j!`2O0Ku?)NyN*Sa6Z2)Yo9 z&U}$(R>DYiD2X2`DoqV8ydJ`LRmSBwk`!@}4jm_zZfxJc;Q_;k--sj#zQA@#j@*Ue zsAXQlLqG+e9IlxvUi}Go!qbN*1UtXbhAOwRfZ}wpjExAA^llXq?n|4a7F5hDWeYD3 zl3IAf|5gyRL5?}|=k6JKa-nfA7HdujZ(t!-EAoy{m#KYES8G?BXuf$7-&hZz7Wz^` ztxeQ>;jU^{XU^Zg`9UHW1j<9P!h}%=+!3u0@DsKL+Fbn4KCIHGOiQ1*S{2PLMI9H2 z>Qf8r?T`)~{S_EU?WBcc@AyM9FQXL{*sdU*2!JXY@V;-Zgz%Th~j16eda)svx z$V;o_6`_BXQe+{P{WJy0E!&K;y%)-;$_BGzla~(OL)O@>Y?}}$A=F+GcdAbnSM~)0 zN(XLfEnSP+8J5HctvYMq{)6<8auqMBGIpIAc9WfWZo;KN%BsA6KtHM~Hz7o^)@b3* zfN@pPTN<@r7e%x*%7b21N%h_9G?<9WC?Ssp!*xUeVN^kORO}JL&3Bp-Xlup7{`G86@CuPYMV8n4 zG1pf*s$xjy`YgpE*Azn!uNOz1#9NQdj2 zHO^x+yt8PZ-+Xp)ne)QBNYfap!LzVjDvTYyS2r}~>I@neCu5$EKy_y8g#Jnj7}Ol@ ziG3<0Xnk;k(JTkyiAeS@7gLOx`gEBlBk5SI{G~{&95{`vHRB}j*}LTchBE%T=_`>6Z|jRw3Nv=MVQ5njUj&&1#oei@~&jbDtTS&D%xJ9ghC(h;n5bNZ-$Xy;f^ z`1|5!DeMI5zkto4RUbYZ&K2IUh?(ew2P~?5y3%6mVk^4(L6Pg<14))uA^oNGBem#p zeo4#;y?*K;_MhD&*Py~IG|LV{0zfVkzL*FXP;i+ESjF4Z@BSpkz?2lb>vCy$gWVzE z-*;i>%%86=du3!@Bj0+*mh;G|w->?k384Tu$veXb3SR8d1~F*Lo3782_0W16lgm zb!gjG!*!-TbxeNy@vsGb@G}Nb(WoXL@Fk&>VdpvrhK!BnMnT{1JGZ~@2Z~+cy>_-R zzu|xrP72IvG4NF6C&KpvWN+a%j2Jn>nllh5wh4EvT+D5T#gDYfI4+!W=6&xWfI+*3 zO~C-GaJQ|BS1tarWGx$QXyl{7g2VRk&X0_~!|NXz(0{X)jEKaM0JtoC!x|bKlMP}_ z`}rov)?8G@`9FyeG2p4K?fg+l$uMmOlP|R7r(B8Hl$LwZ2_hx3o;tsmByQ=)e~-1j zrWwfpW8sn&`WmAeQ%dbq1Oz1rX%5@W!@@jrHLE9K$jOGNMB2$EgXYP))`qyrZm_2V z_2-Yw7p8#Rlob^XbgGoGey;SE>eEv__}6aY)pJ+LSSo6Yr%u((tny^LP>3(ta3BoE zLBKeq{|2j^Z&~3w!Q*o|tcJiDv1d&&BC>EN+wlB94zgr7HEunH+p|u3zxYJV>i)%C zkn7>0swzkw(WXyA^0zH>lI){4*}?J~tiwS(U|(FkT`P2>dv)z_&CIDvP%cPZ6d@Ql z#3FO%l9L$czcJ&Ia0)G0ASn+mOY2w*k`kE?u*lvBF8IAYi#yMo(R{-GHp4P#{!F?W>$D z&Lp|x3MMyay?_)sU{k6F>_?M}y}lM7aZvvOh&x}4dHM6aX? zdIopRpLjdmO(>$dMF9vhqXG*erWDoah&n>cHr{6#OuxqF32KMf~N9Y zC4}&y>!SQt(^pjKwPuBm^wm;*U>&K}OPPT_y7!3Zg%4&-i|9cq7jRlCcn`FL^I~N~2$&1G| zt)KV#1qyz#r76|M%N42g&a8C4biwhj_jOGRP^`EbMNeQHvZ_O1U=&uEf|PKA1_(WX zg}r6B|BwP{t;(*f$6xUxwycr{x{+M!j`L!{#b(Ruz66k41Xsb`JZil;$#O(U+hsri zR*&coT@r5B&6eoY`E~OzUsK{pW6EsBB9ME7!xMSXaD-;vLP-3nyMKo@a{<$u4cb{# zB_TqVlSS4$3YK>E%57D(O8Y(2TRBAiSt^lYSx2jd3t1CSUpT};B;7gWHd~G%bfxK8i~aG7ntVgL0JX%2_FTDl zP96oaqy}ZPOcx`V)>feG5CGYOC%?WBnGmc|olt!Bj!DAAeU*$ML>aO=106_*e4*%M zPDqK(SrV|oWZj+F_v3UJ7i_z@NSD>S7G1M5&0#Zr@&#(6#i|Tao%4SJaK8JK1QUaa zrN412|Dv60^h+n}C@gAU2d6Ni(Z4G9^Fjg4B4(EJEZ=(99$f)9UVLtTY97O4m4{!F z;CHl4tdVdKHuX0;AXQ0Vp=?$<^sZ;$-@|W#S$1*9do7Jm4BXrtjOo&8lv#JD4-2hE zn*MZ=A3ZuDyppowE=e<1o%1etj;9{Y9j-0s5!B?W#ABB9nQ>D+@BeAR)ccn!pn56@ zPhr|Ps3)rzNU#_E1Q#ASrg3@uznEXw^$24v+jG;t{g|?Sx09xLbY!$&dShjM{qQ>M zD{srKVo{b-5ms^J{GTquqgPqVc14-lRfK}iY9)1AqZq+>Co5ui_aZ`Sl4gL%#=a8( zYW|%n5Y*JCGj{=zT+opdf!v64eDf?1{Q~X9?GOiX-w39{Bp>M?X(_oAvca6w`&o38 zF_q)#k`Mbd0-lW1H0xKK|0fG;?$u)zaunm1|BnS!#AL zWgt0vtll#v8ham$EI@79k*@Q~&2XwlHJnCk)BeLm0~zW?^f+i@j3g6lYzFL`q>b*m&d&BZ0g9kfOEVez>jHZ*1B?CVk}L+TnWC>+7kTaYm6V;}^zV z?Z-EX!nZeE&J$P3W3;WFd`JcDUp7gEYdU7+?>m(5Jt%z0tY44`j#qBNP0Q(Qu&g5m zTnXXY$@dyjw<9(I5RgJuJcPrvQ1dYXegBMp9n6k)9-%u3Z(%mLKiM^B@j-y<*YbI^ zRVwG9WL~`0>6>lgTUzMfBVb5u%63U{F{0^dSr4BD{F-FSqC{mFF%0obWu8I8_puAV zG8TXmG5ELg+8*6n^$=9#!ha{(btHkd{?a))p%iFvxI?Gz0=pHf8eigNv4hl(#dGCd z$@CpN4pQ5Dejb((c4#UHIbJ>wG)v_=^z4kcKYZfx18#P@apfib>m5E^(y?MQklxr6 z&}dBvLE$*~KZ%rh1oy;CzV!o~D9dRc@$+qxk-ef`mucPo1Q)N+_gW`iGMNEa{@j=d zoP(3@*vf;${t2V@+s}*bR3{X-5pi)4a-tmg2Hk zrdplX;!KOMHH5A9a*of|wl8ZuwvG#u(`AQTeoK}f5BYe!v~HaO^fxA4L*88~5k$}% zoU>JVN)e{=K`dS+f$=JwZ3p2!XeP2P$2@;RC-kYux>GE|Fl7kgIy(+`hMoM`o%Fwg z&wgk1r=Iw4(lx)yUxX0uYaQs$mSb2#E`Ht6ZOgs7K5xVO>Q}UR)ME(poVo!V+|>BN zR&ep5006`@7pVNCf5Rm`tyNQViB%fef`O(|R;Omws|OxW!f-92ckk=3D-YML`Rvk& zn}$)C`O?UjHaUird&u#>G-F&)F^($Jg+CWnv^#E{)pjL%9#2xh5m5ZsW2+``OAX%i zheyrrU)=2?$g6)P%k<)A3fV2rsS z%;f_V2#Zz1l6OTClL{(SNK1R!3oSc@hb3-^{fk?uX$HRh);~VMyN|#nfo^1n(E!k~ zc@~xw0LrMHtoQ8%WW9NQi?y&l-A8(=yZfD4ov&H}-3v3#f7pF1Jc?+;KxQ+ww%l9i zo(^EWu4BX#N8o$J#_J1l!9a*D-kmPLVZsDu1n(1h3^Kg0gQPlu{Fb@ctcB0W`hLl} zk065g{(meayH4|$PdmXu&ujK#6QQ5bSe;>Ygvw*tY~!Tp9}jo82VX(DgGP-1gqIJ` zcIPL;7e4>VJjNf%FhB^9)6lA?+X_OFh=mkA&_4)d-iaP&IrkY8jawyU0tJHJ9uVZ+ zGZ|-eG1$FfWn!svx`8qZ9e}siw!oA`Kxf8d>(*s%7yOe4AQyPIY3Tj38K0h;y`NO) zoNV%Ng_Fo!*iQTmn=oo-lI}Xa@Tk1-DsmBi=FCrJj@esb;Rfybd3pHm8;;s1+C6EK z|A{}6O7#Zp|FG_EtWGO!#yn`WT6i#FfO2KRds->G{*exlOt;V-V?Bwl{6Y`M4@+-& z_A%~v4m-573!?uErhCC|r%(AjzRfe39Mw_?N_L}(dc@Gv{+vx2)wICvO^_4OT2cNQtcdXlP*eqb=6cNObXly*9Oy=kGO1w5QL`yzZbK6HmW z;W$y0TTGqG!&2A8`ZH>m$ka^p8ha{_rW>g^;O*G}q>jk<>D;vOs-L-JuSV~&u~;ga z=AmGA{!z9YU8p7rw-G=f^Pi_d!h$MigN_>1p7N%!PeLgs4u1dC`G3nRNi)n#Y4Y@e zK#PW*v<(5N9d8S~Pg6nH0%UDf*}utu#jo__Pu3KRWT!{BkBj-YpOl5uKqac-0UrBj zUAK+G|6B#fiA^R^YF|u2%5$`NcPNWQK~C=kg19z$4dSN!_DlC&SZKV8)^}{ODszCd z_(CnvI-X(KuLf_&>=secf~jLvV3qYjO~?#6>kagD6v>d{z2g#+Ff3>)2A+ z9Pp$5WJ8|vC#WQGx|yZ!y86ZL+>xJTZvr8?k+g7qVUEEa;?AEfG^evL8E3N{a|Z>8 zH49?u*F)Ng(5>rw$DXz9nUOZDYqEloGO;^Xp8#du2D?VHjxACdL+HJ#XM;@D_|H48 zPiX1up||_qxXVUk&s_M(e<2jkuPJR3$=CB~W> z!6UMOj?ZNR5hTb74`g*-Bz|xu6DGD$_|^F;PQZ(qeaAhbZ0_gM^!qYQ^+Nkxh1oGvgA!(oBIIPzEmqdV^1jjOcgvSJnjr176{xub zGoU8wnRw5&sh>T^fB-k}Fk)EJw3Ecdw}1LSRX8#gX>fz> zBm#({{ZVvZRvR!_3Q6$7SrZz_k*8zI15HjhQQlPn(uUFx+6yBH@njM#qTxmiG+`7W z>Y+2qqd;@G;1j3)#O5jujwgj|h^zZ%&dWFV&AcZ^Pmx|Rx)90~HVXrKT5^WL&hl|n zipA-9tScEX!VvJvLb2{>5%A7>d!dfUH8EBLY>o?9PcMdA*qsSC4@%-5zYzp5YnU*-J9#^b!7F}o(eNoxCbvRXh{la22B@~5z~{N=tE$U6&w z?ctfZ4Ppo@_Yyv`)Aw;^9CoK_@(A-X2y=qdw;@>Ax)qh9Z=A|1$Dhi;ES&HHagO2M zFNWCL)iSp`qiy&Txo~8)1}e-S24=(rl7q=a9q`v5@K!w#Yz!`?z7{8YH6{DECwq^N z8(ck73;+B}hW5Y@cE0o$QX35euqH?4e9s2Lt*thG=JTcHjQmh&&_j{J}-X>eyf1UTN>ZXMa+9n-b z8^SvDo_^bQd;#?FY{g``u=yZ^!;>dIN-xn9@1lI2FBPPv<$DRaAfoWFu+N6C{bT=a zh8P&%>c$Dbx#l^n4Dkfs%i3}b#|Oq(l0HBTlV-;U@Wx*MaSwC^M?0*@Wk7C+TVs`Y zx#mcRuH0J3XvY8b0*I)~1(alY$r9;zi5!5vR`RpS&bzJk+&WA$iWK1cEc*JR4{ko@7$pJ&*(>c0K!@RwK6 z8+YF(XPuc*`mQS@@ZgjVynk)=TYayk3y^^4C^=6t*C5IXJ$%BO-BW6Zim@)MkpBx$ zJo#a5Rm^Jf?%~iB-v(Y@PefYe6co3BK8jBvs`!pJ=0>S3GxYxCWd0E60q|t)7BmL) z%=!%dufW^KJmya0_}}i^>x4+I+|G7RpKyEX7}M1Re=0TJy!mdd8>HrZ1KiOjG?x={ zSwVUQW8v{vL$1NcNkH2DFEj99p~PcQU;(Ev@VrCHQ6;>)>NK}i(s9k-i!kBDJWh91 zwDw}PrUr2O=u)mn;ikNurUQWd+g-wLhpdV~{ z+)ly{0rAiAR@aZ~%pxNG40ieIMei_3q>?w&j@ zW#&`0{jV%CW0tG<5tZvway9Ag@d2o3p9DjIN>m4-5fOh^oX>JrIfLRc_kA zetV#@NyDW`A1qMX?#0z@|6WU6<5@RXmw<)77S)EnDIrqB>{mH%>V6cpR3?H5r$d#% zofFkS1PH;?wZ5!m(f)NFLku$;Uo%+);az!l@9jL zHm{z@p!Gqe0b04#k}hdjQ?$s^nLDph6yuC-Nz#VCZ~&4;ha*@R=qw|Dfk@RItfN?x zs1)*(Ad3r-f;I&d6{gI{-MUE8eHovox6~~I zToeDv20@~YUICX?G`T#iHjUvAP!)GHiENvT*Fd4S3b0l^c}x+$A@<;`fBw(?zX&7d zGS%eL#EyrRbe(|VM88{#BF=P8R$_EX;YGg-55ZTX5^7^zNUd~jdE9a`&c|AehABrH z-kk|vxOY|+3I&6(f}o-?e1MDHd!`B7!slj%Bky@6B^Use1f=@$)C{KRg^(xoogH#f zLs{>Ya)kjle`QT<<|Pe8s+2$db?Kp9FKcTaFFABIV~5`e7DRD_o_h~yuvF;f3EfR_ zA)&q(QnFA{N-l0$rpF+?j|l?>6=*NjXlGiEeRA>uJkzQh^jV20SGVVy?&2TN+0&OE zHH;4cXd!1$aH3Q*~V$TXY&ieITWGq3dMUik7;1l#wt>4HI!}})f}C&DaBz-)jag# zKE|2pjVf_G;R9OYZ|LjzN}#n%fX44*3ZQgRl%?~{?~~VU3Dw_D(Lnf@U+j{}V}qRO zh1I)3RWAk?;b?roz;bFtFj83RT-&fa?g)Ow)95e=CyagnBa281=g~plpai z#smr`3jBwNYcq}DsP&5uMVbB-Px{n$DtOMZQHoBjb(ca%eI$o`+N1g@ALlf{Rgh=JP+e@ z9tX&NKHM=hEzHK}!Gaukk>I@f{;LeoNv8n9wu5wHoys5l^=RDgUq7B*ZO#=oHzR1* zRnWzcQco1;(KdG2-DtU&q4N==Z@8*2I1&Yp5m3OCSls3C4e20aO`xB~fBoM|2qWxo zeL-`j{iznm>1knwdoJ^P8%librQzneZ6JpDOoc=A^#x5 zs=V&K^pD!n-V%=hy3M#fby;TwO8)1G)#s-}z8+3*S!R;EAJU}>=CipiqGW9kj0?o? z;^)U+Qy*zI?<|AOuxk)ai@Ncbaes1V3-&l^Rj2rqVUYC`O{75N9xo7m6l-J5zEt3< zemy~Z{I^KRA67)var;ME4j7)=%x7c46dIt;0L98!xjuRU(BLEqhw{16P+VUe8IQ@T zUI#6?l`6vx7XR@h`^ure0f2>9&B4U^ffEDZR>5=M5)aGd5kasxayk6_CRwkwJK(qd zrfF@#A@m7{=EK{bsMpyYg;h1G8^Wx6#yW)j!YqmZf907&&QBfOf)2-)mmfb>BmVQ` z+H8+T^!Kg<-)G~E|Mb|#5fu~{c?<5_a)Ktu4SyvaH=R z(>d&@B|#U(5DC5y<8JPv!X;s5}#eKGVU!&?Ikxx8;wF zb9mew0>bt7Sjdzepg^+z33!?CH{V|3&=;K|8=J(HD6pI#c-aRYdpp!v$o|@5cSF)D zUpse5DgXrMC->F**vel;8723T6P`zyYae}N;GsmiY|4Pi;^RYhIcdI<@Mr~TP{SBET9BhznH*2ZB)yx&Xu7~ zm|aO(UM z9VGjArsPk6h55zPmnK>k?J>?Q`O?7Fpwlu8s6?8d6-l_t z0N-M^SBZmwsJtRC;WmTn$FoiuA}^B3i%%S(x+{C$NhhzEj1%*Jw?o|(_4V~-kWUB5 zlT7()A*%GV?59oAtD`6cjzDCm_icD8&~7$k(p7KOybh>^C%3&+e!<&wq7k;T803Q zKaoeJ1XHfRIpvb=bCmmPuf4b@UgSFe!DtMXz;%d)2fbh}Iu zINEN^38}1o_&4Z)Ec=F?Gm9l$M$p4_ymHSBR-bk`KP3t4K%^0S?a|2n;{7)ECtlw< zN!vkF)lPEzDZT5Qq3<8>()$VoO9@o4ziC5XgXTPlt5?0%=uTeY6#V^;7;6HhSJT^Gf*lvA^eLpWV&e5$hW7o z`V~jU4UtUahx5fH^Q13^WNjmpfgm8t1iJ*$1l09&4scAy1U z?lNY$W4PNhV*m?g0zG70;`&~w$toE$* z(T&stg*&Ga-x_VOf3SXC z$HS4k-lqB|l_#{O4-`Po0^j-3diefdg*OJM@EW~Vcrm*~G|Cxcn>ld31(Q?#%M*OO z_0G{?sRYCndk24-{D+VxAnyz#+LE`&j$sT)Uq*YB*)x=f*AaO$e^V+M{v@K*ViQ8D z8wrvs^9sKg;(+up>Yqqz9RZMpNTV7guU<4 z&01ga$pfAt$w*B7M2fY>y(^mVM(slk0A-t(?_9keIfm;vL8`7p+h8eTFDIP}H#*#r zc|S4H(&G5s*Wz&0^)0FARmq17JZGxv#dNc~(7IgUN9$&X5>kG$CVfAg1^5@|&>vHRj!RP==mCBp$vVe4&Rv zYZjL@pyC7Z*sE}GiNQ5GYsk_aG3UN%J-)9fD+7;p#}giOhfxdM!RmhsIZJJ13$z75 zvPrlSn7{5F6@N`|QC3#f&xX(eKVp|LxP31700PYuz-j^0kGHw_-@#XXZt*ju2jJfU zSY+gl98en)K)p%{4ugKpyf<+p@R8_`+DA2OAQ()d$q){q&FGo}kViSnuXn*!E?Eh0 zi5B{;XEi?U7mVEG%$I)Yk0x>BK0a&P0WfKZ5d4PyhjLr$?sw%QmTv%_^!={8kF0{9{KV__Z0~!0ENVN{b4IrC=S2xBkbNjW|#c`bG+26eGri`4(wwdd0} z&ixOo78^wHiTCqb^kwQC8$6aoL%&z~pUKcZs6jh{G&3O1`Ha)}o1(;@U-W7el~OSN~0L+XTAk6}ju-e~GPJZHBZg@;3**@qMEF78d8j zjtT-8HfU-tYS56Dp$4BVXLPA0M*yqNv5%D%rj@VODVW;za{r-x>9<`4Qz;zRzg`hm zz;@LB*^z1r7e}swU#F*T*61v^lig$1bdL4C)kg&wy= zHlLX0la2k&(u>0c(CuBNg)604kr1zazw3ekjvgH@i%!}(`m`L4zA-^?+Px=$71eE) zJaG$xTT@u-Oza38|ESvt3o(1i?)6*ln)~Sg)WTMaq=#ACK%V|bmx!m(`U@%mzsmS= zcgzxNIz(-zFJ9N_9{?b}*d+s5=ewl|4SsLn)##YM z`(@kiLHp6iK%WQnwc{t?Ey$w~(F=?G%<(Zbv@A+ZqrsR&zz`$LR&R#Ju zf}K~Nklwa<)cGF3|2eo?e12OJA>Q(YGj*)mH zjtMi5V-4?sL*T`-@hLZQFnoi_*Pz!32N#!TDOy5AMAK*$2}zEO+-+j7t;MzY7?^aQ zu}Zo#Gs&aCmagcuTCzX-0M+U!DJ?0@RJ~tO+sW8Tv)Ml!w_vuzY1$#apC{dgMof)! zJ>m0KrlUdRN!$D>)ZUL^SMNP?v@N0wN{mcNzWn~Rzn@=5Y(E#;COjEg7GKSfNr)b> zQPWXQkT4h#g-T1TrC#SHxN<9s)#JQwU9&jZFKqPIE0fS;1+7=Y*>od&^lcj*og-g^ zkCinfxKTh7k>L_dMqw3J@{6*Rw-E~K9C~((@^4mI>Yalbn+y#l7?}1%5nIhRtQbA> zwJV_sWyz{3mHTJ57PsOHu370`pWoieDt~@aI#SNXs*Ef!H!6yPh8}j=<8s}V@1d4n zZ$D(lAfBY9xEA)tokfs6ZHV8}=)vo6st|>E)v%XFQ`HWKUqb0^RKGq`1UtjV zot__qFQ{npsf8(|t>)OJy67L?nm*CE9{22Z_*zWL^iK5bC_f4P#RZSKoMioNUM^W9 zr=z5oEi{$diO(P^ZJ|pa9yt#+mKVw-**ziJJqh9Ts=6ym#~mhR%_!0tro8q4(4r^n$>H|W)}fyTOXuVoxR7SUVd zg9lIl{9(p6gk?WghSg-rrqzlg8@4OFD>54}4z{Bbx5H~(eIhGctv2XkP`H|!k!MVh z-}zyYswfz9|2D>+6`8PMyoPlwr@l>?Bh9(yU{jCw$9xYKOh=3^_w;H*P05qF19K)$ z^=B5$B6lkqmmjXDfrov$+13u6W?I~JbB^Dmlp-a>l6?r*-+A%$hWW?lEDcGG_E7#3)bPeiq)uc$x%`AT`#pmqSc zX{V1wWTzo^q;UZYd{HcJYRa(OL>9N<0|Tw!&@nwp(tr#F3E?^yOASOuG20F2_o3K7H0rFffOQ*F zS#$$oGC$`Rj1c_UQP+R%HjGzjhqH`RnramF9TNLj4(gc86-1% zA^u8Js=|o(BDyWjS=OH+H^w5WkSoQS3|vIFAS-q)8oErfRj9hAGWB&KUeE{$FuIjpZO)0|yF}i8w|q0aeU^8NHNb*>KWYLT%?}_sRc7SIxA8a;J(GS7BZuZ-7v-i12(PTn#O5#@ zIR%A6rv~1Qjg~C(1UMQoTm5b58$eOtNf9$3*BtqmGHGe6_qE6s-+I!=9}Lu041JZ3 z?^ki{sL7vCc8<|o<~YTKHI`KpzIrEL+#iL?wK(oqJ&$!O%!(KFBi1V^CQ`Sa&I1=`5KG+ryTD$AYu90&9P{F%4c`!Is>N?@*{P2{L)xd`H&h*Jh&TsSK0g_ zFC7*oSryBdlrVvg$a=@a6IsIoItjt2%T7i$SKD9D6PLHQOcf??#VMe7RaXqFJj?iF zcbCoMdN0cl>m|o^2*z&Dg2onR+Lf(UHljuv8-!N|nc6YZ0jUSpcKkd4oJ37+I0#!? z(KFR*8#tiwz!=V|;spDAby$A2>?AVlpjeykKNGbJTd?93sb4oA(}Om<2? z`j<}DOt0i7y#iXr(wCxz^&>q3TrbDFDe3`^| z2dDK)_QD;8hRZU?V^Jm8doypA`l~bt+Ui>}wJAQ7j-ajxHFV&}dXKbyO^N-e#@wZ* z-eyV;2|iteDZ?~G2jr~()3)_F1DcFZ!mP_I>Ihr*V_+F4xB2|g0z>F%H&H+S?4&_P zk|)RQ>u`24mmhC6VL6NpgC$kf?m3NmHGx-wqfn6vK|GW~1TSIe4~8Ffg4SLC%y%3F zb@*HFuwvTF+S?0^{=}v%Y@6EKriqT5*oRudY@;8c?LBhSk8{os+X1)8PlY&p+$@t#%6fA z@Cm1mjFsJtH%D+(q9%z#CL{IK9$(bO4_IZR5=D~TI5nZfp5moLgo1yDEfpL*8myL) zmP>X1@;}pq&1v{}14DC;R5&3}LcOx3wJdyC3GpA9_+WZwUYN~*Ltr%+jx4efyXbeW zSP#Q!^EY$EfCdm;)!B zlO3;kmL{xM{V_Vk1ZO-}j|ORrESO-U(R5Pb(}7hW*D>>{FYgsL`EiMdla?k#8 z6uW|tK8pU>F<@d~{xER=cP3Q)Dg2dq-~sDeZhm{KF%%z)^@7Zuv8BOc{N{nAb%GjG z4aq1p=Q2>s;=?}^?{%9?@~^!t!)}4PMl!{^v+R0C!L2Lo=K9HHzdb~;1H^D5hJNEs zXJfivzlrrNZ;Xe0D7;q#&P>=a5ms@0Vih#c7Mo+A;%GksxPO8FasRr2`(NYLUbvLV zbu>OLFd0sU7}Zds=OI}3QJq9jL`qWDO@~;2zhipRFH>g`&KQj3kF2K(pd45JB-Ez# zrs^{t;6*+^Js7*kDCaC)eh`W}#SuRKH?q7${bRkDO_JEHh70P-$#a`Srl+luSoCYP5I_8H zNl?r2G#{(Djov$N5fdlxv@-Qi1}0dst>^C}|2;(visJwt1|RS+vXQ*P!ukY30ov5u zpv(eVt;u>cS0j$Cn~Er^p^`{TNJGqsUH^*CP5kF&>Rf(O*`e-)5N)HNoHOU80xqty z!91>(Le#S9Q)@3{_g2?_rQEumc9xNuyR*dDR2I2H{{8)w0?(?>YOo_+2B8PL>Qw@P z0Y^L$&yxVA46n+@F>BMNaxhG>-+!?H`iN!m@!4;_*S&4h^qv(3jBQ?%4=~1~Mhntu z-bW#A&e0|jhv%zvAyg+x3S4WZ@+W2;_hb(D0=JmOl-vDYM$n%3o2hv@hw0i%=mMlD zJHggvpENJha-lW7`F=M*i~cE42R{3A9tUSeL4?lUKO;96k0v`juKX{B1@+W;6&dScL0#GI`=;>6 zBI1_!E?Pj0HHv$ri`7+^sLM^6f6lNxuJkq(9gwt-QlvD^q`-D{roEJ+WkN?Oad8Mr z{^qyQ5+x67&2@6}U5}FjA1a3{Rc;=Rieem#gDm27buBTdO-X5X^M?U`&gi@~6-LUy_|^FBSeU?KZ3Xe81bYd1K$3{J#0S-rrx*e=ks6NKGM3whz?{WM)Jzh*6v=xW(q z>UW-N!ZkCzV&$z0buuwY(oY8GK7BrFN|sk0eQ5g|8AnV^P$anf7VE(p$~1nB zOZ}l&5~1+?J3o%f9n2P0VT&Xkwxuzblh5WsZJN;yvN7^TL&yVoL9V}(oFo+Om2G7T zwe~UpT;CsWQU|PSg2eOs34Lk(J>9wCZlV=VJn)H~x>s(OSB&+r{-{WJ|i)zZy7fzeREM(RCleT$*#4B0$UX; z=4JP&|JC6w>3%l<(lhplI2053zaTB~LEXWRO- z`w?pqLk&!X{_!)HVMvf(8uDGH+TGyBIx0^(7)*{F)L&1Bb50WL%M{l7vE5htY%lX3 zq1e@GndF@t)hxq-;@Zwl;hUsz)kRSW6Uzx6QE?cPe{ zQZc)p_7l7>L{=m3wmhpfiiwXqS*!Sx3Y=>m2RX(_rEf(ott{k|Ve8P=d zOeGH^4_5Z)?v}}V`tNQHcY!Y?i!|l~v+PH6vh=0rO=o~FF@N#H3);vQ!)h$5Es%ry>QQVQlPEAlB~Zv6$Xhdr9PvrgUDE^!fn9(HtnyLI z0#~y{eaDSMo>Jx-A=aLW3*G58l9H*lMJLoo4EDo%tm!XdkDik=oGTrcAe;XFz$R$XYUzpAj5zEdWejf zM5aTBL^RGPN-yg7v9vbHCD`*uYC&Xy6ga`Y?31yBXbc!I4ST~tK_~?F7c; zE@mQDbE^Mm<+mXDjoyoI<|PL=`o?ji5sOtu;J*cDj9CJW8%oHwj|h|_x6Z_9wN3!6 zff_v6XNmGN8{~xzXtoR#2~wx}OIvD1l7fS`&Kf2@xY6PFM(J{tE_?ob=mRTBZjDlI z1Bud&uw>W*eP-dY=Lcz37Ny7ykNVRH=POG$g5%jYt3F8x8r`ZX zRD@@0zWjO!!e!1(x>w@edzahNJ~QY3Pd~FbK6MyvS~HXTi5eVY$1%#t!yJ$?=Lu@#6iD`k zT5qM+B*$doW8tW1&2qM0pePz&sC$81`W5iCG`WC!R>deWA#wP08l~9ei5^`32(eeV zm2~zeGg^wWfQi_ON!#Rn49Hg<1{@y1KmuD|G?S1C7%pvgteVsV(Yq@wrga<*>nU*R z0C%uwPwzyzAnZs7(bg7jTw|iWSkTY>wLHGLJ=k|7uYT;IQ!>NH#T{?*Fd@%`Eeqv; z{-mXnVh3O7&=|sRlMpnx^v&v>YZb@~1(4YYy)MAqW=*-_6ed>S_kFe zl?n31$(RZDBy1WLRhI~n$lSEqtCt+;h+Ewvq!PR2?F8V)yA-u&J20@Ok|+g!4GLJ7 z8d{Rg?k+l%Ji4@f$}44Jz^Sv#Z`qTh37DvsmHfzt8~Wk(kG&);W+fpHjj55*#vL)a zKkdZZUt(HQIxsQgL5x#hrO*b9)ZDzx?qt=uIJrraj+>fK^1rDruL6~>?CDRKF^=h% zj2j*uvX;&E)?M>l(^U)23B`v9V~~h&RN95-Z}nPeJwwD^@yO7)O$UTiOXqy3Z)?fT zZix7WO(s%2#!tlnEMsl^`;Zyto5t;b{Kd9`YQWZ&hd9^a!Q+oX8^NeOjGA8kw_{u} zyY3b&V&dZU#HK&ZNJ$9?-suXiKu33C9gF52-sa)H3))z#EV2|-9w^Re3JkjMgceGe zukeo8R=&Qrdm6M(VP)Me9rU14MR*+FlG!NUbCuj&qc=FH7GsN|Cmo-{$<0Iya zZ8?=3N|NU_kVE}dCt}M@mlPKmYp5_|tFYnpn0~JSMHkt{5@=4oDB{W3LB#04q0l|% z+*);r&}H7h&}y5PGVI~pA1cO+Ci^iIdJ1r`G97nBksc!t@0?oArK{;i+*s>&-Z3S` zN|0>8z9Xh2<|D|i%BKd1#HfRnKiFp;#Wb7|kSK)j1z3y3FKa6LXKZ7;Dd?=xmPRk#Zm^^9Dw zHDrEy(hGm`-)2{C?F`{*QJitG6=luyk3hZINiVtgJHclEa0wwZwA|O{nCa3;C}oNp z_~LGn5d5o~fU5EUT^0n0Gs1~L52Ib8>Y2#9$;8A#c(cxM;)T-I$O6Rm={m^nQf4&{)od57POnS3yD)V#A^pZ+v$OGRKeE z?k4udy9IrL_#mJE2D82sMdEhsAFvtowPM3-VvZNYyjn&{C3U)@Yo5}`)@)O{Pz|T1 zp6x7AI@)Ix1nu+;_uvqiN|kMT7bIjVl>py{M~8ej{y-CS81VK_g8duQa{F0`GSQ-H z_IH3^>^6Vfn77Y75 zJWCM;5SJ)-f4xd9nA~ONpdN6zcjc$E{n|wyCD%f}yCqU(-Ah8AzNH50J*MffbcMg* zDBF$cl(fK&M+KCMX5V~ouImkt4G$%EUjuH)4XsSED}u@JV3C;jI`02ix`@$4x0u0* zo9mxZ-0r_D@0h`ozWQi5n6wV82nVuq*ew@J)M%@BB%Sr@bXI&a)a|7;7>>1!7CL^2H;sR6>2v=6y^krmRHE7mo#`9=U_Q5A;U z*|ZuoVfHjQ6`!AaHukErv{dqRX=i6AW9-mPAayL7FDqGza1y-)ZGO_@$NcH?QqrOe zdCuaGWmFqJOjw4lfDGIr2J9{u;&s{=>vu>%`N3E!e7Pt0DD_3)v(Rb1B$>kC(=*%Z zswqRO(fZGWJ0H+IrjQ#D+KA|c7KTw*jqdxC)V$~S@?<_wXvkv4X!v-tD}Mz(tIhi& zPalBSnW#)|b&qw2wj^$UvCR8ZB_PMB7p>_X8F<3>xbEr*{91h@q-S9|@`O`yz25Zo|6nq;6oqp4rj*B@q z>BX=JIz#u_BY+21cOc0jqvO3*!W$UAJBb(X=13wk!p21J-)L>|%H4nGWEjSPQ(g?mOy{GhmhO z5&@pRI#~Hv=gMQ{68E#{%5rfc2AmwdOqe%d6bWf<6)kgn=Ruxr(l5TKrJ8%;gcWzN zjeyV`E0(Jf=CxwoXK=Jzouvie!@}?0(lNbZnVaao=SYD}tw^hleP{+#00Wo^!o?h? zOv{*xi$v2cp*=(v|MoLr!hGqQTcBf>xO1NDE<}6@-cFe@vu~_ua0}D(aJh)l0enmD z8s#BWbMLXwBkk)=12r3hJFuME3@gQ7&p}1`0Dg`X5N*`-8#KAlWwa$4N7_|KYVyn6 z(ajduX%Yj#LW3ns(Qda5y%JR~wkD#1&*xRN*s1B0EIZ#Wl)$x%F@92hMdauMDD?O^J=H637gtd!wGo8sRo8RTqe ztaO9e9*{q5?L9gzsDHA)NID4FBLv%J!T_P}%99}rm*zG(W_<0w#42u#hFE__7CyU9 zt!D9f`|${~Hech~Snq+>5^g9d-epc!_oqGl^h3w80J1WV0F7-dMyFU6NUVmHMjGB2 zJIpmcz*;3*5=Na5ZHv!vJV*K`bosJBE;C6)0Na;Txu39_;KV2u^x&&MfNGa^IHs)R zwDb2DWZb$>?27S3qEsp=W9ifaTPzVd`GaB=wi5y#Bn~6*XuwX|9~SW2mq1K@PW2|; zEwD#|-My8@0NaVZ-=A2u0@c<8U#yR+>)pY1=qj-PG&WtEaZ`D>{B}Y$&z6==)5u za^1l!b)(68A4#nD;j%fab}N=A+R=G8mgW!fQn9r^uuljFvuU7u-3n84Lth^)D;T&` zwT2eu2;cPjaPcdHBBy0)@pe&=dX)M;562#uB|IA&W?4BmY8}Ly2FH*7kPLKlb9KDD zax6L}co!tFP`e-C++_ro^wa+kFK|R-N9kk(PJcsJT*IimnRsT=r>t>a!3 z`PyPWXVd<8gPy}Ax6|;Tq9Mwgq-v8RUInSv{Q=4rejYURn`X;#eWr<~!?{ysE2r5R zpJz{EH>IO$9i1qGU)uL5IMrRK15{QN>68>KA)C=2Ik-Lv0+V9qnP8=bcdSJ|ssydE!peH%nicEV6voY`?cYl( z&02jsD-qA(!$E6bZJJ?7Bpx*Gwc~{jQk7Yo|9Y;zmpFN+K@@-_%SKrHVfdji9~mnz zU%kQ#UPjz;np(61-_R?h`s5wz#nS!oekf~RNN_6HjG0DZiX()8{*tvL{qt1dpt7=T zd>qli-T3Fxn>v2H2u{b6VdQU~izFrrB_%B8qgJKB#(=P8 zR7HhtOnB|VHxw)IgCiYW(Qd%G(<7$s-Pb0YMa$(1mq{$r<2zVJQ;OcFBa37ucMQOR zeF6J-L9NiV+UJ*Y9rRl$7l<`1Wm|F~`SJWE%PL0&@gJy1uiQJ1&$HfM5h^eVeA^?`ij()y! z0?d`l9vVow)wat^_Qo^F>S9ZcVMw0K#mQ6s^Yj^CES|W!CR%=Xlvh|DKS<=`1VbvpEp+c<7g~c) zV{3x7V3>vb#Z(Op%N0*xWP%#zxzA1)@YtK%gGl z)K(Qz4TMs_S0nnfeu+XtOfTzGKlZD3{O!ih$q;$bu}Uen6n@`)`2+CeE5f0@mm?E zlo@FjL}Rxl;g|FS-Q%wP$AnwRHw@z#Kq5BQb}hD)PPZo?w<4c6QFzEJv40@7kOv#V z4pG1#vvr7f+TTfFa%hcCiK}MARK^c*h8-T;9lMheo`%AJyn}<;pJrnm<+lSK?Rcf_ zV#1w%7JhEL76TgKhTH(vU3Ly9kLr#0Rucw-+#)yhjP7D2{?%wJpY=)z=-RbT| z$A{a(KVrKWit=GWA}m+_sh&Q;&fQmebLl{1Tvxtv+xF5j$QGy$ZBx?khRRWFE@lqU z!nOBS>*nVntxpm>9q3HJRJ;cnu1r`qpYWBNZie%pNU^W+&-OdJB^=HuU)}kz;2Yw- zGH!o!X+l| zAzi-l7#S;@ED?n*6{E51(P3*fz=g4-gY7RHZk+63D`R;Jj0`}xrh@afji-)}NDdSe zRF7&2pH7ac4LTXY&_aYdb#Gp8;QST2Xmyc#)B$6W<_{+~R*LyxzMb(OwuIdLKEJSm z^M5#LlPnDnS!ja%6)=mO&}GIh?ua!(-#P&q!2y&r*V6s4byxhnthim@5vXc@KVH$x z#U};}8iMNX7a#7%(G-r{;b>`ZBc**_gPGHO{KW71)6o1)5;o$}7kz%`WPu~`MVv;Z zG`Ku0a4K_&I8}hFi=W-B0f#6HKJfc72L2hlcXuUo7K1UWNGa0IsY1so-}9aaUhhr| z+}-^qWRP;L(ufb7^1$o969 zzBDAJ>hi0OhCE=MC^SB_BG|RxIaGXK2n&$!ZFpEGQ1fP5pYL|a-hA!Dh`QGxEQRZ1 z)8WSGwyzEoNaT+2;5kFHTs3y+_q&M{c&9zCHxcgLk<LfAMdDEbS(x0C=d&| zH3At0-`V?zTS~FCI$8#kOG5NJr?s9>UP_WY>xuUd934p=U` z(XVA<0(CHq+f%jAq|*Ft5(nZEj6mkSGN=0PjwQ13txm?xpN;$53}N9UPZu#EK-EF< zHB~E_bWVLg1-AH`@TqvrouEB>M*d@tJ*nUjHD6tp-%Efp{xh|Lo_IL^1{d+V?_AUXiWPq25-a-?6;m#5syJbf@) znm0qC>Q;=(jA)n*`U4naHgY^%(hjE1ev1=q@eCn6n&bP&cC+}Bh*5KRB zK;E`&3s)Yt2u0B*J6;9Se_!OuYbzHi9Zt|q@7&HnsQsS zr4fg<`1^TqqLA%=)!{+9zbRy*T2_HX0nSt+khXD7EPPM(C<1_NNY0a(6RDkL5;VE( zH(~10g8)&r_cq%`=Hk>%h&hTesc_LcbvZT|fO=$dEg>9)N#r+1Bix}V6;u1^8C?vU z1AD<{I`WVB5KAMf4DD2p+hX}>#a3?S2L zuRDJypkQzUL~*O%H+*Py@^@BZ7~Q|HlG{p%QU8=Hon8KPwQGXBs15@vP>gbXW-YcE zv~7}l=!SzN(Hy5);^&2kOI-Yrp0j%N1sMVM8+ahG{#2C^2-e8AV!%FAq%W3N?6W!A ztbD#!l0vl`(D^SG09J<)BvMR3+RDs-UtJJl8KrNUqn<{zHkUqmnj&6w|CQhIbC%p& zM8Fe!^WcPqj(ReEagHX}L z;4iBYhxPF$e-H$R74cqluwm;3k`&RLD2%$kA z`hE~-6P|N1~|Yf%!Nf6;FVTf(0sJ1YT>Pu zA3(*7IhfE8eosOm;Uo?6=HYBTSY%kukKbH(kZYEe@zTrT;nrYy=7x9`6YF7KmcaN1 z0s6$su)bbH8gYiEzLIHH!lqE+WCd3`1>S##xk;Tx`<4b68Ip*r)eK@6Urw6<)S#1u zEvy(XuM*W*^h!s2nP1Q6S(+%y>aXlyPK`wFc-pLNP>?l=!>-OVmq|o!GpfY`BGt?N z#A>fbxOBj6(F_*yC$#-1V6$nhFJ>} zJbx%_S&C#ra-0YCyy{52I!$-sOS>;}-Jve+x8^1e^4jPWPE+dI2`xR2G$P3=eh_G~ zKYg`-=U#!3U|k*|5s2Z8PlbhkFO1`!-G}+UH zLbtbDXA&0NnDo>NlW-&@`x@gWIh`2{Wj%5g#-0~aI4y9$-4jW@9P)M&3#bC!4$YWr z8K$b^wwcA$8!HLSAER-F$co;;;m`ug@<4`#XK-gvfra;M&!6ejnxuz;6swA~=OEX3 z78$c+F!HwLC&(K_?__V?%M(ALJ8pTr6W&tFwDKXZmecR(aQ6#PzlPQMC{p!m2dvO^ zO`f_C){U{^W_(l0F=A-4) z3XoMt%x1XXj~q)b7RHyST^EJ00Et^ zf|#vmgc(Dvhc@427MHm(j5n@odbj`g@QNo2zUeE-o0o^%dgu0EW^wd#bmu|uRx@5y z)kdMs=TU==D|~-!TKHx&@7MmFAs?2_S&x!B1P?PNK9u$0+@k#tza}&mm>kdu6bUnX zGqvN8?59B>JK9v&FL?*F0vahBwpu#fK~lwymC>1|sw9q=(V+#vUjcMlAn?HyG;NaR zLtkVoS?MpmP^VnwWcvJd7Gwwt{nE)J(>%X-C;^DhOR?VE`eC|UPFJiC%BKG@n`Wd1 z8`BE;y#7`Ps&Qn{8LjT!0aVg{U+URfJ_h@B5v98Ye3ZRE_{1CZTHSPja`BZ^wA1W; zGC4v6ZOJ!n_$_u6EX8MaqN%Uqkxk;Ln)bhI43@^wsPjO}KLU9X0=l$^?c09aFRqdA zbT)esmqoo3cgrgV%~{RR{4E{kD@Wz@A|98ZNc5cO-ze0!S%Q&6s8=!va$%sc4a;$!|EkUm6W@W@ z}JFw4?<*fO895A85lnZ9-*Fhh0T15v{Qd_cwU6?!=vkg(9V zSM>Ft5)hc?_px~{R^DUrCy_gNc~tbWeXn2)$|Bn2AokOF39?_gbexin&g$pbLx%e6 z%BhVzfB9tNt_)C*7oeW8Boo8)M$NPa5`}192xm;vuHUxVVCe}?Xsr{?0kQSVK?~eG zQOXcV#MJKM=}G5#*+K2GOsm5ZEl_+#h?H3xyI~9p4#1Y8P7%WSR}CWzV28X3YO?0< zXO+}Zu-yN$FJlGwC7?;261Cf5wqm$mTp#jO5Kkd4!(@G@=b3J}6FzSeWr#<#wPPMJ zNb(~meQNzihQv6bS%U$H%fnmX%JO{Mr>9aZGSS!9dA5WEWdSq36)0}Jh2RJ2vK~A0 z-tq?z_T2zDkbIEld^__C05Q47pEi@najPoBWy=k=sd3)q9KU=I0MOs+LADM-r zo4YSv$S!ipWXqdN>+pe1$c=_!3~DGEZ`5MZv=>~zJhBD~x7`k&na_Bjm_xsKt zaDG3tW(~NO?7g2muKT*Ly`OpfZ%is>(adq(?95g#m^}VRBYr~tO$>v9XcBaDx>IUY zV8iEFPvq^BUL1%Ni^zcw=ENq)rG6z6{qNuJaIWWGl<<2p{B|$tOtVgY`6p%p+UMH@r;U_4PZqkdy0Jk}>7WYs3*(+N)mOz@SMk`OCw%zASsjHFu^_Z+dG>IPw$iM`R%?n zZjHx~zyvDK3ilq39Oq}%fT=0z`2||BWWz^giK4#i+C*9U*k>)K(j*WlOW%z8#ZuDe zxat_zN4lp`og?=rh0C(N@W4>RPqYIKh@a0gZd1f3KqCTU$$cv<4mEUFU+uj=z@Hh9 zuREh7p0@($#Z{zG!mN6tB(2d{-QZ|XfS{@Y3+B-IcuZql5*o(Hd3W{euKFwDgG3EK zWO4$}{JCRBF%h_W?%`}MS9?T=OMHsaS?Gs4GtwvlsyGubXYo+=boG(zZ|}(nmCbJ! znHDd-Ehef*9qQRfeWpk-_$^pZ+G}>?UN(@cl*-9l*4BL~GKC?%^+y{$vngeotSuec z@fbxtp&FM{30mSWkBYZQuki1FCWr9c^;dy7TTgw=i6*@^bz&x(h&Gq-HA92?ZHHy`Pb_FwFn6*T*G z`Rlm3elNylHUbL_bX?T=7CKP6G|kAnUTR=IPLn|=Sh60oWJD$Bi_UMFEWQV+ViGwp zo@mFYu-g1_WHp_t&r)KsU*S0;Fp`w4Ec(zjvE!ho%!bSCEkWmtOO!f)>A2D^Ed46Z z43KJzj@kY%UtGu@OPno7g!!LzuYKn@IR)QxHWE+aQ|Z2qhP(BubT&|J;LjI+cM+hp z{W7Tf^=bU2d!4Zv7&DRZyCW{%FfL~SVq-SW$MEQ2oBr~J(BnHi_1#W!Z3^S*2Eu;F z=!Qz2UX9xUNxU=OMiY4$weD5hK-FP6hVSWc*L3Qo>EzEuny4%;T5H@ySZ5tJ&R&85 z!K~K+bL5BQQN(Yb)!hfFOH1CGoS{DZYWPbpd@k<#nz)heUV?6K!f_he7Qu}HN8i<8lrlT_VPh_(A&(e`}6gt zDyM82I?6g9`mO@8euQnzp<147)ki+UsagFes$T9F`5DTV$WgTRgdum#VS&tetmF0- zDP-+7Aj@q_hv}Z9_4NH5V7bbY@Vtymw}rVN>2JYL%l5B)uD~LV=AHAJ$FI9Bka{V9om13GK0{kEK-a4R40jhv(V&i#TBDve&$h zTGM~dvMngEnKi5Ee7NfA(9`1l3zvXQHu%`RpZHq7K0@i^ktq?@?@LwqvmEqhFhR)` z*3f$i7oWp;xvq@iP8vVOlETvmKm62njY8l19;Nq;}Rw45N2 z=HsIQ3B40A97Q=lRB@eBHqxQ76Z3L6FR8bcKpkuzJ_nX;c~<(G{l=1dH&uY*ioT!?yVv<>#D)&Kj$;Khe0Q)p_@#?) z{o1`Fw6`&T=3UBk&!qRQPwL`mfY@w5nwcXvj8pqM_ubOMLOdb&Jdn|tt^IvpAOAHv zX5$;`NSp5YS2#*P>xpJPqmZFOygry>n|^4yPXLjW%&8u!K0`!Z%XA9N^pPwK?P{F_ zciwCdappXy?FkX!x^UvHpzTCMxc6>`bsH$7#EdI>9eTpr^O84a=$HH6UJrdORahMG zH~!#k!n^?<1&`%?NGB}d!xV!QZ!oGnbhlo|4u*j{+75Z=GAO={^KxC(I@o>~+{1p~ z430i2-!k;%a6aan-R!l~aYKs0%y$=^x#!E|wJ|pGPSYu!+_u$379;Do;vs zQR*bsGMw{Ts<;`BIyc%zNWgdAwQ{X)(V&EFM+CGfR!h-hK9P(BFr5=mD-E1;M#!&E zAgN7V(1Mt&+5YtAMzMT5DzMK}PE-B;-7@aHyW*^Ozb(mR={@d@`z~{1RabZ381}I6 zboAj5egVLgfl=78lb!okPQ|ZS~)xqYT5EW+9Y8bRp5Fj ziglqhe7(8qqe&37ae%H#^-_g>w+A9-IdwMhYZA<2{UtdR$JK&N1 z#_A3lFS5AcxN6uX`9kYT(4jCq>l3yfUX;g0A8G0q_qg&cN6)pOp|9k3du+(ujrQ{J zq&K-@50k}|;LOzDA#o;Wd1(fCMW{2mjknF^h!(t-_vry}-YBC9)u z@#J**udRIy9tu9L=s`w^+)AUxlGU@eGyrCBI-pbEII*N!B1FZ=P8M=MiL=Mcq}$Ie z#k~rpl&*3M$htA^%7e%{j5ll@+=Z=}>#Sv7X3dw2%1e(c|MrO3Vm5fASMBK>I-OK5 z%l6I>lFd2FTJ%nxLh!c-9M|RMB`?o(MUofNy*oAe8*|ToMc(~Z6A5Vp>Y=z`r|@}4 z%dYRyCNb^Or4Z9(r-0m8&fFC^i9c@KRyoNe*7!4zA|Do@y0juwTaJNxt=ulX4YKp1 z{`0KS}nue9{X>^T=W+qrGZ&SpMNwz+IDcNe_j^R-fZ9c~)`|$3OxXOs`@or&$J3}8nGdXjG$hwny;7b2>8Q`{kbi6 zU#K(`#w_A*2f{ZudLyklD7IE)I1M~Dy1OCsw;+vCJ024e55&ly&h~$-n;QHR+3e@? ziKJU&JV&;^=zLi>-bY_IvF*;yu`KM^%+bV)?Cpl}#@GWRawb}gZQeIpE!u;zhQqtp zaJ4N?zk$Ga+INl!APdJX^JTb*nz<6!ezCIOMm35JWW653Lr{lN&M&kNAE3zi;TcMw zuQnjL;0TYn=Wz0_*ZY5jwP?Xz$%hfa^8R=IJMB12Qdl^WC7s`Hgb=d7wuDN(-@}63 zb4vP%dg{l|-5FY>u6Rz-uE6TgWHdZWGKTkN?{)QNV3jWHX&2ZzyYC`~tk}F$I(%8$@|#T{{5h?^ z7Wv_K|HilUa z6Or2G>Uq|Vl%TEogg<<{mv>V!>U~Tw*4X+({v&1P%J-WxY}@lBJKk9T8RC8OWF4Px z-+)l`Umb9S>0y-B{;pEf$o9!kCuW+@cJ|@Tm_J|G;u!_$1{?xJ z3qQ|})`6|O>$JjDoAsyr-MKJa-8{f|T5j_&&%1Yb zS*5VzYBn67>)|aEa!5Bgy%}&OWpjRLbjR*oO=<4&bs%|pl6>a=VNT*)i@gROCBgKb zVAV0o-b!-y{;&JpVeU7JE?m@7kVpfk6lzuz*AppOE(T5wH;^4Sd26G^VD?MD8cCO`}mXxzR^hrC|{{2(#k$h$8hcpxmTIwcL0;fl_;CFeT3E( z>*@k`U=3>N>Zr2n+N&r~FCynPhGLBA*mQe? zd>u{~rmdcS(LX+-_-24Sz&ECc(cSj)uk!pMqkI2@w9~Kt1K*&-`7+$8x<9Nc5S6aU=IrMXh9>6U4sU1Fkulsjf zc@3-{LOdi_9&vq%ZnzUofKKO0Grg~fn>>AuboZCrq$4aALHC~A=13gJ0kX&6l-e?k zueoi0F?)|wQ!8SD%TTgEBTMjDv3Z#9khYPJfx_NiY=drJmL7@qUyrZ=&7Ym089tum zfhyLfsBjrl!al%@S_a)}-S&LO48Ie(Tt>OBr%sJ)Q$P5Jf{z>lF zBauC;oq|Xi2PMI)&x7XMMH7$ShCGV2f3$+~3iEu7P=W-|gEIYN~$uaZ4{TA4Dk1!(pQ4B9=$-OAF1Rr-y3V zeS5K0F$RpeBTH;-w9``4XK8Ow>YPSn1Q}&!HKdm%)v=JWU>%V-Y z1Woyj!0h$-+-J=4SY5)_^r5FPA{xBF3J3ss5FilSQz$k&;XMDTDMGkHBO;i6Q#MoI zjF{NIbmU9(kdN7X20O1{oA!*~g&t|Dmy;ZQnEdnD7E=rY_0(P|5=ARAG!R9-G)$Ju zVAQ$f_mWXOgZH>Z*_+f(@LywljXU1+_3I{xcoe`+i1*NJ%2i(#pT&Q74g5v~?dL)piXQvI)7f3w(IM@SMizx9-TU_alyU$eL(O?$B9!~p$1W!{}( z+t;pk?XZ2t0?n=F)~Y?dUA(1P&DFsB(B|HO0hT!N%}i55Gf#IL*zAtmgn5u(MHtUbSTy5 zoiqmr!7DutYKtK)UCm0YQS0^`a?hM4XRY$#)m-mz&@~WqaqFCHlOErzCN4bHJoPl6 zidXip`K}U9|CVB`)=W6|469MfhkvS4kn=3`ZeJk*p#DO3QX=~Ijpk=6JZ(Y_w;7qy zLaG-Oib|4O^u3RQ8v{MK+@6=h^(#uw;cBGM8XFaZTaJsdLIxe2K8 z{3&GR%7R{uy^Jw>_kHS_>a@$upWM9%Qr7DS;Sllto6x?HxAD46y4KsB;p0zdN-mf= z-ogSJ71J;(r%OQjYq0!?yPeZIBAy6Q%6Cu`gNziZ?5e4_B9IlY#Eem<>{62>dtV65 zTiqc{=`vfRAaOU%*g)>ilS>5tJ-3v)$XX@S8ZW^+O2hhZ?+G%(%fCD0XunRUbb_$X zy8m^$pUAWR?QZ&F_~+4v2d?YM9{%1Qe!6C~F5P$W6-6_vX(Pt>lw;w$+nyiR*Kpfa zg`qLy5}kiYQ4-`&h?AboUKEP>=B++$RB5eTg30y6ti9RJ+abT6f^9~~MXo)K_jHD^ zW(#)#UlJ-m)tC3p{e)I|^`rAOL<(ojv)|63(H#2u>ZD-2Hf?lVnHqnq6iYs2!QKOF7_X>&=~_7+r5I`R5{1(=s>b``jws^`2*zQBiu`4^z?K4~fxo zr7-P!hfUA@ZtOsBXNdAk0OY^IJUy@emAa#LcBq!^ulkDcv6CA`3iI;m9|wArr|^lb zW9;|(m$M5Ft}t4z?lp=Qg!CT+N-FDr7H%}*l|{0l1p@zu5zt`=hd8S!<4C@LND~l1 zyTo4?P!Przk|kD4P9lkOo0yNCmFRX1(ZkB1l39a!v7!dmy9ONJa}Y|0UZQJ@bVr5! zDJ)nE;+0T0e}R9u*{6|V)eSLhHI4Z7HOeVNmmR%? zV#Ahwb(@bx?BxS3$5R#+4DcBG_2zA8Ibedq<{NW*;Mcb`nC(ft7B zHx9Ui=M<^C9Mcj0M>>WI;c^)!N0jqpGH|+7`n{wmWE-->8lPZ*JSLj9OVuOscnKeA zz|UN|?YVF655sdE)A-^k<3_Tw=QKWH+46bI-fH<4_nzPVHV(Py9M0$=B;2O91X)IS zoWxNnJUl1bG>u7!eW@L7>3eS>jCckM9=AoY&XlnVyFRGvnVQso+cS?e7cFfHJNdSS zG=kBp`Txope{Gm{Y@1=*;_@E3fMpf2;ZW7``ZKAN<72An1b0(!tNjn983=kP(xu`0 zG^ZWvqN6%b84YgQHx!;1_a0%Pr7bh5#lYqlos!!juWWGKA+Tb2$@TKULGyXu;OYVY z$e@%P?Do%`ZI0|+`LXln?R8`|72kPjnP%}!xz_NH>%u0lpFxi;O7al&dmCk!*}~h~ ztYh=?t`EYz5(?(Ty%5h?R82( ze(Py=m9K~2d|C9PW^Pr`QCjb!xW*%aergG3gG$>Q)ugoSc|2+m5YpiIF+9h{_z|() zrmx?D)Rn5&u{;5x3GwU8!cq>gbhPiAIrQVz`e=8f!u8tzCC#UB8-b+rrhKHH^wzQh z)e+~FY6cTKJ8S8ohSjk!(YE_e!E_*CT#GbS~~HX;TE0_ zFur-Qy~Ez5r_ac^2}7WW04JMA!nircD&bcIK_Fj z;01TeOqI84i)f**cXec#Y385$;>_mr(~71Jrz*KnXZkjV9>l!xUQ)B>RBp@jSbC^1 zu{?%fWf{paTcaM@hSFrO%$cNQ0UohRaQnu7m`)g=DeZtu^u(_ApwC?@!|z(}&4+5e zig(Dnu_3nAb+4aZ(CfsL@O{xM<#Z)`Uv*bSM^!&WBjwtbU!>feMMtxQwBEZm2md_h z2$g0XN&7p4Ewvx9e0Fi~z>itf@ttD>{F^h(A8dyj95iQS`<8GlsWQzn*jG;2%f-2* z>o$~CJ}7p5dj>7k zisaKEnKE~jjn1J8&uJE0PSQ(YxY~=Zc`x1ct5o|>$%C!$C*RVi-_^4Bs5;4JL>QEZ zhVPLMmQ#pt(A8n%-NPkJz^ot6`w3~YdQLD|rdf>Qk6r~vufPV@4T@zPRrtk2cvjq3yc%9aYSI(kyxS#>}L?1cl%el4W!jmWvc#2QyU9 z7wovPk*2h5s+Me()O(TT*O|c{H)yyO3V7(cX=P~YmE@iT+FrWoMA7TXc6-hdX*9AN=Yf@R7ryNQQp@$FmMie`gnOX{&^Ggl&Yr>!09 zXEeJ~_C$QEn}WuQCzwN06W{MmaTMxJtdy-Y$Vd%6HoSj)JjQg-Ur@#4RB~}~D<(JE zy`ySHgbsSe5an`_$S~{K6pc1lx@a19cbJIK9>$CpK02)7_Ll8<4%QVGyn@FKC@!#< z(CXu3qdm@3FLytvRa!ipq_ASMAs#WhInb`QVU}A_WjYcc6Jc_q}MKZ~#p%bN9y#x!OIU zlaT=_&f2msCoC5HRzYj;5MLf_bs%wylH1{P|DspAti#og5u`EJKVul$%&S}!m1Or4 zC!cdPo9H(FeIu~@OPM6MgtvUdLvd(7HF`IEiMk=R~_vz%Dbr zpNd-dA;%~S())9wZKGxtA{62pA6S$S7gFN7z1|DiNF=~IJ;xG{rU$EA&@xm(9}9Fe zpRZ|qN|mk{q?h&Q6!loaIsKx`_`-AakY%f`9N!DURv~dIvS0*O)t&y8A9>HpcVzd8 zjH#}*LUWi#eBJ|=O9O! zE7#K=xe|UFzbB3U`i&mCwWe}BfV4Bck|0C%kta-BS0m`VMp|cSy=j}z`@`s=ANXx# zJFY2(jEJAkcl83*@D$W@n#AD)WLm>v%X!|9$|6mF;|N4ZJC9CTsTaDPFTuFFbGMyb z)8MDta&q&0FjHSobWN-TjAKueeT!#z*~v@1P1IdNF@`HSa5FJ3T;aw4tG zd>idUvedz?DRGbE6DYaZI9R#ZN(wpAjc=C>8fc->dKJtewpBu7+^{%htFdjkN2}SjAm5k?-$gdeh*J-jaoZSQCd+BJdI8koisNwBMDfL04t}j)tkbM zd@FD167i^;M2RrSh~z-ig2&1~s>Gy@Ek!pzSYYUisjueYat)tjfU0VZcx&l3gm3MJ*G1X4&+p*WTac-F9dPHHRPPm7@|Wz_Y`|458jNz6~j9XoIO@4{42(QWrxM}za7GLcAjwi<33-SgXp zlouULZi}sIkT($v1T>PdaTDfyV%LA0mpwgmIqhr7UBesP4kB{oR?=|_wlC&9b|M5q z1oPU*CkDYU+73#NrPRUochgvGkN!-lDk! zkVRQd(=er}g~r?Hjh$ao%^m5d8uU}vj)gK@(8`?j1eVPRi)gdl{aDRHt8DSm2Awrc z;pu`%UjAq5Vp37QRAdZROpQgEBg3ZBMi<({5f>G84A4rQ+)ie1xCkxnOV(cVyZBYw zB_9^~r;H!q<5x}EY8H=~S0Qcri~G#0ktO^$|C+MZjLTwH%P^=d=UTfqd3gml{%VLo-Bl0CDJ(y;`m-i0^ zZW{#^_%x)H3U%_!IWm)ytBB(wo*pXD#j$BonIg)Z_HZVJV}GK#!aC62N5!5_5!P;? z`a1kf#Aar!FMBR-7b#b4Z(LxXVi|5mQoC+z&?T(5cQ(1CIxT693NY2VTQ&7?+SMil z$yNPUOiz1M=aLgkoSOy|T;E(XKCvC)8higYes7ZK3uWedkYzG3@~=5Y92{(0C5;wd zhSm=&s~;tg${f|=Af=&7mUcoDoM}$$Ki0fqxTQI>V(+4bM#>2*bvF{4m|FOCFs0R{ z>ZYY=pJkguu1WZ&Y?Vsa({A5Xt+6ADQgx*fd3yn0)a;!rRaexgYS;8%88L;q9Caz7 z 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 literal 0 HcmV?d00001 diff --git a/docs/_static/beem-logo.svg b/docs/_static/beem-logo.svg new file mode 100755 index 0000000..4065ec5 --- /dev/null +++ b/docs/_static/beem-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_static/beem-logo_2.png b/docs/_static/beem-logo_2.png new file mode 100755 index 0000000000000000000000000000000000000000..8488b6e0a5907297726f310b931921175b485691 GIT binary patch literal 58281 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 literal 0 HcmV?d00001 diff --git a/docs/_static/beem-logo_2.svg b/docs/_static/beem-logo_2.svg new file mode 100755 index 0000000..014be47 --- /dev/null +++ b/docs/_static/beem-logo_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_static/beem-logo_bw.png b/docs/_static/beem-logo_bw.png new file mode 100755 index 0000000000000000000000000000000000000000..e986635206f9726afc747131b678c907c2373807 GIT binary patch literal 27709 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 \ No newline at end of file diff --git a/docs/apidefinitions.rst b/docs/apidefinitions.rst new file mode 100755 index 0000000..d64cb7c --- /dev/null +++ b/docs/apidefinitions.rst @@ -0,0 +1,793 @@ +Api Definitions +=============== + +condenser_api +------------- + +broadcast_block +~~~~~~~~~~~~~~~ +not implemented + +broadcast_transaction +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.transactionbuilder import TransactionBuilder + t = TransactionBuilder() + t.broadcast() + +broadcast_transaction_synchronous +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.transactionbuilder import TransactionBuilder + t = TransactionBuilder() + t.broadcast() + +get_account_bandwidth +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + account = Account("test") + account.get_account_bandwidth() + +get_account_count +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + b = Blockchain() + b.get_account_count() + +get_account_history +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("dsocial") + for h in acc.get_account_history(1,0): + print(h) + +get_account_reputations +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + b = Blockchain() + for h in b.get_account_reputations(): + print(h) + +get_account_votes +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for h in acc.get_account_votes(): + print(h) + +get_active_votes +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.vote import ActiveVotes + acc = Account("gtg") + post = acc.get_feed(0,1)[0] + a = ActiveVotes(post["authorperm"]) + a.printAsTable() + +get_active_witnesses +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.witness import Witnesses + w = Witnesses() + w.printAsTable() + +get_block +~~~~~~~~~ + +.. code-block:: python + + from dpaycli.block import Block + print(Block(1)) + +get_block_header +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.block import BlockHeader + print(BlockHeader(1)) + +get_blog +~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for h in acc.get_blog(): + print(h) + +get_blog_authors +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for h in acc.get_blog_authors(): + print(h) + +get_blog_entries +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for h in acc.get_blog_entries(): + print(h) + +get_chain_properties +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_chain_properties()) + +get_comment_discussions_by_payout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Comment_discussions_by_payout + q = Query(limit=10) + for h in Comment_discussions_by_payout(q): + print(h) + +get_config +~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_config()) + +get_content +~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + from dpaycli.comment import Comment + acc = Account("gtg") + post = acc.get_feed(0,1)[0] + print(Comment(post["authorperm"])) + + +get_content_replies +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + from dpaycli.comment import Comment + acc = Account("gtg") + post = acc.get_feed(0,1)[0] + c = Comment(post["authorperm"]) + for h in c.get_replies(): + print(h) + +get_conversion_requests +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_conversion_requests()) + +get_current_median_history_price +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_current_median_history()) + + +get_discussions_by_active +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_active + q = Query(limit=10) + for h in Discussions_by_active(q): + print(h) + +get_discussions_by_author_before_date +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_author_before_date + for h in Discussions_by_author_before_date(limit=10, author="gtg"): + print(h) + +get_discussions_by_blog +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_blog + q = Query(limit=10) + for h in Discussions_by_blog(q): + print(h) + +get_discussions_by_cashout +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_cashout + q = Query(limit=10) + for h in Discussions_by_cashout(q): + print(h) + +get_discussions_by_children +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_children + q = Query(limit=10) + for h in Discussions_by_children(q): + print(h) + +get_discussions_by_comments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_comments + q = Query(limit=10, start_author="dsocial", start_permlink="firstpost") + for h in Discussions_by_comments(q): + print(h) + +get_discussions_by_created +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_created + q = Query(limit=10) + for h in Discussions_by_created(q): + print(h) + +get_discussions_by_feed +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_feed + q = Query(limit=10, tag="whitehorse") + for h in Discussions_by_feed(q): + print(h) + +get_discussions_by_hot +~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_hot + q = Query(limit=10, tag="whitehorse") + for h in Discussions_by_hot(q): + print(h) + +get_discussions_by_promoted +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_promoted + q = Query(limit=10, tag="whitehorse") + for h in Discussions_by_promoted(q): + print(h) + +get_discussions_by_trending +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_trending + q = Query(limit=10, tag="whitehorse") + for h in Discussions_by_trending(q): + print(h) + +get_discussions_by_votes +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Discussions_by_votes + q = Query(limit=10) + for h in Discussions_by_votes(q): + print(h) + +get_dynamic_global_properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_dynamic_global_properties()) + +get_escrow +~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_escrow()) + +get_expiring_vesting_delegations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_expiring_vesting_delegations()) + +get_feed +~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for f in acc.get_feed(): + print(f) + +get_feed_entries +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for f in acc.get_feed_entries(): + print(f) + +get_feed_history +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_feed_history()) + +get_follow_count +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_follow_count()) + +get_followers +~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for f in acc.get_followers(): + print(f) + +get_following +~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for f in acc.get_following(): + print(f) + +get_hardfork_version +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_hardfork_properties()["hf_version"]) + +get_key_references +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + from dpaycli.wallet import Wallet + acc = Account("gtg") + w = Wallet() + print(w.getAccountFromPublicKey(acc["posting"]["key_auths"][0][0])) + +get_market_history +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + for t in m.market_history(): + print(t) + +get_market_history_buckets +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + for t in m.market_history_buckets(): + print(t) + +get_next_scheduled_hardfork +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_hardfork_properties()) + +get_open_orders +~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + print(m.accountopenorders(account="gtg")) + +get_ops_in_block +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.block import Block + b = Block(2e6, only_ops=True) + print(b) + +get_order_book +~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + print(m.orderbook()) + +get_owner_history +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_owner_history()) + +get_post_discussions_by_payout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Post_discussions_by_payout + q = Query(limit=10) + for h in Post_discussions_by_payout(q): + print(h) + +get_potential_signatures +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.transactionbuilder import TransactionBuilder + from dpaycli.blockchain import Blockchain + b = Blockchain() + block = b.get_current_block() + trx = block.json()["transactions"][0] + t = TransactionBuilder(trx) + print(t.get_potential_signatures()) + + +get_reblogged_by +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + from dpaycli.comment import Comment + acc = Account("gtg") + post = acc.get_feed(0,1)[0] + c = Comment(post["authorperm"]) + for h in c.get_reblogged_by(): + print(h) + +get_recent_trades +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + for t in m.recent_trades(): + print(t) + +get_recovery_request +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_recovery_request()) + +get_replies_by_last_update +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Replies_by_last_update + q = Query(limit=10, start_author="dsocial", start_permlink="firstpost") + for h in Replies_by_last_update(q): + print(h) + +get_required_signatures +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.transactionbuilder import TransactionBuilder + from dpaycli.blockchain import Blockchain + b = Blockchain() + block = b.get_current_block() + trx = block.json()["transactions"][0] + t = TransactionBuilder(trx) + print(t.get_required_signatures()) + +get_reward_fund +~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_reward_funds()) + +get_savings_withdraw_from +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_savings_withdrawals(direction="from")) + +get_savings_withdraw_to +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_savings_withdrawals(direction="to")) + +get_state +~~~~~~~~~ + +.. code-block:: python + + from dpaycli.comment import RecentByPath + for p in RecentByPath(path="promoted"): + print(p) + +get_tags_used_by_author +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_tags_used_by_author()) + +get_ticker +~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + print(m.ticker()) + +get_trade_history +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + for t in m.trade_history(): + print(t) + +get_transaction +~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + b = Blockchain() + print(b.get_transaction("6fde0190a97835ea6d9e651293e90c89911f933c")) + +get_transaction_hex +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + b = Blockchain() + block = b.get_current_block() + trx = block.json()["transactions"][0] + print(b.get_transaction_hex(trx)) + +get_trending_tags +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.discussions import Query, Trending_tags + q = Query(limit=10, start_tag="dsocial") + for h in Trending_tags(q): + print(h) + +get_version +~~~~~~~~~~~ +not implemented + +get_vesting_delegations +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for v in acc.get_vesting_delegations(): + print(v) + +get_volume +~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.market import Market + m = Market() + print(m.volume24h()) + +get_withdraw_routes +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + print(acc.get_withdraw_routes()) + +get_witness_by_account +~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.witness import Witness + w = Witness("gtg") + print(w) + +get_witness_count +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.witness import Witnesses + w = Witnesses() + print(w.witness_count) + +get_witness_schedule +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + stm = DPay() + print(stm.get_witness_schedule()) + +get_witnesses +~~~~~~~~~~~~~ +not implemented + +get_witnesses_by_vote +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.witness import WitnessesRankedByVote + for w in WitnessesRankedByVote(): + print(w) + +lookup_account_names +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg", full=False) + print(acc.json()) + +lookup_accounts +~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.account import Account + acc = Account("gtg") + for a in acc.get_similar_account_names(limit=100): + print(a) + +lookup_witness_accounts +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.witness import ListWitnesses + for w in ListWitnesses(): + print(w) + +verify_account_authority +~~~~~~~~~~~~~~~~~~~~~~~~ +disabled and not implemented + +verify_authority +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli.transactionbuilder import TransactionBuilder + from dpaycli.blockchain import Blockchain + b = Blockchain() + block = b.get_current_block() + trx = block.json()["transactions"][0] + t = TransactionBuilder(trx) + t.verify_authority() + print("ok") diff --git a/docs/beem.account.rst b/docs/beem.account.rst new file mode 100755 index 0000000..a1feac8 --- /dev/null +++ b/docs/beem.account.rst @@ -0,0 +1,7 @@ +dpaycli\.account +============= + +.. automodule:: dpaycli.account + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.aes.rst b/docs/beem.aes.rst new file mode 100755 index 0000000..f11ea16 --- /dev/null +++ b/docs/beem.aes.rst @@ -0,0 +1,7 @@ +dpaycli\.aes +========= + +.. automodule:: dpaycli.aes + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.amount.rst b/docs/beem.amount.rst new file mode 100755 index 0000000..e823b73 --- /dev/null +++ b/docs/beem.amount.rst @@ -0,0 +1,7 @@ +dpaycli\.amount +============ + +.. automodule:: dpaycli.amount + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.asciichart.rst b/docs/beem.asciichart.rst new file mode 100755 index 0000000..85eb3b9 --- /dev/null +++ b/docs/beem.asciichart.rst @@ -0,0 +1,7 @@ +dpaycli\.asciichart +================ + +.. automodule:: dpaycli.asciichart + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.asset.rst b/docs/beem.asset.rst new file mode 100755 index 0000000..8e899ef --- /dev/null +++ b/docs/beem.asset.rst @@ -0,0 +1,7 @@ +dpaycli\.asset +=========== + +.. automodule:: dpaycli.asset + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.block.rst b/docs/beem.block.rst new file mode 100755 index 0000000..8bced72 --- /dev/null +++ b/docs/beem.block.rst @@ -0,0 +1,7 @@ +dpaycli\.block +=========== + +.. automodule:: dpaycli.block + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.blockchain.rst b/docs/beem.blockchain.rst new file mode 100755 index 0000000..e50640e --- /dev/null +++ b/docs/beem.blockchain.rst @@ -0,0 +1,7 @@ +dpaycli\.blockchain +================ + +.. automodule:: dpaycli.blockchain + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.blockchainobject.rst b/docs/beem.blockchainobject.rst new file mode 100755 index 0000000..f4ace25 --- /dev/null +++ b/docs/beem.blockchainobject.rst @@ -0,0 +1,7 @@ +dpaycli\.blockchainobject +====================== + +.. automodule:: dpaycli.blockchainobject + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.comment.rst b/docs/beem.comment.rst new file mode 100755 index 0000000..d23b3ad --- /dev/null +++ b/docs/beem.comment.rst @@ -0,0 +1,7 @@ +dpaycli\.comment +============= + +.. automodule:: dpaycli.comment + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.conveyor.rst b/docs/beem.conveyor.rst new file mode 100755 index 0000000..9032bde --- /dev/null +++ b/docs/beem.conveyor.rst @@ -0,0 +1,7 @@ +dpaycli\.conveyor +============== + +.. automodule:: dpaycli.conveyor + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.discussions.rst b/docs/beem.discussions.rst new file mode 100755 index 0000000..459397b --- /dev/null +++ b/docs/beem.discussions.rst @@ -0,0 +1,7 @@ +dpaycli\.discussions +================= + +.. automodule:: dpaycli.discussions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.exceptions.rst b/docs/beem.exceptions.rst new file mode 100755 index 0000000..e9f3957 --- /dev/null +++ b/docs/beem.exceptions.rst @@ -0,0 +1,7 @@ +dpaycli\.exceptions +================ + +.. automodule:: dpaycli.exceptions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.imageuploader.rst b/docs/beem.imageuploader.rst new file mode 100755 index 0000000..e62159b --- /dev/null +++ b/docs/beem.imageuploader.rst @@ -0,0 +1,7 @@ +dpaycli\.imageuploader +=================== + +.. automodule:: dpaycli.imageuploader + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.instance.rst b/docs/beem.instance.rst new file mode 100755 index 0000000..f490d70 --- /dev/null +++ b/docs/beem.instance.rst @@ -0,0 +1,7 @@ +dpaycli\.instance +============== + +.. automodule:: dpaycli.instance + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.market.rst b/docs/beem.market.rst new file mode 100755 index 0000000..84abdcc --- /dev/null +++ b/docs/beem.market.rst @@ -0,0 +1,7 @@ +dpaycli\.market +============ + +.. automodule:: dpaycli.market + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.memo.rst b/docs/beem.memo.rst new file mode 100755 index 0000000..c7351c8 --- /dev/null +++ b/docs/beem.memo.rst @@ -0,0 +1,7 @@ +dpaycli\.memo +========== + +.. automodule:: dpaycli.memo + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.message.rst b/docs/beem.message.rst new file mode 100755 index 0000000..08ec41c --- /dev/null +++ b/docs/beem.message.rst @@ -0,0 +1,7 @@ +dpaycli\.message +============= + +.. automodule:: dpaycli.message + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.nodelist.rst b/docs/beem.nodelist.rst new file mode 100755 index 0000000..142f054 --- /dev/null +++ b/docs/beem.nodelist.rst @@ -0,0 +1,7 @@ +dpaycli\.nodelist +============== + +.. automodule:: dpaycli.nodelist + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beem.notify.rst b/docs/beem.notify.rst new file mode 100755 index 0000000..f546842 --- /dev/null +++ b/docs/beem.notify.rst @@ -0,0 +1,7 @@ +dpaycli\.notify +============ + +.. automodule:: dpaycli.notify + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.price.rst b/docs/beem.price.rst new file mode 100755 index 0000000..40195ff --- /dev/null +++ b/docs/beem.price.rst @@ -0,0 +1,7 @@ +dpaycli\.price +=========== + +.. automodule:: dpaycli.price + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.rc.rst b/docs/beem.rc.rst new file mode 100755 index 0000000..43fa715 --- /dev/null +++ b/docs/beem.rc.rst @@ -0,0 +1,7 @@ +dpaycli\.rc +======== + +.. automodule:: dpaycli.rc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beem.snapshot.rst b/docs/beem.snapshot.rst new file mode 100755 index 0000000..197becd --- /dev/null +++ b/docs/beem.snapshot.rst @@ -0,0 +1,7 @@ +dpaycli\.snapshot +============== + +.. automodule:: dpaycli.snapshot + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beem.steem.rst b/docs/beem.steem.rst new file mode 100755 index 0000000..13142ba --- /dev/null +++ b/docs/beem.steem.rst @@ -0,0 +1,7 @@ +dpaycli\.dpay +=========== + +.. automodule:: dpaycli.dpay + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beem.steemconnect.rst b/docs/beem.steemconnect.rst new file mode 100755 index 0000000..4ab2d5b --- /dev/null +++ b/docs/beem.steemconnect.rst @@ -0,0 +1,7 @@ +dpaycli\.dpayid +================== + +.. automodule:: dpaycli.dpayid + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beem.storage.rst b/docs/beem.storage.rst new file mode 100755 index 0000000..326e57a --- /dev/null +++ b/docs/beem.storage.rst @@ -0,0 +1,7 @@ +dpaycli\.storage +============= + +.. automodule:: dpaycli.storage + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.transactionbuilder.rst b/docs/beem.transactionbuilder.rst new file mode 100755 index 0000000..7656611 --- /dev/null +++ b/docs/beem.transactionbuilder.rst @@ -0,0 +1,7 @@ +dpaycli\.transactionbuilder +======================== + +.. automodule:: dpaycli.transactionbuilder + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.utils.rst b/docs/beem.utils.rst new file mode 100755 index 0000000..04e1e44 --- /dev/null +++ b/docs/beem.utils.rst @@ -0,0 +1,7 @@ +dpaycli\.utils +=========== + +.. automodule:: dpaycli.utils + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.vote.rst b/docs/beem.vote.rst new file mode 100755 index 0000000..a9e2612 --- /dev/null +++ b/docs/beem.vote.rst @@ -0,0 +1,7 @@ +dpaycli\.vote +========== + +.. automodule:: dpaycli.vote + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.wallet.rst b/docs/beem.wallet.rst new file mode 100755 index 0000000..141f166 --- /dev/null +++ b/docs/beem.wallet.rst @@ -0,0 +1,7 @@ +dpaycli\.wallet +============ + +.. automodule:: dpaycli.wallet + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beem.witness.rst b/docs/beem.witness.rst new file mode 100755 index 0000000..309c9e5 --- /dev/null +++ b/docs/beem.witness.rst @@ -0,0 +1,8 @@ +dpaycli\.witness +============= + +.. automodule:: dpaycli.witness + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/beemapi.exceptions.rst b/docs/beemapi.exceptions.rst new file mode 100755 index 0000000..6d617db --- /dev/null +++ b/docs/beemapi.exceptions.rst @@ -0,0 +1,7 @@ +dpaycliapi\.exceptions +=================== + +.. automodule:: dpaycliapi.exceptions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beemapi.graphenenerpc.rst b/docs/beemapi.graphenenerpc.rst new file mode 100755 index 0000000..3f67c09 --- /dev/null +++ b/docs/beemapi.graphenenerpc.rst @@ -0,0 +1,15 @@ +dpaycliapi\.graphenerpc +==================== + + +.. note:: This is a low level class that can be used in combination with + GrapheneClient + +This class allows to call API methods exposed by the witness node via +websockets. It does **not** support notifications and is not run +asynchronously. + +.. automodule:: dpaycliapi.graphenerpc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beemapi.node.rst b/docs/beemapi.node.rst new file mode 100755 index 0000000..fe430f9 --- /dev/null +++ b/docs/beemapi.node.rst @@ -0,0 +1,7 @@ +dpaycliapi\.node +============= + +.. automodule:: dpaycliapi.node + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beemapi.steemnoderpc.rst b/docs/beemapi.steemnoderpc.rst new file mode 100755 index 0000000..b3e376e --- /dev/null +++ b/docs/beemapi.steemnoderpc.rst @@ -0,0 +1,7 @@ +dpaycliapi\.dpaynoderpc +===================== + +.. automodule:: dpaycliapi.dpaynoderpc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beemapi.websocket.rst b/docs/beemapi.websocket.rst new file mode 100755 index 0000000..b088556 --- /dev/null +++ b/docs/beemapi.websocket.rst @@ -0,0 +1,27 @@ +dpaycliapi\.websocket +================== + +This class allows subscribe to push notifications from the DPay +node. + +.. code-block:: python + + from pprint import pprint + from dpaycliapi.websocket import DPayWebsocket + + ws = DPayWebsocket( + "wss://gtg.dpay.house:8090", + accounts=["test"], + on_block=print, + ) + + ws.run_forever() + + +.. autoclass:: dpaycliapi.websocket.DPayWebsocket + :members: + :undoc-members: + :private-members: + :special-members: + + diff --git a/docs/beembase.memo.rst b/docs/beembase.memo.rst new file mode 100755 index 0000000..2421ff6 --- /dev/null +++ b/docs/beembase.memo.rst @@ -0,0 +1,7 @@ +dpayclibase\.memo +============== + +.. automodule:: dpayclibase.memo + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.objects.rst b/docs/beembase.objects.rst new file mode 100755 index 0000000..06802b3 --- /dev/null +++ b/docs/beembase.objects.rst @@ -0,0 +1,7 @@ +dpayclibase\.objects +================= + +.. automodule:: dpayclibase.objects + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.objecttypes.rst b/docs/beembase.objecttypes.rst new file mode 100755 index 0000000..8b29193 --- /dev/null +++ b/docs/beembase.objecttypes.rst @@ -0,0 +1,7 @@ +dpayclibase\.objecttypes +===================== + +.. automodule:: dpayclibase.objecttypes + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.operationids.rst b/docs/beembase.operationids.rst new file mode 100755 index 0000000..cacbfe0 --- /dev/null +++ b/docs/beembase.operationids.rst @@ -0,0 +1,8 @@ +dpayclibase\.operationids +====================== + +.. automodule:: dpayclibase.operationids + :members: + :noindex: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.operations.rst b/docs/beembase.operations.rst new file mode 100755 index 0000000..89f7ea9 --- /dev/null +++ b/docs/beembase.operations.rst @@ -0,0 +1,7 @@ +dpayclibase\.operations +==================== + +.. automodule:: dpayclibase.operationids + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.signedtransactions.rst b/docs/beembase.signedtransactions.rst new file mode 100755 index 0000000..6832e81 --- /dev/null +++ b/docs/beembase.signedtransactions.rst @@ -0,0 +1,7 @@ +dpayclibase\.signedtransactions +============================ + +.. automodule:: dpayclibase.signedtransactions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beembase.transactions.rst b/docs/beembase.transactions.rst new file mode 100755 index 0000000..1834558 --- /dev/null +++ b/docs/beembase.transactions.rst @@ -0,0 +1,7 @@ +dpayclibase\.transactions +====================== + +.. automodule:: dpayclibase.transactions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/beemgraphenebase.account.rst b/docs/beemgraphenebase.account.rst new file mode 100755 index 0000000..5c559b7 --- /dev/null +++ b/docs/beemgraphenebase.account.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.account +========================= + +.. automodule:: dpaycligraphenebase.account + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.base58.rst b/docs/beemgraphenebase.base58.rst new file mode 100755 index 0000000..e7d699e --- /dev/null +++ b/docs/beemgraphenebase.base58.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.base58 +======================== + +.. automodule:: dpaycligraphenebase.base58 + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.bip38.rst b/docs/beemgraphenebase.bip38.rst new file mode 100755 index 0000000..f8cf38c --- /dev/null +++ b/docs/beemgraphenebase.bip38.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.bip38 +======================= + +.. automodule:: dpaycligraphenebase.bip38 + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.ecdsasig.rst b/docs/beemgraphenebase.ecdsasig.rst new file mode 100755 index 0000000..0123722 --- /dev/null +++ b/docs/beemgraphenebase.ecdsasig.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.ecdsasig +========================== + +.. automodule:: dpaycligraphenebase.ecdsasig + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.objects.rst b/docs/beemgraphenebase.objects.rst new file mode 100755 index 0000000..2dbac91 --- /dev/null +++ b/docs/beemgraphenebase.objects.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.objects +========================= + +.. automodule:: dpaycligraphenebase.objects + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.objecttypes.rst b/docs/beemgraphenebase.objecttypes.rst new file mode 100755 index 0000000..1e80af1 --- /dev/null +++ b/docs/beemgraphenebase.objecttypes.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.objecttypes +============================= + +.. automodule:: dpaycligraphenebase.objecttypes + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.operations.rst b/docs/beemgraphenebase.operations.rst new file mode 100755 index 0000000..3214c00 --- /dev/null +++ b/docs/beemgraphenebase.operations.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.operations +============================ + +.. automodule:: dpaycligraphenebase.operationids + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.signedtransactions.rst b/docs/beemgraphenebase.signedtransactions.rst new file mode 100755 index 0000000..73f48e6 --- /dev/null +++ b/docs/beemgraphenebase.signedtransactions.rst @@ -0,0 +1,7 @@ +dpaycligraphenebase\.signedtransactions +==================================== + +.. automodule:: dpaycligraphenebase.signedtransactions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100755 index 0000000..63774c2 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,179 @@ +dpay CLI +~~~~~~~~~~ +`dpay` is a convenient CLI utility that enables you to manage your wallet, transfer funds, check +balances and more. + +Using the Wallet +---------------- +`dpay` lets you leverage your BIP38 encrypted wallet to perform various actions on your accounts. + +The first time you use `dpay`, you will be prompted to enter a password. This password will be used to encrypt +the `dpay` wallet, which contains your private keys. + +You can change the password via `changewalletpassphrase` command. + +:: + + dpay changewalletpassphrase + + +From this point on, every time an action requires your private keys, you will be prompted ot enter +this password (from CLI as well as while using `dpay` library). + +To bypass password entry, you can set an environment variable ``UNLOCK``. + +:: + + UNLOCK=mysecretpassword dpay transfer 100 BEX + +Common Commands +--------------- +First, you may like to import your dPay account: + +:: + + dpay importaccount + + +You can also import individual private keys: + +:: + + dpay addkey + +Listing accounts: + +:: + + dpay listaccounts + +Show balances: + +:: + + dpay balance account_name1 account_name2 + +Sending funds: + +:: + + dpay transfer --account 100 BEX memo + +Upvoting a post: + +:: + + dpay upvote --account https://dsite.io/funny/@mynameisbrian/the-content-stand-a-comic + + +Setting Defaults +---------------- +For a more convenient use of ``dpay`` as well as the ``dpaycli`` library, you can set some defaults. +This is especially useful if you have a single DPay account. + +:: + + dpay set default_account test + dpay set default_vote_weight 100 + + dpay config + +---------------------+--------+ + | Key | Value | + +---------------------+--------+ + | default_account | test | + | default_vote_weight | 100 | + +---------------------+--------+ + +If you've set up your `default_account`, you can now send funds by omitting this field: + +:: + + dpay transfer 100 BEX memo + +Commands +-------- + +.. click:: dpaycli.cli:cli + :prog: dpay + :show-nested: + +dpay --help +------------- +You can see all available commands with ``dpay --help`` + +:: + + ~ % dpay --help + Usage: cli.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + + Options: + -n, --node TEXT URL for public DPay API (e.g. + https://api.dpays.io) + -o, --offline Prevent connecting to network + -d, --no-broadcast Do not broadcast + -p, --no-wallet Do not load the wallet + -x, --unsigned Nothing will be signed + -e, --expires INTEGER Delay in seconds until transactions are supposed to + expire(defaults to 60) + -v, --verbose INTEGER Verbosity + --version Show the version and exit. + --help Show this message and exit. + + Commands: + addkey Add key to wallet When no [OPTION] is given,... + allow Allow an account/key to interact with your... + approvewitness Approve a witnesses + balance Shows balance + broadcast broadcast a signed transaction + buy Buy BEX or BBD from the internal market... + cancel Cancel order in the internal market + changewalletpassphrase Change wallet password + claimreward Claim reward balances By default, this will... + config Shows local configuration + convert Convert BEXDollars to DPay (takes a week... + createwallet Create new wallet with a new password + currentnode Sets the currently working node at the first... + delkey Delete key from the wallet PUB is the public... + delprofile Delete a variable in an account's profile + disallow Remove allowance an account/key to interact... + disapprovewitness Disapprove a witnesses + downvote Downvote a post/comment POST is... + follow Follow another account + follower Get information about followers + following Get information about following + importaccount Import an account using a passphrase + info Show basic blockchain info General... + interest Get information about interest payment + listaccounts Show stored accounts + listkeys Show stored keys + mute Mute another account + muter Get information about muter + muting Get information about muting + newaccount Create a new account + nextnode Uses the next node in list + openorders Show open orders + orderbook Obtain orderbook of the internal market + parsewif Parse a WIF private key without importing + permissions Show permissions of an account + pingnode Returns the answer time in milliseconds + power Shows vote power and bandwidth + powerdown Power down (start withdrawing VESTS from... + powerdownroute Setup a powerdown route + powerup Power up (vest BEX as BEX POWER) + pricehistory Show price history + repost Repost an existing post + sell Sell BEX or BBD from the internal market... + set Set default_account, default_vote_weight or... + setprofile Set a variable in an account's profile + sign Sign a provided transaction with available... + ticker Show ticker + tradehistory Show price history + transfer Transfer BBD/BEX + unfollow Unfollow/Unmute another account + updatememokey Update an account's memo key + upvote Upvote a post/comment POST is... + votes List outgoing/incoming account votes + walletinfo Show info about wallet + witnesscreate Create a witness + witnesses List witnesses + witnessupdate Change witness properties diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..9ae1c25 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# dpaycli documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 5 14:06:38 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../scripts/')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx_click.ext"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'dpaycli' +copyright = '2017, ChainSquad GmbH, 2018, Holger Nahrstaedt' +author = 'Holger Nahrstaedt' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.20' +# The full version, including alpha/beta/rc tags. +release = '0.20.2' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = '_static/dpaycli-logo_2.svg' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = '_static/dpaycli-icon_bw.png' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'dpayclidoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'dpaycli.tex', 'dpaycli Documentation', + author, 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'dpaycli', 'dpaycli Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'dpaycli', 'dpaycli Documentation', + author, 'dpaycli', 'python library for dpay', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100755 index 0000000..1a14dcf --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,175 @@ +************* +Configuration +************* + +The dpay-python library comes with its own local configuration database +that stores information like + +* API node URLs +* default account name +* the encrypted master password +* the default voting weight +* if keyring should be used for unlocking the wallet + +and potentially more. + +You can access those variables like a regular dictionary by using + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + print(dpay.config.items()) + +Keys can be added and changed like they are for regular dictionaries. + +If you don't want to load the :class:`dpaycli.DPay` class, you +can load the configuration directly by using: + +.. code-block:: python + + from dpaycli.storage import configStorage as config + +It is also possible to access the configuration with the commandline tool `dpay`: + +.. code-block:: bash + + dpay config + +API node URLs +------------- + +The default node URLs which will be used when `node` is `None` in :class:`dpaycli.DPay` class +is stored in `config["nodes"]` as string. The list can be get and set by: + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + node_list = dpay.get_default_nodes() + node_list = node_list[1:] + [node_list[0]] + dpay.set_default_nodes(node_list) + +dpay can also be used to set nodes: + +.. code-block:: bash + + dpay set nodes wss://greatchain.dpaynodes.com + dpay set nodes "['wss://greatchain.dpaynodes.com', 'wss://greatchain.dpays.io']" + +The default nodes can be resetted to the default value. When the first node does not +answer, dpay should be set to the offline mode. This can be done by: + +.. code-block:: bash + + dpay -o set nodes "" + +or + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay(offline=True) + dpay.set_default_nodes("") + +Default account +--------------- + +The default account name is used in some functions, when no account name is given. +It is also used in `dpay` for all account related functions. + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.set_default_account("test") + dpay.config["default_account"] = "test" + +or by dpay with + +.. code-block:: bash + + dpay set default_account test + +Default voting weight +--------------------- + +The default vote weight is used for voting, when no vote weight is given. + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.config["default_vote_weight"] = 100 + +or by dpay with + +.. code-block:: bash + + dpay set default_vote_weight 100 + + +Setting password_storage +------------------------ + +The password_storage can be set to: + +* environment, this is the default setting. The master password for the wallet can be provided in the environment variable `UNLOCK`. +* keyring (when set with dpay, it asks for the wallet password) + +.. code-block:: bash + + dpay set password_storage environment + dpay set password_storage keyring + + + +Environment variable for storing the master password +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When `password_storage` is set to `environment`, the master password can be stored in `UNLOCK` +for unlocking automatically the wallet. + +Keyring support for dpay and wallet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to use keyring for storing the wallet password, the following steps are necessary: + +* Install keyring: `pip install keyring` +* Change `password_storage` to `keyring` with `dpay` and enter the wallet password. + +It also possible to change the password in the keyring by + +.. code-block:: bash + + python -m keyring set dpaycli wallet + +The stored master password can be displayed in the terminal by + +.. code-block:: bash + + python -m keyring get dpaycli wallet + +When keyring is set as `password_storage` and the stored password in the keyring +is identically to the set master password of the wallet, the wallet is automatically +unlocked everytime it is used. + +Testing if unlocking works +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Testing if the master password is correctly provided by keyring or the `UNLOCK` variable: + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + print(dpay.wallet.locked()) + +When the output is False, automatic unlocking with keyring or the `UNLOCK` variable works. +It can also tested by dpay with + +.. code-block:: bash + + dpay walletinfo --test-unlock + +When no password prompt is shown, unlocking with keyring or the `UNLOCK` variable works. diff --git a/docs/contribute.rst b/docs/contribute.rst new file mode 100755 index 0000000..95a514a --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,56 @@ +Contributing to dpaycli +==================== + +We welcome your contributions to our project. + +Repository +---------- + +The repository of dpaycli is currently located at: + + https://github.com/holgern/dpaycli + +Flow +---- + +This project makes heavy use of `git flow `_. +If you are not familiar with it, then the most important thing for your +to understand is that: + + pull requests need to be made against the develop branch + +How to Contribute +----------------- + +0. Familiarize yourself with `contributing on github ` +1. Fork or branch from the master. +2. Create commits following the commit style +3. Start a pull request to the master branch +4. Wait for a @holger80 or another member to review + +Issues +------ + +Feel free to submit issues and enhancement requests. + +Contributing +------------ + +Please refer to each project's style guidelines and guidelines for +submitting patches and additions. In general, we follow the +"fork-and-pull" Git workflow. + +1. **Fork** the repo on GitHub +2. **Clone** the project to your own machine +3. **Commit** changes to your own branch +4. **Push** your work back up to your fork +5. Submit a **Pull request** so that we can review your changes + +NOTE: Be sure to merge the latest from "upstream" before making a pull +request! + +Copyright and Licensing +----------------------- + +This library is open sources under the MIT license. We require your to +release your code under that license as well. diff --git a/docs/index.rst b/docs/index.rst new file mode 100755 index 0000000..df40ddc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,134 @@ +.. dpay-python documentation master file, created by + sphinx-quickstart on Fri Jun 5 14:06:38 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. http://sphinx-doc.org/rest.html + http://sphinx-doc.org/markup/index.html + http://sphinx-doc.org/markup/para.html + http://openalea.gforge.inria.fr/doc/openalea/doc/_build/html/source/sphinx/rest_syntax.html + http://rest-sphinx-memo.readthedocs.org/en/latest/ReST.html + +.. image:: _static/dpaycli-logo.svg + :width: 300 px + :alt: dpaycli + :align: center + +Welcome to dpaycli's documentation! +================================ + +DPay is a blockchain-based rewards platform for publishers to monetize +content and grow community. + +It is based on *Graphene* (tm), a blockchain technology stack (i.e. +software) that allows for fast transactions and ascalable blockchain +solution. In case of DPay, it comes with decentralized publishing of +content. + +The dpaycli library has been designed to allow developers to easily +access its routines and make use of the network without dealing with all +the related blockchain technology and cryptography. This library can be +used to do anything that is allowed according to the DPay +blockchain protocol. + + +About this Library +------------------ + +The purpose of *dpaycli* is to simplify development of products and +services that use the DPay blockchain. It comes with + +* its own (bip32-encrypted) wallet +* RPC interface for the Blockchain backend +* JSON-based blockchain objects (accounts, blocks, prices, markets, etc) +* a simple to use yet powerful API +* transaction construction and signing +* push notification API +* *and more* + +Quickstart +---------- + +.. note:: All methods that construct and sign a transaction can be given + the ``account=`` parameter to identify the user that is going + to affected by this transaction, e.g.: + + * the source account in a transfer + * the accout that buys/sells an asset in the exchange + * the account whos collateral will be modified + + **Important**, If no ``account`` is given, then the + ``default_account`` according to the settings in ``config`` is + used instead. + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + account = Account("test", dpay_instance=dpay) + account.transfer("", "", "", "") + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + blockchain = Blockchain() + for op in blockchain.stream(): + print(op) + +.. code-block:: python + + from dpaycli.block import Block + print(Block(1)) + +.. code-block:: python + + from dpaycli.account import Account + account = Account("test") + print(account.balances) + for h in account.history(): + print(h) + +.. code-block:: python + + from dpaycli.dpay import DPay + stm = DPay() + stm.wallet.wipe(True) + stm.wallet.create("wallet-passphrase") + stm.wallet.unlock("wallet-passphrase") + stm.wallet.addPrivateKey("512345678") + stm.wallet.lock() + +.. code-block:: python + + from dpaycli.market import Market + market = Market("BBD:BEX") + print(market.ticker()) + market.dpay.wallet.unlock("wallet-passphrase") + print(market.sell(300, 100) # sell 100 BEX for 300 BEX/BBD + + +General +------- +.. toctree:: + :maxdepth: 1 + + installation + quickstart + tutorials + cli + configuration + apidefinitions + modules + contribute + support + indices + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/indices.rst b/docs/indices.rst new file mode 100755 index 0000000..47168c8 --- /dev/null +++ b/docs/indices.rst @@ -0,0 +1,5 @@ +Indices and Tables +================== + +* :ref:`genindex` +* :ref:`modindex` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100755 index 0000000..c836108 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,92 @@ +Installation +============ +The minimal working python version is 2.7.x. or 3.4.x + +dpaycli can be installed parallel to python-dpay. + +For Debian and Ubuntu, please ensure that the following packages are installed: + +.. code:: bash + + sudo apt-get install build-essential libssl-dev python-dev + +For Fedora and RHEL-derivatives, please ensure that the following packages are installed: + +.. code:: bash + + sudo yum install gcc openssl-devel python-devel + +For OSX, please do the following:: + + brew install openssl + export CFLAGS="-I$(brew --prefix openssl)/include $CFLAGS" + export LDFLAGS="-L$(brew --prefix openssl)/lib $LDFLAGS" + +For Termux on Android, please install the following packages: + +.. code:: bash + + pkg install clang openssl-dev python-dev + +Signing and Verify can be fasten (200 %) by installing cryptography: + +.. code:: bash + + pip install -U cryptography + +Install dpaycli by pip:: + + pip install -U dpaycli + +Sometimes this does not work. Please try:: + + pip3 install -U dpaycli + +or:: + + python -m pip install dpaycli + +Manual installation +------------------- + +You can install dpaycli from this repository if you want the latest +but possibly non-compiling version:: + + git clone https://github.com/holgern/dpaycli.git + cd dpaycli + python setup.py build + + python setup.py install --user + +Run tests after install:: + + pytest + + +Installing dpaycli with conda-forge +-------------------------------- + +Installing dpaycli from the conda-forge channel can be achieved by adding conda-forge to your channels with:: + + conda config --add channels conda-forge + +Once the conda-forge channel has been enabled, dpaycli can be installed with:: + + conda install dpaycli + +Signing and Verify can be fasten (200 %) by installing cryptography:: + + conda install cryptography + +Enable Logging +-------------- + +Add the following for enabling logging in your python script:: + + import logging + log = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO) + +When you want to see only critical errors, replace the last line by:: + + logging.basicConfig(level=logging.CRITICAL) diff --git a/docs/make.bat b/docs/make.bat new file mode 100755 index 0000000..f801af9 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-graphenelib.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-graphenelib.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100755 index 0000000..c7d0744 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,77 @@ +Modules +======= + +dpaycli Modules +--------------- + +.. toctree:: + + dpaycli.account + dpaycli.aes + dpaycli.asciichart + dpaycli.amount + dpaycli.asset + dpaycli.dpay + dpaycli.nodelist + dpaycli.dpayid + dpaycli.block + dpaycli.blockchain + dpaycli.blockchainobject + dpaycli.comment + dpaycli.conveyor + dpaycli.discussions + dpaycli.exceptions + dpaycli.imageuploader + dpaycli.instance + dpaycli.market + dpaycli.memo + dpaycli.message + dpaycli.notify + dpaycli.price + dpaycli.rc + dpaycli.snapshot + dpaycli.storage + dpaycli.transactionbuilder + dpaycli.utils + dpaycli.vote + dpaycli.wallet + dpaycli.witness + +dpaycliapi Modules +------------------ + +.. toctree:: + + dpaycliapi.dpaynoderpc + dpaycliapi.exceptions + dpaycliapi.websocket + dpaycliapi.node + dpaycliapi.graphenenerpc + +dpayclibase Modules +------------------- + +.. toctree:: + + dpayclibase.memo + dpayclibase.objects + dpayclibase.objecttypes + dpayclibase.operationids + dpayclibase.operations + dpayclibase.signedtransactions + dpayclibase.transactions + + +dpaycligraphenebase Modules +--------------------------- + +.. toctree:: + + dpaycligraphenebase.account + dpaycligraphenebase.base58 + dpaycligraphenebase.bip38 + dpaycligraphenebase.ecdsasig + dpaycligraphenebase.objects + dpaycligraphenebase.objecttypes + dpaycligraphenebase.operations + dpaycligraphenebase.signedtransactions \ No newline at end of file diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100755 index 0000000..4602103 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,206 @@ +Quickstart +========== + +DPay +----- +The dpay object is the connection to the dPay blockchain. +By creating this object different options can be set. + +.. note:: All init methods of dpaycli classes can be given + the ``dpay_instance=`` parameter to assure that + all objects use the same dpay object. When the + ``dpay_instance=`` parameter is not used, the + dpay object is taken from get_shared_dpay_instance(). + + ``get_shared_dpay_instance()`` returns a global instance of dpay. + It can be set by ``set_shared_dpay_instance`` otherwise it is created + on the first call. + +.. code-block:: python + + from dpaycli import DPay + from dpaycli.account import Account + stm = DPay() + account = Account("test", dpay_instance=stm) + +.. code-block:: python + + from dpaycli import DPay + from dpaycli.account import Account + from dpaycli.instance import set_shared_dpay_instance + stm = DPay() + set_shared_dpay_instance(stm) + account = Account("test") + +Wallet and Keys +--------------- +Each account has the following keys: + +* Posting key (allows accounts to post, vote, edit, repost and follow/mute) +* Active key (allows accounts to transfer, power up/down, voting for witness, ...) +* Memo key (Can be used to encrypt/decrypt memos) +* Owner key (The most important key, should not be used with dpaycli) + +Outgoing operation, which will be stored in the dPay blockchain, have to be +signed by a private key. E.g. Comment or Vote operation need to be signed by the posting key +of the author or upvoter. Private keys can be provided to dpaycli temporary or can be +stored encrypted in a sql-database (wallet). + +.. note:: Before using the wallet the first time, it has to be created and a password has + to set. The wallet content is available to dpay and all python scripts, which have + access to the sql database file. + +Creating a wallet +~~~~~~~~~~~~~~~~~ +``dpay.wallet.wipe(True)`` is only necessary when there was already an wallet created. + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.wipe(True) + dpay.wallet.unlock("wallet-passphrase") + +Adding keys to the wallet +~~~~~~~~~~~~~~~~~~~~~~~~~ +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + dpay.wallet.addPrivateKey("xxxxxxx") + dpay.wallet.addPrivateKey("xxxxxxx") + +Using the keys in the wallet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + account = Account("test", dpay_instance=dpay) + account.transfer("", "", "", "") + +Private keys can also set temporary +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay(keys=["xxxxxxxxx"]) + account = Account("test", dpay_instance=dpay) + account.transfer("", "", "", "") + +Receiving information about blocks, accounts, votes, comments, market and witness +--------------------------------------------------------------------------------- + +Receive all Blocks from the Blockchain + +.. code-block:: python + + from dpaycli.blockchain import Blockchain + blockchain = Blockchain() + for op in blockchain.stream(): + print(op) + +Access one Block + +.. code-block:: python + + from dpaycli.block import Block + print(Block(1)) + +Access an account + +.. code-block:: python + + from dpaycli.account import Account + account = Account("test") + print(account.balances) + for h in account.history(): + print(h) + +A single vote + +.. code-block:: python + + from dpaycli.vote import Vote + vote = Vote(u"@gtg/ffdhu-gtg-witness-log|gandalf") + print(vote.json()) + +All votes from an account + +.. code-block:: python + + from dpaycli.vote import AccountVotes + allVotes = AccountVotes("gtg") + +Access a post + +.. code-block:: python + + from dpaycli.comment import Comment + comment = Comment("@gtg/ffdhu-gtg-witness-log") + print(comment["active_votes"]) + +Access the market + +.. code-block:: python + + from dpaycli.market import Market + market = Market("BBD:BEX") + print(market.ticker()) + +Access a witness + +.. code-block:: python + + from dpaycli.witness import Witness + witness = Witness("gtg") + print(witness.is_active) + +Sending transaction to the blockchain +------------------------------------- + +Sending a Transfer + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + account = Account("test", dpay_instance=dpay) + account.transfer("null", 1, "BBD", "test") + +Upvote a post + +.. code-block:: python + + from dpaycli.comment import Comment + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + comment = Comment("@gtg/ffdhu-gtg-witness-log", dpay_instance=dpay) + comment.upvote(weight=10, voter="test") + +Publish a post to the blockchain + +.. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("wallet-passphrase") + dpay.post("title", "body", author="test", tags=["a", "b", "c", "d", "e"], self_vote=True) + +Sell BEX on the market + +.. code-block:: python + + from dpaycli.market import Market + from dpaycli import DPay + dpay.wallet.unlock("wallet-passphrase") + market = Market("BBD:BEX", dpay_instance=dpay) + print(market.ticker()) + market.dpay.wallet.unlock("wallet-passphrase") + print(market.sell(300, 100)) # sell 100 BEX for 300 BEX/BBD diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100755 index 0000000..fed1bfa --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,19 @@ +autobahn>=0.14 +future +ecdsa +requests +websocket-client +pytz +pycryptodomex>=3.4.6 +scrypt>=0.7.1 +Events>=0.2.2 +pyyaml +pytest +pytest-mock +coverage +mock +appdirs +Click +prettytable +sphinx_rtd_theme +sphinx-click diff --git a/docs/support.rst b/docs/support.rst new file mode 100755 index 0000000..e0e3f1d --- /dev/null +++ b/docs/support.rst @@ -0,0 +1,7 @@ +********************* +Support and Questions +********************* + +Help and discussion channel for dpaycli can be found here: + +* https://discord.gg/4HM592V diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100755 index 0000000..3dcac74 --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,319 @@ +********* +Tutorials +********* + +Bundle Many Operations +---------------------- + +With DPay, you can bundle multiple operations into a single +transactions. This can be used to do a multi-send (one sender, multiple +receivers), but it also allows to use any other kind of operation. The +advantage here is that the user can be sure that the operations are +executed in the same order as they are added to the transaction. + +A block can only include one vote operation and +one comment operation from each sender. + +.. code-block:: python + + from pprint import pprint + from dpaycli import DPay + from dpaycli.account import Account + from dpaycli.comment import Comment + from dpaycli.instance import set_shared_dpay_instance + + # not a real working key + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + stm = DPay( + bundle=True, # Enable bundle broadcast + # nobroadcast=True, # Enable this for testing + keys=[wif], + ) + # Set stm as shared instance + set_shared_dpay_instance(stm) + + # Account and Comment will use now stm + account = Account("test") + + # Post + c = Comment("@gtg/witness-gtg-log") + + account.transfer("test1", 1, "BEX") + account.transfer("test2", 1, "BEX") + account.transfer("test3", 1, "BBD") + # Upvote post with 25% + c.upvote(25, voter=account) + + pprint(stm.broadcast()) + + +Use nobroadcast for testing +--------------------------- + +When using `nobroadcast=True` the transaction is not broadcasted but printed. + +.. code-block:: python + + from pprint import pprint + from dpaycli import DPay + from dpaycli.account import Account + from dpaycli.instance import set_shared_dpay_instance + + # Only for testing not a real working key + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + # set nobroadcast always to True, when testing + testnet = DPay( + nobroadcast=True, # Set to false when want to go live + keys=[wif], + ) + # Set testnet as shared instance + set_shared_dpay_instance(testnet) + + # Account will use now testnet + account = Account("test") + + pprint(account.transfer("test1", 1, "BEX")) + +When executing the script above, the output will be similar to the following: + +.. code-block:: js + + Not broadcasting anything! + {'expiration': '2018-05-01T16:16:57', + 'extensions': [], + 'operations': [['transfer', + {'amount': '1.000 BEX', + 'from': 'test', + 'memo': '', + 'to': 'test1'}]], + 'ref_block_num': 33020, + 'ref_block_prefix': 2523628005, + 'signatures': ['1f57da50f241e70c229ed67b5d61898e792175c0f18ae29df8af414c46ae91eb5729c867b5d7dcc578368e7024e414c237f644629cb0aa3ecafac3640871ffe785']} + +Clear BlockchainObject Caching +------------------------------ + +Each BlockchainObject (Account, Comment, Vote, Witness, Amount, ...) has a glocal cache. This cache +stores all objects and could lead to increased memory consumption. The global cache can be cleared +with a `clear_cache()` call from any BlockchainObject. + +.. code-block:: python + + from pprint import pprint + from dpaycli.account import Account + + account = Account("test") + pprint(str(account._cache)) + account1 = Account("test1") + pprint(str(account._cache)) + pprint(str(account1._cache)) + account.clear_cache() + pprint(str(account._cache)) + pprint(str(account1._cache)) + +Simple Sell Script +------------------ + +.. code-block:: python + + from dpaycli import DPay + from dpaycli.market import Market + from dpaycli.price import Price + from dpaycli.amount import Amount + + # Only for testing not a real working key + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + # + # Instantiate DPay (pick network via API node) + # + dpay = DPay( + nobroadcast=True, # <<--- set this to False when you want to fire! + keys=[wif] # <<--- use your real keys, when going live! + ) + + # + # This defines the market we are looking at. + # The first asset in the first argument is the *quote* + # Sell and buy calls always refer to the *quote* + # + market = Market("BBD:BEX", + dpay_instance=dpay + ) + + # + # Sell an asset for a price with amount (quote) + # + print(market.sell( + Price(100.0, "BEX/BBD"), + Amount("0.01 BBD") + )) + + +Sell at a timely rate +--------------------- + +.. code-block:: python + + import threading + from dpaycli import DPay + from dpaycli.market import Market + from dpaycli.price import Price + from dpaycli.amount import Amount + + # Only for testing not a real working key + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + def sell(): + """ Sell an asset for a price with amount (quote) + """ + print(market.sell( + Price(100.0, "BBD/BEX"), + Amount("0.01 BEX") + )) + + threading.Timer(60, sell).start() + + + if __name__ == "__main__": + # + # Instantiate DPay (pick network via API node) + # + dpay = DPay( + nobroadcast=True, # <<--- set this to False when you want to fire! + keys=[wif] # <<--- use your real keys, when going live! + ) + + # + # This defines the market we are looking at. + # The first asset in the first argument is the *quote* + # Sell and buy calls always refer to the *quote* + # + market = Market("BEX:BBD", + dpay_instance=dpay + ) + + sell() + +Batch api calls on AppBase +-------------------------- + +Batch api calls are possible with AppBase RPC nodes. +If you call a Api-Call with add_to_queue=True it is not submitted but stored in rpc_queue. +When a call with add_to_queue=False (default setting) is started, +the complete queue is sended at once to the node. The result is a list with replies. + +.. code-block:: python + + from dpaycli import DPay + stm = DPay("https://api.dpays.io") + stm.rpc.get_config(add_to_queue=True) + stm.rpc.rpc_queue + +.. code-block:: python + + [{'method': 'condenser_api.get_config', 'jsonrpc': '2.0', 'params': [], 'id': 6}] + +.. code-block:: python + + result = stm.rpc.get_block({"block_num":1}, api="block", add_to_queue=False) + len(result) + +.. code-block:: python + + 2 + + +Account history +--------------- +Lets calculate the curation reward from the last 7 days: + +.. code-block:: python + + from datetime import datetime, timedelta + from dpaycli.account import Account + from dpaycli.amount import Amount + + acc = Account("gtg") + stop = datetime.utcnow() - timedelta(days=7) + reward_vests = Amount("0 VESTS") + for reward in acc.history_reverse(stop=stop, only_ops=["curation_reward"]): + reward_vests += Amount(reward['reward']) + curation_rewards_SP = acc.dpay.vests_to_sp(reward_vests.amount) + print("Rewards are %.3f BP" % curation_rewards_SP) + +Lets display all Posts from an account: + +.. code-block:: python + + from dpaycli.account import Account + from dpaycli.comment import Comment + from dpaycli.exceptions import ContentDoesNotExistsException + account = Account("holger80") + c_list = {} + for c in map(Comment, account.history(only_ops=["comment"])): + if c.permlink in c_list: + continue + try: + c.refresh() + except ContentDoesNotExistsException: + continue + c_list[c.permlink] = 1 + if not c.is_comment(): + print("%s " % c.title) + +Transactionbuilder +------------------ +Sign transactions with dpaycli without using the wallet and build the transaction by hand. +Example with one operation with and without the wallet: + +.. code-block:: python + + from dpaycli import DPay + from dpaycli.transactionbuilder import TransactionBuilder + from dpayclibase import operations + stm = DPay() + # Uncomment the following when using a wallet: + # stm.wallet.unlock("secret_password") + tx = TransactionBuilder(dpay_instance=stm) + op = operations.Transfer(**{"from": 'user_a', + "to": 'user_b', + "amount": '1.000 BBD', + "memo": 'test 2'})) + tx.appendOps(op) + # Comment appendWif out and uncomment appendSigner when using a stored key from the wallet + tx.appendWif('5.....') # `user_a` + # tx.appendSigner('user_a', 'active') + tx.sign() + tx.broadcast() + +Example with signing and broadcasting two operations: + +.. code-block:: python + + from dpaycli import DPay + from dpaycli.transactionbuilder import TransactionBuilder + from dpayclibase import operations + stm = DPay() + # Uncomment the following when using a wallet: + # stm.wallet.unlock("secret_password") + tx = TransactionBuilder(dpay_instance=stm) + ops = [] + op = operations.Transfer(**{"from": 'user_a', + "to": 'user_b', + "amount": '1.000 BBD', + "memo": 'test 2'})) + ops.append(op) + op = operations.Vote(**{"voter": v, + "author": author, + "permlink": permlink, + "weight": int(percent * 100)}) + ops.append(op) + tx.appendOps(ops) + # Comment appendWif out and uncomment appendSigner when using a stored key from the wallet + tx.appendWif('5.....') # `user_a` + # tx.appendSigner('user_a', 'active') + tx.sign() + tx.broadcast() diff --git a/dpay-onedir.spec b/dpay-onedir.spec new file mode 100755 index 0000000..09cc486 --- /dev/null +++ b/dpay-onedir.spec @@ -0,0 +1,64 @@ +# -*- mode: python -*- + +import os +import glob +import platform +from PyInstaller.utils.hooks import exec_statement +import websocket +from os.path import join, dirname, basename + +block_cipher = None +os_name = platform.system() +binaries = [] + +websocket_lib_path = dirname(websocket.__file__) +websocket_cacert_file_path = join(websocket_lib_path, 'cacert.pem') +analysis_data = [ + # For websocket library to find "cacert.pem" file, it must be in websocket + # directory inside of distribution directory. + # This line can be removed once PyInstaller adds hook-websocket.py + (websocket_cacert_file_path, join('.', basename(websocket_lib_path))) +] + +a = Analysis(['dpaycli/cli.py'], + pathex=['dpaycli'], + binaries=binaries, + datas=analysis_data, + hiddenimports=['scrypt', 'websocket'], + hookspath=[], + runtime_hooks=[], + excludes=['matplotlib', 'scipy', 'pandas', 'numpy', 'PyQt5', 'tkinter'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name='dpay', + debug=False, + strip=False, + upx=False, + console=True, + icon='dpay.ico', +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + name='dpay', + strip=False, + upx=False +) + +if platform.system() == 'Darwin': + info_plist = {'NSHighResolutionCapable': 'True', 'NSPrincipalClass': 'NSApplication'} + app = BUNDLE(exe, + name='dpay.app', + icon='dpay.ico', + bundle_identifier=None + ) \ No newline at end of file diff --git a/dpay-onefile.spec b/dpay-onefile.spec new file mode 100755 index 0000000..fa81554 --- /dev/null +++ b/dpay-onefile.spec @@ -0,0 +1,57 @@ +# -*- mode: python -*- + +import os +import glob +import platform +from PyInstaller.utils.hooks import exec_statement +import websocket +from os.path import join, dirname, basename + +block_cipher = None +os_name = platform.system() +binaries = [] + +websocket_lib_path = dirname(websocket.__file__) +websocket_cacert_file_path = join(websocket_lib_path, 'cacert.pem') +analysis_data = [ + # For websocket library to find "cacert.pem" file, it must be in websocket + # directory inside of distribution directory. + # This line can be removed once PyInstaller adds hook-websocket.py + (websocket_cacert_file_path, join('.', basename(websocket_lib_path))) +] + + +a = Analysis(['dpaycli/cli.py'], + pathex=['dpaycli'], + binaries=binaries, + datas=analysis_data, + hiddenimports=['scrypt', 'websocket'], + hookspath=[], + runtime_hooks=[], + excludes=['matplotlib', 'scipy', 'pandas', 'numpy', 'PyQt5', 'tkinter'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='dpay', + debug=False, + strip=False, + upx=False, + runtime_tmpdir=None, + console=True, + icon='dpay.ico', + ) + +if platform.system() == 'Darwin': + info_plist = {'NSHighResolutionCapable': 'True', 'NSPrincipalClass': 'NSApplication'} + app = BUNDLE(exe, + name='dpay.app', + icon='dpay.ico', + bundle_identifier=None + ) \ No newline at end of file diff --git a/dpay.ico b/dpay.ico new file mode 100644 index 0000000000000000000000000000000000000000..9f200aec82aacc5d4b6973adf7481f7aedcad93e GIT binary patch literal 4286 zcmeI$>rY#C9LMqFf8Z0B?AhEC^&c>CiDI%SYDPhJZ%T16xrFw#+qlT)L>!2_$lS#! zW7*;oBRGakon*;|OafB}juu9_6e#7k(iNm@>GhnqztSg$r-fn+r};KHrw?AA-}n6b zJHH%BdPaP{H$Qp2;7v`Lbr7e$Fn((9sP*Ow3R+sY#?Esc%Z=)r$=4jjnNV*j3< z>o!NtCIk0dE}>bc|A}lUKy`Oh|A}#3dLSF}!E0g3@(sm>x};}V_Wk`tXKyiay9({z z%%o;m_Jf0{o>u&izNX{5GBldxtru~>A9e8#5mzG%52~p@ou9NG#Qi>0U%SZAgx7W! zcTXiHCT&tO#C-?ZK+_Ss~ny7-8Ih;_bm|a5`F`U?;4)rYFz!tX#dGX zdb(y!GsuQK2nj#ZDeUV+eOK{6InV4sIWx8osQ)r234X8eJ5U47MEzHY1ir#GdYtK@ zQYMFsDJ$8xygr)sDV(i-lbXsm&}Y4~D!Jn0e<=LlP(v-kzbyQVcs(^tjuhjxA7;QR z(^&ixeU+OTy7@ArohjJvZ{zct?Fnay`+q_79%}SQqDz;E1TPQ@ond;Sgo!aT4u=VA z=Pv#{x(Un0=b60w7UTD}WAA#6Lx#5#${-uX{SWsDza13e2V(v?!r@cQFMNn<21) zWNfmCKbl{q`^0mMf1kqi?HxGVQn^~2x=Q}9sEX)+=rZBp7lgu}5eR>bV@hUpstEgZ zAtN&dbk@B&tFBvq{?pOuCTa3R=_&=aTgdeIE{-*?^AK_j+jD1Gfv-u1^ z&SSu#r=xZ=Q`b_NzpReKW}e{5go9J?j_JHl@gCH(Mdf{_yhqQ_WLjxh7IkiofJ zY)^8qc@8izpUv-WuP|4a!Os<03Hzm`g@l$`g?$~Bdcu)Y!mkv5x$qC+jS7F+eUC>A z+4TRNMc<;1-W%I!J-RyUli8eu)p{O9ts|nG6uqw?pp@ZPP59IToc@@tkK0}y)~8x} zJ-cYAPVD}#+!t%LnHY`xFv@#G?H08YgPci`oI#%9Uu8@uPn0fuZJ!S-^KG0bZ*TU0 M==I9?0m(l92TEM4eE>> from dpaycli.account import Account + >>> account = Account("gtg") + >>> print(account) + + >>> print(account.balances) # doctest: +SKIP + + .. note:: This class comes with its own caching function to reduce the + load on the API server. Instances of this class can be + refreshed with ``Account.refresh()``. The cache can be + cleared with ``Account.clear_cache()`` + + """ + + type_id = 2 + + def __init__( + self, + account, + full=True, + lazy=False, + dpay_instance=None + ): + """Initialize an account + + :param str account: Name of the account + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :param bool lazy: Use lazy loading + :param bool full: Obtain all account data including orders, positions, + etc. + """ + self.full = full + self.lazy = lazy + self.dpay = dpay_instance or shared_dpay_instance() + if isinstance(account, dict): + account = self._parse_json_data(account) + super(Account, self).__init__( + account, + lazy=lazy, + full=full, + id_item="name", + dpay_instance=dpay_instance + ) + + def refresh(self): + """ Refresh/Obtain an account's data from the API server + """ + if not self.dpay.is_connected(): + return + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + account = self.dpay.rpc.find_accounts({'accounts': [self.identifier]}, api="database") + else: + if self.full: + account = self.dpay.rpc.get_accounts( + [self.identifier], api="database") + else: + account = self.dpay.rpc.lookup_account_names( + [self.identifier], api="database") + if self.dpay.rpc.get_use_appbase() and "accounts" in account: + account = account["accounts"] + if account and isinstance(account, list) and len(account) == 1: + account = account[0] + if not account: + raise AccountDoesNotExistsException(self.identifier) + account = self._parse_json_data(account) + self.identifier = account["name"] + # self.dpay.refresh_data() + + super(Account, self).__init__(account, id_item="name", lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + + def _parse_json_data(self, account): + parse_int = [ + "bbd_seconds", "savings_bbd_seconds", "average_bandwidth", "lifetime_bandwidth", "lifetime_market_bandwidth", "reputation", "withdrawn", "to_withdraw", + ] + for p in parse_int: + if p in account and isinstance(account.get(p), string_types): + account[p] = int(account.get(p, 0)) + if "proxied_vsf_votes" in account: + proxied_vsf_votes = [] + for p_int in account["proxied_vsf_votes"]: + if isinstance(p_int, string_types): + proxied_vsf_votes.append(int(p_int)) + else: + proxied_vsf_votes.append(p_int) + account["proxied_vsf_votes"] = proxied_vsf_votes + parse_times = [ + "last_owner_update", "last_account_update", "created", "last_owner_proved", "last_active_proved", + "last_account_recovery", "last_vote_time", "bbd_seconds_last_update", "bbd_last_interest_payment", + "savings_bbd_seconds_last_update", "savings_bbd_last_interest_payment", "next_vesting_withdrawal", + "last_market_bandwidth_update", "last_post", "last_root_post", "last_bandwidth_update" + ] + for p in parse_times: + if p in account and isinstance(account.get(p), string_types): + account[p] = formatTimeString(account.get(p, "1970-01-01T00:00:00")) + # Parse Amounts + amounts = [ + "balance", + "savings_balance", + "bbd_balance", + "savings_bbd_balance", + "reward_bbd_balance", + "reward_dpay_balance", + "reward_vesting_balance", + "reward_vesting_dpay", + "vesting_shares", + "delegated_vesting_shares", + "received_vesting_shares", + "vesting_withdraw_rate", + "vesting_balance", + ] + for p in amounts: + if p in account and isinstance(account.get(p), (string_types, list, dict)): + account[p] = Amount(account[p], dpay_instance=self.dpay) + return account + + def json(self): + output = self.copy() + parse_int = [ + "bbd_seconds", "savings_bbd_seconds", + ] + parse_int_without_zero = [ + "withdrawn", "to_withdraw", "lifetime_bandwidth", 'average_bandwidth', + ] + for p in parse_int: + if p in output and isinstance(output[p], integer_types): + output[p] = str(output[p]) + for p in parse_int_without_zero: + if p in output and isinstance(output[p], integer_types) and output[p] != 0: + output[p] = str(output[p]) + if "proxied_vsf_votes" in output: + proxied_vsf_votes = [] + for p_int in output["proxied_vsf_votes"]: + if isinstance(p_int, integer_types) and p_int != 0: + proxied_vsf_votes.append(str(p_int)) + else: + proxied_vsf_votes.append(p_int) + output["proxied_vsf_votes"] = proxied_vsf_votes + parse_times = [ + "last_owner_update", "last_account_update", "created", "last_owner_proved", "last_active_proved", + "last_account_recovery", "last_vote_time", "bbd_seconds_last_update", "bbd_last_interest_payment", + "savings_bbd_seconds_last_update", "savings_bbd_last_interest_payment", "next_vesting_withdrawal", + "last_market_bandwidth_update", "last_post", "last_root_post", "last_bandwidth_update" + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date, time)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + amounts = [ + "balance", + "savings_balance", + "bbd_balance", + "savings_bbd_balance", + "reward_bbd_balance", + "reward_dpay_balance", + "reward_vesting_balance", + "reward_vesting_dpay", + "vesting_shares", + "delegated_vesting_shares", + "received_vesting_shares", + "vesting_withdraw_rate", + "vesting_balance", + ] + for p in amounts: + if p in output: + if p in output: + output[p] = output.get(p).json() + return json.loads(str(json.dumps(output))) + + def getSimilarAccountNames(self, limit=5): + """Deprecated, please use get_similar_account_names""" + return self.get_similar_account_names(limit=limit) + + def get_rc(self): + """Return RC of account""" + b = Blockchain(dpay_instance=self.dpay) + return b.find_rc_accounts(self["name"]) + + def get_rc_manabar(self): + """Returns current_mana and max_mana for RC""" + rc_param = self.get_rc() + max_mana = int(rc_param["max_rc"]) + last_mana = int(rc_param["rc_manabar"]["current_mana"]) + last_update_time = rc_param["rc_manabar"]["last_update_time"] + last_update = datetime.utcfromtimestamp(last_update_time) + diff_in_seconds = (datetime.utcnow() - last_update).total_seconds() + current_mana = int(last_mana + diff_in_seconds * max_mana / DPAY_VOTING_MANA_REGENERATION_SECONDS) + if current_mana > max_mana: + current_mana = max_mana + if max_mana > 0: + current_pct = current_mana / max_mana * 100 + else: + current_pct = 0 + max_rc_creation_adjustment = Amount(rc_param["max_rc_creation_adjustment"], dpay_instance=self.dpay) + return {"last_mana": last_mana, "last_update_time": last_update_time, "current_mana": current_mana, + "max_mana": max_mana, "current_pct": current_pct, "max_rc_creation_adjustment": max_rc_creation_adjustment} + + def get_similar_account_names(self, limit=5): + """ Returns ``limit`` account names similar to the current account + name as a list + + :param int limit: limits the number of accounts, which will be + returned + :returns: Similar account names as list + :rtype: list + + This is a wrapper around ``Blockchain.get_similar_account_names()`` + using the current account name as reference. + + """ + b = Blockchain(dpay_instance=self.dpay) + return b.get_similar_account_names(self.name, limit=limit) + + @property + def name(self): + """ Returns the account name + """ + return self["name"] + + @property + def profile(self): + """ Returns the account profile + """ + metadata = self.json_metadata + if "profile" in metadata: + return metadata["profile"] + else: + return {} + + @property + def rep(self): + """ Returns the account reputation + """ + return self.get_reputation() + + @property + def bp(self): + """ Returns the accounts DPay Power + """ + return self.get_dpay_power() + + @property + def vp(self): + """ Returns the account voting power in the range of 0-100% + """ + return self.get_voting_power() + + @property + def json_metadata(self): + if self["json_metadata"] == '': + return {} + return json.loads(self["json_metadata"]) + + def print_info(self, force_refresh=False, return_str=False, use_table=False, **kwargs): + """ Prints import information about the account + """ + if force_refresh: + self.refresh() + self.dpay.refresh_data(True) + bandwidth = self.get_bandwidth() + if bandwidth["allocated"] > 0: + remaining = 100 - bandwidth["used"] / bandwidth["allocated"] * 100 + used_kb = bandwidth["used"] / 1024 + allocated_mb = bandwidth["allocated"] / 1024 / 1024 + last_vote_time_str = formatTimedelta(addTzInfo(datetime.utcnow()) - self["last_vote_time"]) + try: + rc_mana = self.get_rc_manabar() + rc = self.get_rc() + rc_calc = RC(dpay_instance=self.dpay) + except: + rc_mana = None + rc_calc = None + + if use_table: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + t.add_row(["Name (rep)", self.name + " (%.2f)" % (self.rep)]) + t.add_row(["Voting Power", "%.2f %%, " % (self.get_voting_power())]) + t.add_row(["Vote Value", "%.2f $" % (self.get_voting_value_BBD())]) + t.add_row(["Last vote", "%s ago" % last_vote_time_str]) + t.add_row(["Full in ", "%s" % (self.get_recharge_time_str())]) + t.add_row(["DPay Power", "%.2f %s" % (self.get_dpay_power(), self.dpay.dpay_symbol)]) + t.add_row(["Balance", "%s, %s" % (str(self.balances["available"][0]), str(self.balances["available"][1]))]) + if False and bandwidth["allocated"] > 0: + t.add_row(["Remaining Bandwidth", "%.2f %%" % (remaining)]) + t.add_row(["used/allocated Bandwidth", "(%.0f kb of %.0f mb)" % (used_kb, allocated_mb)]) + if rc_mana is not None: + estimated_rc = int(rc["max_rc"]) * rc_mana["current_pct"] / 100 + t.add_row(["Remaining RC", "%.2f %%" % (rc_mana["current_pct"])]) + t.add_row(["Remaining RC", "(%.0f G RC of %.0f G RC)" % (estimated_rc / 10**9, int(rc["max_rc"]) / 10**9)]) + t.add_row(["Full in ", "%s" % (self.get_manabar_recharge_time_str(rc_mana))]) + t.add_row(["Est. RC for a comment", "%.2f G RC" % (rc_calc.comment() / 10**9)]) + t.add_row(["Est. RC for a vote", "%.2f G RC" % (rc_calc.vote() / 10**9)]) + t.add_row(["Est. RC for a transfer", "%.2f G RC" % (rc_calc.transfer() / 10**9)]) + t.add_row(["Est. RC for a custom_json", "%.2f G RC" % (rc_calc.custom_json() / 10**9)]) + + t.add_row(["Comments with current RC", "%d comments" % (int(estimated_rc / rc_calc.comment()))]) + t.add_row(["Votes with current RC", "%d votes" % (int(estimated_rc / rc_calc.vote()))]) + t.add_row(["Transfer with current RC", "%d transfers" % (int(estimated_rc / rc_calc.transfer()))]) + t.add_row(["Custom_json with current RC", "%d transfers" % (int(estimated_rc / rc_calc.custom_json()))]) + + if return_str: + return t.get_string(**kwargs) + else: + print(t.get_string(**kwargs)) + else: + ret = self.name + " (%.2f) \n" % (self.rep) + ret += "--- Voting Power ---\n" + ret += "%.2f %%, " % (self.get_voting_power()) + ret += " VP = %.2f $\n" % (self.get_voting_value_BBD()) + ret += "full in %s \n" % (self.get_recharge_time_str()) + ret += "--- Balance ---\n" + ret += "%.2f BP, " % (self.get_dpay_power()) + ret += "%s, %s\n" % (str(self.balances["available"][0]), str(self.balances["available"][1])) + if False and bandwidth["allocated"] > 0: + ret += "--- Bandwidth ---\n" + ret += "Remaining: %.2f %%" % (remaining) + ret += " (%.0f kb of %.0f mb)\n" % (used_kb, allocated_mb) + if rc_mana is not None: + estimated_rc = int(rc["max_rc"]) * rc_mana["current_pct"] / 100 + ret += "--- RC manabar ---\n" + ret += "Remaining: %.2f %%" % (rc_mana["current_pct"]) + ret += " (%.0f G RC of %.0f G RC)\n" % (estimated_rc / 10**9, int(rc["max_rc"]) / 10**9) + ret += "full in %s\n" % (self.get_manabar_recharge_time_str(rc_mana)) + ret += "--- Approx Costs ---\n" + ret += "comment - %.2f G RC - enough RC for %d comments\n" % (rc_calc.comment() / 10**9, int(estimated_rc / rc_calc.comment())) + ret += "vote - %.2f G RC - enough RC for %d votes\n" % (rc_calc.vote() / 10**9, int(estimated_rc / rc_calc.vote())) + ret += "transfer - %.2f G RC - enough RC for %d transfers\n" % (rc_calc.transfer() / 10**9, int(estimated_rc / rc_calc.transfer())) + ret += "custom_json - %.2f G RC - enough RC for %d custom_json\n" % (rc_calc.custom_json() / 10**9, int(estimated_rc / rc_calc.custom_json())) + if return_str: + return ret + print(ret) + + def get_reputation(self): + """ Returns the account reputation in the (dsocial) normalized form + """ + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + rep = self.dpay.rpc.get_account_reputations({'account_lower_bound': self["name"], 'limit': 1}, api="follow")['reputations'] + if len(rep) > 0: + rep = int(rep[0]['reputation']) + else: + rep = int(self['reputation']) + return reputation_to_score(rep) + + def get_manabar(self): + """"Return manabar""" + max_mana = self.get_effective_vesting_shares() + if max_mana == 0: + props = self.dpay.get_chain_properties() + required_fee_dpay = Amount(props["account_creation_fee"], dpay_instance=self.dpay) + max_mana = int(self.dpay.bp_to_vests(required_fee_dpay)) + last_mana = int(self["voting_manabar"]["current_mana"]) + last_update_time = self["voting_manabar"]["last_update_time"] + last_update = datetime.utcfromtimestamp(last_update_time) + diff_in_seconds = (addTzInfo(datetime.utcnow()) - addTzInfo(last_update)).total_seconds() + current_mana = int(last_mana + diff_in_seconds * max_mana / DPAY_VOTING_MANA_REGENERATION_SECONDS) + if current_mana > max_mana: + current_mana = max_mana + if max_mana > 0: + current_mana_pct = current_mana / max_mana * 100 + else: + current_mana_pct = 0 + return {"last_mana": last_mana, "last_update_time": last_update_time, + "current_mana": current_mana, "max_mana": max_mana, "current_mana_pct": current_mana_pct} + + def get_voting_power(self, with_regeneration=True): + """ Returns the account voting power in the range of 0-100% + """ + if "voting_manabar" in self: + manabar = self.get_manabar() + if with_regeneration: + total_vp = manabar["current_mana_pct"] + else: + if manabar["max_mana"] > 0: + total_vp = manabar["last_mana"] / manabar["max_mana"] * 100 + else: + total_vp = 0 + elif "voting_power" in self: + if with_regeneration: + last_vote_time = self["last_vote_time"] + diff_in_seconds = (addTzInfo(datetime.utcnow()) - (last_vote_time)).total_seconds() + regenerated_vp = diff_in_seconds * DPAY_100_PERCENT / DPAY_VOTE_REGENERATION_SECONDS / 100 + else: + regenerated_vp = 0 + total_vp = (self["voting_power"] / 100 + regenerated_vp) + if total_vp > 100: + return 100 + if total_vp < 0: + return 0 + return total_vp + + def get_vests(self, only_own_vests=False): + """ Returns the account vests + """ + vests = (self["vesting_shares"]) + if not only_own_vests and "delegated_vesting_shares" in self and "received_vesting_shares" in self: + vests = vests - (self["delegated_vesting_shares"]) + (self["received_vesting_shares"]) + + return vests + + def get_effective_vesting_shares(self): + """Returns the effective vesting shares""" + vesting_shares = int(self["vesting_shares"]) + if "delegated_vesting_shares" in self and "received_vesting_shares" in self: + vesting_shares = vesting_shares - int(self["delegated_vesting_shares"]) + int(self["received_vesting_shares"]) + timestamp = (self["next_vesting_withdrawal"] - addTzInfo(datetime(1970, 1, 1))).total_seconds() + if timestamp > 0 and "vesting_withdraw_rate" in self and "to_withdraw" in self and "withdrawn" in self: + vesting_shares -= min(int(self["vesting_withdraw_rate"]), int(self["to_withdraw"]) - int(self["withdrawn"])) + return vesting_shares + + def get_dpay_power(self, onlyOwnSP=False): + """ Returns the account dpay power + """ + return self.dpay.vests_to_sp(self.get_vests(only_own_vests=onlyOwnSP)) + + def get_voting_value_BBD(self, voting_weight=100, voting_power=None, dpay_power=None, not_broadcasted_vote=True): + """ Returns the account voting value in BBD + """ + if voting_power is None: + voting_power = self.get_voting_power() + if dpay_power is None: + bp = self.get_dpay_power() + else: + bp = dpay_power + + VoteValue = self.dpay.bp_to_bbd(bp, voting_power=voting_power * 100, vote_pct=voting_weight * 100, not_broadcasted_vote=not_broadcasted_vote) + return VoteValue + + def get_vote_pct_for_BBD(self, bbd, voting_power=None, dpay_power=None, not_broadcasted_vote=True): + """ Returns the voting percentage needed to have a vote worth a given number of BBD. + + If the returned number is bigger than 10000 or smaller than -10000, + the given BBD value is too high for that account + + :param str/int/Amount bbd: The amount of BBD in vote value + + """ + if voting_power is None: + voting_power = self.get_voting_power() + if dpay_power is None: + dpay_power = self.get_dpay_power() + + if isinstance(bbd, Amount): + bbd = Amount(bbd, dpay_instance=self.dpay) + elif isinstance(bbd, string_types): + bbd = Amount(bbd, dpay_instance=self.dpay) + else: + bbd = Amount(bbd, 'BBD', dpay_instance=self.dpay) + if bbd['symbol'] != 'BBD': + raise AssertionError('Should input BBD, not any other asset!') + + vote_pct = self.dpay.rshares_to_vote_pct(self.dpay.bbd_to_rshares(bbd, not_broadcasted_vote=not_broadcasted_vote), voting_power=voting_power * 100, dpay_power=dpay_power) + return vote_pct + + def get_creator(self): + """ Returns the account creator or `None` if the account was mined + """ + if self['mined']: + return None + ops = list(self.get_account_history(0, 0)) + if not ops or 'creator' not in ops[0]: + return None + return ops[0]['creator'] + + def get_recharge_time_str(self, voting_power_goal=100, starting_voting_power=None): + """ Returns the account recharge time as string + + :param float voting_power_goal: voting power goal in percentage (default is 100) + :param float starting_voting_power: returns recharge time if current voting power is + the provided value. + + """ + remainingTime = self.get_recharge_timedelta(voting_power_goal=voting_power_goal, starting_voting_power=starting_voting_power) + return formatTimedelta(remainingTime) + + def get_recharge_timedelta(self, voting_power_goal=100, starting_voting_power=None): + """ Returns the account voting power recharge time as timedelta object + + :param float voting_power_goal: voting power goal in percentage (default is 100) + :param float starting_voting_power: returns recharge time if current voting power is + the provided value. + + """ + if starting_voting_power is None: + missing_vp = voting_power_goal - self.get_voting_power() + elif isinstance(starting_voting_power, int) or isinstance(starting_voting_power, float): + missing_vp = voting_power_goal - starting_voting_power + else: + raise ValueError('starting_voting_power must be a number.') + if missing_vp < 0: + return 0 + recharge_seconds = missing_vp * 100 * DPAY_VOTING_MANA_REGENERATION_SECONDS / DPAY_100_PERCENT + return timedelta(seconds=recharge_seconds) + + def get_recharge_time(self, voting_power_goal=100, starting_voting_power=None): + """ Returns the account voting power recharge time in minutes + + :param float voting_power_goal: voting power goal in percentage (default is 100) + :param float starting_voting_power: returns recharge time if current voting power is + the provided value. + + """ + return addTzInfo(datetime.utcnow()) + self.get_recharge_timedelta(voting_power_goal, starting_voting_power) + + def get_manabar_recharge_time_str(self, manabar, recharge_pct_goal=100): + """ Returns the account manabar recharge time as string + + :param dict manabar: manabar dict from get_manabar() or get_rc_manabar() + :param float recharge_pct_goal: mana recovery goal in percentage (default is 100) + + """ + remainingTime = self.get_manabar_recharge_timedelta(manabar, recharge_pct_goal=recharge_pct_goal) + return formatTimedelta(remainingTime) + + def get_manabar_recharge_timedelta(self, manabar, recharge_pct_goal=100): + """ Returns the account mana recharge time as timedelta object + + :param dict manabar: manabar dict from get_manabar() or get_rc_manabar() + :param float recharge_pct_goal: mana recovery goal in percentage (default is 100) + + """ + if "current_mana_pct" in manabar: + missing_rc_pct = recharge_pct_goal - manabar["current_mana_pct"] + else: + missing_rc_pct = recharge_pct_goal - manabar["current_pct"] + if missing_rc_pct < 0: + return 0 + recharge_seconds = missing_rc_pct * 100 * DPAY_VOTING_MANA_REGENERATION_SECONDS / DPAY_100_PERCENT + return timedelta(seconds=recharge_seconds) + + def get_manabar_recharge_time(self, manabar, recharge_pct_goal=100): + """ Returns the account mana recharge time in minutes + + :param dict manabar: manabar dict from get_manabar() or get_rc_manabar() + :param float recharge_pct_goal: mana recovery goal in percentage (default is 100) + + """ + return addTzInfo(datetime.utcnow()) + self.get_manabar_recharge_timedelta(manabar, recharge_pct_goal) + + def get_feed(self, start_entry_id=0, limit=100, raw_data=False, short_entries=False, account=None): + """ Returns a list of items in an account’s feed + + :param int start_entry_id: default is 0 + :param int limit: default is 100 + :param bool raw_data: default is False + :param bool short_entries: when set to True and raw_data is True, get_feed_entries is used istead of get_feed + :param str account: When set, a different account name is used (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> account.get_feed(0, 1, raw_data=True) + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if raw_data and short_entries and self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_feed_entries({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["feed"] + ] + elif raw_data and short_entries and not self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_feed_entries(account, start_entry_id, limit, api='follow') + ] + elif raw_data and self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_feed({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["feed"] + ] + elif raw_data and not self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_feed(account, start_entry_id, limit, api='follow') + ] + elif not raw_data and self.dpay.rpc.get_use_appbase(): + from .comment import Comment + return [ + Comment(c['comment'], dpay_instance=self.dpay) for c in self.dpay.rpc.get_feed({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["feed"] + ] + else: + from .comment import Comment + return [ + Comment(c['comment'], dpay_instance=self.dpay) for c in self.dpay.rpc.get_feed(account, start_entry_id, limit, api='follow') + ] + + def get_feed_entries(self, start_entry_id=0, limit=100, raw_data=True, + account=None): + """ Returns a list of entries in an account’s feed + + :param int start_entry_id: default is 0 + :param int limit: default is 100 + :param bool raw_data: default is False + :param bool short_entries: when set to True and raw_data is True, get_feed_entries is used istead of get_feed + :param str account: When set, a different account name is used (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> account.get_feed_entries(0, 1) + [] + + """ + return self.get_feed(start_entry_id=start_entry_id, limit=limit, raw_data=raw_data, short_entries=True, account=account) + + def get_blog_entries(self, start_entry_id=0, limit=100, raw_data=True, + account=None): + """ Returns the list of blog entries for an account + + :param int start_entry_id: default is 0 + :param int limit: default is 100 + :param bool raw_data: default is False + :param str account: When set, a different account name is used (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> entry = account.get_blog_entries(0, 1, raw_data=True)[0] + >>> print("%s - %s - %s - %s" % (entry["author"], entry["permlink"], entry["blog"], entry["reblog_on"])) + dsocial - firstpost - dsocial - 1970-01-01T00:00:00 + + """ + return self.get_blog(start_entry_id=start_entry_id, limit=limit, raw_data=raw_data, short_entries=True, account=account) + + def get_blog(self, start_entry_id=0, limit=100, raw_data=False, short_entries=False, account=None): + """ Returns the list of blog entries for an account + + :param int start_entry_id: default is 0 + :param int limit: default is 100 + :param bool raw_data: default is False + :param bool short_entries: when set to True and raw_data is True, get_blog_entries is used istead of get_blog + :param str account: When set, a different account name is used (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> account.get_blog(0, 1) + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if raw_data and short_entries and self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_blog_entries({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["blog"] + ] + elif raw_data and short_entries and not self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_blog_entries(account, start_entry_id, limit, api='follow') + ] + elif raw_data and self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_blog({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["blog"] + ] + elif raw_data and not self.dpay.rpc.get_use_appbase(): + return [ + c for c in self.dpay.rpc.get_blog(account, start_entry_id, limit, api='follow') + ] + elif not raw_data and self.dpay.rpc.get_use_appbase(): + from .comment import Comment + return [ + Comment(c["comment"], dpay_instance=self.dpay) for c in self.dpay.rpc.get_blog({'account': account, 'start_entry_id': start_entry_id, 'limit': limit}, api='follow')["blog"] + ] + else: + from .comment import Comment + return [ + Comment(c["comment"], dpay_instance=self.dpay) for c in self.dpay.rpc.get_blog(account, start_entry_id, limit, api='follow') + ] + + def get_blog_authors(self, account=None): + """ Returns a list of authors that have had their content reblogged on a given blog account + + :param str account: When set, a different account name is used (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> account.get_blog_authors() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.get_blog_authors({'blog_account': account}, api='follow')['blog_authors'] + else: + return self.dpay.rpc.get_blog_authors(account, api='follow') + + def get_follow_count(self, account=None): + """ get_follow_count """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.get_follow_count({'account': account}, api='follow') + else: + return self.dpay.rpc.get_follow_count(account, api='follow') + + def get_followers(self, raw_name_list=True, limit=100): + """ Returns the account followers as list + """ + name_list = [x['follower'] for x in self._get_followers(direction="follower", limit=limit)] + if raw_name_list: + return name_list + else: + return Accounts(name_list, dpay_instance=self.dpay) + + def get_following(self, raw_name_list=True, limit=100): + """ Returns who the account is following as list + """ + name_list = [x['following'] for x in self._get_followers(direction="following", limit=limit)] + if raw_name_list: + return name_list + else: + return Accounts(name_list, dpay_instance=self.dpay) + + def get_muters(self, raw_name_list=True, limit=100): + """ Returns the account muters as list + """ + name_list = [x['follower'] for x in self._get_followers(direction="follower", what="ignore", limit=limit)] + if raw_name_list: + return name_list + else: + return Accounts(name_list, dpay_instance=self.dpay) + + def get_mutings(self, raw_name_list=True, limit=100): + """ Returns who the account is muting as list + """ + name_list = [x['following'] for x in self._get_followers(direction="following", what="ignore", limit=limit)] + if raw_name_list: + return name_list + else: + return Accounts(name_list, dpay_instance=self.dpay) + + def _get_followers(self, direction="follower", last_user="", what="blog", limit=100): + """ Help function, used in get_followers and get_following + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + followers_list = [] + limit_reached = True + cnt = 0 + while limit_reached: + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + query = {'account': self.name, 'start': last_user, 'type': what, 'limit': limit} + if direction == "follower": + followers = self.dpay.rpc.get_followers(query, api='follow')['followers'] + elif direction == "following": + followers = self.dpay.rpc.get_following(query, api='follow')['following'] + else: + if direction == "follower": + followers = self.dpay.rpc.get_followers(self.name, last_user, what, limit, api='follow') + elif direction == "following": + followers = self.dpay.rpc.get_following(self.name, last_user, what, limit, api='follow') + if cnt == 0: + followers_list = followers + elif len(followers) > 1: + followers_list += followers[1:] + if len(followers) >= limit: + last_user = followers[-1][direction] + limit_reached = True + cnt += 1 + else: + limit_reached = False + + return followers_list + + @property + def available_balances(self): + """ List balances of an account. This call returns instances of + :class:`dpaycli.amount.Amount`. + """ + amount_list = ["balance", "bbd_balance", "vesting_shares"] + available_amount = [] + for amount in amount_list: + if amount in self: + available_amount.append(self[amount]) + return available_amount + + @property + def saving_balances(self): + savings_amount = [] + amount_list = ["savings_balance", "savings_bbd_balance"] + for amount in amount_list: + if amount in self: + savings_amount.append(self[amount]) + return savings_amount + + @property + def reward_balances(self): + amount_list = ["reward_dpay_balance", "reward_bbd_balance", "reward_vesting_balance"] + rewards_amount = [] + for amount in amount_list: + if amount in self: + rewards_amount.append(self[amount]) + return rewards_amount + + @property + def total_balances(self): + symbols = [] + for balance in self.available_balances: + symbols.append(balance["symbol"]) + ret = [] + for i in range(len(symbols)): + ret.append(self.get_balance(self.available_balances, symbols[i]) + self.get_balance(self.saving_balances, symbols[i]) + + self.get_balance(self.reward_balances, symbols[i])) + return ret + + @property + def balances(self): + """ Returns all account balances as dictionary + """ + return self.get_balances() + + def get_balances(self): + """ Returns all account balances as dictionary + + :returns: Account balances + :rtype: dictionary + + Sample output: + + .. code-block:: js + + { + 'available': [102.985 BEX, 0.008 BBD, 146273.695970 VESTS], + 'savings': [0.000 BEX, 0.000 BBD], + 'rewards': [0.000 BEX, 0.000 BBD, 0.000000 VESTS], + 'total': [102.985 BEX, 0.008 BBD, 146273.695970 VESTS] + } + + """ + return { + 'available': self.available_balances, + 'savings': self.saving_balances, + 'rewards': self.reward_balances, + 'total': self.total_balances, + } + + def get_balance(self, balances, symbol): + """ Obtain the balance of a specific Asset. This call returns instances of + :class:`dpaycli.amount.Amount`. Available balance types: + + * "available" + * "saving" + * "reward" + * "total" + + :param str balances: Defines the balance type + :param (str, dict) symbol: Can be "BBD", "BEX" or "VESTS + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_balance("rewards", "BBD") + 0.000 BBD + + """ + if isinstance(balances, string_types): + if balances == "available": + balances = self.available_balances + elif balances == "savings": + balances = self.saving_balances + elif balances == "rewards": + balances = self.reward_balances + elif balances == "total": + balances = self.total_balances + else: + return + from .amount import Amount + if isinstance(symbol, dict) and "symbol" in symbol: + symbol = symbol["symbol"] + + for b in balances: + if b["symbol"] == symbol: + return b + return Amount(0, symbol, dpay_instance=self.dpay) + + def interest(self): + """ Calculate interest for an account + + :param str account: Account name to get interest for + :rtype: dictionary + + Sample output: + + .. code-block:: js + + { + 'interest': 0.0, + 'last_payment': datetime.datetime(2018, 1, 26, 5, 50, 27, tzinfo=), + 'next_payment': datetime.datetime(2018, 2, 25, 5, 50, 27, tzinfo=), + 'next_payment_duration': datetime.timedelta(-65, 52132, 684026), + 'interest_rate': 0.0 + } + + """ + last_payment = (self["bbd_last_interest_payment"]) + next_payment = last_payment + timedelta(days=30) + interest_rate = self.dpay.get_dynamic_global_properties()[ + "bbd_interest_rate"] / 100 # percent + interest_amount = (interest_rate / 100) * int( + int(self["bbd_seconds"]) / (60 * 60 * 24 * 356)) * 10**-3 + return { + "interest": interest_amount, + "last_payment": last_payment, + "next_payment": next_payment, + "next_payment_duration": next_payment - addTzInfo(datetime.utcnow()), + "interest_rate": interest_rate, + } + + @property + def is_fully_loaded(self): + """ Is this instance fully loaded / e.g. all data available? + + :rtype: bool + """ + return (self.full) + + def ensure_full(self): + """Ensure that all data are loaded""" + if not self.is_fully_loaded: + self.full = True + self.refresh() + + def get_account_bandwidth(self, bandwidth_type=1, account=None): + """ get_account_bandwidth """ + if account is None: + account = self["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + # return self.dpay.rpc.get_account_bandwidth({'account': account, 'type': 'post'}, api="witness") + return self.dpay.rpc.get_account_bandwidth(account, bandwidth_type) + else: + return self.dpay.rpc.get_account_bandwidth(account, bandwidth_type) + + def get_bandwidth(self): + """ Returns used and allocated bandwidth + + :rtype: dict + + Sample output: + + .. code-block:: js + + { + 'used': 0, + 'allocated': 2211037 + } + + """ + account = self["name"] + global_properties = self.dpay.get_dynamic_global_properties() + reserve_ratio = self.dpay.get_reserve_ratio() + if "received_vesting_shares" in self: + received_vesting_shares = self["received_vesting_shares"].amount + else: + received_vesting_shares = 0 + vesting_shares = self["vesting_shares"].amount + max_virtual_bandwidth = float(reserve_ratio["max_virtual_bandwidth"]) + total_vesting_shares = Amount(global_properties["total_vesting_shares"], dpay_instance=self.dpay).amount + allocated_bandwidth = (max_virtual_bandwidth * (vesting_shares + received_vesting_shares) / total_vesting_shares) + allocated_bandwidth = round(allocated_bandwidth / 1000000) + + if not not self.dpay.is_connected() and self.dpay.rpc.get_use_appbase(): + account_bandwidth = self.get_account_bandwidth(bandwidth_type=1, account=account) + if account_bandwidth is None: + return {"used": 0, + "allocated": allocated_bandwidth} + last_bandwidth_update = formatTimeString(account_bandwidth["last_bandwidth_update"]) + average_bandwidth = float(account_bandwidth["average_bandwidth"]) + else: + last_bandwidth_update = (self["last_bandwidth_update"]) + average_bandwidth = float(self["average_bandwidth"]) + total_seconds = 604800 + + seconds_since_last_update = addTzInfo(datetime.utcnow()) - last_bandwidth_update + seconds_since_last_update = seconds_since_last_update.total_seconds() + used_bandwidth = 0 + if seconds_since_last_update < total_seconds: + used_bandwidth = (((total_seconds - seconds_since_last_update) * average_bandwidth) / total_seconds) + used_bandwidth = round(used_bandwidth / 1000000) + + return {"used": used_bandwidth, + "allocated": allocated_bandwidth} + # print("bandwidth percent used: " + str(100 * used_bandwidth / allocated_bandwidth)) + # print("bandwidth percent remaining: " + str(100 - (100 * used_bandwidth / allocated_bandwidth))) + + def get_owner_history(self, account=None): + """ Returns the owner history of an account. + + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_owner_history() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_owner_histories({'owner': account}, api="database")['owner_auths'] + else: + return self.dpay.rpc.get_owner_history(account) + + def get_conversion_requests(self, account=None): + """ Returns a list of BBD conversion request + + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_conversion_requests() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_bbd_conversion_requests({'account': account}, api="database")['requests'] + else: + return self.dpay.rpc.get_conversion_requests(account) + + def get_vesting_delegations(self, start_account="", limit=100, account=None): + """ Returns the vesting delegations by an account. + + :param str account: When set, a different account is used for the request (Default is object account name) + :param str start_account: Only used in pre-appbase nodes + :param int limit: Only used in pre-appbase nodes + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_vesting_delegations() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_vesting_delegations({'account': account}, api="database")['delegations'] + else: + return self.dpay.rpc.get_vesting_delegations(account, start_account, limit) + + def get_withdraw_routes(self, account=None): + """ Returns the withdraw routes for an account. + + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_withdraw_routes() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_withdraw_vesting_routes({'account': account, 'order': 'by_withdraw_route'}, api="database")['routes'] + else: + return self.dpay.rpc.get_withdraw_routes(account, 'all') + + def get_savings_withdrawals(self, direction="from", account=None): + """ Returns the list of savings withdrawls for an account. + + :param str account: When set, a different account is used for the request (Default is object account name) + :param str direction: Can be either from or to (only non appbase nodes) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_savings_withdrawals() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_savings_withdrawals({'account': account}, api="database")['withdrawals'] + elif direction == "from": + return self.dpay.rpc.get_savings_withdraw_from(account) + elif direction == "to": + return self.dpay.rpc.get_savings_withdraw_to(account) + + def get_recovery_request(self, account=None): + """ Returns the recovery request for an account + + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_recovery_request() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_account_recovery_requests({'account': account}, api="database")['requests'] + else: + return self.dpay.rpc.get_recovery_request(account) + + def get_escrow(self, escrow_id=0, account=None): + """ Returns the escrow for a certain account by id + + :param int escrow_id: Id (only pre appbase) + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_escrow(1234) + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_escrows({'from': account}, api="database")['escrows'] + else: + return self.dpay.rpc.get_escrow(account, escrow_id) + + def verify_account_authority(self, keys, account=None): + """ Returns true if the signers have enough authority to authorize an account. + + :param list keys: public key + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: dict + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dsocial") + >>> print(account.verify_account_authority(["DWB7Q2rLBqzPzFeteQZewv9Lu3NLE69fZoLeL6YK59t7UmssCBNTU"])["valid"]) + False + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + if not isinstance(keys, list): + keys = [keys] + self.dpay.rpc.set_next_node_on_empty_reply(False) + try: + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.verify_account_authority({'account': account, 'signers': keys}, api="database") + else: + return self.dpay.rpc.verify_account_authority(account, keys) + except MissingRequiredActiveAuthority: + return {'valid': False} + + def get_tags_used_by_author(self, account=None): + """ Returns a list of tags used by an author. + + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_tags_used_by_author() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.get_tags_used_by_author({'author': account}, api="tags")['tags'] + else: + return self.dpay.rpc.get_tags_used_by_author(account, api="tags") + + def get_expiring_vesting_delegations(self, after=None, limit=1000, account=None): + """ Returns the expirations for vesting delegations. + + :param datetime after : expiration after (only for pre appbase nodes) + :param int limit: limits number of shown entries (only for pre appbase nodes) + :param str account: When set, a different account is used for the request (Default is object account name) + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_expiring_vesting_delegations() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if after is None: + after = addTzInfo(datetime.utcnow()) - timedelta(days=8) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.find_vesting_delegation_expirations({'account': account}, api="database")['delegations'] + else: + return self.dpay.rpc.get_expiring_vesting_delegations(account, formatTimeString(after), limit) + + def get_account_votes(self, account=None): + """ Returns all votes that the account has done + + :rtype: list + + .. code-block:: python + + >>> from dpaycli.account import Account + >>> account = Account("dpaycli.app") + >>> account.get_account_votes() + [] + + """ + if account is None: + account = self["name"] + elif isinstance(account, Account): + account = account["name"] + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.get_account_votes(account, api="condenser") + else: + return self.dpay.rpc.get_account_votes(account) + + def get_vote(self, comment): + """Returns a vote if the account has already voted for comment. + + :param str/Comment comment: can be a Comment object or a authorpermlink + """ + from dpaycli.comment import Comment + c = Comment(comment, dpay_instance=self.dpay) + for v in c["active_votes"]: + if v["voter"] == self["name"]: + return v + return None + + def has_voted(self, comment): + """Returns if the account has already voted for comment + + :param str/Comment comment: can be a Comment object or a authorpermlink + """ + from dpaycli.comment import Comment + c = Comment(comment, dpay_instance=self.dpay) + active_votes = {v["voter"]: v for v in c["active_votes"]} + return self["name"] in active_votes + + def virtual_op_count(self, until=None): + """ Returns the number of individual account transactions + + :rtype: list + """ + if until is not None: + return self.estimate_virtual_op_num(until, stop_diff=1) + else: + try: + op_count = 0 + op_count = self._get_account_history(start=-1, limit=0) + if isinstance(op_count, list) and len(op_count) > 0 and len(op_count[0]) > 0: + return op_count[0][0] + else: + return 0 + except IndexError: + return 0 + + def _get_account_history(self, account=None, start=-1, limit=0): + if account is None: + account = self + account = Account(account, dpay_instance=self.dpay) + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + try: + ret = self.dpay.rpc.get_account_history({'account': account["name"], 'start': start, 'limit': limit}, api="account_history")['history'] + except ApiNotSupported: + ret = self.dpay.rpc.get_account_history(account["name"], start, limit, api="condenser") + else: + ret = self.dpay.rpc.get_account_history(account["name"], start, limit, api="database") + return ret + + def estimate_virtual_op_num(self, blocktime, stop_diff=0, max_count=100): + """ Returns an estimation of an virtual operation index for a given time or blockindex + + :param int/datetime blocktime: start time or start block index from which account + operation should be fetched + :param int stop_diff: Sets the difference between last estimation and + new estimation at which the estimation stops. Must not be zero. (default is 1) + :param int max_count: sets the maximum number of iterations. -1 disables this (default 100) + + .. testsetup:: + + import pytz + from dpaycli.account import Account + from dpaycli.blockchain import Blockchain + from datetime import datetime, timedelta + from timeit import time as t + + .. testcode:: + + utc = pytz.timezone('UTC') + start_time = utc.localize(datetime.utcnow()) - timedelta(days=7) + acc = Account("gtg") + start_op = acc.estimate_virtual_op_num(start_time) + + b = Blockchain() + start_block_num = b.get_estimated_block_num(start_time) + start_op2 = acc.estimate_virtual_op_num(start_block_num) + + .. testcode:: + + acc = Account("gtg") + block_num = 21248120 + start = t.time() + op_num = acc.estimate_virtual_op_num(block_num, stop_diff=1, max_count=10) + stop = t.time() + print(stop - start) + for h in acc.get_account_history(op_num, 0): + block_est = h["block"] + print(block_est - block_num) + + """ + def get_blocknum(index): + op = self._get_account_history(start=(index)) + return op[0][1]['block'] + + max_index = self.virtual_op_count() + if max_index < stop_diff: + return 0 + + # calculate everything with block numbers + created = get_blocknum(0) + + # convert blocktime to block number if given as a datetime/date/time + if isinstance(blocktime, (datetime, date, time)): + b = Blockchain(dpay_instance=self.dpay) + target_blocknum = b.get_estimated_block_num(addTzInfo(blocktime), accurate=True) + else: + target_blocknum = blocktime + + # the requested blocknum/timestamp is before the account creation date + if target_blocknum <= created: + return 0 + + # get the block number from the account's latest operation + latest_blocknum = get_blocknum(-1) + + # requested blocknum/timestamp is after the latest account operation + if target_blocknum >= latest_blocknum: + return max_index + + # all account ops in a single block + if latest_blocknum - created == 0: + return 0 + + # set initial search range + op_num = 0 + op_lower = 0 + block_lower = created + op_upper = max_index + block_upper = latest_blocknum + last_op_num = None + cnt = 0 + + while True: + # check if the maximum number of iterations was reached + if max_count != -1 and cnt >= max_count: + # did not converge, return the current state + return op_num + + # linear approximation between the known upper and + # lower bounds for the first iteration + if cnt < 1: + op_num = int((target_blocknum - block_lower) / + (block_upper - block_lower) * + (op_upper - op_lower) + op_lower) + else: + # divide and conquer for the following iterations + op_num = int((op_upper + op_lower) / 2) + if op_upper == op_lower + 1: # round up if we're close to target + op_num += 1 + + # get block number for current op number estimation + if op_num != last_op_num: + block_num = get_blocknum(op_num) + last_op_num = op_num + + # check if the required accuracy was reached + if op_upper - op_lower <= stop_diff or \ + op_upper == op_lower + 1: + return op_num + + # set new upper/lower boundaries for next iteration + if block_num < target_blocknum: + # current op number was too low -> search upwards + op_lower = op_num + block_lower = block_num + else: + # current op number was too high or matched the target block + # -> search downwards + op_upper = op_num + block_upper = block_num + cnt += 1 + + def get_curation_reward(self, days=7): + """Returns the curation reward of the last `days` days + + :param int days: limit number of days to be included int the return value + """ + stop = addTzInfo(datetime.utcnow()) - timedelta(days=days) + reward_vests = Amount(0, self.dpay.vests_symbol, dpay_instance=self.dpay) + for reward in self.history_reverse(stop=stop, use_block_num=False, only_ops=["curation_reward"]): + reward_vests += Amount(reward['reward'], dpay_instance=self.dpay) + return self.dpay.vests_to_sp(reward_vests.amount) + + def curation_stats(self): + """Returns the curation reward of the last 24h and 7d and the average + of the last 7 days + + :returns: Account curation + :rtype: dictionary + + Sample output: + + .. code-block:: js + + { + '24hr': 0.0, + '7d': 0.0, + 'avg': 0.0 + } + + """ + return {"24hr": self.get_curation_reward(days=1), + "7d": self.get_curation_reward(days=7), + "avg": self.get_curation_reward(days=7) / 7} + + def get_account_history(self, index, limit, order=-1, start=None, stop=None, use_block_num=True, only_ops=[], exclude_ops=[], raw_output=False): + """ Returns a generator for individual account transactions. This call can be used in a + ``for`` loop. + + :param int index: first number of transactions to return + :param int limit: limit number of transactions to return + :param int/datetime start: start number/date of transactions to + return (*optional*) + :param int/datetime stop: stop number/date of transactions to + return (*optional*) + :param bool use_block_num: if true, start and stop are block numbers, otherwise virtual OP count numbers. + :param array only_ops: Limit generator by these + operations (*optional*) + :param array exclude_ops: Exclude thse operations from + generator (*optional*) + :param int batch_size: internal api call batch size (*optional*) + :param int order: 1 for chronological, -1 for reverse order + :param bool raw_output: if False, the output is a dict, which + includes all values. Otherwise, the output is list. + + ... note:: + only_ops and exclude_ops takes an array of strings: + The full list of operation ID's can be found in + dpayclibase.operationids.ops. + Example: ['transfer', 'vote'] + + """ + if order != -1 and order != 1: + raise ValueError("order must be -1 or 1!") + # self.dpay.rpc.set_next_node_on_empty_reply(True) + txs = self._get_account_history(start=index, limit=limit) + if txs is None: + return + start = addTzInfo(start) + stop = addTzInfo(stop) + + if order == -1: + txs_list = reversed(txs) + else: + txs_list = txs + for item in txs_list: + item_index, event = item + if start and isinstance(start, (datetime, date, time)): + timediff = start - formatTimeString(event["timestamp"]) + if timediff.total_seconds() * float(order) > 0: + continue + elif start is not None and use_block_num and order == 1 and event['block'] < start: + continue + elif start is not None and use_block_num and order == -1 and event['block'] > start: + continue + elif start is not None and not use_block_num and order == 1 and item_index < start: + continue + elif start is not None and not use_block_num and order == -1 and item_index > start: + continue + if stop is not None and isinstance(stop, (datetime, date, time)): + timediff = stop - formatTimeString(event["timestamp"]) + if timediff.total_seconds() * float(order) < 0: + return + elif stop is not None and use_block_num and order == 1 and event['block'] > stop: + return + elif stop is not None and use_block_num and order == -1 and event['block'] < stop: + return + elif stop is not None and not use_block_num and order == 1 and item_index > stop: + return + elif stop is not None and not use_block_num and order == -1 and item_index < stop: + return + + if isinstance(event['op'], list): + op_type, op = event['op'] + else: + op_type = event['op']['type'] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + op = event['op']['value'] + block_props = remove_from_dict(event, keys=['op'], keep_keys=False) + + def construct_op(account_name): + # verbatim output from dpayd + if raw_output: + return item + + # index can change during reindexing in + # future hard-forks. Thus we cannot take it for granted. + immutable = op.copy() + immutable.update(block_props) + immutable.update({ + 'account': account_name, + 'type': op_type, + }) + _id = Blockchain.hash_op(immutable) + immutable.update({ + '_id': _id, + 'index': item_index, + }) + return immutable + + if exclude_ops and op_type in exclude_ops: + continue + if not only_ops or op_type in only_ops: + yield construct_op(self["name"]) + + def history( + self, start=None, stop=None, use_block_num=True, + only_ops=[], exclude_ops=[], batch_size=1000, raw_output=False + ): + """ Returns a generator for individual account transactions. The + earlist operation will be first. This call can be used in a + ``for`` loop. + + :param int/datetime start: start number/date of transactions to + return (*optional*) + :param int/datetime stop: stop number/date of transactions to + return (*optional*) + :param bool use_block_num: if true, start and stop are block numbers, + otherwise virtual OP count numbers. + :param array only_ops: Limit generator by these + operations (*optional*) + :param array exclude_ops: Exclude thse operations from + generator (*optional*) + :param int batch_size: internal api call batch size (*optional*) + :param bool raw_output: if False, the output is a dict, which + includes all values. Otherwise, the output is list. + + ... note:: + only_ops and exclude_ops takes an array of strings: + The full list of operation ID's can be found in + dpayclibase.operationids.ops. + Example: ['transfer', 'vote'] + + .. testsetup:: + + from dpaycli.account import Account + from datetime import datetime + + .. testcode:: + + acc = Account("gtg") + max_op_count = acc.virtual_op_count() + # Returns the 100 latest operations + acc_op = [] + for h in acc.history(start=max_op_count - 99, stop=max_op_count, use_block_num=False): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 100 + + .. testcode:: + + acc = Account("test") + max_block = 21990141 + # Returns the account operation inside the last 100 block. This can be empty. + acc_op = [] + for h in acc.history(start=max_block - 99, stop=max_block, use_block_num=True): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 0 + + .. testcode:: + + acc = Account("test") + start_time = datetime(2018, 3, 1, 0, 0, 0) + stop_time = datetime(2018, 3, 2, 0, 0, 0) + # Returns the account operation from 1.4.2018 back to 1.3.2018 + acc_op = [] + for h in acc.history(start=start_time, stop=stop_time): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 0 + + """ + _limit = batch_size + max_index = self.virtual_op_count() + if not max_index: + return + start = addTzInfo(start) + stop = addTzInfo(stop) + if start is not None and not use_block_num and not isinstance(start, (datetime, date, time)): + start_index = start + elif start is not None and max_index > batch_size: + op_est = self.estimate_virtual_op_num(start, stop_diff=1) + est_diff = 0 + if isinstance(start, (datetime, date, time)): + for h in self.get_account_history(op_est, 0): + block_date = formatTimeString(h["timestamp"]) + while(op_est > est_diff + batch_size and block_date > start): + est_diff += batch_size + if op_est - est_diff < 0: + est_diff = op_est + for h in self.get_account_history(op_est - est_diff, 0): + block_date = formatTimeString(h["timestamp"]) + elif not isinstance(start, (datetime, date, time)): + for h in self.get_account_history(op_est, 0): + block_num = h["block"] + while(op_est > est_diff + batch_size and block_num > start): + est_diff += batch_size + if op_est - est_diff < 0: + est_diff = op_est + for h in self.get_account_history(op_est - est_diff, 0): + block_num = h["block"] + start_index = op_est - est_diff + else: + start_index = 0 + + first = start_index + _limit + if first > max_index: + _limit = max_index - start_index + 1 + first = start_index + _limit + last_round = False + if _limit < 0: + return + while True: + # RPC call + for item in self.get_account_history(first, _limit, start=None, stop=None, order=1, raw_output=raw_output): + if raw_output: + item_index, event = item + op_type, op = event['op'] + timestamp = event["timestamp"] + block_num = event["block"] + else: + item_index = item['index'] + op_type = item['type'] + timestamp = item["timestamp"] + block_num = item["block"] + if start is not None and isinstance(start, (datetime, date, time)): + timediff = start - formatTimeString(timestamp) + if timediff.total_seconds() > 0: + continue + elif start is not None and use_block_num and block_num < start: + continue + elif start is not None and not use_block_num and item_index < start: + continue + if stop is not None and isinstance(stop, (datetime, date, time)): + timediff = stop - formatTimeString(timestamp) + if timediff.total_seconds() < 0: + first = max_index + _limit + return + elif stop is not None and use_block_num and block_num > stop: + return + elif stop is not None and not use_block_num and item_index > stop: + return + if exclude_ops and op_type in exclude_ops: + continue + if not only_ops or op_type in only_ops: + yield item + if first < max_index and first + _limit >= max_index and not last_round: + _limit = max_index - first - 1 + first = max_index + last_round = True + else: + first += (_limit + 1) + if stop is not None and not use_block_num and isinstance(stop, int) and first >= stop + _limit: + break + elif first > max_index or last_round: + break + + def history_reverse( + self, start=None, stop=None, use_block_num=True, + only_ops=[], exclude_ops=[], batch_size=1000, raw_output=False + ): + """ Returns a generator for individual account transactions. The + latest operation will be first. This call can be used in a + ``for`` loop. + + :param int/datetime start: start number/date of transactions to + return. If negative the virtual_op_count is added. (*optional*) + :param int/datetime stop: stop number/date of transactions to + return. If negative the virtual_op_count is added. (*optional*) + :param bool use_block_num: if true, start and stop are block numbers, + otherwise virtual OP count numbers. + :param array only_ops: Limit generator by these + operations (*optional*) + :param array exclude_ops: Exclude thse operations from + generator (*optional*) + :param int batch_size: internal api call batch size (*optional*) + :param bool raw_output: if False, the output is a dict, which + includes all values. Otherwise, the output is list. + + ... note:: + only_ops and exclude_ops takes an array of strings: + The full list of operation ID's can be found in + dpayclibase.operationids.ops. + Example: ['transfer', 'vote'] + + .. testsetup:: + + from dpaycli.account import Account + from datetime import datetime + + .. testcode:: + + acc = Account("gtg") + max_op_count = acc.virtual_op_count() + # Returns the 100 latest operations + acc_op = [] + for h in acc.history_reverse(start=max_op_count, stop=max_op_count - 99, use_block_num=False): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 100 + + .. testcode:: + + max_block = 21990141 + acc = Account("test") + # Returns the account operation inside the last 100 block. This can be empty. + acc_op = [] + for h in acc.history_reverse(start=max_block, stop=max_block-100, use_block_num=True): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 0 + + .. testcode:: + + acc = Account("test") + start_time = datetime(2018, 4, 1, 0, 0, 0) + stop_time = datetime(2018, 3, 1, 0, 0, 0) + # Returns the account operation from 1.4.2018 back to 1.3.2018 + acc_op = [] + for h in acc.history_reverse(start=start_time, stop=stop_time): + acc_op.append(h) + len(acc_op) + + .. testoutput:: + + 0 + + """ + _limit = batch_size + first = self.virtual_op_count() + start = addTzInfo(start) + stop = addTzInfo(stop) + if not first or not batch_size: + return + if start is not None and isinstance(start, int) and start < 0 and not use_block_num: + start += first + elif start is not None and isinstance(start, int) and not use_block_num: + first = start + elif start is not None and first > batch_size: + op_est = self.estimate_virtual_op_num(start, stop_diff=1) + est_diff = 0 + if isinstance(start, (datetime, date, time)): + for h in self.get_account_history(op_est, 0): + block_date = formatTimeString(h["timestamp"]) + while(op_est + est_diff + batch_size < first and block_date < start): + est_diff += batch_size + if op_est + est_diff > first: + est_diff = first - op_est + for h in self.get_account_history(op_est + est_diff, 0): + block_date = formatTimeString(h["timestamp"]) + else: + for h in self.get_account_history(op_est, 0): + block_num = h["block"] + while(op_est + est_diff + batch_size < first and block_num < start): + est_diff += batch_size + if op_est + est_diff > first: + est_diff = first - op_est + for h in self.get_account_history(op_est + est_diff, 0): + block_num = h["block"] + first = op_est + est_diff + if stop is not None and isinstance(stop, int) and stop < 0 and not use_block_num: + stop += first + + while True: + # RPC call + if first - _limit < 0: + _limit = first + for item in self.get_account_history(first, _limit, start=None, stop=None, order=-1, only_ops=only_ops, exclude_ops=exclude_ops, raw_output=raw_output): + if raw_output: + item_index, event = item + op_type, op = event['op'] + timestamp = event["timestamp"] + block_num = event["block"] + else: + item_index = item['index'] + op_type = item['type'] + timestamp = item["timestamp"] + block_num = item["block"] + if start is not None and isinstance(start, (datetime, date, time)): + timediff = start - formatTimeString(timestamp) + if timediff.total_seconds() < 0: + continue + elif start is not None and use_block_num and block_num > start: + continue + elif start is not None and not use_block_num and item_index > start: + continue + if stop is not None and isinstance(stop, (datetime, date, time)): + timediff = stop - formatTimeString(timestamp) + if timediff.total_seconds() > 0: + first = 0 + return + elif stop is not None and use_block_num and block_num < stop: + first = 0 + return + elif stop is not None and not use_block_num and item_index < stop: + first = 0 + return + if exclude_ops and op_type in exclude_ops: + continue + if not only_ops or op_type in only_ops: + yield item + first -= (_limit + 1) + if first < 1: + break + + def mute(self, mute, account=None): + """ Mute another account + + :param str mute: Mute this account + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + return self.follow(mute, what=["ignore"], account=account) + + def unfollow(self, unfollow, account=None): + """ Unfollow/Unmute another account's blog + + :param str unfollow: Unfollow/Unmute this account + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + return self.follow(unfollow, what=[], account=account) + + def follow(self, other, what=["blog"], account=None): + """ Follow/Unfollow/Mute/Unmute another account's blog + + :param str other: Follow this account + :param list what: List of states to follow. + ``['blog']`` means to follow ``other``, + ``[]`` means to unfollow/unmute ``other``, + ``['ignore']`` means to ignore ``other``, + (defaults to ``['blog']``) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + if account is None: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + if not other: + raise ValueError("You need to provide an account to follow/unfollow/mute/unmute") + + json_body = [ + 'follow', { + 'follower': account, + 'following': other, + 'what': what + } + ] + return self.dpay.custom_json( + "follow", json_body, required_posting_auths=[account]) + + def update_account_profile(self, profile, account=None, **kwargs): + """ Update an account's profile in json_metadata + + :param dict profile: The new profile to use + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + Sample profile structure: + + .. code-block:: js + + { + 'name': 'Holger', + 'about': 'dpaycli Developer', + 'location': 'Germany', + 'profile_image': 'https://c1.staticflickr.com/5/4715/38733717165_7070227c89_n.jpg', + 'cover_image': 'https://farm1.staticflickr.com/894/26382750057_69f5c8e568.jpg', + 'website': 'https://github.com/holgern/dpaycli' + } + + .. code-block:: python + + from dpaycli.account import Account + account = Account("test") + profile = account.profile + profile["about"] = "test account" + account.update_account_profile(profile) + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + if not isinstance(profile, dict): + raise ValueError("Profile must be a dict type!") + + if self['json_metadata'] == '': + metadata = {} + else: + metadata = json.loads(self['json_metadata']) + metadata["profile"] = profile + return self.update_account_metadata(metadata) + + def update_account_metadata(self, metadata, account=None, **kwargs): + """ Update an account's profile in json_metadata + + :param dict metadata: The new metadata to use + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + if isinstance(metadata, dict): + metadata = json.dumps(metadata) + elif not isinstance(metadata, str): + raise ValueError("Profile must be a dict or string!") + op = operations.Account_update( + **{ + "account": account["name"], + "memo_key": account["memo_key"], + "json_metadata": metadata, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + # ------------------------------------------------------------------------- + # Approval and Disapproval of witnesses + # ------------------------------------------------------------------------- + def approvewitness(self, witness, account=None, approve=True, **kwargs): + """ Approve a witness + + :param list witness: list of Witness name or id + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + # if not isinstance(witnesses, (list, set, tuple)): + # witnesses = {witnesses} + + # for witness in witnesses: + # witness = Witness(witness, dpay_instance=self) + + op = operations.Account_witness_vote(**{ + "account": account["name"], + "witness": witness, + "approve": approve, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def disapprovewitness(self, witness, account=None, **kwargs): + """ Disapprove a witness + + :param list witness: list of Witness name or id + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + return self.approvewitness( + witness=witness, account=account, approve=False) + + def update_memo_key(self, key, account=None, **kwargs): + """ Update an account's memo public key + + This method does **not** add any private keys to your + wallet but merely changes the memo public key. + + :param str key: New memo public key + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + PublicKey(key, prefix=self.dpay.prefix) + + account["memo_key"] = key + op = operations.Account_update(**{ + "account": account["name"], + "memo_key": account["memo_key"], + "json_metadata": account["json_metadata"], + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + # ------------------------------------------------------------------------- + # Simple Transfer + # ------------------------------------------------------------------------- + def transfer(self, to, amount, asset, memo="", account=None, **kwargs): + """ Transfer an asset to another account. + + :param str to: Recipient + :param float amount: Amount to transfer + :param str asset: Asset to transfer + :param str memo: (optional) Memo, may begin with `#` for encrypted + messaging + :param str account: (optional) the source account for the transfer + if not ``default_account`` + + + transfer example: + .. code-block:: python + + from dpaycli.account import Account + from dpaycli import DPay + active_wif = "5xxxx" + stm = DPay(keys=[active_wif]) + acc = Account("test", dpay_instance=stm) + acc.transfer("test1", 1, "BEX", "test") + + """ + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + amount = Amount(amount, asset, dpay_instance=self.dpay) + to = Account(to, dpay_instance=self.dpay) + if memo and memo[0] == "#": + from .memo import Memo + memoObj = Memo( + from_account=account, + to_account=to, + dpay_instance=self.dpay + ) + memo = memoObj.encrypt(memo[1:])["message"] + + op = operations.Transfer(**{ + "amount": amount, + "to": to["name"], + "memo": memo, + "from": account["name"], + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def transfer_to_vesting(self, amount, to=None, account=None, **kwargs): + """ Vest BEX + + :param float amount: Amount to transfer + :param str to: Recipient (optional) if not set equal to account + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + if to is None: + to = self # powerup on the same account + else: + to = Account(to, dpay_instance=self.dpay) + amount = self._check_amount(amount, self.dpay.dpay_symbol) + + to = Account(to, dpay_instance=self.dpay) + + op = operations.Transfer_to_vesting(**{ + "from": account["name"], + "to": to["name"], + "amount": amount, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def convert(self, amount, account=None, request_id=None): + """ Convert DPayDollars to DPay (takes 3.5 days to settle) + + :param float amount: amount of BBD to convert + :param str account: (optional) the source account for the transfer + if not ``default_account`` + :param str request_id: (optional) identifier for tracking the + conversion` + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + amount = self._check_amount(amount, self.dpay.bbd_symbol) + if request_id: + request_id = int(request_id) + else: + request_id = random.getrandbits(32) + op = operations.Convert( + **{ + "owner": account["name"], + "requestid": request_id, + "amount": amount, + "prefix": self.dpay.prefix, + }) + + return self.dpay.finalizeOp(op, account, "active") + + def transfer_to_savings(self, amount, asset, memo, to=None, account=None, **kwargs): + """ Transfer BBD or BEX into a 'savings' account. + + :param float amount: BEX or BBD amount + :param float asset: 'BEX' or 'BBD' + :param str memo: (optional) Memo + :param str to: (optional) the source account for the transfer if + not ``default_account`` + :param str account: (optional) the source account for the transfer + if not ``default_account`` + + """ + if asset not in ['BEX', 'BBD']: + raise AssertionError() + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + amount = Amount(amount, asset, dpay_instance=self.dpay) + if to is None: + to = account # move to savings on same account + else: + to = Account(to, dpay_instance=self.dpay) + + op = operations.Transfer_to_savings( + **{ + "from": account["name"], + "to": to["name"], + "amount": amount, + "memo": memo, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def transfer_from_savings(self, + amount, + asset, + memo, + request_id=None, + to=None, + account=None, **kwargs): + """ Withdraw BBD or BEX from 'savings' account. + + :param float amount: BEX or BBD amount + :param float asset: 'BEX' or 'BBD' + :param str memo: (optional) Memo + :param str request_id: (optional) identifier for tracking or + cancelling the withdrawal + :param str to: (optional) the source account for the transfer if + not ``default_account`` + :param str account: (optional) the source account for the transfer + if not ``default_account`` + + """ + if asset not in ['BEX', 'BBD']: + raise AssertionError() + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + if to is None: + to = account # move to savings on same account + else: + to = Account(to, dpay_instance=self.dpay) + amount = Amount(amount, asset, dpay_instance=self.dpay) + if request_id: + request_id = int(request_id) + else: + request_id = random.getrandbits(32) + + op = operations.Transfer_from_savings( + **{ + "from": account["name"], + "request_id": request_id, + "to": to["name"], + "amount": amount, + "memo": memo, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def cancel_transfer_from_savings(self, request_id, account=None, **kwargs): + """ Cancel a withdrawal from 'savings' account. + + :param str request_id: Identifier for tracking or cancelling + the withdrawal + :param str account: (optional) the source account for the transfer + if not ``default_account`` + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + op = operations.Cancel_transfer_from_savings(**{ + "from": account["name"], + "request_id": request_id, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def _check_amount(self, amount, symbol): + if isinstance(amount, (float, integer_types)): + amount = Amount(amount, symbol, dpay_instance=self.dpay) + elif isinstance(amount, string_types) and amount.replace('.', '', 1).replace(',', '', 1).isdigit(): + amount = Amount(float(amount), symbol, dpay_instance=self.dpay) + else: + amount = Amount(amount, dpay_instance=self.dpay) + if not amount["symbol"] == symbol: + raise AssertionError() + return amount + + def claim_reward_balance(self, + reward_dpay=0, + reward_bbd=0, + reward_vests=0, + account=None, **kwargs): + """ Claim reward balances. + By default, this will claim ``all`` outstanding balances. To bypass + this behaviour, set desired claim amount by setting any of + `reward_dpay`, `reward_bbd` or `reward_vests`. + + :param str reward_dpay: Amount of BEX you would like to claim. + :param str reward_bbd: Amount of BBD you would like to claim. + :param str reward_vests: Amount of VESTS you would like to claim. + :param str account: The source account for the claim if not + ``default_account`` is used. + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + if not account: + raise ValueError("You need to provide an account") + + # if no values were set by user, claim all outstanding balances on + # account + + reward_dpay = self._check_amount(reward_dpay, self.dpay.dpay_symbol) + reward_bbd = self._check_amount(reward_bbd, self.dpay.bbd_symbol) + reward_vests = self._check_amount(reward_vests, self.dpay.vests_symbol) + + if reward_dpay.amount == 0 and reward_bbd.amount == 0 and reward_vests.amount == 0: + reward_dpay = account.balances["rewards"][0] + reward_bbd = account.balances["rewards"][1] + reward_vests = account.balances["rewards"][2] + + op = operations.Claim_reward_balance( + **{ + "account": account["name"], + "reward_dpay": reward_dpay, + "reward_bbd": reward_bbd, + "reward_vests": reward_vests, + "prefix": self.dpay.prefix, + }) + print(op) + return self.dpay.finalizeOp(op, account, "posting", **kwargs) + + def delegate_vesting_shares(self, to_account, vesting_shares, + account=None, **kwargs): + """ Delegate BP to another account. + + :param str to_account: Account we are delegating shares to + (delegatee). + :param str vesting_shares: Amount of VESTS to delegate eg. `10000 + VESTS`. + :param str account: The source account (delegator). If not specified, + ``default_account`` is used. + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + to_account = Account(to_account, dpay_instance=self.dpay) + if to_account is None: + raise ValueError("You need to provide a to_account") + vesting_shares = self._check_amount(vesting_shares, self.dpay.vests_symbol) + + op = operations.Delegate_vesting_shares( + **{ + "delegator": account["name"], + "delegatee": to_account["name"], + "vesting_shares": vesting_shares, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def withdraw_vesting(self, amount, account=None, **kwargs): + """ Withdraw VESTS from the vesting account. + + :param float amount: number of VESTS to withdraw over a period of + 13 weeks + :param str account: (optional) the source account for the transfer + if not ``default_account`` + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + amount = self._check_amount(amount, self.dpay.vests_symbol) + + op = operations.Withdraw_vesting( + **{ + "account": account["name"], + "vesting_shares": amount, + "prefix": self.dpay.prefix, + }) + + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def set_withdraw_vesting_route(self, + to, + percentage=100, + account=None, + auto_vest=False, **kwargs): + """ Set up a vesting withdraw route. When vesting shares are + withdrawn, they will be routed to these accounts based on the + specified weights. + + :param str to: Recipient of the vesting withdrawal + :param float percentage: The percent of the withdraw to go + to the 'to' account. + :param str account: (optional) the vesting account + :param bool auto_vest: Set to true if the 'to' account + should receive the VESTS as VESTS, or false if it should + receive them as BEX. (defaults to ``False``) + + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + op = operations.Set_withdraw_vesting_route( + **{ + "from_account": account["name"], + "to_account": to, + "percent": int(percentage * DPAY_1_PERCENT), + "auto_vest": auto_vest + }) + + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def allow( + self, foreign, weight=None, permission="posting", + account=None, threshold=None, **kwargs + ): + """ Give additional access to an account by some other public + key or account. + + :param str foreign: The foreign account that will obtain access + :param int weight: (optional) The weight to use. If not + define, the threshold will be used. If the weight is + smaller than the threshold, additional signatures will + be required. (defaults to threshold) + :param str permission: (optional) The actual permission to + modify (defaults to ``posting``) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + :param int threshold: (optional) The threshold that needs to be + reached by signatures to be able to interact + """ + from copy import deepcopy + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + if permission not in ["owner", "posting", "active"]: + raise ValueError( + "Permission needs to be either 'owner', 'posting', or 'active" + ) + account = Account(account, dpay_instance=self.dpay) + + if permission not in account: + account = Account(account, dpay_instance=self.dpay, lazy=False, full=True) + account.clear_cache() + account.refresh() + if permission not in account: + account = Account(account, dpay_instance=self.dpay) + if permission not in account: + raise AssertionError("Could not access permission") + + if not weight: + weight = account[permission]["weight_threshold"] + + authority = deepcopy(account[permission]) + try: + pubkey = PublicKey(foreign, prefix=self.dpay.prefix) + authority["key_auths"].append([ + str(pubkey), + weight + ]) + except: + try: + foreign_account = Account(foreign, dpay_instance=self.dpay) + authority["account_auths"].append([ + foreign_account["name"], + weight + ]) + except: + raise ValueError( + "Unknown foreign account or invalid public key" + ) + if threshold: + authority["weight_threshold"] = threshold + self.dpay._test_weights_treshold(authority) + + op = operations.Account_update(**{ + "account": account["name"], + permission: authority, + "memo_key": account["memo_key"], + "json_metadata": account["json_metadata"], + "prefix": self.dpay.prefix + }) + if permission == "owner": + return self.dpay.finalizeOp(op, account, "owner", **kwargs) + else: + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def disallow( + self, foreign, permission="posting", + account=None, threshold=None, **kwargs + ): + """ Remove additional access to an account by some other public + key or account. + + :param str foreign: The foreign account that will obtain access + :param str permission: (optional) The actual permission to + modify (defaults to ``posting``) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + :param int threshold: The threshold that needs to be reached + by signatures to be able to interact + """ + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + if permission not in ["owner", "active", "posting"]: + raise ValueError( + "Permission needs to be either 'owner', 'posting', or 'active" + ) + authority = account[permission] + + try: + pubkey = PublicKey(foreign, prefix=self.dpay.prefix) + affected_items = list( + [x for x in authority["key_auths"] if x[0] == str(pubkey)]) + authority["key_auths"] = list([x for x in authority["key_auths"] if x[0] != str(pubkey)]) + except: + try: + foreign_account = Account(foreign, dpay_instance=self.dpay) + affected_items = list( + [x for x in authority["account_auths"] if x[0] == foreign_account["name"]]) + authority["account_auths"] = list([x for x in authority["account_auths"] if x[0] != foreign_account["name"]]) + except: + raise ValueError( + "Unknown foreign account or unvalid public key" + ) + + if not affected_items: + raise ValueError("Changes nothing!") + removed_weight = affected_items[0][1] + + # Define threshold + if threshold: + authority["weight_threshold"] = threshold + + # Correct threshold (at most by the amount removed from the + # authority) + try: + self.dpay._test_weights_treshold(authority) + except: + log.critical( + "The account's threshold will be reduced by %d" + % (removed_weight) + ) + authority["weight_threshold"] -= removed_weight + self.dpay._test_weights_treshold(authority) + + op = operations.Account_update(**{ + "account": account["name"], + permission: authority, + "memo_key": account["memo_key"], + "json_metadata": account["json_metadata"], + "prefix": self.dpay.prefix, + }) + if permission == "owner": + return self.dpay.finalizeOp(op, account, "owner", **kwargs) + else: + return self.dpay.finalizeOp(op, account, "active", **kwargs) + + def feed_history(self, limit=None, start_author=None, start_permlink=None, + account=None): + """ stream the feed entries of an account in reverse time order. + Note that RPC nodes keep a limited history of entries for the + user feed. Older entries may not be available via this call due + to these node limitations. + + :param int limit: (optional) stream the latest `limit` + feed entries. If unset (default), all available entries + are streamed. + :param str start_author: (optional) start streaming the + replies from this author. `start_permlink=None` + (default) starts with the latest available entry. + If set, `start_permlink` has to be set as well. + :param str start_permlink: (optional) start streaming the + replies from this permlink. `start_permlink=None` + (default) starts with the latest available entry. + If set, `start_author` has to be set as well. + :param str account: (optional) the account to get replies + to (defaults to ``default_account``) + + comment_history_reverse example: + + .. code-block:: python + + from dpaycli.account import Account + acc = Account("ned") + for reply in acc.feed_history(limit=10): + print(reply) + + """ + if limit is not None: + if not isinstance(limit, integer_types) or limit <= 0: + raise AssertionError("`limit` has to be greater than 0`") + if (start_author is None and start_permlink is not None) or \ + (start_author is not None and start_permlink is None): + raise AssertionError("either both or none of `start_author` and " + "`start_permlink` have to be set") + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + feed_count = 0 + while True: + query_limit = 100 + if limit is not None: + query_limit = min(limit - feed_count + 1, query_limit) + from .discussions import Query, Discussions_by_feed + query = Query(start_author=start_author, + start_permlink=start_permlink, limit=query_limit, + tag=account['name']) + results = Discussions_by_feed(query, dpay_instance=self.dpay) + if len(results) == 0 or (start_permlink and len(results) == 1): + return + if feed_count > 0 and start_permlink: + results = results[1:] # strip duplicates from previous iteration + for entry in results: + feed_count += 1 + yield entry + start_permlink = entry['permlink'] + start_author = entry['author'] + if feed_count == limit: + return + + def blog_history(self, limit=None, start=-1, reblogs=True, account=None): + """ stream the blog entries done by an account in reverse time order. + Note that RPC nodes keep a limited history of entries for the + user blog. Older blog posts of an account may not be available + via this call due to these node limitations. + + :param int limit: (optional) stream the latest `limit` + blog entries. If unset (default), all available blog + entries are streamed. + :param int start: (optional) start streaming the blog + entries from this index. `start=-1` (default) starts + with the latest available entry. + :param bool reblogs: (optional) if set `True` (default) + reblogs / reposts are included. If set `False`, + reblogs/reposts are omitted. + :param str account: (optional) the account to stream blog + entries for (defaults to ``default_account``) + + blog_history_reverse example: + + .. code-block:: python + + from dpaycli.account import Account + acc = Account("dsocialblog") + for post in acc.blog_history(limit=10): + print(post) + + """ + if limit is not None: + if not isinstance(limit, integer_types) or limit <= 0: + raise AssertionError("`limit` has to be greater than 0`") + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + post_count = 0 + start_permlink = None + start_author = None + while True: + query_limit = 100 + if limit is not None and reblogs: + query_limit = min(limit - post_count + 1, query_limit) + if not start_permlink: + # first iteration uses `get_blog` + results = self.get_blog(start_entry_id=start, + account=account, + limit=query_limit) + else: + # all following iterations use `get_discussions_by_blog` + from .discussions import Query, Discussions_by_blog + query = Query(start_author=start_author, + start_permlink=start_permlink, + limit=query_limit, tag=account['name']) + results = Discussions_by_blog(query, + dpay_instance=self.dpay) + if len(results) == 0 or (start_permlink and len(results) == 1): + return + if start_permlink: + results = results[1:] # strip duplicates from previous iteration + for post in results: + if post['author'] == '': + continue + if (reblogs or post['author'] == account['name']): + post_count += 1 + yield post + start_permlink = post['permlink'] + start_author = post['author'] + if post_count == limit: + return + + def comment_history(self, limit=None, start_permlink=None, + account=None): + """ stream the comments done by an account in reverse time order. + Note that RPC nodes keep a limited history of entries + for the user comments. Older comments by an account + may not be available via this call due to these node + limitations. + + :param int limit: (optional) stream the latest `limit` + comments. If unset (default), all available comments + are streamed. + :param str start_permlink: (optional) start streaming the + comments from this permlink. `start_permlink=None` + (default) starts with the latest available entry. + :param str account: (optional) the account to stream + comments for (defaults to ``default_account``) + + comment_history_reverse example: + + .. code-block:: python + + from dpaycli.account import Account + acc = Account("ned") + for comment in acc.comment_history(limit=10): + print(comment) + + """ + if limit is not None: + if not isinstance(limit, integer_types) or limit <= 0: + raise AssertionError("`limit` has to be greater than 0`") + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + comment_count = 0 + while True: + query_limit = 100 + if limit is not None: + query_limit = min(limit - comment_count + 1, query_limit) + from .discussions import Query, Discussions_by_comments + query = Query(start_author=account['name'], + start_permlink=start_permlink, + limit=query_limit, tag=account['name']) + results = Discussions_by_comments(query, + dpay_instance=self.dpay) + if len(results) == 0 or (start_permlink and len(results) == 1): + return + if comment_count > 0 and start_permlink: + results = results[1:] # strip duplicates from previous iteration + for comment in results: + if comment["permlink"] == '': + continue + comment_count += 1 + yield comment + start_permlink = comment['permlink'] + if comment_count == limit: + return + + def reply_history(self, limit=None, start_author=None, + start_permlink=None, account=None): + """ stream the replies to an account in reverse time order. + Note that RPC nodes keep a limited history of entries + for the replies to an author. Older replies to an account + may not be available via this call due to these node + limitations. + + :param int limit: (optional) stream the latest `limit` + replies. If unset (default), all available replies + are streamed. + :param str start_author: (optional) start streaming the + replies from this author. `start_permlink=None` + (default) starts with the latest available entry. + If set, `start_permlink` has to be set as well. + :param str start_permlink: (optional) start streaming the + replies from this permlink. `start_permlink=None` + (default) starts with the latest available entry. + If set, `start_author` has to be set as well. + :param str account: (optional) the account to get replies + to (defaults to ``default_account``) + + comment_history_reverse example: + + .. code-block:: python + + from dpaycli.account import Account + acc = Account("ned") + for reply in acc.reply_history(limit=10): + print(reply) + + """ + if limit is not None: + if not isinstance(limit, integer_types) or limit <= 0: + raise AssertionError("`limit` has to be greater than 0`") + if (start_author is None and start_permlink is not None) or \ + (start_author is not None and start_permlink is None): + raise AssertionError("either both or none of `start_author` and " + "`start_permlink` have to be set") + + if account is None: + account = self + else: + account = Account(account, dpay_instance=self.dpay) + + if start_author is None: + start_author = account['name'] + + reply_count = 0 + while True: + query_limit = 100 + if limit is not None: + query_limit = min(limit - reply_count + 1, query_limit) + from .discussions import Query, Replies_by_last_update + query = Query(start_parent_author=start_author, + start_permlink=start_permlink, + limit=query_limit) + results = Replies_by_last_update(query, + dpay_instance=self.dpay) + if len(results) == 0 or (start_permlink and len(results) == 1): + return + if reply_count > 0 and start_permlink: + results = results[1:] # strip duplicates from previous iteration + for reply in results: + if reply['author'] == '': + continue + reply_count += 1 + yield reply + start_author = reply['author'] + start_permlink = reply['permlink'] + if reply_count == limit: + return + + +class AccountsObject(list): + def printAsTable(self): + t = PrettyTable(["Name"]) + t.align = "l" + for acc in self: + t.add_row([acc['name']]) + print(t) + + def print_summarize_table(self, tag_type="Follower", return_str=False, **kwargs): + t = PrettyTable([ + "Key", "Value" + ]) + t.align = "r" + t.add_row([tag_type + " count", str(len(self))]) + own_mvest = [] + eff_sp = [] + rep = [] + last_vote_h = [] + last_post_d = [] + no_vote = 0 + no_post = 0 + for f in self: + rep.append(f.rep) + own_mvest.append(f.balances["available"][2].amount / 1e6) + eff_sp.append(f.get_dpay_power()) + last_vote = addTzInfo(datetime.utcnow()) - (f["last_vote_time"]) + if last_vote.days >= 365: + no_vote += 1 + else: + last_vote_h.append(last_vote.total_seconds() / 60 / 60) + last_post = addTzInfo(datetime.utcnow()) - (f["last_root_post"]) + if last_post.days >= 365: + no_post += 1 + else: + last_post_d.append(last_post.total_seconds() / 60 / 60 / 24) + + t.add_row(["Summed MVest value", "%.2f" % sum(own_mvest)]) + if (len(rep) > 0): + t.add_row(["Mean Rep.", "%.2f" % (sum(rep) / len(rep))]) + t.add_row(["Max Rep.", "%.2f" % (max(rep))]) + if (len(eff_sp) > 0): + t.add_row(["Summed eff. BP", "%.2f" % sum(eff_sp)]) + t.add_row(["Mean eff. BP", "%.2f" % (sum(eff_sp) / len(eff_sp))]) + t.add_row(["Max eff. BP", "%.2f" % max(eff_sp)]) + if (len(last_vote_h) > 0): + t.add_row(["Mean last vote diff in hours", "%.2f" % (sum(last_vote_h) / len(last_vote_h))]) + if len(last_post_d) > 0: + t.add_row(["Mean last post diff in days", "%.2f" % (sum(last_post_d) / len(last_post_d))]) + t.add_row([tag_type + " without vote in 365 days", no_vote]) + t.add_row([tag_type + " without post in 365 days", no_post]) + if return_str: + return t.get_string(**kwargs) + else: + print(t.get_string(**kwargs)) + + +class Accounts(AccountsObject): + """ Obtain a list of accounts + + :param list name_list: list of accounts to fetch + :param int batch_limit: (optional) maximum number of accounts + to fetch per call, defaults to 100 + :param dpay dpay_instance: DPay() instance to use when + accessing a RPCcreator = Account(creator, dpay_instance=self) + """ + def __init__(self, name_list, batch_limit=100, lazy=False, full=True, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + if not self.dpay.is_connected(): + return + accounts = [] + name_cnt = 0 + + while name_cnt < len(name_list): + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + accounts += self.dpay.rpc.find_accounts({'accounts': name_list[name_cnt:batch_limit + name_cnt]}, api="database")["accounts"] + else: + accounts += self.dpay.rpc.get_accounts(name_list[name_cnt:batch_limit + name_cnt]) + name_cnt += batch_limit + + super(Accounts, self).__init__( + [ + Account(x, lazy=lazy, full=full, dpay_instance=self.dpay) + for x in accounts + ] + ) diff --git a/dpaycli/aes.py b/dpaycli/aes.py new file mode 100755 index 0000000..517bb07 --- /dev/null +++ b/dpaycli/aes.py @@ -0,0 +1,54 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import chr +from builtins import object +import hashlib +import base64 +try: + from Cryptodome import Random + from Cryptodome.Cipher import AES +except ImportError: + try: + from Crypto import Random + from Crypto.Cipher import AES + except ImportError: + raise ImportError("Missing dependency: pyCryptodome") + + +class AESCipher(object): + """ + A classical AES Cipher. Can use any size of data and any size of password thanks to padding. + Also ensure the coherence and the type of the data with a unicode to byte converter. + """ + def __init__(self, key): + self.bs = 32 + self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest() + + @staticmethod + def str_to_bytes(data): + u_type = type(b''.decode('utf8')) + if isinstance(data, u_type): + return data.encode('utf8') + return data + + def _pad(self, s): + return s + (self.bs - len(s) % self.bs) * AESCipher.str_to_bytes(chr(self.bs - len(s) % self.bs)) + + @staticmethod + def _unpad(s): + return s[:-ord(s[len(s) - 1:])] + + def encrypt(self, raw): + raw = self._pad(AESCipher.str_to_bytes(raw)) + iv = Random.new().read(AES.block_size) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8') + + def decrypt(self, enc): + enc = base64.b64decode(enc) + iv = enc[:AES.block_size] + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') diff --git a/dpaycli/amount.py b/dpaycli/amount.py new file mode 100755 index 0000000..eedaa41 --- /dev/null +++ b/dpaycli/amount.py @@ -0,0 +1,359 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +from future.utils import python_2_unicode_compatible +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from dpaycli.instance import shared_dpay_instance +from dpaycli.asset import Asset + + +def check_asset(other, self): + if isinstance(other, dict) and "asset" in other and isinstance(self, dict) and "asset" in self: + if not other["asset"] == self["asset"]: + raise AssertionError() + else: + if not other == self: + raise AssertionError() + + +@python_2_unicode_compatible +class Amount(dict): + """ This class deals with Amounts of any asset to simplify dealing with the tuple:: + + (amount, asset) + + :param list args: Allows to deal with different representations of an amount + :param float amount: Let's create an instance with a specific amount + :param str asset: Let's you create an instance with a specific asset (symbol) + :param dpay.dpay.DPay dpay_instance: DPay instance + :returns: All data required to represent an Amount/Asset + :rtype: dict + :raises ValueError: if the data provided is not recognized + + Way to obtain a proper instance: + + * ``args`` can be a string, e.g.: "1 BBD" + * ``args`` can be a dictionary containing ``amount`` and ``asset_id`` + * ``args`` can be a dictionary containing ``amount`` and ``asset`` + * ``args`` can be a list of a ``float`` and ``str`` (symbol) + * ``args`` can be a list of a ``float`` and a :class:`dpaycli.asset.Asset` + * ``amount`` and ``asset`` are defined manually + + An instance is a dictionary and comes with the following keys: + + * ``amount`` (float) + * ``symbol`` (str) + * ``asset`` (instance of :class:`dpaycli.asset.Asset`) + + Instances of this class can be used in regular mathematical expressions + (``+-*/%``) such as: + + .. testcode:: + + from dpaycli.amount import Amount + from dpaycli.asset import Asset + a = Amount("1 BEX") + b = Amount(1, "BEX") + c = Amount("20", Asset("BEX")) + a + b + a * 2 + a += b + a /= 2.0 + + .. testoutput:: + + 2.000 BEX + 2.000 BEX + + """ + def __init__(self, amount, asset=None, new_appbase_format=True, dpay_instance=None): + self["asset"] = {} + self.new_appbase_format = new_appbase_format + self.dpay = dpay_instance or shared_dpay_instance() + if amount and asset is None and isinstance(amount, Amount): + # Copy Asset object + self["amount"] = amount["amount"] + self["symbol"] = amount["symbol"] + self["asset"] = amount["asset"] + + elif amount and asset is None and isinstance(amount, list) and len(amount) == 3: + # Copy Asset object + self["amount"] = int(amount[0]) / (10 ** amount[1]) + self["asset"] = Asset(amount[2], dpay_instance=self.dpay) + self["symbol"] = self["asset"]["symbol"] + + elif amount and asset is None and isinstance(amount, dict) and "amount" in amount and "nai" in amount and "precision" in amount: + # Copy Asset object + self.new_appbase_format = True + self["amount"] = int(amount["amount"]) / (10 ** amount["precision"]) + self["asset"] = Asset(amount["nai"], dpay_instance=self.dpay) + self["symbol"] = self["asset"]["symbol"] + + elif amount is not None and asset is None and isinstance(amount, string_types): + self["amount"], self["symbol"] = amount.split(" ") + self["asset"] = Asset(self["symbol"], dpay_instance=self.dpay) + + elif (amount and asset is None and + isinstance(amount, dict) and + "amount" in amount and + "asset_id" in amount): + self["asset"] = Asset(amount["asset_id"], dpay_instance=self.dpay) + self["symbol"] = self["asset"]["symbol"] + self["amount"] = int(amount["amount"]) / 10 ** self["asset"]["precision"] + + elif (amount and asset is None and + isinstance(amount, dict) and + "amount" in amount and + "asset" in amount): + self["asset"] = Asset(amount["asset"], dpay_instance=self.dpay) + self["symbol"] = self["asset"]["symbol"] + self["amount"] = int(amount["amount"]) / 10 ** self["asset"]["precision"] + + elif amount and asset and isinstance(asset, Asset): + self["amount"] = amount + self["symbol"] = asset["symbol"] + self["asset"] = asset + + elif amount and asset and isinstance(asset, string_types): + self["amount"] = amount + self["asset"] = Asset(asset, dpay_instance=self.dpay) + self["symbol"] = self["asset"]["symbol"] + + elif isinstance(amount, (integer_types, float)) and asset and isinstance(asset, Asset): + self["amount"] = amount + self["asset"] = asset + self["symbol"] = self["asset"]["symbol"] + + elif isinstance(amount, (integer_types, float)) and asset and isinstance(asset, dict): + self["amount"] = amount + self["asset"] = asset + self["symbol"] = self["asset"]["symbol"] + + elif isinstance(amount, (integer_types, float)) and asset and isinstance(asset, string_types): + self["amount"] = amount + self["asset"] = Asset(asset, dpay_instance=self.dpay) + self["symbol"] = asset + else: + raise ValueError + + # make sure amount is a float + self["amount"] = float(self["amount"]) + + def copy(self): + """ Copy the instance and make sure not to use a reference + """ + return Amount( + amount=self["amount"], + asset=self["asset"].copy(), + new_appbase_format=self.new_appbase_format, + dpay_instance=self.dpay) + + @property + def amount(self): + """ Returns the amount as float + """ + return self["amount"] + + @property + def symbol(self): + """ Returns the symbol of the asset + """ + return self["symbol"] + + def tuple(self): + return float(self), self.symbol + + @property + def asset(self): + """ Returns the asset as instance of :class:`dpay.asset.Asset` + """ + if not self["asset"]: + self["asset"] = Asset(self["symbol"], dpay_instance=self.dpay) + return self["asset"] + + def json(self): + if self.dpay.is_connected() and self.dpay.rpc.get_use_appbase(): + if self.new_appbase_format: + return {'amount': str(int(self)), 'nai': self["asset"]["asset"], 'precision': self["asset"]["precision"]} + else: + return [str(int(self)), self["asset"]["precision"], self["asset"]["asset"]] + else: + return str(self) + + def __str__(self): + return "{:.{prec}f} {}".format( + self["amount"], + self["symbol"], + prec=self["asset"]["precision"] + ) + + def __float__(self): + return float(self["amount"]) + + def __int__(self): + return int(self["amount"] * 10 ** self["asset"]["precision"]) + + def __add__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + a["amount"] += other["amount"] + else: + a["amount"] += float(other) + return a + + def __sub__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + a["amount"] -= other["amount"] + else: + a["amount"] -= float(other) + return a + + def __mul__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + a["amount"] *= other["amount"] + else: + a["amount"] *= other + return a + + def __floordiv__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + from .price import Price + return Price(self, other, dpay_instance=self.dpay) + else: + a["amount"] //= other + return a + + def __div__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + from .price import Price + return Price(self, other, dpay_instance=self.dpay) + else: + a["amount"] /= other + return a + + def __mod__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + a["amount"] %= other["amount"] + else: + a["amount"] %= other + return a + + def __pow__(self, other): + a = self.copy() + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + a["amount"] **= other["amount"] + else: + a["amount"] **= other + return a + + def __iadd__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + self["amount"] += other["amount"] + else: + self["amount"] += other + return self + + def __isub__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + self["amount"] -= other["amount"] + else: + self["amount"] -= other + return self + + def __imul__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + self["amount"] *= other["amount"] + else: + self["amount"] *= other + return self + + def __idiv__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] / other["amount"] + else: + self["amount"] /= other + return self + + def __ifloordiv__(self, other): + if isinstance(other, Amount): + self["amount"] //= other["amount"] + else: + self["amount"] //= other + return self + + def __imod__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + self["amount"] %= other["amount"] + else: + self["amount"] %= other + return self + + def __ipow__(self, other): + self["amount"] **= other + return self + + def __lt__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] < other["amount"] + else: + return self["amount"] < float(other or 0) + + def __le__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] <= other["amount"] + else: + return self["amount"] <= float(other or 0) + + def __eq__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] == other["amount"] + else: + return self["amount"] == float(other or 0) + + def __ne__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] != other["amount"] + else: + return self["amount"] != float(other or 0) + + def __ge__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] >= other["amount"] + else: + return self["amount"] >= float(other or 0) + + def __gt__(self, other): + if isinstance(other, Amount): + check_asset(other["asset"], self["asset"]) + return self["amount"] > other["amount"] + else: + return self["amount"] > float(other or 0) + + __repr__ = __str__ + __truediv__ = __div__ + __truemul__ = __mul__ diff --git a/dpaycli/asciichart.py b/dpaycli/asciichart.py new file mode 100755 index 0000000..306608d --- /dev/null +++ b/dpaycli/asciichart.py @@ -0,0 +1,271 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +import sys +from math import cos +from math import sin +from math import pi +from math import floor +from math import ceil + +# Basic idea from https://github.com/kroitor/asciichart +# ╱ ╲ ╳ ─ └┲┲┲─ + + +class AsciiChart(object): + """Can be used to plot price and trade history + + :param int height: Height of the plot + :param int width: Width of the plot + :param int offset: Offset between tick strings and y-axis (default is 3) + :param str placeholder: Defines how the numbers on the y-axes are formated (default is '{:8.2f} ') + :param str charset: sets the charset for plotting, uft8 or ascii (default: utf8) + """ + def __init__(self, height=None, width=None, offset=3, placeholder=u'{:8.2f} ', charset=u'utf8'): + self.height = height + self.width = width + self.offset = offset + self.placeholder = placeholder + self.clear_data() + if charset == u'ascii' or sys.version_info[0] < 3: + self.char_set = {'first_axis_elem': u'|', + 'axis_elem': u'|', + 'axis_elem_with_graph': u'|', + 'curve_ar': u'\\', + 'curve_lb': u'\\', + 'curve_br': u'/', + 'curve_la': u'/', + 'curve_hl': u'-', + 'curve_vl': u'|', + 'curve_hl_dot': u'-', + 'curve_vl_dot': u'|'} + else: + self.char_set = {'first_axis_elem': u'┼', + 'axis_elem': u'┤', + 'axis_elem_with_graph': u'┼', + 'curve_ar': u'╰', + 'curve_lb': u'╮', + 'curve_br': u'╭', + 'curve_la': u'╯', + 'curve_hl': u'─', + 'curve_vl': u'│', + 'curve_hl_dot': u'┈', + 'curve_vl_dot': u'┊'} + + def clear_data(self): + """Clears all data""" + self.canvas = [] + self.minimum = None + self.maximum = None + self.n = None + self.skip = 1 + + def set_parameter(self, height=None, offset=None, placeholder=None): + """Can be used to change parameter""" + if height is not None: + self.height = height + if offset is not None: + self.offset = offset + if placeholder is not None: + self.placeholder = placeholder + self._calc_plot_parameter() + + def adapt_on_series(self, series): + """Calculates the minimum, maximum and length from the given list + + :param list series: time series to plot + + .. testcode:: + + from dpaycli.asciichart import AsciiChart + chart = AsciiChart() + series = [1, 2, 3, 7, 2, -4, -2] + chart.adapt_on_series(series) + chart.new_chart() + chart.add_axis() + chart.add_curve(series) + print(str(chart)) + + """ + self.minimum = min(series) + self.maximum = max(series) + self.n = len(series) + self._calc_plot_parameter() + + def _calc_plot_parameter(self, minimum=None, maximum=None, n=None): + """Calculates parameter from minimum, maximum and length + """ + if minimum is not None: + self.minimum = minimum + if maximum is not None: + self.maximum = maximum + if n is not None: + self.n = n + if self.n is None or self.maximum is None or self.minimum is None: + return + interval = abs(float(self.maximum) - float(self.minimum)) + if interval == 0: + interval = 1 + if self.height is None: + self.height = interval + self.ratio = self.height / interval + self.min2 = floor(float(self.minimum) * self.ratio) + self.max2 = ceil(float(self.maximum) * self.ratio) + if self.min2 == self.max2: + self.max2 += 1 + intmin2 = int(self.min2) + intmax2 = int(self.max2) + self.rows = abs(intmax2 - intmin2) + if self.width is not None: + self.skip = int(self.n / self.width) + if self.skip < 1: + self.skip = 1 + else: + self.skip = 1 + + def plot(self, series, return_str=False): + """All in one function for plotting + + .. testcode:: + + from dpaycli.asciichart import AsciiChart + chart = AsciiChart() + series = [1, 2, 3, 7, 2, -4, -2] + chart.plot(series) + """ + self.clear_data() + self.adapt_on_series(series) + self.new_chart() + self.add_axis() + self.add_curve(series) + if not return_str: + print(str(self)) + else: + return str(self) + + def new_chart(self, minimum=None, maximum=None, n=None): + """Clears the canvas + + .. testcode:: + + from dpaycli.asciichart import AsciiChart + chart = AsciiChart() + series = [1, 2, 3, 7, 2, -4, -2] + chart.adapt_on_series(series) + chart.new_chart() + chart.add_axis() + chart.add_curve(series) + print(str(chart)) + + """ + if minimum is not None: + self.minimum = minimum + if maximum is not None: + self.maximum = maximum + if n is not None: + self.n = n + self._calc_plot_parameter() + self.canvas = [[u' '] * (int(self.n / self.skip) + self.offset) for i in range(self.rows + 1)] + + def add_axis(self): + """Adds a y-axis to the canvas + + .. testcode:: + + from dpaycli.asciichart import AsciiChart + chart = AsciiChart() + series = [1, 2, 3, 7, 2, -4, -2] + chart.adapt_on_series(series) + chart.new_chart() + chart.add_axis() + chart.add_curve(series) + print(str(chart)) + + """ + # axis and labels + interval = abs(float(self.maximum) - float(self.minimum)) + intmin2 = int(self.min2) + intmax2 = int(self.max2) + for y in range(intmin2, intmax2 + 1): + label = self.placeholder.format(float(self.maximum) - ((y - intmin2) * interval / self.rows)) + if label: + self._set_y_axis_elem(y, label) + + def _set_y_axis_elem(self, y, label): + intmin2 = int(self.min2) + self.canvas[y - intmin2][max(self.offset - len(label), 0)] = label + if y == 0: + self.canvas[y - intmin2][self.offset - 1] = self.char_set["first_axis_elem"] + else: + self.canvas[y - intmin2][self.offset - 1] = self.char_set["axis_elem"] + + def _map_y(self, y_float): + intmin2 = int(self.min2) + return int(round(y_float * self.ratio) - intmin2) + + def add_curve(self, series): + """Add a curve to the canvas + + :param list series: List width float data points + + .. testcode:: + + from dpaycli.asciichart import AsciiChart + chart = AsciiChart() + series = [1, 2, 3, 7, 2, -4, -2] + chart.adapt_on_series(series) + chart.new_chart() + chart.add_axis() + chart.add_curve(series) + print(str(chart)) + + """ + if self.n is None: + self.adapt_on_series(series) + if len(self.canvas) == 0: + self.new_chart() + y0 = self._map_y(series[0]) + self._set_elem(y0, -1, self.char_set["axis_elem_with_graph"]) + for x in range(0, len(series[::self.skip]) - 1): + y0 = self._map_y(series[::self.skip][x + 0]) + y1 = self._map_y(series[::self.skip][x + 1]) + if y0 == y1: + self._draw_h_line(y0, x, x + 1, line=self.char_set["curve_hl"]) + else: + self._draw_diag(y0, y1, x) + start = min(y0, y1) + 1 + end = max(y0, y1) + self._draw_v_line(start, end, x, line=self.char_set["curve_vl"]) + + def _draw_diag(self, y0, y1, x): + """Plot diagonal element""" + if y0 > y1: + c1 = self.char_set["curve_ar"] + c0 = self.char_set["curve_lb"] + else: + c1 = self.char_set["curve_br"] + c0 = self.char_set["curve_la"] + self._set_elem(y1, x, c1) + self._set_elem(y0, x, c0) + + def _draw_h_line(self, y, x_start, x_end, line=u'-'): + """Plot horizontal line""" + for x in range(x_start, x_end): + self._set_elem(y, x, line) + + def _draw_v_line(self, y_start, y_end, x, line=u'|'): + """Plot vertical line""" + for y in range(y_start, y_end): + self._set_elem(y, x, line) + + def _set_elem(self, y, x, c): + """Plot signle element into canvas""" + self.canvas[self.rows - y][x + self.offset] = c + + def __repr__(self): + return '\n'.join([''.join(row) for row in self.canvas]) + + __str__ = __repr__ diff --git a/dpaycli/asset.py b/dpaycli/asset.py new file mode 100755 index 0000000..558a92e --- /dev/null +++ b/dpaycli/asset.py @@ -0,0 +1,85 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import json +from .exceptions import AssetDoesNotExistsException +from .blockchainobject import BlockchainObject + + +class Asset(BlockchainObject): + """ Deals with Assets of the network. + + :param str Asset: Symbol name or object id of an asset + :param bool lazy: Lazy loading + :param bool full: Also obtain bitasset-data and dynamic asset dat + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :returns: All data of an asset + + .. note:: This class comes with its own caching function to reduce the + load on the API server. Instances of this class can be + refreshed with ``Asset.refresh()``. + """ + type_id = 3 + + def __init__( + self, + asset, + lazy=False, + full=False, + dpay_instance=None + ): + self.full = full + super(Asset, self).__init__( + asset, + lazy=lazy, + full=full, + dpay_instance=dpay_instance + ) + # self.refresh() + + def refresh(self): + """ Refresh the data from the API server + """ + self.chain_params = self.dpay.get_network() + if self.chain_params is None: + from dpaycligraphenebase.chains import known_chains + self.chain_params = known_chains["DPAY"] + self["asset"] = "" + found_asset = False + for asset in self.chain_params["chain_assets"]: + if self.identifier in [asset["symbol"], asset["asset"], asset["id"]]: + self["asset"] = asset["asset"] + self["precision"] = asset["precision"] + self["id"] = asset["id"] + self["symbol"] = asset["symbol"] + found_asset = True + break + if not found_asset: + raise AssetDoesNotExistsException(self.identifier + " chain_assets:" + str(self.chain_params["chain_assets"])) + + @property + def symbol(self): + return self["symbol"] + + @property + def asset(self): + return self["asset"] + + @property + def precision(self): + return self["precision"] + + def __eq__(self, other): + if isinstance(other, (Asset, dict)): + return self["symbol"] == other["symbol"] and self["asset"] == other["asset"] and self["precision"] == other["precision"] + else: + return self["symbol"] == other + + def __ne__(self, other): + if isinstance(other, (Asset, dict)): + return self["symbol"] != other["symbol"] or self["asset"] != other["asset"] or self["precision"] != other["precision"] + else: + return self["symbol"] != other diff --git a/dpaycli/block.py b/dpaycli/block.py new file mode 100755 index 0000000..faa12ea --- /dev/null +++ b/dpaycli/block.py @@ -0,0 +1,375 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from datetime import datetime, timedelta, date +import json +from .exceptions import BlockDoesNotExistsException +from .utils import parse_time, formatTimeString +from .blockchainobject import BlockchainObject +from dpaycliapi.exceptions import ApiNotSupported +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type + + +class Block(BlockchainObject): + """ Read a single block from the chain + + :param int block: block number + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :param bool lazy: Use lazy loading + :param bool only_ops: Includes only operations, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + + Instances of this class are dictionaries that come with additional + methods (see below) that allow dealing with a block and its + corresponding functions. + + When only_virtual_ops is set to True, only_ops is always set to True. + + In addition to the block data, the block number is stored as self["id"] or self.identifier. + + .. code-block:: python + + >>> from dpaycli.block import Block + >>> block = Block(1) + >>> print(block) + + + .. note:: This class comes with its own caching function to reduce the + load on the API server. Instances of this class can be + refreshed with ``Account.refresh()``. + + """ + def __init__( + self, + block, + only_ops=False, + only_virtual_ops=False, + full=True, + lazy=False, + dpay_instance=None + ): + """ Initilize a block + + :param int block: block number + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :param bool lazy: Use lazy loading + :param bool only_ops: Includes only operations, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + + """ + self.full = full + self.lazy = lazy + self.only_ops = only_ops + self.only_virtual_ops = only_virtual_ops + if isinstance(block, float): + block = int(block) + elif isinstance(block, dict): + block = self._parse_json_data(block) + super(Block, self).__init__( + block, + lazy=lazy, + full=full, + dpay_instance=dpay_instance + ) + + def _parse_json_data(self, block): + parse_times = [ + "timestamp", + ] + for p in parse_times: + if p in block and isinstance(block.get(p), string_types): + block[p] = formatTimeString(block.get(p, "1970-01-01T00:00:00")) + if "transactions" in block: + for i in range(len(block["transactions"])): + if 'expiration' in block["transactions"][i] and isinstance(block["transactions"][i]["expiration"], string_types): + block["transactions"][i]["expiration"] = formatTimeString(block["transactions"][i]["expiration"]) + elif "operations" in block: + for i in range(len(block["operations"])): + if 'timestamp' in block["operations"][i] and isinstance(block["operations"][i]["timestamp"], string_types): + block["operations"][i]["timestamp"] = formatTimeString(block["operations"][i]["timestamp"]) + return block + + def json(self): + output = self.copy() + parse_times = [ + "timestamp", + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + + if "transactions" in output: + for i in range(len(output["transactions"])): + if 'expiration' in output["transactions"][i] and isinstance(output["transactions"][i]["expiration"], (datetime, date)): + output["transactions"][i]["expiration"] = formatTimeString(output["transactions"][i]["expiration"]) + elif "operations" in output: + for i in range(len(output["operations"])): + if 'timestamp' in output["operations"][i] and isinstance(output["operations"][i]["timestamp"], (datetime, date)): + output["operations"][i]["timestamp"] = formatTimeString(output["operations"][i]["timestamp"]) + + ret = json.loads(str(json.dumps(output))) + output = self._parse_json_data(output) + return ret + + def refresh(self): + """ Even though blocks never change, you freshly obtain its contents + from an API with this method + """ + if self.identifier is None: + return + if not self.dpay.is_connected(): + return + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.only_ops or self.only_virtual_ops: + if self.dpay.rpc.get_use_appbase(): + try: + ops = self.dpay.rpc.get_ops_in_block({"block_num": self.identifier, 'only_virtual': self.only_virtual_ops}, api="account_history")["ops"] + except ApiNotSupported: + ops = self.dpay.rpc.get_ops_in_block(self.identifier, self.only_virtual_ops, api="condenser") + else: + ops = self.dpay.rpc.get_ops_in_block(self.identifier, self.only_virtual_ops) + if bool(ops): + block = {'block': ops[0]["block"], + 'timestamp': ops[0]["timestamp"], + 'operations': ops} + else: + block = {} + else: + if self.dpay.rpc.get_use_appbase(): + try: + block = self.dpay.rpc.get_block({"block_num": self.identifier}, api="block") + if block and "block" in block: + block = block["block"] + except ApiNotSupported: + block = self.dpay.rpc.get_block(self.identifier, api="condenser") + else: + block = self.dpay.rpc.get_block(self.identifier) + if not block: + raise BlockDoesNotExistsException("output: %s of identifier %s" % (str(block), str(self.identifier))) + block = self._parse_json_data(block) + super(Block, self).__init__(block, lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + + @property + def block_num(self): + """Returns the block number""" + if "block_id" in self: + return int(self['block_id'][:8], base=16) + else: + return None + + def time(self): + """Return a datetime instance for the timestamp of this block""" + return self['timestamp'] + + @property + def transactions(self): + """ Returns all transactions as list""" + if self.only_ops or self.only_virtual_ops: + return list() + trxs = [] + if "transactions" not in self: + return [] + trx_id = 0 + for trx in self["transactions"]: + trx_new = {"transaction_id": self['transaction_ids'][trx_id]} + trx_new.update(trx.copy()) + trx_new.update({"block_num": self.block_num, + "transaction_num": trx_id}) + trxs.append(trx_new) + trx_id += 1 + return trxs + + @property + def operations(self): + """Returns all block operations as list""" + if self.only_ops or self.only_virtual_ops: + return self["operations"] + ops = [] + trxs = [] + if "transactions" in self: + trxs = self["transactions"] + for tx in trxs: + if "operations" not in tx: + continue + for op in tx["operations"]: + # Replace opid by op name + # op[0] = getOperationNameForId(op[0]) + if isinstance(op, list): + ops.append(list(op)) + else: + ops.append(op.copy()) + return ops + + @property + def json_transactions(self): + """ Returns all transactions as list, all dates are strings.""" + if self.only_ops or self.only_virtual_ops: + return list() + trxs = [] + if "transactions" not in self: + return [] + trx_id = 0 + for trx in self["transactions"]: + trx_new = {"transaction_id": self['transaction_ids'][trx_id]} + trx_new.update(trx.copy()) + trx_new.update({"block_num": self.block_num, + "transaction_num": trx_id}) + if 'expiration' in trx: + p_date = trx.get('expiration', datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + trx_new.update({'expiration': formatTimeString(p_date)}) + + trxs.append(trx_new) + trx_id += 1 + return trxs + + @property + def json_operations(self): + """Returns all block operations as list, all dates are strings.""" + if self.only_ops or self.only_virtual_ops: + return self["operations"] + ops = [] + for tx in self["transactions"]: + for op in tx["operations"]: + if "operations" not in tx: + continue + # Replace opid by op name + # op[0] = getOperationNameForId(op[0]) + if isinstance(op, list): + op_new = list(op) + else: + op_new = op.copy() + if 'timestamp' in op: + p_date = op.get('timestamp', datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + op_new.update({'timestamp': formatTimeString(p_date)}) + ops.append(op_new) + return ops + + def ops_statistics(self, add_to_ops_stat=None): + """Returns a statistic with the occurrence of the different operation types""" + if add_to_ops_stat is None: + import dpayclibase.operationids + ops_stat = dpayclibase.operationids.operations.copy() + for key in ops_stat: + ops_stat[key] = 0 + else: + ops_stat = add_to_ops_stat.copy() + for op in self.operations: + if "op" in op: + op = op["op"] + if isinstance(op, dict) and 'type' in op: + op_type = op["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + else: + op_type = op[0] + ops_stat[op_type] += 1 + return ops_stat + + +class BlockHeader(BlockchainObject): + """ Read a single block header from the chain + + :param int block: block number + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :param bool lazy: Use lazy loading + + In addition to the block data, the block number is stored as self["id"] or self.identifier. + + .. code-block:: python + + >>> from dpaycli.block import BlockHeader + >>> block = BlockHeader(1) + >>> print(block) + + + """ + def __init__( + self, + block, + full=True, + lazy=False, + dpay_instance=None + ): + """ Initilize a block + + :param int block: block number + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + :param bool lazy: Use lazy loading + + """ + self.full = full + self.lazy = lazy + if isinstance(block, float): + block = int(block) + super(BlockHeader, self).__init__( + block, + lazy=lazy, + full=full, + dpay_instance=dpay_instance + ) + + def refresh(self): + """ Even though blocks never change, you freshly obtain its contents + from an API with this method + """ + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + block = self.dpay.rpc.get_block_header({"block_num": self.identifier}, api="block") + if "header" in block: + block = block["header"] + else: + block = self.dpay.rpc.get_block_header(self.identifier) + if not block: + raise BlockDoesNotExistsException(str(self.identifier)) + block = self._parse_json_data(block) + super(BlockHeader, self).__init__( + block, lazy=self.lazy, full=self.full, + dpay_instance=self.dpay + ) + + def time(self): + """ Return a datetime instance for the timestamp of this block + """ + return self['timestamp'] + + @property + def block_num(self): + """Returns the block number""" + return self.identifier + + def _parse_json_data(self, block): + parse_times = [ + "timestamp", + ] + for p in parse_times: + if p in block and isinstance(block.get(p), string_types): + block[p] = formatTimeString(block.get(p, "1970-01-01T00:00:00")) + return block + + def json(self): + output = self.copy() + parse_times = [ + "timestamp", + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + return json.loads(str(json.dumps(output))) diff --git a/dpaycli/blockchain.py b/dpaycli/blockchain.py new file mode 100755 index 0000000..4bea51c --- /dev/null +++ b/dpaycli/blockchain.py @@ -0,0 +1,940 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from future.utils import python_2_unicode_compatible +from builtins import str +from builtins import range +from builtins import object +import sys +import time +import hashlib +import json +import math +from threading import Thread, Event +from time import sleep +import logging +from datetime import datetime, timedelta +from .utils import formatTimeString, addTzInfo +from .block import Block +from dpaycliapi.node import Nodes +from dpaycliapi.dpaynoderpc import DPayNodeRPC +from .exceptions import BatchedCallsNotSupported, BlockDoesNotExistsException, BlockWaitTimeExceeded, OfflineHasNoRPCException +from dpaycliapi.exceptions import NumRetriesReached +from dpaycligraphenebase.py23 import py23_bytes +from dpaycli.instance import shared_dpay_instance +from .amount import Amount +import dpaycli as stm +log = logging.getLogger(__name__) +if sys.version_info < (3, 0): + from Queue import Queue +else: + from queue import Queue +FUTURES_MODULE = None +if not FUTURES_MODULE: + try: + from concurrent.futures import ThreadPoolExecutor, wait, as_completed + FUTURES_MODULE = "futures" + # FUTURES_MODULE = None + except ImportError: + FUTURES_MODULE = None + + +# default exception handler. if you want to take some action on failed tasks +# maybe add the task back into the queue, then make your own handler and pass it in +def default_handler(name, exception, *args, **kwargs): + log.warn('%s raised %s with args %s and kwargs %s' % (name, str(exception), repr(args), repr(kwargs))) + pass + + +class Worker(Thread): + """Thread executing tasks from a given tasks queue""" + def __init__(self, name, queue, results, abort, idle, exception_handler): + Thread.__init__(self) + self.name = name + self.queue = queue + self.results = results + self.abort = abort + self.idle = idle + self.exception_handler = exception_handler + self.daemon = True + self.start() + + def run(self): + """Thread work loop calling the function with the params""" + # keep running until told to abort + while not self.abort.is_set(): + try: + # get a task and raise immediately if none available + func, args, kwargs = self.queue.get(False) + self.idle.clear() + except: + # no work to do + # if not self.idle.is_set(): + # print >> stdout, '%s is idle' % self.name + self.idle.set() + # time.sleep(1) + continue + + try: + # the function may raise + result = func(*args, **kwargs) + # print(result) + if(result is not None): + self.results.put(result) + except Exception as e: + # so we move on and handle it in whatever way the caller wanted + self.exception_handler(self.name, e, args, kwargs) + finally: + # task complete no matter what happened + self.queue.task_done() + + +# class for thread pool +class Pool: + """Pool of threads consuming tasks from a queue""" + def __init__(self, thread_count, batch_mode=True, exception_handler=default_handler): + # batch mode means block when adding tasks if no threads available to process + self.queue = Queue(thread_count if batch_mode else 0) + self.resultQueue = Queue(0) + self.thread_count = thread_count + self.exception_handler = exception_handler + self.aborts = [] + self.idles = [] + self.threads = [] + + def __del__(self): + """Tell my threads to quit""" + self.abort() + + def run(self, block=False): + """Start the threads, or restart them if you've aborted""" + # either wait for them to finish or return false if some arent + if block: + while self.alive(): + sleep(1) + elif self.alive(): + return False + + # go start them + self.aborts = [] + self.idles = [] + self.threads = [] + for n in range(self.thread_count): + abort = Event() + idle = Event() + self.aborts.append(abort) + self.idles.append(idle) + self.threads.append(Worker('thread-%d' % n, self.queue, self.resultQueue, abort, idle, self.exception_handler)) + return True + + def enqueue(self, func, *args, **kargs): + """Add a task to the queue""" + self.queue.put((func, args, kargs)) + + def join(self): + """Wait for completion of all the tasks in the queue""" + self.queue.join() + + def abort(self, block=False): + """Tell each worker that its done working""" + # tell the threads to stop after they are done with what they are currently doing + for a in self.aborts: + a.set() + # wait for them to finish if requested + while block and self.alive(): + sleep(1) + + def alive(self): + """Returns True if any threads are currently running""" + return True in [t.is_alive() for t in self.threads] + + def idle(self): + """Returns True if all threads are waiting for work""" + return False not in [i.is_set() for i in self.idles] + + def done(self): + """Returns True if not tasks are left to be completed""" + return self.queue.empty() + + def results(self, sleep_time=0): + """Get the set of results that have been processed, repeatedly call until done""" + sleep(sleep_time) + results = [] + try: + while True: + # get a result, raises empty exception immediately if none available + results.append(self.resultQueue.get(False)) + self.resultQueue.task_done() + except: + return results + return results + + +@python_2_unicode_compatible +class Blockchain(object): + """ This class allows to access the blockchain and read data + from it + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + :param str mode: (default) Irreversible block (``irreversible``) or + actual head block (``head``) + :param int max_block_wait_repetition: maximum wait repetition for next block + where each repetition is block_interval long (default is 3) + + This class let's you deal with blockchain related data and methods. + Read blockchain related data: + + .. testsetup:: + + from dpaycli.blockchain import Blockchain + chain = Blockchain() + + Read current block and blockchain info + + .. testcode:: + + print(chain.get_current_block()) + print(chain.dpay.info()) + + Monitor for new blocks. When ``stop`` is not set, monitoring will never stop. + + .. testcode:: + + blocks = [] + current_num = chain.get_current_block_num() + for block in chain.blocks(start=current_num - 99, stop=current_num): + blocks.append(block) + len(blocks) + + .. testoutput:: + + 100 + + or each operation individually: + + .. testcode:: + + ops = [] + current_num = chain.get_current_block_num() + for operation in chain.ops(start=current_num - 99, stop=current_num): + ops.append(operation) + + """ + def __init__( + self, + dpay_instance=None, + mode="irreversible", + max_block_wait_repetition=None, + data_refresh_time_seconds=900, + ): + self.dpay = dpay_instance or shared_dpay_instance() + + if mode == "irreversible": + self.mode = 'last_irreversible_block_num' + elif mode == "head": + self.mode = "head_block_number" + else: + raise ValueError("invalid value for 'mode'!") + if max_block_wait_repetition: + self.max_block_wait_repetition = max_block_wait_repetition + else: + self.max_block_wait_repetition = 3 + self.block_interval = self.dpay.get_block_interval() + + def is_irreversible_mode(self): + return self.mode == 'last_irreversible_block_num' + + def get_transaction(self, transaction_id): + """ Returns a transaction from the blockchain + + :param str transaction_id: transaction_id + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + ret = self.dpay.rpc.get_transaction({'id': transaction_id}, api="account_history") + else: + ret = self.dpay.rpc.get_transaction(transaction_id, api="database") + return ret + + def get_transaction_hex(self, transaction): + """ Returns a hexdump of the serialized binary form of a transaction. + + :param dict transaction: transaction + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + ret = self.dpay.rpc.get_transaction_hex({'trx': transaction}, api="database")["hex"] + else: + ret = self.dpay.rpc.get_transaction_hex(transaction, api="database") + return ret + + def get_current_block_num(self): + """ This call returns the current block number + + .. note:: The block number returned depends on the ``mode`` used + when instantiating from this class. + """ + props = self.dpay.get_dynamic_global_properties(False) + if props is None: + raise ValueError("Could not receive dynamic_global_properties!") + if self.mode not in props: + raise ValueError(self.mode + " is not in " + str(props)) + return int(props.get(self.mode)) + + def get_current_block(self, only_ops=False, only_virtual_ops=False): + """ This call returns the current block + + :param bool only_ops: Returns block with operations only, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + + .. note:: The block number returned depends on the ``mode`` used + when instantiating from this class. + """ + return Block( + self.get_current_block_num(), + only_ops=only_ops, + only_virtual_ops=only_virtual_ops, + dpay_instance=self.dpay + ) + + def get_estimated_block_num(self, date, estimateForwards=False, accurate=True): + """ This call estimates the block number based on a given date + + :param datetime date: block time for which a block number is estimated + + .. note:: The block number returned depends on the ``mode`` used + when instantiating from this class. + """ + last_block = self.get_current_block() + date = addTzInfo(date) + if estimateForwards: + block_offset = 10 + first_block = Block(block_offset, dpay_instance=self.dpay) + time_diff = date - first_block.time() + block_number = math.floor(time_diff.total_seconds() / self.block_interval + block_offset) + else: + time_diff = last_block.time() - date + block_number = math.floor(last_block.identifier - time_diff.total_seconds() / self.block_interval) + if block_number < 1: + block_number = 1 + + if accurate: + if block_number > last_block.identifier: + block_number = last_block.identifier + block_time_diff = timedelta(seconds=10) + while block_time_diff.total_seconds() > self.block_interval or block_time_diff.total_seconds() < -self.block_interval: + block = Block(block_number, dpay_instance=self.dpay) + block_time_diff = date - block.time() + delta = block_time_diff.total_seconds() // self.block_interval + if delta == 0 and block_time_diff.total_seconds() < 0: + delta = -1 + elif delta == 0 and block_time_diff.total_seconds() > 0: + delta = 1 + block_number += delta + if block_number < 1: + break + if block_number > last_block.identifier: + break + + return int(block_number) + + def block_time(self, block_num): + """ Returns a datetime of the block with the given block + number. + + :param int block_num: Block number + """ + return Block( + block_num, + dpay_instance=self.dpay + ).time() + + def block_timestamp(self, block_num): + """ Returns the timestamp of the block with the given block + number as integer. + + :param int block_num: Block number + """ + block_time = Block( + block_num, + dpay_instance=self.dpay + ).time() + return int(time.mktime(block_time.timetuple())) + + def blocks(self, start=None, stop=None, max_batch_size=None, threading=False, thread_num=8, only_ops=False, only_virtual_ops=False): + """ Yields blocks starting from ``start``. + + :param int start: Starting block + :param int stop: Stop at this block + :param int max_batch_size: only for appbase nodes. When not None, batch calls of are used. + Cannot be combined with threading + :param bool threading: Enables threading. Cannot be combined with batch calls + :param int thread_num: Defines the number of threads, when `threading` is set. + :param bool only_ops: Only yield operations (default: False). + Cannot be combined with ``only_virtual_ops=True``. + :param bool only_virtual_ops: Only yield virtual operations (default: False) + + .. note:: If you want instant confirmation, you need to instantiate + class:`dpaycli.blockchain.Blockchain` with + ``mode="head"``, otherwise, the call will wait until + confirmed in an irreversible block. + + """ + # Let's find out how often blocks are generated! + current_block = self.get_current_block() + current_block_num = current_block.block_num + if not start: + start = current_block_num + head_block_reached = False + if threading and FUTURES_MODULE is not None: + pool = ThreadPoolExecutor(max_workers=thread_num) + elif threading: + pool = Pool(thread_num, batch_mode=True) + if threading: + dpay_instance = [self.dpay] + nodelist = self.dpay.rpc.nodes.export_working_nodes() + for i in range(thread_num - 1): + dpay_instance.append(stm.DPay(node=nodelist, + num_retries=self.dpay.rpc.num_retries, + num_retries_call=self.dpay.rpc.num_retries_call, + timeout=self.dpay.rpc.timeout)) + # We are going to loop indefinitely + latest_block = 0 + while True: + if stop: + head_block = stop + else: + current_block_num = self.get_current_block_num() + head_block = current_block_num + if threading and not head_block_reached: + latest_block = start - 1 + result_block_nums = [] + for blocknum in range(start, head_block + 1, thread_num): + # futures = [] + i = 0 + if FUTURES_MODULE is not None: + futures = [] + block_num_list = [] + # freeze = self.dpay.rpc.nodes.freeze_current_node + num_retries = self.dpay.rpc.nodes.num_retries + # self.dpay.rpc.nodes.freeze_current_node = True + self.dpay.rpc.nodes.num_retries = thread_num + error_cnt = self.dpay.rpc.nodes.node.error_cnt + while i < thread_num and blocknum + i <= head_block: + block_num_list.append(blocknum + i) + results = [] + if FUTURES_MODULE is not None: + futures.append(pool.submit(Block, blocknum + i, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=dpay_instance[i])) + else: + pool.enqueue(Block, blocknum + i, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=dpay_instance[i]) + i += 1 + if FUTURES_MODULE is not None: + try: + results = [r.result() for r in as_completed(futures)] + except Exception as e: + log.error(str(e)) + else: + pool.run(True) + pool.join() + for result in pool.results(): + results.append(result) + pool.abort() + self.dpay.rpc.nodes.num_retries = num_retries + # self.dpay.rpc.nodes.freeze_current_node = freeze + new_error_cnt = self.dpay.rpc.nodes.node.error_cnt + self.dpay.rpc.nodes.node.error_cnt = error_cnt + if new_error_cnt > error_cnt: + self.dpay.rpc.nodes.node.error_cnt += 1 + # self.dpay.rpc.next() + + checked_results = [] + for b in results: + if b.block_num is not None and int(b.block_num) not in result_block_nums: + b["id"] = b.block_num + b.identifier = b.block_num + checked_results.append(b) + result_block_nums.append(int(b.block_num)) + + missing_block_num = list(set(block_num_list).difference(set(result_block_nums))) + while len(missing_block_num) > 0: + for blocknum in missing_block_num: + try: + block = Block(blocknum, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=self.dpay) + checked_results.append(block) + result_block_nums.append(int(block.block_num)) + except Exception as e: + log.error(str(e)) + missing_block_num = list(set(block_num_list).difference(set(result_block_nums))) + from operator import itemgetter + blocks = sorted(checked_results, key=itemgetter('id')) + for b in blocks: + if latest_block < int(b.block_num): + latest_block = int(b.block_num) + yield b + + if latest_block <= head_block: + for blocknum in range(latest_block + 1, head_block + 1): + if blocknum not in result_block_nums: + block = Block(blocknum, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=self.dpay) + result_block_nums.append(blocknum) + yield block + elif max_batch_size is not None and (head_block - start) >= max_batch_size and not head_block_reached: + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + latest_block = start - 1 + batches = max_batch_size + for blocknumblock in range(start, head_block + 1, batches): + # Get full block + if (head_block - blocknumblock) < batches: + batches = head_block - blocknumblock + 1 + for blocknum in range(blocknumblock, blocknumblock + batches - 1): + if only_virtual_ops: + if self.dpay.rpc.get_use_appbase(): + # self.dpay.rpc.get_ops_in_block({"block_num": blocknum, 'only_virtual': only_virtual_ops}, api="account_history", add_to_queue=True) + self.dpay.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=True) + else: + self.dpay.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=True) + else: + if self.dpay.rpc.get_use_appbase(): + self.dpay.rpc.get_block({"block_num": blocknum}, api="block", add_to_queue=True) + else: + self.dpay.rpc.get_block(blocknum, add_to_queue=True) + latest_block = blocknum + if batches >= 1: + latest_block += 1 + if latest_block <= head_block: + if only_virtual_ops: + if self.dpay.rpc.get_use_appbase(): + # self.dpay.rpc.get_ops_in_block({"block_num": blocknum, 'only_virtual': only_virtual_ops}, api="account_history", add_to_queue=False) + block_batch = self.dpay.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=False) + else: + block_batch = self.dpay.rpc.get_ops_in_block(blocknum, only_virtual_ops, add_to_queue=False) + else: + if self.dpay.rpc.get_use_appbase(): + block_batch = self.dpay.rpc.get_block({"block_num": latest_block}, api="block", add_to_queue=False) + else: + block_batch = self.dpay.rpc.get_block(latest_block, add_to_queue=False) + if not bool(block_batch): + raise BatchedCallsNotSupported() + blocknum = latest_block - len(block_batch) + 1 + if not isinstance(block_batch, list): + block_batch = [block_batch] + for block in block_batch: + if not bool(block): + continue + if self.dpay.rpc.get_use_appbase(): + if only_virtual_ops: + block = block["ops"] + else: + block = block["block"] + block = Block(block, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=self.dpay) + block["id"] = block.block_num + block.identifier = block.block_num + yield block + blocknum = block.block_num + else: + # Blocks from start until head block + for blocknum in range(start, head_block + 1): + # Get full block + block = self.wait_for_and_get_block(blocknum, only_ops=only_ops, only_virtual_ops=only_virtual_ops, block_number_check_cnt=5, last_current_block_num=current_block_num) + yield block + # Set new start + start = head_block + 1 + head_block_reached = True + + if stop and start > stop: + return + + # Sleep for one block + time.sleep(self.block_interval) + + def wait_for_and_get_block(self, block_number, blocks_waiting_for=None, only_ops=False, only_virtual_ops=False, block_number_check_cnt=-1, last_current_block_num=None): + """ Get the desired block from the chain, if the current head block is smaller (for both head and irreversible) + then we wait, but a maxmimum of blocks_waiting_for * max_block_wait_repetition time before failure. + + :param int block_number: desired block number + :param int blocks_waiting_for: difference between block_number and current head and defines + how many blocks we are willing to wait, positive int (default: None) + :param bool only_ops: Returns blocks with operations only, when set to True (default: False) + :param bool only_virtual_ops: Includes only virtual operations (default: False) + :param int block_number_check_cnt: limit the number of retries when greater than -1 + :param int last_current_block_num: can be used to reduce the number of get_current_block_num() api calls + + """ + if last_current_block_num is None: + last_current_block_num = self.get_current_block_num() + elif last_current_block_num - block_number < 50: + last_current_block_num = self.get_current_block_num() + + if not blocks_waiting_for: + blocks_waiting_for = max( + 1, block_number - last_current_block_num) + + repetition = 0 + # can't return the block before the chain has reached it (support future block_num) + while last_current_block_num < block_number: + repetition += 1 + time.sleep(self.block_interval) + if last_current_block_num - block_number < 50: + last_current_block_num = self.get_current_block_num() + if repetition > blocks_waiting_for * self.max_block_wait_repetition: + raise BlockWaitTimeExceeded("Already waited %d s" % (blocks_waiting_for * self.max_block_wait_repetition * self.block_interval)) + # block has to be returned properly + repetition = 0 + cnt = 0 + block = None + while (block is None or block.block_num is None or int(block.block_num) != block_number) and (block_number_check_cnt < 0 or cnt < block_number_check_cnt): + try: + block = Block(block_number, only_ops=only_ops, only_virtual_ops=only_virtual_ops, dpay_instance=self.dpay) + cnt += 1 + except BlockDoesNotExistsException: + block = None + if repetition > blocks_waiting_for * self.max_block_wait_repetition: + raise BlockWaitTimeExceeded("Already waited %d s" % (blocks_waiting_for * self.max_block_wait_repetition * self.block_interval)) + repetition += 1 + time.sleep(self.block_interval) + + return block + + def ops(self, start=None, stop=None, only_virtual_ops=False, **kwargs): + """ Blockchain.ops() is deprecated. Please use Blockchain.stream() instead. + """ + raise DeprecationWarning('Blockchain.ops() is deprecated. Please use Blockchain.stream() instead.') + + def ops_statistics(self, start, stop=None, add_to_ops_stat=None, with_virtual_ops=True, verbose=False): + """ Generates statistics for all operations (including virtual operations) starting from + ``start``. + + :param int start: Starting block + :param int stop: Stop at this block, if set to None, the current_block_num is taken + :param dict add_to_ops_stat: if set, the result is added to add_to_ops_stat + :param bool verbose: if True, the current block number and timestamp is printed + + This call returns a dict with all possible operations and their occurrence. + + """ + if add_to_ops_stat is None: + import dpayclibase.operationids + ops_stat = dpayclibase.operationids.operations.copy() + for key in ops_stat: + ops_stat[key] = 0 + else: + ops_stat = add_to_ops_stat.copy() + current_block = self.get_current_block_num() + if start > current_block: + return + if stop is None: + stop = current_block + for block in self.blocks(start=start, stop=stop, only_ops=False, only_virtual_ops=False): + if verbose: + print(block["identifier"] + " " + block["timestamp"]) + ops_stat = block.ops_statistics(add_to_ops_stat=ops_stat) + if with_virtual_ops: + for block in self.blocks(start=start, stop=stop, only_ops=True, only_virtual_ops=True): + if verbose: + print(block["identifier"] + " " + block["timestamp"]) + ops_stat = block.ops_statistics(add_to_ops_stat=ops_stat) + return ops_stat + + def stream(self, opNames=[], raw_ops=False, *args, **kwargs): + """ Yield specific operations (e.g. comments) only + + :param array opNames: List of operations to filter for + :param bool raw_ops: When set to True, it returns the unmodified operations (default: False) + :param int start: Start at this block + :param int stop: Stop at this block + :param int max_batch_size: only for appbase nodes. When not None, batch calls of are used. + Cannot be combined with threading + :param bool threading: Enables threading. Cannot be combined with batch calls + :param int thread_num: Defines the number of threads, when `threading` is set. + :param bool only_ops: Only yield operations (default: False) + Cannot be combined with ``only_virtual_ops=True`` + :param bool only_virtual_ops: Only yield virtual operations (default: False) + + The dict output is formated such that ``type`` carries the + operation type. Timestamp and block_num are taken from the + block the operation was stored in and the other keys depend + on the actual operation. + + .. note:: If you want instant confirmation, you need to instantiate + class:`dpaycli.blockchain.Blockchain` with + ``mode="head"``, otherwise, the call will wait until + confirmed in an irreversible block. + + output when `raw_ops=False` is set: + + .. code-block:: js + + { + 'type': 'transfer', + 'from': 'dsocial', + 'to': 'jared', + 'amount': '0.080 BBD', + 'memo': 'https://dsocial.io/dsocial/@dsocial/dsocial-has-arrived', + '_id': '6d4c5f2d4d8ef1918acaee4a8dce34f9da384786', + 'timestamp': datetime.datetime(2018, 5, 9, 11, 23, 6, tzinfo=), + 'block_num': 22277588, 'trx_num': 35, 'trx_id': 'cf11b2ac8493c71063ec121b2e8517ab1e0e6bea' + } + + output when `raw_ops=True` is set: + + .. code-block:: js + + { + 'block_num': 22277588, + 'op': + [ + 'transfer', + { + 'from': 'dsocial', 'to': 'jared', + 'amount': '0.080 BBD', + 'memo': 'https://dsite.io/dsocial/@dsocial/dsocial-has-arrived' + } + ], + 'timestamp': datetime.datetime(2018, 5, 9, 11, 23, 6, tzinfo=) + } + + """ + for block in self.blocks(**kwargs): + if "transactions" in block: + trx = block["transactions"] + else: + trx = [block] + block_num = 0 + trx_id = "" + _id = "" + timestamp = "" + for trx_nr in range(len(trx)): + if "operations" not in trx[trx_nr]: + continue + for event in trx[trx_nr]["operations"]: + if isinstance(event, list): + op_type, op = event + trx_id = block["transaction_ids"][trx_nr] + block_num = block.get("id") + _id = self.hash_op(event) + timestamp = block.get("timestamp") + elif isinstance(event, dict) and "type" in event and "value" in event: + op_type = event["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + op = event["value"] + trx_id = block["transaction_ids"][trx_nr] + block_num = block.get("id") + _id = self.hash_op(event) + timestamp = block.get("timestamp") + elif "op" in event and isinstance(event["op"], dict) and "type" in event["op"] and "value" in event["op"]: + op_type = event["op"]["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + op = event["op"]["value"] + trx_id = event.get("trx_id") + block_num = event.get("block") + _id = self.hash_op(event["op"]) + timestamp = event.get("timestamp") + else: + op_type, op = event["op"] + trx_id = event.get("trx_id") + block_num = event.get("block") + _id = self.hash_op(event["op"]) + timestamp = event.get("timestamp") + if not bool(opNames) or op_type in opNames and block_num > 0: + if raw_ops: + yield {"block_num": block_num, + "trx_num": trx_nr, + "op": [op_type, op], + "timestamp": timestamp} + else: + updated_op = {"type": op_type} + updated_op.update(op.copy()) + updated_op.update({"_id": _id, + "timestamp": timestamp, + "block_num": block_num, + "trx_num": trx_nr, + "trx_id": trx_id}) + yield updated_op + + def awaitTxConfirmation(self, transaction, limit=10): + """ Returns the transaction as seen by the blockchain after being + included into a block + :param dict transaction: transaction to wait for + :param int limit: (optional) number of blocks to wait for the transaction (default: 10) + + .. note:: If you want instant confirmation, you need to instantiate + class:`dpaycli.blockchain.Blockchain` with + ``mode="head"``, otherwise, the call will wait until + confirmed in an irreversible block. + + .. note:: This method returns once the blockchain has included a + transaction with the **same signature**. Even though the + signature is not usually used to identify a transaction, + it still cannot be forfeited and is derived from the + transaction contented and thus identifies a transaction + uniquely. + """ + counter = 0 + for block in self.blocks(): + counter += 1 + for tx in block["transactions"]: + if sorted( + tx["signatures"] + ) == sorted(transaction["signatures"]): + return tx + if counter > limit: + raise Exception( + "The operation has not been added after %d blocks!" % (limit)) + + @staticmethod + def hash_op(event): + """ This method generates a hash of blockchain operation. """ + if isinstance(event, dict) and "type" in event and "value" in event: + op_type = event["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + op = event["value"] + event = [op_type, op] + data = json.dumps(event, sort_keys=True) + return hashlib.sha1(py23_bytes(data, 'utf-8')).hexdigest() + + def get_all_accounts(self, start='', stop='', steps=1e3, limit=-1, **kwargs): + """ Yields account names between start and stop. + + :param str start: Start at this account name + :param str stop: Stop at this account name + :param int steps: Obtain ``steps`` ret with a single call from RPC + """ + cnt = 1 + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + if self.dpay.rpc.get_use_appbase() and start == "": + lastname = None + else: + lastname = start + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + while True: + if self.dpay.rpc.get_use_appbase(): + ret = self.dpay.rpc.list_accounts({'start': lastname, 'limit': steps, 'order': 'by_name'}, api="database")["accounts"] + else: + ret = self.dpay.rpc.lookup_accounts(lastname, steps) + for account in ret: + if isinstance(account, dict): + account_name = account["name"] + else: + account_name = account + if account_name != lastname: + yield account_name + cnt += 1 + if account_name == stop or (limit > 0 and cnt > limit): + return + if lastname == account_name: + return + lastname = account_name + if len(ret) < steps: + return + + def get_account_count(self): + """ Returns the number of accounts""" + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + ret = self.dpay.rpc.get_account_count(api="condenser") + else: + ret = self.dpay.rpc.get_account_count() + return ret + + def get_account_reputations(self, start='', stop='', steps=1e3, limit=-1, **kwargs): + """ Yields account reputation between start and stop. + + :param str start: Start at this account name + :param str stop: Stop at this account name + :param int steps: Obtain ``steps`` ret with a single call from RPC + """ + cnt = 1 + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + if self.dpay.rpc.get_use_appbase() and start == "": + lastname = None + else: + lastname = start + self.dpay.rpc.set_next_node_on_empty_reply(False) + while True: + if self.dpay.rpc.get_use_appbase(): + ret = self.dpay.rpc.get_account_reputations({'account_lower_bound': lastname, 'limit': steps}, api="follow")["reputations"] + else: + ret = self.dpay.rpc.get_account_reputations(lastname, steps, api="follow") + for account in ret: + if isinstance(account, dict): + account_name = account["account"] + else: + account_name = account + if account_name != lastname: + yield account + cnt += 1 + if account_name == stop or (limit > 0 and cnt > limit): + return + if lastname == account_name: + return + lastname = account_name + if len(ret) < steps: + return + + def get_similar_account_names(self, name, limit=5): + """ Returns limit similar accounts with name as list + + :param str name: account name to search similars for + :param int limit: limits the number of accounts, which will be returned + :returns: Similar account names as list + :rtype: list + + .. code-block:: python + + >>> from dpaycli.blockchain import Blockchain + >>> blockchain = Blockchain() + >>> ret = blockchain.get_similar_account_names("test", limit=5) + >>> len(ret) == 5 + True + + """ + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + account = self.dpay.rpc.list_accounts({'start': name, 'limit': limit}, api="database") + if bool(account): + return account["accounts"] + else: + return self.dpay.rpc.lookup_accounts(name, limit) + + def find_rc_accounts(self, name): + """ Returns limit similar accounts with name as list + + :param str name: account name to search rc params for (can also be a list of accounts) + :returns: RC params + :rtype: list + + .. code-block:: python + + >>> from dpaycli.blockchain import Blockchain + >>> blockchain = Blockchain() + >>> ret = blockchain.find_rc_accounts(["test"]) + >>> len(ret) == 1 + True + + """ + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if isinstance(name, list): + account = self.dpay.rpc.find_rc_accounts({'accounts': name}, api="rc") + if bool(account): + return account["rc_accounts"] + else: + account = self.dpay.rpc.find_rc_accounts({'accounts': [name]}, api="rc") + if bool(account): + return account["rc_accounts"][0] diff --git a/dpaycli/blockchainobject.py b/dpaycli/blockchainobject.py new file mode 100755 index 0000000..274f856 --- /dev/null +++ b/dpaycli/blockchainobject.py @@ -0,0 +1,224 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from future.utils import python_2_unicode_compatible +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from dpaycli.instance import shared_dpay_instance +from datetime import datetime, timedelta +import json +import threading + + +@python_2_unicode_compatible +class ObjectCache(dict): + + def __init__(self, initial_data={}, default_expiration=10, auto_clean=True): + super(ObjectCache, self).__init__(initial_data) + self.default_expiration = default_expiration + self.auto_clean = auto_clean + self.lock = threading.RLock() + + def __setitem__(self, key, value): + data = { + "expires": datetime.utcnow() + timedelta( + seconds=self.default_expiration), + "data": value + } + with self.lock: + if key in self: + del self[key] + dict.__setitem__(self, key, data) + if self.auto_clean: + self.clear_expired_items() + + def __getitem__(self, key): + with self.lock: + if key in self: + value = dict.__getitem__(self, key) + if value is not None: + return value["data"] + + def get(self, key, default): + with self.lock: + if key in self: + if self[key] is not None: + return self[key] + else: + return default + else: + return default + + def clear_expired_items(self): + with self.lock: + del_list = [] + utc_now = datetime.utcnow() + for key in self: + value = dict.__getitem__(self, key) + if value is None: + del_list.append(key) + continue + if utc_now >= value["expires"]: + del_list.append(key) + for key in del_list: + del self[key] + + def __contains__(self, key): + with self.lock: + if dict.__contains__(self, key): + value = dict.__getitem__(self, key) + if value is None: + return False + if datetime.utcnow() < value["expires"]: + return True + else: + value["data"] = None + return False + + def __str__(self): + if self.auto_clean: + self.clear_expired_items() + n = 0 + with self.lock: + n = len(list(self.keys())) + return "ObjectCache(n={}, default_expiration={})".format( + n, self.default_expiration) + + +class BlockchainObject(dict): + + space_id = 1 + type_id = None + type_ids = [] + + _cache = ObjectCache() + + def __init__( + self, + data, + klass=None, + space_id=1, + object_id=None, + lazy=False, + use_cache=True, + id_item=None, + dpay_instance=None, + *args, + **kwargs + ): + self.dpay = dpay_instance or shared_dpay_instance() + self.cached = False + self.identifier = None + + # We don't read lists, sets, or tuples + if isinstance(data, (list, set, tuple)): + raise ValueError( + "Cannot interpret lists! Please load elements individually!") + + if id_item and isinstance(id_item, string_types): + self.id_item = id_item + else: + self.id_item = "id" + if klass and isinstance(data, klass): + self.identifier = data.get(self.id_item) + super(BlockchainObject, self).__init__(data) + elif isinstance(data, dict): + self.identifier = data.get(self.id_item) + super(BlockchainObject, self).__init__(data) + elif isinstance(data, integer_types): + # This is only for block number basically + self.identifier = data + if not lazy and not self.cached: + self.refresh() + # make sure to store the blocknumber for caching + self[self.id_item] = (data) + # Set identifier again as it is overwritten in super() in refresh() + self.identifier = data + elif isinstance(data, string_types): + self.identifier = data + if not lazy and not self.cached: + self.refresh() + self[self.id_item] = str(data) + self.identifier = data + else: + self.identifier = data + if self.test_valid_objectid(self.identifier): + # Here we assume we deal with an id + self.testid(self.identifier) + if self.iscached(data): + super(BlockchainObject, self).__init__(self.getcache(data)) + elif not lazy and not self.cached: + self.refresh() + + if use_cache and not lazy: + self.cache() + self.cached = True + + @staticmethod + def clear_cache(): + BlockchainObject._cache = ObjectCache() + + def test_valid_objectid(self, i): + if isinstance(i, string_types): + return True + elif isinstance(i, integer_types): + return True + else: + return False + + def testid(self, id): + if not self.type_id: + return + + if not self.type_ids: + self.type_ids = [self.type_id] + + def cache(self): + # store in cache + if dict.__contains__(self, self.id_item): + BlockchainObject._cache[self.get(self.id_item)] = self + + def clear_cache_from_expired_items(self): + BlockchainObject._cache.clear_expired_items() + + def set_cache_expiration(self, expiration): + BlockchainObject._cache.default_expiration = expiration + + def set_cache_auto_clean(self, auto_clean): + BlockchainObject._cache.auto_clean = auto_clean + + def get_cache_expiration(self): + return BlockchainObject._cache.default_expiration + + def get_cache_auto_clean(self): + return BlockchainObject._cache.auto_clean + + def iscached(self, id): + return id in BlockchainObject._cache + + def getcache(self, id): + return BlockchainObject._cache.get(id, None) + + def __getitem__(self, key): + if not self.cached: + self.refresh() + return super(BlockchainObject, self).__getitem__(key) + + def items(self): + if not self.cached: + self.refresh() + return list(super(BlockchainObject, self).items()) + + def __contains__(self, key): + if not self.cached: + self.refresh() + return super(BlockchainObject, self).__contains__(key) + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, str(self.identifier)) + + def json(self): + return json.loads(str(json.dumps(self))) diff --git a/dpaycli/cli.py b/dpaycli/cli.py new file mode 100755 index 0000000..5cef080 --- /dev/null +++ b/dpaycli/cli.py @@ -0,0 +1,3189 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +import os +import ast +import json +import sys +from prettytable import PrettyTable +from datetime import datetime, timedelta +import pytz +import time +import math +import random +import logging +import click +import re +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance +from dpaycli.amount import Amount +from dpaycli.price import Price +from dpaycli.account import Account +from dpaycli.dpay import DPay +from dpaycli.comment import Comment +from dpaycli.market import Market +from dpaycli.block import Block +from dpaycli.profile import Profile +from dpaycli.wallet import Wallet +from dpaycli.dpayid import DPayID +from dpaycli.asset import Asset +from dpaycli.witness import Witness, WitnessesRankedByVote, WitnessesVotedByAccount +from dpaycli.blockchain import Blockchain +from dpaycli.utils import formatTimeString, construct_authorperm +from dpaycli.vote import AccountVotes, ActiveVotes +from dpaycli import exceptions +from dpaycli.version import version as __version__ +from dpaycli.asciichart import AsciiChart +from dpaycli.transactionbuilder import TransactionBuilder +from timeit import default_timer as timer +from dpayclibase import operations +from dpaycligraphenebase.account import PrivateKey, PublicKey, BrainKey +from dpaycligraphenebase.base58 import Base58 +from dpaycli.nodelist import NodeList +from dpaycli.conveyor import Conveyor +from dpaycli.rc import RC + + +click.disable_unicode_literals_warning = True +log = logging.getLogger(__name__) +try: + import keyring + if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): + KEYRING_AVAILABLE = True + else: + KEYRING_AVAILABLE = False +except ImportError: + KEYRING_AVAILABLE = False + +FUTURES_MODULE = None +if not FUTURES_MODULE: + try: + from concurrent.futures import ThreadPoolExecutor, wait, as_completed + FUTURES_MODULE = "futures" + except ImportError: + FUTURES_MODULE = None + + +availableConfigurationKeys = [ + "default_account", + "default_vote_weight", + "nodes", + "password_storage", + "client_id", +] + + +def prompt_callback(ctx, param, value): + if value in ["yes", "y", "ye"]: + value = True + else: + print("Please write yes, ye or y to confirm!") + ctx.abort() + + +def asset_callback(ctx, param, value): + if value not in ["BEX", "BBD"]: + print("Please BEX or BBD as asset!") + ctx.abort() + else: + return value + + +def prompt_flag_callback(ctx, param, value): + if not value: + ctx.abort() + + +def unlock_wallet(stm, password=None): + if stm.unsigned and stm.nobroadcast: + return True + password_storage = stm.config["password_storage"] + if not password and KEYRING_AVAILABLE and password_storage == "keyring": + password = keyring.get_password("dpaycli", "wallet") + if not password and password_storage == "environment" and "UNLOCK" in os.environ: + password = os.environ.get("UNLOCK") + if bool(password): + stm.wallet.unlock(password) + else: + password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + stm.wallet.unlock(password) + + if stm.wallet.locked(): + if password_storage == "keyring" or password_storage == "environment": + print("Wallet could not be unlocked with %s!" % password_storage) + password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + if bool(password): + unlock_wallet(stm, password=password) + if not stm.wallet.locked(): + return True + else: + print("Wallet could not be unlocked!") + return False + else: + print("Wallet Unlocked!") + return True + + +def node_answer_time(node): + try: + stm_local = DPay(node=node, num_retries=2, num_retries_call=2, timeout=10) + start = timer() + stm_local.get_config(use_stored_data=False) + stop = timer() + rpc_answer_time = stop - start + except KeyboardInterrupt: + rpc_answer_time = float("inf") + raise KeyboardInterrupt() + except: + rpc_answer_time = float("inf") + return rpc_answer_time + + +@click.group(chain=True) +@click.option( + '--node', '-n', default="", help="URL for public DPay API (e.g. https://api.dpays.io)") +@click.option( + '--offline', '-o', is_flag=True, default=False, help="Prevent connecting to network") +@click.option( + '--no-broadcast', '-d', is_flag=True, default=False, help="Do not broadcast") +@click.option( + '--no-wallet', '-p', is_flag=True, default=False, help="Do not load the wallet") +@click.option( + '--unsigned', '-x', is_flag=True, default=False, help="Nothing will be signed") +@click.option( + '--create-link', '-l', is_flag=True, default=False, help="Creates dpayid links from all broadcast operations") +@click.option( + '--dpayid', '-s', is_flag=True, default=False, help="Uses a dpayid token to broadcast (only broadcast operation with posting permission)") +@click.option( + '--expires', '-e', default=30, + help='Delay in seconds until transactions are supposed to expire(defaults to 60)') +@click.option( + '--verbose', '-v', default=3, help='Verbosity') +@click.version_option(version=__version__) +def cli(node, offline, no_broadcast, no_wallet, unsigned, create_link, dpayid, expires, verbose): + + # Logging + log = logging.getLogger(__name__) + verbosity = ["critical", "error", "warn", "info", "debug"][int( + min(verbose, 4))] + log.setLevel(getattr(logging, verbosity.upper())) + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch = logging.StreamHandler() + ch.setLevel(getattr(logging, verbosity.upper())) + ch.setFormatter(formatter) + log.addHandler(ch) + if create_link: + dpid = DPayID() + no_broadcast = True + unsigned = True + else: + dpid = None + debug = verbose > 0 + stm = DPay( + node=node, + nobroadcast=no_broadcast, + offline=offline, + nowallet=no_wallet, + unsigned=unsigned, + use_dpid=dpayid, + expiration=expires, + dpayid=dpid, + debug=debug, + num_retries=10, + num_retries_call=3, + timeout=15, + autoconnect=False + ) + set_shared_dpay_instance(stm) + + pass + + +@cli.command() +@click.argument('key') +@click.argument('value') +def set(key, value): + """ Set default_account, default_vote_weight or nodes + + set [key] [value] + + Examples: + + Set the default vote weight to 50 %: + set default_vote_weight 50 + """ + stm = shared_dpay_instance() + if key == "default_account": + if stm.rpc is not None: + stm.rpc.rpcconnect() + stm.set_default_account(value) + elif key == "default_vote_weight": + stm.set_default_vote_weight(value) + elif key == "nodes" or key == "node": + if bool(value) or value != "default": + stm.set_default_nodes(value) + else: + stm.set_default_nodes("") + elif key == "password_storage": + stm.config["password_storage"] = value + if KEYRING_AVAILABLE and value == "keyring": + password = click.prompt("Password to unlock wallet (Will be stored in keyring)", confirmation_prompt=False, hide_input=True) + password = keyring.set_password("dpaycli", "wallet", password) + elif KEYRING_AVAILABLE and value != "keyring": + try: + keyring.delete_password("dpaycli", "wallet") + except keyring.errors.PasswordDeleteError: + print("") + if value == "environment": + print("The wallet password can be stored in the UNLOCK environment variable to skip password prompt!") + elif key == "client_id": + stm.config["client_id"] = value + elif key == "hot_sign_redirect_uri": + stm.config["hot_sign_redirect_uri"] = value + elif key == "dpid_api_url": + stm.config["dpid_api_url"] = value + elif key == "oauth_base_url": + stm.config["oauth_base_url"] = value + else: + print("wrong key") + + +@cli.command() +@click.option('--results', is_flag=True, default=False, help="Shows result of changing the node.") +def nextnode(results): + """ Uses the next node in list + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + stm.move_current_node_to_front() + node = stm.get_default_nodes() + offline = stm.offline + if len(node) < 2: + print("At least two nodes are needed!") + return + node = node[1:] + [node[0]] + if not offline: + stm.rpc.next() + stm.get_blockchain_version() + while not offline and node[0] != stm.rpc.url and len(node) > 1: + node = node[1:] + [node[0]] + stm.set_default_nodes(node) + if not results: + return + + t = PrettyTable(["Key", "Value"]) + t.align = "l" + if not offline: + t.add_row(["Node-Url", stm.rpc.url]) + else: + t.add_row(["Node-Url", node[0]]) + if not offline: + t.add_row(["Version", stm.get_blockchain_version()]) + else: + t.add_row(["Version", "dpay is in offline mode..."]) + print(t) + + +@cli.command() +@click.option( + '--raw', is_flag=True, default=False, + help="Returns only the raw value") +@click.option( + '--sort', is_flag=True, default=False, + help="Sort all nodes by ping value") +@click.option( + '--remove', is_flag=True, default=False, + help="Remove node with errors from list") +@click.option( + '--threading', is_flag=True, default=False, + help="Use a thread for each node") +def pingnode(raw, sort, remove, threading): + """ Returns the answer time in milliseconds + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + nodes = stm.get_default_nodes() + if not raw: + t = PrettyTable(["Node", "Answer time [ms]"]) + t.align = "l" + if sort: + ping_times = [] + for node in nodes: + ping_times.append(1000.) + if threading and FUTURES_MODULE: + pool = ThreadPoolExecutor(max_workers=len(nodes) + 1) + futures = [] + for i in range(len(nodes)): + try: + if not threading or not FUTURES_MODULE: + ping_times[i] = node_answer_time(nodes[i]) + else: + futures.append(pool.submit(node_answer_time, nodes[i])) + if not threading or not FUTURES_MODULE: + print("node %s results in %.2f" % (nodes[i], ping_times[i])) + except KeyboardInterrupt: + ping_times[i] = float("inf") + break + if threading and FUTURES_MODULE: + ping_times = [r.result() for r in as_completed(futures)] + sorted_arg = sorted(range(len(ping_times)), key=ping_times.__getitem__) + sorted_nodes = [] + for i in sorted_arg: + if not remove or ping_times[i] != float("inf"): + sorted_nodes.append(nodes[i]) + stm.set_default_nodes(sorted_nodes) + if not raw: + for i in sorted_arg: + t.add_row([nodes[i], "%.2f" % (ping_times[i] * 1000)]) + print(t) + else: + print(ping_times[sorted_arg]) + else: + node = stm.rpc.url + rpc_answer_time = node_answer_time(node) + rpc_time_str = "%.2f" % (rpc_answer_time * 1000) + if raw: + print(rpc_time_str) + return + t.add_row([node, rpc_time_str]) + print(t) + + +@cli.command() +@click.option( + '--version', is_flag=True, default=False, + help="Returns only the raw version value") +@click.option( + '--url', is_flag=True, default=False, + help="Returns only the raw url value") +def currentnode(version, url): + """ Sets the currently working node at the first place in the list + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + offline = stm.offline + stm.move_current_node_to_front() + node = stm.get_default_nodes() + if version and not offline: + print(stm.get_blockchain_version()) + return + elif version and offline: + print("Node is offline") + return + if url and not offline: + print(stm.rpc.url) + return + t = PrettyTable(["Key", "Value"]) + t.align = "l" + if not offline: + t.add_row(["Node-Url", stm.rpc.url]) + else: + t.add_row(["Node-Url", node[0]]) + if not offline: + t.add_row(["Version", stm.get_blockchain_version()]) + else: + t.add_row(["Version", "dpay is in offline mode..."]) + print(t) + + +@cli.command() +@click.option( + '--show', '-s', is_flag=True, default=False, + help="Prints the updated nodes") +@click.option( + '--test', '-t', is_flag=True, default=False, + help="Do change the node list, only print the newest nodes setup.") +@click.option( + '--only-https', '-h', is_flag=True, default=False, + help="Use only https nodes.") +@click.option( + '--only-wss', '-w', is_flag=True, default=False, + help="Use only websocket nodes.") +@click.option( + '--only-appbase', '-a', is_flag=True, default=False, + help="Use only appbase nodes") +@click.option( + '--only-non-appbase', '-n', is_flag=True, default=False, + help="Use only non-appbase nodes") +def updatenodes(show, test, only_https, only_wss, only_appbase, only_non_appbase): + """ Update the nodelist from @fullnodeupdate + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + t = PrettyTable(["node", "Version", "score"]) + t.align = "l" + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=stm) + nodes = nodelist.get_nodes(normal=not only_appbase, appbase=not only_non_appbase, wss=not only_https, https=not only_wss) + if show or test: + sorted_nodes = sorted(nodelist, key=lambda node: node["score"], reverse=True) + for node in sorted_nodes: + if node["url"] in nodes: + score = float("{0:.1f}".format(node["score"])) + t.add_row([node["url"], node["version"], score]) + print(t) + if not test: + stm.set_default_nodes(nodes) + + +@cli.command() +def config(): + """ Shows local configuration + """ + stm = shared_dpay_instance() + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in stm.config: + # hide internal config data + if key in availableConfigurationKeys and key != "nodes" and key != "node": + t.add_row([key, stm.config[key]]) + node = stm.get_default_nodes() + nodes = json.dumps(node, indent=4) + t.add_row(["nodes", nodes]) + if "password_storage" not in availableConfigurationKeys: + t.add_row(["password_storage", stm.config["password_storage"]]) + t.add_row(["data_dir", stm.config.data_dir]) + print(t) + + +@cli.command() +@click.option('--wipe', is_flag=True, default=False, + help="Wipe old wallet without prompt.") +def createwallet(wipe): + """ Create new wallet with a new password + """ + stm = shared_dpay_instance() + if stm.wallet.created() and not wipe: + wipe_answer = click.prompt("'Do you want to wipe your wallet? Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account! [y/n]", + default="n") + if wipe_answer in ["y", "ye", "yes"]: + stm.wallet.wipe(True) + else: + return + elif wipe: + stm.wallet.wipe(True) + password = None + password = click.prompt("New wallet password", confirmation_prompt=True, hide_input=True) + if not bool(password): + print("Password cannot be empty! Quitting...") + return + password_storage = stm.config["password_storage"] + if KEYRING_AVAILABLE and password_storage == "keyring": + password = keyring.set_password("dpaycli", "wallet", password) + elif password_storage == "environment": + print("The new wallet password can be stored in the UNLOCK environment variable to skip password prompt!") + stm.wallet.create(password) + set_shared_dpay_instance(stm) + + +@cli.command() +@click.option('--test-unlock', is_flag=True, default=False, help='test if unlock is sucessful') +def walletinfo(test_unlock): + """ Show info about wallet + """ + stm = shared_dpay_instance() + t = PrettyTable(["Key", "Value"]) + t.align = "l" + t.add_row(["created", stm.wallet.created()]) + t.add_row(["locked", stm.wallet.locked()]) + t.add_row(["Number of stored keys", len(stm.wallet.getPublicKeys())]) + t.add_row(["sql-file", stm.wallet.keyStorage.sqlDataBaseFile]) + password_storage = stm.config["password_storage"] + t.add_row(["password_storage", password_storage]) + password = os.environ.get("UNLOCK") + if password is not None: + t.add_row(["UNLOCK env set", "yes"]) + else: + t.add_row(["UNLOCK env set", "no"]) + if KEYRING_AVAILABLE: + t.add_row(["keyring installed", "yes"]) + else: + t.add_row(["keyring installed", "no"]) + if test_unlock: + if unlock_wallet(stm): + t.add_row(["Wallet unlock", "successful"]) + else: + t.add_row(["Wallet unlock", "not working"]) + # t.add_row(["getPublicKeys", str(stm.wallet.getPublicKeys())]) + print(t) + + +@cli.command() +@click.option('--unsafe-import-key', + help='WIF key to parse (unsafe, unless shell history is deleted afterwards)', multiple=True) +def parsewif(unsafe_import_key): + """ Parse a WIF private key without importing + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if unsafe_import_key: + for key in unsafe_import_key: + try: + pubkey = PrivateKey(key, prefix=stm.prefix).pubkey + print(pubkey) + account = stm.wallet.getAccountFromPublicKey(str(pubkey)) + account = Account(account, dpay_instance=stm) + key_type = stm.wallet.getKeyType(account, str(pubkey)) + print("Account: %s - %s" % (account["name"], key_type)) + except Exception as e: + print(str(e)) + else: + while True: + wifkey = click.prompt("Enter private key", confirmation_prompt=False, hide_input=True) + if not wifkey or wifkey == "quit" or wifkey == "exit": + break + try: + pubkey = PrivateKey(wifkey, prefix=stm.prefix).pubkey + print(pubkey) + account = stm.wallet.getAccountFromPublicKey(str(pubkey)) + account = Account(account, dpay_instance=stm) + key_type = stm.wallet.getKeyType(account, str(pubkey)) + print("Account: %s - %s" % (account["name"], key_type)) + except Exception as e: + print(str(e)) + continue + + +@cli.command() +@click.option('--unsafe-import-key', + help='Private key to import to wallet (unsafe, unless shell history is deleted afterwards)') +def addkey(unsafe_import_key): + """ Add key to wallet + + When no [OPTION] is given, a password prompt for unlocking the wallet + and a prompt for entering the private key are shown. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + if not unsafe_import_key: + unsafe_import_key = click.prompt("Enter private key", confirmation_prompt=False, hide_input=True) + stm.wallet.addPrivateKey(unsafe_import_key) + set_shared_dpay_instance(stm) + + +@cli.command() +@click.option('--confirm', + prompt='Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account!', + hide_input=False, callback=prompt_flag_callback, is_flag=True, + confirmation_prompt=False, help='Please confirm!') +@click.argument('pub') +def delkey(confirm, pub): + """ Delete key from the wallet + + PUB is the public key from the private key + which will be deleted from the wallet + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + stm.wallet.removePrivateKeyFromPublicKey(pub) + set_shared_dpay_instance(stm) + + +@cli.command() +@click.option('--import-brain-key', help='Imports a brain key and derives a private and public key', is_flag=True, default=False) +@click.option('--sequence', help='Sequence number, influences the derived private key. (default is 0)', default=0) +def keygen(import_brain_key, sequence): + """ Creates a new random brain key and prints its derived private key and public key. + The generated key is not stored. + """ + if import_brain_key: + brain_key = click.prompt("Enter brain key", confirmation_prompt=False, hide_input=True) + else: + brain_key = None + bk = BrainKey(brainkey=brain_key, sequence=sequence) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + t.add_row(["Brain Key", bk.get_brainkey()]) + t.add_row(["Private Key", str(bk.get_private())]) + t.add_row(["Public Key", format(bk.get_public(), "DWB")]) + print(t) + + +@cli.command() +@click.argument('name') +@click.option('--unsafe-import-token', + help='Private key to import to wallet (unsafe, unless shell history is deleted afterwards)') +def addtoken(name, unsafe_import_token): + """ Add key to wallet + + When no [OPTION] is given, a password prompt for unlocking the wallet + and a prompt for entering the private key are shown. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + if not unsafe_import_token: + unsafe_import_token = click.prompt("Enter private token", confirmation_prompt=False, hide_input=True) + stm.wallet.addToken(name, unsafe_import_token) + set_shared_dpay_instance(stm) + + +@cli.command() +@click.option('--confirm', + prompt='Are your sure?', + hide_input=False, callback=prompt_flag_callback, is_flag=True, + confirmation_prompt=False, help='Please confirm!') +@click.argument('name') +def deltoken(confirm, name): + """ Delete name from the wallet + + name is the public name from the private token + which will be deleted from the wallet + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + stm.wallet.removeTokenFromPublicName(name) + set_shared_dpay_instance(stm) + + +@cli.command() +def listkeys(): + """ Show stored keys + """ + stm = shared_dpay_instance() + t = PrettyTable(["Available Key"]) + t.align = "l" + for key in stm.wallet.getPublicKeys(): + t.add_row([key]) + print(t) + + +@cli.command() +def listtoken(): + """ Show stored token + """ + stm = shared_dpay_instance() + t = PrettyTable(["name", "scope", "status"]) + t.align = "l" + if not unlock_wallet(stm): + return + dpid = DPayID(dpay_instance=stm) + for name in stm.wallet.getPublicNames(): + ret = dpid.me(username=name) + if "error" in ret: + t.add_row([name, "-", ret["error"]]) + else: + t.add_row([name, ret["scope"], "ok"]) + print(t) + + +@cli.command() +def listaccounts(): + """Show stored accounts""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + t = PrettyTable(["Name", "Type", "Available Key"]) + t.align = "l" + for account in stm.wallet.getAccounts(): + t.add_row([ + account["name"] or "n/a", account["type"] or "n/a", + account["pubkey"] + ]) + print(t) + + +@cli.command() +@click.argument('post', nargs=1) +@click.argument('vote_weight', nargs=1, required=False) +@click.option('--weight', '-w', help='Vote weight (from 0.1 to 100.0)') +@click.option('--account', '-a', help='Voter account name') +def upvote(post, vote_weight, account, weight): + """Upvote a post/comment + + POST is @author/permlink + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not weight and vote_weight: + weight = vote_weight + if not weight.replace('.', '', 1).isdigit(): + raise ValueError("vote_weight must be a float!") + else: + weight = float(weight) + if weight > 100: + raise ValueError("Maximum vote weight is 100.0!") + elif weight < -100: + raise ValueError("Minimum vote weight is -100.0!") + elif not weight and not vote_weight: + weight = stm.config["default_vote_weight"] + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + try: + post = Comment(post, dpay_instance=stm) + tx = post.upvote(weight, voter=account) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + except exceptions.VotingInvalidOnArchivedPost: + print("Post/Comment is older than 7 days! Did not upvote.") + tx = {} + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('post', nargs=1) +@click.argument('vote_weight', nargs=1, required=False) +@click.option('--account', '-a', help='Voter account name') +@click.option('--weight', '-w', default=100.0, help='Vote weight (from 0.1 to 100.0)') +def downvote(post, vote_weight, account, weight): + """Downvote a post/comment + + POST is @author/permlink + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not weight and vote_weight: + weight = vote_weight + if not weight.replace('.', '', 1).isdigit(): + raise ValueError("vote_weight must be a float!") + else: + weight = float(weight) + if weight > 100: + raise ValueError("Maximum vote weight is 100.0!") + elif weight < -100: + raise ValueError("Minimum vote weight is -100.0!") + elif not weight and not vote_weight: + weight = stm.config["default_vote_weight"] + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + try: + post = Comment(post, dpay_instance=stm) + tx = post.downvote(weight, voter=account) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + except exceptions.VotingInvalidOnArchivedPost: + print("Post/Comment is older than 7 days! Did not downvote.") + tx = {} + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('to', nargs=1) +@click.argument('amount', nargs=1) +@click.argument('asset', nargs=1, callback=asset_callback) +@click.argument('memo', nargs=1, required=False) +@click.option('--account', '-a', help='Transfer from this account') +def transfer(to, amount, asset, memo, account): + """Transfer BBD/BEX""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not bool(memo): + memo = '' + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.transfer(to, amount, asset, memo) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.option('--account', '-a', help='Powerup from this account') +@click.option('--to', help='Powerup this account', default=None) +def powerup(amount, account, to): + """Power up (vest BEX as BEX POWER)""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + try: + amount = float(amount) + except: + amount = str(amount) + tx = acc.transfer_to_vesting(amount, to=to) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.option('--account', '-a', help='Powerup from this account') +def powerdown(amount, account): + """Power down (start withdrawing VESTS from DPay POWER) + + amount is in VESTS + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + try: + amount = float(amount) + except: + amount = str(amount) + tx = acc.withdraw_vesting(amount) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.argument('to_account', nargs=1) +@click.option('--account', '-a', help='Powerup from this account') +def delegate(amount, to_account, account): + """Delegate (start delegate VESTS to another account) + + amount is in VESTS / DPay + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + try: + amount = float(amount) + except: + amount = Amount(str(amount), dpay_instance=stm) + if amount.symbol == stm.dpay_symbol: + amount = stm.bp_to_vests(float(amount)) + + tx = acc.delegate_vesting_shares(to_account, amount) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('to', nargs=1) +@click.option('--percentage', default=100, help='The percent of the withdraw to go to the "to" account') +@click.option('--account', '-a', help='Powerup from this account') +@click.option('--auto_vest', help='Set to true if the from account should receive the VESTS as' + 'VESTS, or false if it should receive them as BEX.', is_flag=True) +def powerdownroute(to, percentage, account, auto_vest): + """Setup a powerdown route""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.set_withdraw_vesting_route(to, percentage, auto_vest=auto_vest) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.option('--account', '-a', help='Powerup from this account') +def convert(amount, account): + """Convert BEXDollars to DPay (takes a week to settle)""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + try: + amount = float(amount) + except: + amount = str(amount) + tx = acc.convert(amount) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +def changewalletpassphrase(): + """ Change wallet password + """ + stm = shared_dpay_instance() + if not unlock_wallet(stm): + return + newpassword = None + newpassword = click.prompt("New wallet password", confirmation_prompt=True, hide_input=True) + if not bool(newpassword): + print("Password cannot be empty! Quitting...") + return + password_storage = stm.config["password_storage"] + if KEYRING_AVAILABLE and password_storage == "keyring": + keyring.set_password("dpaycli", "wallet", newpassword) + elif password_storage == "environment": + print("The new wallet password can be stored in the UNLOCK invironment variable to skip password prompt!") + stm.wallet.changePassphrase(newpassword) + + +@cli.command() +@click.argument('account', nargs=-1) +def power(account): + """ Shows vote power and bandwidth + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if len(account) == 0: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for name in account: + a = Account(name, dpay_instance=stm) + print("\n@%s" % a.name) + a.print_info(use_table=True) + + +@cli.command() +@click.argument('account', nargs=-1) +def balance(account): + """ Shows balance + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if len(account) == 0: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for name in account: + a = Account(name, dpay_instance=stm) + print("\n@%s" % a.name) + t = PrettyTable(["Account", "BEX", "BBD", "VESTS"]) + t.align = "r" + t.add_row([ + 'Available', + str(a.balances['available'][0]), + str(a.balances['available'][1]), + str(a.balances['available'][2]), + ]) + t.add_row([ + 'Rewards', + str(a.balances['rewards'][0]), + str(a.balances['rewards'][1]), + str(a.balances['rewards'][2]), + ]) + t.add_row([ + 'Savings', + str(a.balances['savings'][0]), + str(a.balances['savings'][1]), + 'N/A', + ]) + t.add_row([ + 'TOTAL', + str(a.balances['total'][0]), + str(a.balances['total'][1]), + str(a.balances['total'][2]), + ]) + print(t) + + +@cli.command() +@click.argument('account', nargs=-1, required=False) +def interest(account): + """ Get information about interest payment + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + + t = PrettyTable([ + "Account", "Last Interest Payment", "Next Payment", + "Interest rate", "Interest" + ]) + t.align = "r" + for a in account: + a = Account(a, dpay_instance=stm) + i = a.interest() + t.add_row([ + a["name"], + i["last_payment"], + "in %s" % (i["next_payment_duration"]), + "%.1f%%" % i["interest_rate"], + "%.3f %s" % (i["interest"], "BBD"), + ]) + print(t) + + +@cli.command() +@click.argument('account', nargs=-1, required=False) +def follower(account): + """ Get information about followers + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for a in account: + a = Account(a, dpay_instance=stm) + print("\nFollowers statistics for @%s (please wait...)" % a.name) + followers = a.get_followers(False) + followers.print_summarize_table(tag_type="Followers") + + +@cli.command() +@click.argument('account', nargs=-1, required=False) +def following(account): + """ Get information about following + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for a in account: + a = Account(a, dpay_instance=stm) + print("\nFollowing statistics for @%s (please wait...)" % a.name) + following = a.get_following(False) + following.print_summarize_table(tag_type="Following") + + +@cli.command() +@click.argument('account', nargs=-1, required=False) +def muter(account): + """ Get information about muter + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for a in account: + a = Account(a, dpay_instance=stm) + print("\nMuters statistics for @%s (please wait...)" % a.name) + muters = a.get_muters(False) + muters.print_summarize_table(tag_type="Muters") + + +@cli.command() +@click.argument('account', nargs=-1, required=False) +def muting(account): + """ Get information about muting + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = [stm.config["default_account"]] + for a in account: + a = Account(a, dpay_instance=stm) + print("\nMuting statistics for @%s (please wait...)" % a.name) + muting = a.get_mutings(False) + muting.print_summarize_table(tag_type="Muting") + + +@cli.command() +@click.argument('account', nargs=1, required=False) +def permissions(account): + """ Show permissions of an account + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + if "default_account" in stm.config: + account = stm.config["default_account"] + account = Account(account, dpay_instance=stm) + t = PrettyTable(["Permission", "Threshold", "Key/Account"], hrules=0) + t.align = "r" + for permission in ["owner", "active", "posting"]: + auths = [] + for type_ in ["account_auths", "key_auths"]: + for authority in account[permission][type_]: + auths.append("%s (%d)" % (authority[0], authority[1])) + t.add_row([ + permission, + account[permission]["weight_threshold"], + "\n".join(auths), + ]) + print(t) + + +@cli.command() +@click.argument('foreign_account', nargs=1, required=False) +@click.option('--permission', default="posting", help='The permission to grant (defaults to "posting")') +@click.option('--account', '-a', help='The account to allow action for') +@click.option('--weight', help='The weight to use instead of the (full) threshold. ' + 'If the weight is smaller than the threshold, ' + 'additional signatures are required') +@click.option('--threshold', help='The permission\'s threshold that needs to be reached ' + 'by signatures to be able to interact') +def allow(foreign_account, permission, account, weight, threshold): + """Allow an account/key to interact with your account + + foreign_account: The account or key that will be allowed to interact with account. + When not given, password will be asked, from which a public key is derived. + This derived key will then interact with your account. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + if permission not in ["posting", "active", "owner"]: + print("Wrong permission, please use: posting, active or owner!") + return + acc = Account(account, dpay_instance=stm) + if not foreign_account: + from dpaycligraphenebase.account import PasswordKey + pwd = click.prompt("Password for Key Derivation", confirmation_prompt=True, hide_input=True) + foreign_account = format(PasswordKey(account, pwd, permission).get_public(), stm.prefix) + if threshold is not None: + threshold = int(threshold) + tx = acc.allow(foreign_account, weight=weight, permission=permission, threshold=threshold) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('foreign_account', nargs=1, required=False) +@click.option('--permission', default="posting", help='The permission to grant (defaults to "posting")') +@click.option('--account', '-a', help='The account to disallow action for') +@click.option('--threshold', help='The permission\'s threshold that needs to be reached ' + 'by signatures to be able to interact') +def disallow(foreign_account, permission, account, threshold): + """Remove allowance an account/key to interact with your account""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + if permission not in ["posting", "active", "owner"]: + print("Wrong permission, please use: posting, active or owner!") + return + if threshold is not None: + threshold = int(threshold) + acc = Account(account, dpay_instance=stm) + if not foreign_account: + from dpaycligraphenebase.account import PasswordKey + pwd = click.prompt("Password for Key Derivation", confirmation_prompt=True) + foreign_account = [format(PasswordKey(account, pwd, permission).get_public(), stm.prefix)] + tx = acc.disallow(foreign_account, permission=permission, threshold=threshold) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('creator', nargs=1, required=True) +@click.option('--fee', help='When fee is 0 (default) a subsidized account is claimed and can be created later with create_claimed_account', default=0) +@click.option('--number', '-n', help='Number of subsidized accounts to be claimed (default = 1), when fee = 0 BEX', default=1) +def claimaccount(creator, fee, number): + """Claim account for claimed account creation.""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not creator: + creator = stm.config["default_account"] + if not unlock_wallet(stm): + return + creator = Account(creator, dpay_instance=stm) + fee = Amount("%.3f %s" % (float(fee), stm.dpay_symbol), dpay_instance=stm) + tx = None + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.claim_account(creator, fee=fee) + tx = stm.dpayid.url_from_tx(tx) + elif float(fee) == 0: + rc = RC(dpay_instance=stm) + current_costs = stm.get_rc_cost(rc.get_resource_count(tx_size=200, new_account_op_count=1)) + current_mana = creator.get_rc_manabar()["current_mana"] + last_mana = current_mana + cnt = 0 + print("Current costs %.2f G RC - current mana %.2f G RC" % (current_costs / 1e9, current_mana / 1e9)) + print("Account can claim %d accounts" % (int(current_mana / current_costs))) + while current_costs + 10 < current_mana and cnt < number: + if cnt > 0: + print("Current costs %.2f G RC - current mana %.2f G RC" % (current_costs / 1e9, current_mana / 1e9)) + tx = json.dumps(tx, indent=4) + print(tx) + cnt += 1 + tx = stm.claim_account(creator, fee=fee) + time.sleep(10) + creator.refresh() + current_mana = creator.get_rc_manabar()["current_mana"] + print("Account claimed and %.2f G RC paid." % ((last_mana - current_mana) / 1e9)) + last_mana = current_mana + if cnt == 0: + print("Not enough RC for a claim!") + else: + tx = stm.claim_account(creator, fee=fee) + if tx is not None: + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('accountname', nargs=1, required=True) +@click.option('--account', '-a', help='Account that pays the fee') +@click.option('--create-claimed-account', '-c', help='Instead of paying the account creation fee a subsidized account is created.', is_flag=True, default=False) +def newaccount(accountname, account, create_claimed_account): + """Create a new account""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + password = click.prompt("New Account Passphrase", confirmation_prompt=True, hide_input=True) + if not password: + print("You cannot chose an empty password") + return + if create_claimed_account: + tx = stm.create_claimed_account(accountname, creator=acc, password=password) + else: + tx = stm.create_account(accountname, creator=acc, password=password) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('variable', nargs=1, required=False) +@click.argument('value', nargs=1, required=False) +@click.option('--account', '-a', help='setprofile as this user') +@click.option('--pair', '-p', help='"Key=Value" pairs', multiple=True) +def setprofile(variable, value, account, pair): + """Set a variable in an account\'s profile""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + keys = [] + values = [] + if pair: + for p in pair: + key, value = p.split("=") + keys.append(key) + values.append(value) + if variable and value: + keys.append(variable) + values.append(value) + + profile = Profile(keys, values) + + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + + json_metadata = Profile(acc["json_metadata"] if acc["json_metadata"] else {}) + json_metadata.update(profile) + tx = acc.update_account_profile(json_metadata) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('variable', nargs=-1, required=True) +@click.option('--account', '-a', help='delprofile as this user') +def delprofile(variable, account): + """Delete a variable in an account\'s profile""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + json_metadata = Profile(acc["json_metadata"]) + + for var in variable: + json_metadata.remove(var) + + tx = acc.update_account_profile(json_metadata) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('account', nargs=1, required=True) +@click.option('--roles', help='Import specified keys (owner, active, posting, memo).', default=["active", "posting", "memo"]) +def importaccount(account, roles): + """Import an account using a passphrase""" + from dpaycligraphenebase.account import PasswordKey + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + account = Account(account, dpay_instance=stm) + imported = False + password = click.prompt("Account Passphrase", confirmation_prompt=False, hide_input=True) + if not password: + print("You cannot chose an empty Passphrase") + return + if "owner" in roles: + owner_key = PasswordKey(account["name"], password, role="owner") + owner_pubkey = format(owner_key.get_public_key(), stm.prefix) + if owner_pubkey in [x[0] for x in account["owner"]["key_auths"]]: + print("Importing owner key!") + owner_privkey = owner_key.get_private_key() + stm.wallet.addPrivateKey(owner_privkey) + imported = True + + if "active" in roles: + active_key = PasswordKey(account["name"], password, role="active") + active_pubkey = format(active_key.get_public_key(), stm.prefix) + if active_pubkey in [x[0] for x in account["active"]["key_auths"]]: + print("Importing active key!") + active_privkey = active_key.get_private_key() + stm.wallet.addPrivateKey(active_privkey) + imported = True + + if "posting" in roles: + posting_key = PasswordKey(account["name"], password, role="posting") + posting_pubkey = format(posting_key.get_public_key(), stm.prefix) + if posting_pubkey in [ + x[0] for x in account["posting"]["key_auths"] + ]: + print("Importing posting key!") + posting_privkey = posting_key.get_private_key() + stm.wallet.addPrivateKey(posting_privkey) + imported = True + + if "memo" in roles: + memo_key = PasswordKey(account["name"], password, role="memo") + memo_pubkey = format(memo_key.get_public_key(), stm.prefix) + if memo_pubkey == account["memo_key"]: + print("Importing memo key!") + memo_privkey = memo_key.get_private_key() + stm.wallet.addPrivateKey(memo_privkey) + imported = True + + if not imported: + print("No matching key(s) found. Password correct?") + + +@cli.command() +@click.option('--account', '-a', help='The account to updatememokey action for') +@click.option('--key', help='The new memo key') +def updatememokey(account, key): + """Update an account\'s memo key""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + if not key: + from dpaycligraphenebase.account import PasswordKey + pwd = click.prompt("Password for Memo Key Derivation", confirmation_prompt=True, hide_input=True) + memo_key = PasswordKey(account, pwd, "memo") + key = format(memo_key.get_public_key(), stm.prefix) + memo_privkey = memo_key.get_private_key() + if not stm.nobroadcast: + stm.wallet.addPrivateKey(memo_privkey) + tx = acc.update_memo_key(key) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.option('--account', '-a', help='Your account') +def approvewitness(witness, account): + """Approve a witnesses""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.approvewitness(witness, approve=True) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.option('--account', '-a', help='Your account') +def disapprovewitness(witness, account): + """Disapprove a witnesses""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.disapprovewitness(witness) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.option('--file', help='Load transaction from file. If "-", read from stdin (defaults to "-")') +def sign(file): + """Sign a provided transaction with available and required keys""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if file and file != "-": + if not os.path.isfile(file): + raise Exception("File %s does not exist!" % file) + with open(file) as fp: + tx = fp.read() + else: + tx = click.get_text_stream('stdin') + tx = ast.literal_eval(tx) + tx = stm.sign(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.option('--file', help='Load transaction from file. If "-", read from stdin (defaults to "-")') +def broadcast(file): + """broadcast a signed transaction""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if file and file != "-": + if not os.path.isfile(file): + raise Exception("File %s does not exist!" % file) + with open(file) as fp: + tx = fp.read() + else: + tx = click.get_text_stream('stdin') + tx = ast.literal_eval(tx) + tx = stm.broadcast(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.option('--bbd-to-bex', '-i', help='Show ticker in BBD/BEX', is_flag=True, default=False) +def ticker(bbd_to_dpay): + """ Show ticker + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + t = PrettyTable(["Key", "Value"]) + t.align = "l" + market = Market(dpay_instance=stm) + ticker = market.ticker() + for key in ticker: + if key in ["highest_bid", "latest", "lowest_ask"] and bbd_to_dpay: + t.add_row([key, str(ticker[key].as_base("BBD"))]) + elif key in "percent_change" and bbd_to_dpay: + t.add_row([key, "%.2f %%" % -ticker[key]]) + elif key in "percent_change": + t.add_row([key, "%.2f %%" % ticker[key]]) + else: + t.add_row([key, str(ticker[key])]) + print(t) + + +@cli.command() +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def pricehistory(width, height, ascii): + """ Show price history + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + feed_history = stm.get_feed_history() + current_base = Amount(feed_history['current_median_history']["base"], dpay_instance=stm) + current_quote = Amount(feed_history['current_median_history']["quote"], dpay_instance=stm) + price_history = feed_history["price_history"] + price = [] + for h in price_history: + base = Amount(h["base"], dpay_instance=stm) + quote = Amount(h["quote"], dpay_instance=stm) + price.append(base.amount / quote.amount) + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=4, placeholder='{:6.2f} $', charset=charset) + print("\n Price history for BEX (median price %4.2f $)\n" % (float(current_base) / float(current_quote))) + + chart.adapt_on_series(price) + chart.new_chart() + chart.add_axis() + chart._draw_h_line(chart._map_y(float(current_base) / float(current_quote)), 1, int(chart.n / chart.skip), line=chart.char_set["curve_hl_dot"]) + chart.add_curve(price) + print(str(chart)) + + +@cli.command() +@click.option('--days', '-d', help='Limit the days of shown trade history (default 7)', default=7.) +@click.option('--hours', help='Limit the intervall history intervall (default 2 hours)', default=2.0) +@click.option('--bbd-to-bex', '-i', help='Show ticker in BBD/BEX', is_flag=True, default=False) +@click.option('--limit', '-l', help='Limit number of trades which is fetched at each intervall point (default 100)', default=100) +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def tradehistory(days, hours, bbd_to_dpay, limit, width, height, ascii): + """ Show price history + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + m = Market(dpay_instance=stm) + utc = pytz.timezone('UTC') + stop = utc.localize(datetime.utcnow()) + start = stop - timedelta(days=days) + intervall = timedelta(hours=hours) + trades = m.trade_history(start=start, stop=stop, limit=limit, intervall=intervall) + price = [] + if bbd_to_dpay: + base_str = stm.dpay_symbol + else: + base_str = stm.bbd_symbol + for trade in trades: + base = 0 + quote = 0 + for order in trade: + base += float(order.as_base(base_str)["base"]) + quote += float(order.as_base(base_str)["quote"]) + price.append(base / quote) + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=3, placeholder='{:6.2f} ', charset=charset) + if bbd_to_dpay: + print("\n Trade history %s - %s \n\nBBD/BEX" % (formatTimeString(start), formatTimeString(stop))) + else: + print("\n Trade history %s - %s \n\nBEX/BBD" % (formatTimeString(start), formatTimeString(stop))) + chart.adapt_on_series(price) + chart.new_chart() + chart.add_axis() + chart.add_curve(price) + print(str(chart)) + + +@cli.command() +@click.option('--chart', help='Enable charting', is_flag=True) +@click.option('--limit', '-l', help='Limit number of returned open orders (default 25)', default=25) +@click.option('--show-date', help='Show dates', is_flag=True, default=False) +@click.option('--width', '-w', help='Plot width (default 75)', default=75) +@click.option('--height', '-h', help='Plot height (default 15)', default=15) +@click.option('--ascii', help='Use only ascii symbols', is_flag=True, default=False) +def orderbook(chart, limit, show_date, width, height, ascii): + """Obtain orderbook of the internal market""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + market = Market(dpay_instance=stm) + orderbook = market.orderbook(limit=limit, raw_data=False) + if not show_date: + header = ["Asks Sum BBD", "Sell Orders", "Bids Sum BBD", "Buy Orders"] + else: + header = ["Asks date", "Sell Orders", "Bids date", "Buy Orders"] + t = PrettyTable(header, hrules=0) + t.align = "r" + asks = [] + bids = [] + asks_date = [] + bids_date = [] + sumsum_asks = [] + sum_asks = 0 + sumsum_bids = [] + sum_bids = 0 + n = 0 + for order in orderbook["asks"]: + asks.append(order) + sum_asks += float(order.as_base("BBD")["base"]) + sumsum_asks.append(sum_asks) + if n < len(asks): + n = len(asks) + for order in orderbook["bids"]: + bids.append(order) + sum_bids += float(order.as_base("BBD")["base"]) + sumsum_bids.append(sum_bids) + if n < len(bids): + n = len(bids) + if show_date: + for order in orderbook["asks_date"]: + asks_date.append(order) + if n < len(asks_date): + n = len(asks_date) + for order in orderbook["bids_date"]: + bids_date.append(order) + if n < len(bids_date): + n = len(bids_date) + if chart: + if ascii: + charset = u'ascii' + else: + charset = u'utf8' + chart = AsciiChart(height=height, width=width, offset=4, placeholder=' {:10.2f} $', charset=charset) + print("\n Orderbook \n") + chart.adapt_on_series(sumsum_asks[::-1] + sumsum_bids) + chart.new_chart() + chart.add_axis() + y0 = chart._map_y(chart.minimum) + y1 = chart._map_y(chart.maximum) + chart._draw_v_line(y0 + 1, y1, int(chart.n / chart.skip / 2), line=chart.char_set["curve_vl_dot"]) + chart.add_curve(sumsum_asks[::-1] + sumsum_bids) + print(str(chart)) + return + for i in range(n): + row = [] + if len(asks_date) > i: + row.append(formatTimeString(asks_date[i])) + elif show_date: + row.append([""]) + if len(sumsum_asks) > i and not show_date: + row.append("%.2f" % sumsum_asks[i]) + elif not show_date: + row.append([""]) + if len(asks) > i: + row.append(str(asks[i])) + else: + row.append([""]) + if len(bids_date) > i: + row.append(formatTimeString(bids_date[i])) + elif show_date: + row.append([""]) + if len(sumsum_bids) > i and not show_date: + row.append("%.2f" % sumsum_bids[i]) + elif not show_date: + row.append([""]) + if len(bids) > i: + row.append(str(bids[i])) + else: + row.append([""]) + t.add_row(row) + print(t) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.argument('asset', nargs=1) +@click.argument('price', nargs=1, required=False) +@click.option('--account', '-a', help='Buy with this account (defaults to "default_account")') +@click.option('--orderid', help='Set an orderid') +def buy(amount, asset, price, account, orderid): + """Buy BEX or BBD from the internal market + + Limit buy price denoted in (BBD per BEX) + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if account is None: + account = stm.config["default_account"] + if asset == stm.bbd_symbol: + market = Market(base=Asset(stm.dpay_symbol), quote=Asset(stm.bbd_symbol), dpay_instance=stm) + else: + market = Market(base=Asset(stm.bbd_symbol), quote=Asset(stm.dpay_symbol), dpay_instance=stm) + if price is None: + orderbook = market.orderbook(limit=1, raw_data=False) + if asset == stm.dpay_symbol and len(orderbook["bids"]) > 0: + p = Price(orderbook["bids"][0]["base"], orderbook["bids"][0]["quote"], dpay_instance=stm).invert() + p_show = p + elif len(orderbook["asks"]) > 0: + p = Price(orderbook["asks"][0]["base"], orderbook["asks"][0]["quote"], dpay_instance=stm).invert() + p_show = p + price_ok = click.prompt("Is the following Price ok: %s [y/n]" % (str(p_show))) + if price_ok not in ["y", "ye", "yes"]: + return + else: + p = Price(float(price), u"%s:%s" % (stm.bbd_symbol, stm.dpay_symbol), dpay_instance=stm) + if not unlock_wallet(stm): + return + + a = Amount(float(amount), asset, dpay_instance=stm) + acc = Account(account, dpay_instance=stm) + tx = market.buy(p, a, account=acc, orderid=orderid) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('amount', nargs=1) +@click.argument('asset', nargs=1) +@click.argument('price', nargs=1, required=False) +@click.option('--account', '-a', help='Sell with this account (defaults to "default_account")') +@click.option('--orderid', help='Set an orderid') +def sell(amount, asset, price, account, orderid): + """Sell BEX or BBD from the internal market + + Limit sell price denoted in (BBD per BEX) + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if asset == stm.bbd_symbol: + market = Market(base=Asset(stm.dpay_symbol), quote=Asset(stm.bbd_symbol), dpay_instance=stm) + else: + market = Market(base=Asset(stm.bbd_symbol), quote=Asset(stm.dpay_symbol), dpay_instance=stm) + if not account: + account = stm.config["default_account"] + if not price: + orderbook = market.orderbook(limit=1, raw_data=False) + if asset == stm.bbd_symbol and len(orderbook["bids"]) > 0: + p = Price(orderbook["bids"][0]["base"], orderbook["bids"][0]["quote"], dpay_instance=stm).invert() + p_show = p + else: + p = Price(orderbook["asks"][0]["base"], orderbook["asks"][0]["quote"], dpay_instance=stm).invert() + p_show = p + price_ok = click.prompt("Is the following Price ok: %s [y/n]" % (str(p_show))) + if price_ok not in ["y", "ye", "yes"]: + return + else: + p = Price(float(price), u"%s:%s" % (stm.bbd_symbol, stm.dpay_symbol), dpay_instance=stm) + if not unlock_wallet(stm): + return + a = Amount(float(amount), asset, dpay_instance=stm) + acc = Account(account, dpay_instance=stm) + tx = market.sell(p, a, account=acc, orderid=orderid) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('orderid', nargs=1) +@click.option('--account', '-a', help='Sell with this account (defaults to "default_account")') +def cancel(orderid, account): + """Cancel order in the internal market""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + market = Market(dpay_instance=stm) + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = market.cancel(orderid, account=acc) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('account', nargs=1, required=False) +def openorders(account): + """Show open orders""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + market = Market(dpay_instance=stm) + if not account: + account = stm.config["default_account"] + acc = Account(account, dpay_instance=stm) + openorders = market.accountopenorders(account=acc) + t = PrettyTable(["Orderid", "Created", "Order", "Account"], hrules=0) + t.align = "r" + for order in openorders: + t.add_row([order["orderid"], + formatTimeString(order["created"]), + str(order["order"]), + account]) + print(t) + + +@cli.command() +@click.argument('identifier', nargs=1) +@click.option('--account', '-a', help='Repost as this user') +def repost(identifier, account): + """Repost an existing post""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + post = Comment(identifier, dpay_instance=stm) + tx = post.repost(account=acc) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('follow', nargs=1) +@click.option('--account', '-a', help='Follow from this account') +@click.option('--what', help='Follow these objects (defaults to ["blog"])', default=["blog"]) +def follow(follow, account, what): + """Follow another account""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if isinstance(what, str): + what = [what] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.follow(follow, what=what) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('mute', nargs=1) +@click.option('--account', '-a', help='Mute from this account') +@click.option('--what', help='Mute these objects (defaults to ["ignore"])', default=["ignore"]) +def mute(mute, account, what): + """Mute another account""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if isinstance(what, str): + what = [what] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.follow(mute, what=what) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('unfollow', nargs=1) +@click.option('--account', '-a', help='UnFollow/UnMute from this account') +def unfollow(unfollow, account): + """Unfollow/Unmute another account""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if not unlock_wallet(stm): + return + acc = Account(account, dpay_instance=stm) + tx = acc.unfollow(unfollow) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.option('--witness', help='Witness name') +@click.option('--maximum_block_size', help='Max block size') +@click.option('--account_creation_fee', help='Account creation fee') +@click.option('--bbd_interest_rate', help='BBD interest rate in percent') +@click.option('--url', help='Witness URL') +@click.option('--signing_key', help='Signing Key') +def witnessupdate(witness, maximum_block_size, account_creation_fee, bbd_interest_rate, url, signing_key): + """Change witness properties""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not witness: + witness = stm.config["default_account"] + if not unlock_wallet(stm): + return + witness = Witness(witness, dpay_instance=stm) + props = witness["props"] + if account_creation_fee is not None: + props["account_creation_fee"] = str( + Amount("%.3f %s" % (float(account_creation_fee), stm.dpay_symbol), dpay_instance=stm)) + if maximum_block_size is not None: + props["maximum_block_size"] = int(maximum_block_size) + if bbd_interest_rate is not None: + props["bbd_interest_rate"] = int(float(bbd_interest_rate) * 100) + tx = witness.update(signing_key or witness["signing_key"], url or witness["url"], props) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +def witnessdisable(witness): + """Disable a witness""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not witness: + witness = stm.config["default_account"] + if not unlock_wallet(stm): + return + witness = Witness(witness, dpay_instance=stm) + if not witness.is_active: + print("Cannot disable a disabled witness!") + return + props = witness["props"] + tx = witness.update('DWB1111111111111111111111111111111114T1Anm', witness["url"], props) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.argument('signing_key', nargs=1) +def witnessenable(witness, signing_key): + """Enable a witness""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not witness: + witness = stm.config["default_account"] + if not unlock_wallet(stm): + return + witness = Witness(witness, dpay_instance=stm) + props = witness["props"] + tx = witness.update(signing_key, witness["url"], props) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.argument('pub_signing_key', nargs=1) +@click.option('--maximum_block_size', help='Max block size', default=65536) +@click.option('--account_creation_fee', help='Account creation fee', default=0.1) +@click.option('--bbd_interest_rate', help='BBD interest rate in percent', default=0.0) +@click.option('--url', help='Witness URL', default="") +def witnesscreate(witness, pub_signing_key, maximum_block_size, account_creation_fee, bbd_interest_rate, url): + """Create a witness""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + props = { + "account_creation_fee": + Amount("%.3f %s" % (float(account_creation_fee), stm.dpay_symbol), dpay_instance=stm), + "maximum_block_size": + int(maximum_block_size), + "bbd_interest_rate": + int(bbd_interest_rate * 100) + } + + tx = stm.witness_update(pub_signing_key, url, props, account=witness) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.argument('wif', nargs=1) +@click.option('--account_creation_fee', help='Account creation fee (float)') +@click.option('--account_subsidy_budget', help='Account subisidy per block') +@click.option('--account_subsidy_decay', help='Per block decay of the account subsidy pool') +@click.option('--maximum_block_size', help='Max block size') +@click.option('--bbd_interest_rate', help='BBD interest rate in percent') +@click.option('--new_signing_key', help='Set new signing key') +@click.option('--url', help='Witness URL') +def witnessproperties(witness, wif, account_creation_fee, account_subsidy_budget, account_subsidy_decay, maximum_block_size, bbd_interest_rate, new_signing_key, url): + """Update witness properties of witness WITNESS with the witness signing key WIF""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + # if not unlock_wallet(stm): + # return + props = {} + if account_creation_fee is not None: + props["account_creation_fee"] = Amount("%.3f %s" % (float(account_creation_fee), stm.dpay_symbol), dpay_instance=stm) + if account_subsidy_budget is not None: + props["account_subsidy_budget"] = int(account_subsidy_budget) + if account_subsidy_decay is not None: + props["account_subsidy_decay"] = int(account_subsidy_decay) + if maximum_block_size is not None: + props["maximum_block_size"] = int(maximum_block_size) + if bbd_interest_rate is not None: + props["bbd_interest_rate"] = int(bbd_interest_rate * 100) + if new_signing_key is not None: + props["new_signing_key"] = new_signing_key + if url is not None: + props["url"] = url + + tx = stm.witness_set_properties(wif, witness, props) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +@click.argument('wif', nargs=1, required=False) +@click.option('--base', '-b', help='Set base manually, when not set the base is automatically calculated.') +@click.option('--quote', '-q', help='DPay quote manually, when not set the base is automatically calculated.') +@click.option('--support-peg', help='Supports peg adjusting the quote, is overwritten by --set-quote!', is_flag=True, default=False) +def witnessfeed(witness, wif, base, quote, support_peg): + """Publish price feed for a witness""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if wif is None: + if not unlock_wallet(stm): + return + witness = Witness(witness, dpay_instance=stm) + market = Market(dpay_instance=stm) + old_base = witness["bbd_exchange_rate"]["base"] + old_quote = witness["bbd_exchange_rate"]["quote"] + last_published_price = Price(witness["bbd_exchange_rate"], dpay_instance=stm) + dpay_usd = None + print("Old price %.3f (base: %s, quote %s)" % (float(last_published_price), old_base, old_quote)) + if quote is None and not support_peg: + quote = Amount("1.000 %s" % stm.dpay_symbol, dpay_instance=stm) + elif quote is None: + latest_price = market.ticker()['latest'] + if dpay_usd is None: + dpay_usd = market.dpay_usd_implied() + bbd_usd = float(latest_price.as_base(stm.bbd_symbol)) * dpay_usd + quote = Amount(1. / bbd_usd, stm.dpay_symbol, dpay_instance=stm) + else: + if str(quote[-5:]).upper() == stm.dpay_symbol: + quote = Amount(quote, dpay_instance=stm) + else: + quote = Amount(quote, stm.dpay_symbol, dpay_instance=stm) + if base is None: + if dpay_usd is None: + dpay_usd = market.dpay_usd_implied() + base = Amount(dpay_usd, stm.bbd_symbol, dpay_instance=stm) + else: + if str(quote[-3:]).upper() == stm.bbd_symbol: + base = Amount(base, dpay_instance=stm) + else: + base = Amount(base, stm.bbd_symbol, dpay_instance=stm) + new_price = Price(base=base, quote=quote, dpay_instance=stm) + print("New price %.3f (base: %s, quote %s)" % (float(new_price), base, quote)) + if wif is not None: + props = {"bbd_exchange_rate": new_price} + tx = stm.witness_set_properties(wif, witness["owner"], props) + else: + tx = witness.feed_publish(base, quote=quote) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('witness', nargs=1) +def witness(witness): + """ List witness information + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + witness = Witness(witness, dpay_instance=stm) + witness_json = witness.json() + witness_schedule = stm.get_witness_schedule() + config = stm.get_config() + if "VIRTUAL_SCHEDULE_LAP_LENGTH2" in config: + lap_length = int(config["VIRTUAL_SCHEDULE_LAP_LENGTH2"]) + else: + lap_length = int(config["DPAY_VIRTUAL_SCHEDULE_LAP_LENGTH2"]) + rank = 0 + active_rank = 0 + found = False + witnesses = WitnessesRankedByVote(limit=250, dpay_instance=stm) + vote_sum = witnesses.get_votes_sum() + for w in witnesses: + rank += 1 + if w.is_active: + active_rank += 1 + if w["owner"] == witness["owner"]: + found = True + break + virtual_time_to_block_num = int(witness_schedule["num_scheduled_witnesses"]) / (lap_length / (vote_sum + 1)) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(witness_json): + value = witness_json[key] + if key in ["props", "bbd_exchange_rate"]: + value = json.dumps(value, indent=4) + t.add_row([key, value]) + if found: + t.add_row(["rank", rank]) + t.add_row(["active_rank", active_rank]) + virtual_diff = int(witness_json["virtual_scheduled_time"]) - int(witness_schedule['current_virtual_time']) + block_diff_est = virtual_diff * virtual_time_to_block_num + if active_rank > 20: + t.add_row(["virtual_time_diff", virtual_diff]) + t.add_row(["block_diff_est", int(block_diff_est)]) + next_block_s = int(block_diff_est) * 3 + next_block_min = next_block_s / 60 + next_block_h = next_block_min / 60 + next_block_d = next_block_h / 24 + time_diff_est = "" + if next_block_d > 1: + time_diff_est = "%.2f days" % next_block_d + elif next_block_h > 1: + time_diff_est = "%.2f hours" % next_block_h + elif next_block_min > 1: + time_diff_est = "%.2f minutes" % next_block_min + else: + time_diff_est = "%.2f seconds" % next_block_s + t.add_row(["time_diff_est", time_diff_est]) + print(t) + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--limit', help='How many witnesses should be shown', default=100) +def witnesses(account, limit): + """ List witnesses + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if account: + account = Account(account, dpay_instance=stm) + account_name = account["name"] + if account["proxy"] != "": + account_name = account["proxy"] + account_type = "Proxy" + else: + account_type = "Account" + witnesses = WitnessesVotedByAccount(account_name, dpay_instance=stm) + print("%s: @%s (%d of 30)" % (account_type, account_name, len(witnesses))) + else: + witnesses = WitnessesRankedByVote(limit=limit, dpay_instance=stm) + witnesses.printAsTable() + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--direction', default=None, help="in or out") +@click.option('--outgoing', '-o', help='Show outgoing votes', is_flag=True, default=False) +@click.option('--incoming', '-i', help='Show incoming votes', is_flag=True, default=False) +@click.option('--days', '-d', default=2., help="Limit shown vote history by this amount of days (default: 2)") +@click.option('--export', '-e', default=None, help="Export results to TXT-file") +def votes(account, direction, outgoing, incoming, days, export): + """ List outgoing/incoming account votes + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + if direction is None and not incoming and not outgoing: + direction = "in" + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=days) + out_votes_str = "" + in_votes_str = "" + if direction == "out" or outgoing: + votes = AccountVotes(account, start=limit_time, dpay_instance=stm) + out_votes_str = votes.printAsTable(start=limit_time, return_str=True) + if direction == "in" or incoming: + account = Account(account, dpay_instance=stm) + votes_list = [] + for v in account.history(start=limit_time, only_ops=["vote"]): + votes_list.append(v) + votes = ActiveVotes(votes_list, dpay_instance=stm) + in_votes_str = votes.printAsTable(votee=account["name"], return_str=True) + if export: + with open(export, 'w') as w: + w.write(out_votes_str) + w.write("\n") + w.write(in_votes_str) + else: + print(out_votes_str) + print(in_votes_str) + + +@cli.command() +@click.argument('authorperm', nargs=1, required=False) +@click.option('--account', '-a', help='Show only curation for this account') +@click.option('--limit', '-m', help='Show only the first minutes') +@click.option('--min-vote', '-v', help='Show only votes higher than the given value') +@click.option('--max-vote', '-w', help='Show only votes lower than the given value') +@click.option('--min-performance', '-x', help='Show only votes with performance higher than the given value') +@click.option('--max-performance', '-y', help='Show only votes with performance lower than the given value') +@click.option('--payout', default=None, help="Show the curation for a potential payout in BBD as float") +@click.option('--export', '-e', default=None, help="Export results to HTML-file") +@click.option('--short', '-s', is_flag=True, default=False, help="Show only Curation without sum") +@click.option('--length', '-l', help='Limits the permlink character length', default=None) +@click.option('--permlink', '-p', help='Show the permlink for each entry', is_flag=True, default=False) +@click.option('--title', '-t', help='Show the title for each entry', is_flag=True, default=False) +@click.option('--days', '-d', default=7., help="Limit shown rewards by this amount of days (default: 7), max is 7 days.") +def curation(authorperm, account, limit, min_vote, max_vote, min_performance, max_performance, payout, export, short, length, permlink, title, days): + """ Lists curation rewards of all votes for authorperm + + When authorperm is empty or "all", the curation rewards + for all account votes are shown. + + authorperm can also be a number. e.g. 5 is equivalent to + the fifth account vote in the given time duration (default is 7 days) + + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if authorperm is None: + authorperm = 'all' + if account is None and authorperm is not 'all': + show_all_voter = True + else: + show_all_voter = False + if authorperm == 'all' or authorperm.isdigit(): + if not account: + account = stm.config["default_account"] + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=days) + votes = AccountVotes(account, start=limit_time, dpay_instance=stm) + authorperm_list = [Comment(vote.authorperm, dpay_instance=stm) for vote in votes] + if authorperm.isdigit(): + if len(authorperm_list) < int(authorperm): + raise ValueError("Authorperm id must be lower than %d" % (len(authorperm_list) + 1)) + authorperm_list = [authorperm_list[int(authorperm) - 1]["authorperm"]] + all_posts = False + else: + all_posts = True + else: + authorperm_list = [authorperm] + all_posts = False + if (all_posts) and permlink: + t = PrettyTable(["Author", "permlink", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + elif (all_posts) and title: + t = PrettyTable(["Author", "permlink", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + elif all_posts: + t = PrettyTable(["Author", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + elif (export) and permlink: + t = PrettyTable(["Author", "permlink", "Voter", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + elif (export) and title: + t = PrettyTable(["Author", "permlink", "Voter", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + elif export: + t = PrettyTable(["Author", "Voter", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + else: + t = PrettyTable(["Voter", "Voting time", "Vote", "Early vote loss", "Curation", "Performance"]) + t.align = "l" + index = 0 + for authorperm in authorperm_list: + index += 1 + comment = Comment(authorperm, dpay_instance=stm) + if payout is not None and comment.is_pending(): + payout = float(payout) + elif payout is not None: + payout = None + curation_rewards_BBD = comment.get_curation_rewards(pending_payout_BBD=True, pending_payout_value=payout) + curation_rewards_SP = comment.get_curation_rewards(pending_payout_BBD=False, pending_payout_value=payout) + rows = [] + sum_curation = [0, 0, 0, 0] + max_curation = [0, 0, 0, 0, 0, 0] + highest_vote = [0, 0, 0, 0, 0, 0] + for vote in comment["active_votes"]: + vote_BBD = stm.rshares_to_bbd(int(vote["rshares"])) + curation_BBD = curation_rewards_BBD["active_votes"][vote["voter"]] + curation_SP = curation_rewards_SP["active_votes"][vote["voter"]] + if vote_BBD > 0: + penalty = ((comment.get_curation_penalty(vote_time=vote["time"])) * vote_BBD) + performance = (float(curation_BBD) / vote_BBD * 100) + else: + performance = 0 + penalty = 0 + vote_befor_min = (((vote["time"]) - comment["created"]).total_seconds() / 60) + sum_curation[0] += vote_BBD + sum_curation[1] += penalty + sum_curation[2] += float(curation_SP) + sum_curation[3] += float(curation_BBD) + row = [vote["voter"], + vote_befor_min, + vote_BBD, + penalty, + float(curation_SP), + performance] + if row[-1] > max_curation[-1]: + max_curation = row + if row[2] > highest_vote[2]: + highest_vote = row + rows.append(row) + sortedList = sorted(rows, key=lambda row: (row[1]), reverse=False) + new_row = [] + new_row2 = [] + voter = [] + voter2 = [] + if (all_posts or export) and permlink: + if length: + new_row = [comment.author, comment.permlink[:int(length)]] + else: + new_row = [comment.author, comment.permlink] + new_row2 = ["", ""] + elif (all_posts or export) and title: + if length: + new_row = [comment.author, comment.title[:int(length)]] + else: + new_row = [comment.author, comment.title] + new_row2 = ["", ""] + elif (all_posts or export): + new_row = [comment.author] + new_row2 = [""] + if not all_posts: + voter = [""] + voter2 = [""] + found_voter = False + for row in sortedList: + if limit is not None and row[1] > float(limit): + continue + if min_vote is not None and float(row[2]) < float(min_vote): + continue + if max_vote is not None and float(row[2]) > float(max_vote): + continue + if min_performance is not None and float(row[5]) < float(min_performance): + continue + if max_performance is not None and float(row[5]) > float(max_performance): + continue + if show_all_voter or account == row[0]: + if not all_posts: + voter = [row[0]] + if all_posts: + new_row[0] = "%d. %s" % (index, comment.author) + if not found_voter: + found_voter = True + t.add_row(new_row + voter + ["%.1f min" % row[1], + "%.3f BBD" % float(row[2]), + "%.3f BBD" % float(row[3]), + "%.3f BP" % (row[4]), + "%.1f %%" % (row[5])]) + if len(authorperm_list) == 1: + new_row = new_row2 + if not short and found_voter: + t.add_row(new_row2 + voter2 + ["", "", "", "", ""]) + if sum_curation[0] > 0: + curation_sum_percentage = sum_curation[3] / sum_curation[0] * 100 + else: + curation_sum_percentage = 0 + sum_line = new_row2 + voter2 + sum_line[-1] = "High. vote" + + t.add_row(sum_line + ["%.1f min" % highest_vote[1], + "%.3f BBD" % float(highest_vote[2]), + "%.3f BBD" % float(highest_vote[3]), + "%.3f BP" % (highest_vote[4]), + "%.1f %%" % (highest_vote[5])]) + sum_line[-1] = "High. Cur." + t.add_row(sum_line + ["%.1f min" % max_curation[1], + "%.3f BBD" % float(max_curation[2]), + "%.3f BBD" % float(max_curation[3]), + "%.3f BP" % (max_curation[4]), + "%.1f %%" % (max_curation[5])]) + sum_line[-1] = "Sum" + t.add_row(sum_line + ["-", + "%.3f BBD" % (sum_curation[0]), + "%.3f BBD" % (sum_curation[1]), + "%.3f BP" % (sum_curation[2]), + "%.2f %%" % curation_sum_percentage]) + if all_posts or export: + t.add_row(new_row2 + voter2 + ["-", "-", "-", "-", "-"]) + if not (all_posts or export): + print("curation for %s" % (authorperm)) + print(t) + if export: + with open(export, 'w') as w: + w.write(str(t.get_html_string())) + elif all_posts: + print("curation for @%s" % account) + print(t) + + +@cli.command() +@click.argument('accounts', nargs=-1, required=False) +@click.option('--only-sum', '-s', help='Show only the sum', is_flag=True, default=False) +@click.option('--post', '-p', help='Show post payout', is_flag=True, default=False) +@click.option('--comment', '-c', help='Show comments payout', is_flag=True, default=False) +@click.option('--curation', '-v', help='Shows curation', is_flag=True, default=False) +@click.option('--length', '-l', help='Limits the permlink character length', default=None) +@click.option('--author', '-a', help='Show the author for each entry', is_flag=True, default=False) +@click.option('--permlink', '-e', help='Show the permlink for each entry', is_flag=True, default=False) +@click.option('--title', '-t', help='Show the title for each entry', is_flag=True, default=False) +@click.option('--days', '-d', default=7., help="Limit shown rewards by this amount of days (default: 7)") +def rewards(accounts, only_sum, post, comment, curation, length, author, permlink, title, days): + """ Lists received rewards + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not accounts: + accounts = [stm.config["default_account"]] + if not comment and not curation and not post: + post = True + permlink = True + if days < 0: + days = 1 + + utc = pytz.timezone('UTC') + now = utc.localize(datetime.utcnow()) + limit_time = now - timedelta(days=days) + for account in accounts: + sum_reward = [0, 0, 0, 0, 0] + account = Account(account, dpay_instance=stm) + median_price = Price(stm.get_current_median_history(), dpay_instance=stm) + m = Market(dpay_instance=stm) + latest = m.ticker()["latest"] + if author and permlink: + t = PrettyTable(["Author", "Permlink", "Payout", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + elif author and title: + t = PrettyTable(["Author", "Title", "Payout", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + elif author: + t = PrettyTable(["Author", "Payout", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + elif not author and permlink: + t = PrettyTable(["Permlink", "Payout", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + elif not author and title: + t = PrettyTable(["Title", "Payout", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + else: + t = PrettyTable(["Received", "BBD", "BP + BEX", "Liquid USD", "Invested USD"]) + t.align = "l" + rows = [] + start_op = account.estimate_virtual_op_num(limit_time) + if start_op > 0: + start_op -= 1 + only_ops = ['author_reward', 'curation_reward'] + progress_length = (account.virtual_op_count() - start_op) / 1000 + with click.progressbar(account.history(start=start_op, use_block_num=False, only_ops=only_ops), length=progress_length) as comment_hist: + for v in comment_hist: + if not curation and v["type"] == "curation_reward": + continue + if not post and not comment and v["type"] == "author_reward": + continue + if v["type"] == "author_reward": + c = Comment(v, dpay_instance=stm) + try: + c.refresh() + except exceptions.ContentDoesNotExistsException: + continue + if not post and not c.is_comment(): + continue + if not comment and c.is_comment(): + continue + payout_BBD = Amount(v["bbd_payout"], dpay_instance=stm) + payout_DPAY = Amount(v["dpay_payout"], dpay_instance=stm) + sum_reward[0] += float(payout_BBD) + sum_reward[1] += float(payout_DPAY) + payout_SP = stm.vests_to_sp(Amount(v["vesting_payout"], dpay_instance=stm)) + sum_reward[2] += float(payout_SP) + liquid_USD = float(payout_BBD) / float(latest) * float(median_price) + float(payout_DPAY) * float(median_price) + sum_reward[3] += liquid_USD + invested_USD = float(payout_SP) * float(median_price) + sum_reward[4] += invested_USD + if c.is_comment(): + permlink_row = c.parent_permlink + else: + if title: + permlink_row = c.title + else: + permlink_row = c.permlink + rows.append([c["author"], + permlink_row, + ((now - formatTimeString(v["timestamp"])).total_seconds() / 60 / 60 / 24), + (payout_BBD), + (payout_DPAY), + (payout_SP), + (liquid_USD), + (invested_USD)]) + elif v["type"] == "curation_reward": + reward = Amount(v["reward"], dpay_instance=stm) + payout_SP = stm.vests_to_sp(reward) + liquid_USD = 0 + invested_USD = float(payout_SP) * float(median_price) + sum_reward[2] += float(payout_SP) + sum_reward[4] += invested_USD + if title: + c = Comment(construct_authorperm(v["comment_author"], v["comment_permlink"]), dpay_instance=stm) + permlink_row = c.title + else: + permlink_row = v["comment_permlink"] + rows.append([v["comment_author"], + permlink_row, + ((now - formatTimeString(v["timestamp"])).total_seconds() / 60 / 60 / 24), + 0.000, + 0.000, + payout_SP, + (liquid_USD), + (invested_USD)]) + sortedList = sorted(rows, key=lambda row: (row[2]), reverse=False) + if only_sum: + sortedList = [] + for row in sortedList: + if length: + permlink_row = row[1][:int(length)] + else: + permlink_row = row[1] + if author and (permlink or title): + t.add_row([row[0], + permlink_row, + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % (float(row[4]) + float(row[5])), + "%.2f $" % (row[6]), + "%.2f $" % (row[7])]) + elif author and not (permlink or title): + t.add_row([row[0], + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % (float(row[4]) + float(row[5])), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + elif not author and (permlink or title): + t.add_row([permlink_row, + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % (float(row[4]) + float(row[5])), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + else: + t.add_row(["%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % (float(row[4]) + float(row[5])), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + + if author and (permlink or title): + if not only_sum: + t.add_row(["", "", "", "", "", "", ""]) + t.add_row(["Sum", + "-", + "-", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1] + sum_reward[2]), + "%.2f $" % (sum_reward[3]), + "%.2f $" % (sum_reward[4])]) + elif not author and not (permlink or title): + t.add_row(["", "", "", "", ""]) + t.add_row(["Sum", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1] + sum_reward[2]), + "%.2f $" % (sum_reward[2]), + "%.2f $" % (sum_reward[3])]) + else: + t.add_row(["", "", "", "", "", ""]) + t.add_row(["Sum", + "-", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1] + sum_reward[2]), + "%.2f $" % (sum_reward[3]), + "%.2f $" % (sum_reward[4])]) + message = "\nShowing " + if post: + if comment + curation == 0: + message += "post " + elif comment + curation == 1: + message += "post and " + else: + message += "post, " + if comment: + if curation == 0: + message += "comment " + else: + message += "comment and " + if curation: + message += "curation " + message += "rewards for @%s" % account.name + print(message) + print(t) + + +@cli.command() +@click.argument('accounts', nargs=-1, required=False) +@click.option('--only-sum', '-s', help='Show only the sum', is_flag=True, default=False) +@click.option('--post', '-p', help='Show pending post payout', is_flag=True, default=False) +@click.option('--comment', '-c', help='Show pending comments payout', is_flag=True, default=False) +@click.option('--curation', '-v', help='Shows pending curation', is_flag=True, default=False) +@click.option('--length', '-l', help='Limits the permlink character length', default=None) +@click.option('--author', '-a', help='Show the author for each entry', is_flag=True, default=False) +@click.option('--permlink', '-e', help='Show the permlink for each entry', is_flag=True, default=False) +@click.option('--title', '-t', help='Show the title for each entry', is_flag=True, default=False) +@click.option('--days', '-d', default=7., help="Limit shown rewards by this amount of days (default: 7), max is 7 days.") +def pending(accounts, only_sum, post, comment, curation, length, author, permlink, title, days): + """ Lists pending rewards + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not accounts: + accounts = [stm.config["default_account"]] + if not comment and not curation and not post: + post = True + permlink = True + if days < 0: + days = 1 + if days > 7: + days = 7 + + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=days) + for account in accounts: + sum_reward = [0, 0, 0, 0] + account = Account(account, dpay_instance=stm) + median_price = Price(stm.get_current_median_history(), dpay_instance=stm) + m = Market(dpay_instance=stm) + latest = m.ticker()["latest"] + if author and permlink: + t = PrettyTable(["Author", "Permlink", "Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + elif author and title: + t = PrettyTable(["Author", "Title", "Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + elif author: + t = PrettyTable(["Author", "Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + elif not author and permlink: + t = PrettyTable(["Permlink", "Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + elif not author and title: + t = PrettyTable(["Title", "Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + else: + t = PrettyTable(["Cashout", "BBD", "BP", "Liquid USD", "Invested USD"]) + t.align = "l" + rows = [] + c_list = {} + start_op = account.estimate_virtual_op_num(limit_time) + if start_op > 0: + start_op -= 1 + progress_length = (account.virtual_op_count() - start_op) / 1000 + with click.progressbar(map(Comment, account.history(start=start_op, use_block_num=False, only_ops=["comment"])), length=progress_length) as comment_hist: + for v in comment_hist: + try: + v.refresh() + except exceptions.ContentDoesNotExistsException: + continue + author_reward = v.get_author_rewards() + if float(author_reward["total_payout_BBD"]) < 0.001: + continue + if v.permlink in c_list: + continue + c_list[v.permlink] = 1 + if not v.is_pending(): + continue + if not post and not v.is_comment(): + continue + if not comment and v.is_comment(): + continue + if v["author"] != account["name"]: + continue + payout_BBD = author_reward["payout_BBD"] + sum_reward[0] += float(payout_BBD) + payout_SP = author_reward["payout_SP"] + sum_reward[1] += float(payout_SP) + liquid_USD = float(author_reward["payout_BBD"]) / float(latest) * float(median_price) + sum_reward[2] += liquid_USD + invested_USD = float(author_reward["payout_SP"]) * float(median_price) + sum_reward[3] += invested_USD + if v.is_comment(): + permlink_row = v.permlink + else: + if title: + permlink_row = v.title + else: + permlink_row = v.permlink + rows.append([v["author"], + permlink_row, + ((v["created"] - limit_time).total_seconds() / 60 / 60 / 24), + (payout_BBD), + (payout_SP), + (liquid_USD), + (invested_USD)]) + if curation: + votes = AccountVotes(account, start=limit_time, dpay_instance=stm) + for vote in votes: + c = Comment(vote["authorperm"], dpay_instance=stm) + rewards = c.get_curation_rewards() + if not rewards["pending_rewards"]: + continue + days_to_payout = ((c["created"] - limit_time).total_seconds() / 60 / 60 / 24) + if days_to_payout < 0: + continue + payout_SP = rewards["active_votes"][account["name"]] + liquid_USD = 0 + invested_USD = float(payout_SP) * float(median_price) + sum_reward[1] += float(payout_SP) + sum_reward[3] += invested_USD + if title: + permlink_row = c.title + else: + permlink_row = c.permlink + rows.append([c["author"], + permlink_row, + days_to_payout, + 0.000, + payout_SP, + (liquid_USD), + (invested_USD)]) + sortedList = sorted(rows, key=lambda row: (row[2]), reverse=True) + if only_sum: + sortedList = [] + for row in sortedList: + if length: + permlink_row = row[1][:int(length)] + else: + permlink_row = row[1] + if author and (permlink or title): + t.add_row([row[0], + permlink_row, + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % float(row[4]), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + elif author and not (permlink or title): + t.add_row([row[0], + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % float(row[4]), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + elif not author and (permlink or title): + t.add_row([permlink_row, + "%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % float(row[4]), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + else: + t.add_row(["%.1f days" % row[2], + "%.3f" % float(row[3]), + "%.3f" % float(row[4]), + "%.2f $" % (row[5]), + "%.2f $" % (row[6])]) + + if author and (permlink or title): + if not only_sum: + t.add_row(["", "", "", "", "", "", ""]) + t.add_row(["Sum", + "-", + "-", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1]), + "%.2f $" % (sum_reward[2]), + "%.2f $" % (sum_reward[3])]) + elif not author and not (permlink or title): + t.add_row(["", "", "", "", ""]) + t.add_row(["Sum", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1]), + "%.2f $" % (sum_reward[2]), + "%.2f $" % (sum_reward[3])]) + else: + t.add_row(["", "", "", "", "", ""]) + t.add_row(["Sum", + "-", + "%.2f BBD" % (sum_reward[0]), + "%.2f BP" % (sum_reward[1]), + "%.2f $" % (sum_reward[2]), + "%.2f $" % (sum_reward[3])]) + message = "\nShowing pending " + if post: + if comment + curation == 0: + message += "post " + elif comment + curation == 1: + message += "post and " + else: + message += "post, " + if comment: + if curation == 0: + message += "comment " + else: + message += "comment and " + if curation: + message += "curation " + message += "rewards for @%s" % account.name + print(message) + print(t) + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--reward_dpay', help='Amount of BEX you would like to claim', default=0) +@click.option('--reward_bbd', help='Amount of BBD you would like to claim', default=0) +@click.option('--reward_vests', help='Amount of VESTS you would like to claim', default=0) +@click.option('--claim_all_dpay', help='Claim all BEX, overwrites reward_dpay', is_flag=True) +@click.option('--claim_all_bbd', help='Claim all BBD, overwrites reward_bbd', is_flag=True) +@click.option('--claim_all_vests', help='Claim all VESTS, overwrites reward_vests', is_flag=True) +def claimreward(account, reward_dpay, reward_bbd, reward_vests, claim_all_dpay, claim_all_bbd, claim_all_vests): + """Claim reward balances + + By default, this will claim ``all`` outstanding balances. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not account: + account = stm.config["default_account"] + acc = Account(account, dpay_instance=stm) + r = acc.balances["rewards"] + if r[0].amount + r[1].amount + r[2].amount == 0: + print("Nothing to claim.") + return + if not unlock_wallet(stm): + return + if claim_all_dpay: + reward_dpay = r[0] + if claim_all_bbd: + reward_bbd = r[1] + if claim_all_vests: + reward_vests = r[2] + + tx = acc.claim_reward_balance(reward_dpay, reward_bbd, reward_vests) + if stm.unsigned and stm.nobroadcast and stm.dpayid is not None: + tx = stm.dpayid.url_from_tx(tx) + tx = json.dumps(tx, indent=4) + print(tx) + + +@cli.command() +@click.argument('blocknumber', nargs=1, required=False) +@click.option('--trx', '-t', help='Show only one transaction number', default=None) +@click.option('--use-api', '-u', help='Uses the get_potential_signatures api call', is_flag=True, default=False) +def verify(blocknumber, trx, use_api): + """Returns the public signing keys for a block""" + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + b = Blockchain(dpay_instance=stm) + i = 0 + if not blocknumber: + blocknumber = b.get_current_block_num() + try: + int(blocknumber) + block = Block(blocknumber, dpay_instance=stm) + if trx is not None: + i = int(trx) + trxs = [block.transactions[int(trx)]] + else: + trxs = block.transactions + except Exception: + trxs = [b.get_transaction(blocknumber)] + blocknumber = trxs[0]["block_num"] + wallet = Wallet(dpay_instance=stm) + t = PrettyTable(["trx", "Signer key", "Account"]) + t.align = "l" + if not use_api: + from dpayclibase.signedtransactions import Signed_Transaction + for trx in trxs: + if not use_api: + # trx is now identical to the output of get_transaction + # This is just for testing porpuse + if True: + signed_tx = Signed_Transaction(trx.copy()) + else: + tx = b.get_transaction(trx["transaction_id"]) + signed_tx = Signed_Transaction(tx) + public_keys = [] + for key in signed_tx.verify(chain=stm.chain_params, recover_parameter=True): + public_keys.append(format(Base58(key, prefix=stm.prefix), stm.prefix)) + else: + tx = TransactionBuilder(tx=trx, dpay_instance=stm) + public_keys = tx.get_potential_signatures() + accounts = [] + empty_public_keys = [] + for key in public_keys: + account = wallet.getAccountFromPublicKey(key) + if account is None: + empty_public_keys.append(key) + else: + accounts.append(account) + new_public_keys = [] + for key in public_keys: + if key not in empty_public_keys or use_api: + new_public_keys.append(key) + if isinstance(new_public_keys, list) and len(new_public_keys) == 1: + new_public_keys = new_public_keys[0] + else: + new_public_keys = json.dumps(new_public_keys, indent=4) + if isinstance(accounts, list) and len(accounts) == 1: + accounts = accounts[0] + else: + accounts = json.dumps(accounts, indent=4) + t.add_row(["%d" % i, new_public_keys, accounts]) + i += 1 + print(t) + + +@cli.command() +@click.argument('objects', nargs=-1) +def info(objects): + """ Show basic blockchain info + + General information about the blockchain, a block, an account, + a post/comment and a public key + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not objects: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + info = stm.get_dynamic_global_properties() + median_price = stm.get_current_median_history() + dpay_per_mvest = stm.get_dpay_per_mvest() + chain_props = stm.get_chain_properties() + price = (Amount(median_price["base"], dpay_instance=stm).amount / Amount( + median_price["quote"], dpay_instance=stm).amount) + for key in info: + t.add_row([key, info[key]]) + t.add_row(["bex per mvest", dpay_per_mvest]) + t.add_row(["internal price", price]) + t.add_row(["account_creation_fee", chain_props["account_creation_fee"]]) + print(t.get_string(sortby="Key")) + # Block + for obj in objects: + if re.match("^[0-9-]*$", obj) or re.match("^-[0-9]*$", obj) or re.match("^[0-9-]*:[0-9]", obj) or re.match("^[0-9-]*:-[0-9]", obj): + tran_nr = '' + if re.match("^[0-9-]*:[0-9-]", obj): + obj, tran_nr = obj.split(":") + if int(obj) < 1: + b = Blockchain(dpay_instance=stm) + block_number = b.get_current_block_num() + int(obj) - 1 + else: + block_number = obj + block = Block(block_number, dpay_instance=stm) + if block: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + block_json = block.json() + for key in sorted(block_json): + value = block_json[key] + if key == "transactions" and not bool(tran_nr): + t.add_row(["Nr. of transactions", len(value)]) + elif key == "transactions" and bool(tran_nr): + if int(tran_nr) < 0: + tran_nr = len(value) + int(tran_nr) + else: + tran_nr = int(tran_nr) + if len(value) > tran_nr - 1 and tran_nr > -1: + t_value = json.dumps(value[tran_nr], indent=4) + t.add_row(["transaction %d/%d" % (tran_nr, len(value)), t_value]) + elif key == "transaction_ids" and not bool(tran_nr): + t.add_row(["Nr. of transaction_ids", len(value)]) + elif key == "transaction_ids" and bool(tran_nr): + if int(tran_nr) < 0: + tran_nr = len(value) + int(tran_nr) + else: + tran_nr = int(tran_nr) + if len(value) > tran_nr - 1 and tran_nr > -1: + t.add_row(["transaction_id %d/%d" % (int(tran_nr), len(value)), value[tran_nr]]) + else: + t.add_row([key, value]) + print(t) + else: + print("Block number %s unknown" % obj) + elif re.match("^[a-zA-Z0-9\-\._]{2,16}$", obj): + account = Account(obj, dpay_instance=stm) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + account_json = account.json() + for key in sorted(account_json): + value = account_json[key] + if key == "json_metadata": + value = json.dumps(json.loads(value or "{}"), indent=4) + elif key in ["posting", "witness_votes", "active", "owner"]: + value = json.dumps(value, indent=4) + elif key == "reputation" and int(value) > 0: + value = int(value) + rep = account.rep + value = "{:.2f} ({:d})".format(rep, value) + elif isinstance(value, dict) and "asset" in value: + value = str(account[key]) + t.add_row([key, value]) + print(t) + + # witness available? + try: + witness = Witness(obj, dpay_instance=stm) + witness_json = witness.json() + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(witness_json): + value = witness_json[key] + if key in ["props", "bbd_exchange_rate"]: + value = json.dumps(value, indent=4) + t.add_row([key, value]) + print(t) + except exceptions.WitnessDoesNotExistsException as e: + print(str(e)) + # Public Key + elif re.match("^" + stm.prefix + ".{48,55}$", obj): + account = stm.wallet.getAccountFromPublicKey(obj) + if account: + account = Account(account, dpay_instance=stm) + key_type = stm.wallet.getKeyType(account, obj) + t = PrettyTable(["Account", "Key_type"]) + t.align = "l" + t.add_row([account["name"], key_type]) + print(t) + else: + print("Public Key %s not known" % obj) + # Post identifier + elif re.match(".*@.{3,16}/.*$", obj): + post = Comment(obj, dpay_instance=stm) + post_json = post.json() + if post_json: + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in sorted(post_json): + if key in ["body", "active_votes"]: + value = "not shown" + else: + value = post_json[key] + if (key in ["json_metadata"]): + value = json.loads(value) + value = json.dumps(value, indent=4) + elif (key in ["tags", "active_votes"]): + value = json.dumps(value, indent=4) + t.add_row([key, value]) + print(t) + else: + print("Post now known" % obj) + else: + print("Couldn't identify object to read") + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--signing-account', '-s', help='Signing account, when empty account is used.') +def userdata(account, signing_account): + """ Get the account's email address and phone number. + + The request has to be signed by the requested account or an admin account. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + if not account: + if "default_account" in stm.config: + account = stm.config["default_account"] + account = Account(account, dpay_instance=stm) + if signing_account is not None: + signing_account = Account(signing_account, dpay_instance=stm) + c = Conveyor(dpay_instance=stm) + user_data = c.get_user_data(account, signing_account=signing_account) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in user_data: + # hide internal config data + t.add_row([key, user_data[key]]) + print(t) + + +@cli.command() +@click.argument('account', nargs=1, required=False) +@click.option('--signing-account', '-s', help='Signing account, when empty account is used.') +def featureflags(account, signing_account): + """ Get the account's feature flags. + + The request has to be signed by the requested account or an admin account. + """ + stm = shared_dpay_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if not unlock_wallet(stm): + return + if not account: + if "default_account" in stm.config: + account = stm.config["default_account"] + account = Account(account, dpay_instance=stm) + if signing_account is not None: + signing_account = Account(signing_account, dpay_instance=stm) + c = Conveyor(dpay_instance=stm) + user_data = c.get_feature_flags(account, signing_account=signing_account) + t = PrettyTable(["Key", "Value"]) + t.align = "l" + for key in user_data: + # hide internal config data + t.add_row([key, user_data[key]]) + print(t) + + +if __name__ == "__main__": + if getattr(sys, 'frozen', False): + os.environ['SSL_CERT_FILE'] = os.path.join(sys._MEIPASS, 'lib', 'cert.pem') + cli(sys.argv[1:]) + else: + cli() diff --git a/dpaycli/comment.py b/dpaycli/comment.py new file mode 100755 index 0000000..3525439 --- /dev/null +++ b/dpaycli/comment.py @@ -0,0 +1,811 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import json +import re +import logging +import pytz +import math +from datetime import datetime, date, time +from .instance import shared_dpay_instance +from .account import Account +from .amount import Amount +from .price import Price +from .utils import resolve_authorperm, construct_authorperm, derive_permlink, remove_from_dict, make_patch, formatTimeString, formatToTimeStamp +from .blockchainobject import BlockchainObject +from .exceptions import ContentDoesNotExistsException, VotingInvalidOnArchivedPost +from dpayclibase import operations +from dpaycligraphenebase.py23 import py23_bytes, bytes_types, integer_types, string_types, text_type +from dpaycli.constants import DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6, DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20, DPAY_100_PERCENT, DPAY_1_PERCENT +log = logging.getLogger(__name__) + + +class Comment(BlockchainObject): + """ Read data about a Comment/Post in the chain + + :param str authorperm: identifier to post/comment in the form of + ``@author/permlink`` + :param dpay dpay_instance: DPay() instance to use when accessing a RPC + + + .. code-block:: python + + >>> from dpaycli.comment import Comment + >>> from dpaycli.account import Account + >>> acc = Account("gtg") + >>> authorperm = acc.get_blog(limit=1)[0]["authorperm"] + >>> c = Comment(authorperm) + >>> postdate = c["created"] + >>> postdate_str = c.json()["created"] + + """ + type_id = 8 + + def __init__( + self, + authorperm, + full=True, + lazy=False, + dpay_instance=None + ): + self.full = full + self.lazy = lazy + self.dpay = dpay_instance or shared_dpay_instance() + if isinstance(authorperm, string_types) and authorperm != "": + [author, permlink] = resolve_authorperm(authorperm) + self["id"] = 0 + self["author"] = author + self["permlink"] = permlink + self["authorperm"] = authorperm + elif isinstance(authorperm, dict) and "author" in authorperm and "permlink" in authorperm: + authorperm["authorperm"] = construct_authorperm(authorperm["author"], authorperm["permlink"]) + authorperm = self._parse_json_data(authorperm) + super(Comment, self).__init__( + authorperm, + id_item="authorperm", + lazy=lazy, + full=full, + dpay_instance=dpay_instance + ) + + def _parse_json_data(self, comment): + parse_times = [ + "active", "cashout_time", "created", "last_payout", "last_update", + "max_cashout_time" + ] + for p in parse_times: + if p in comment and isinstance(comment.get(p), string_types): + comment[p] = formatTimeString(comment.get(p, "1970-01-01T00:00:00")) + # Parse Amounts + bbd_amounts = [ + "total_payout_value", + "max_accepted_payout", + "pending_payout_value", + "curator_payout_value", + "total_pending_payout_value", + "promoted", + ] + for p in bbd_amounts: + if p in comment and isinstance(comment.get(p), (string_types, list, dict)): + comment[p] = Amount(comment.get(p, "0.000 %s" % (self.dpay.bbd_symbol)), dpay_instance=self.dpay) + + # turn json_metadata into python dict + meta_str = comment.get("json_metadata", "{}") + if meta_str == "{}": + comment['json_metadata'] = meta_str + if isinstance(meta_str, (string_types, bytes_types, bytearray)): + try: + comment['json_metadata'] = json.loads(meta_str) + except: + comment['json_metadata'] = {} + + comment["tags"] = [] + comment['community'] = '' + if isinstance(comment['json_metadata'], dict): + if "tags" in comment['json_metadata']: + comment["tags"] = comment['json_metadata']["tags"] + if 'community' in comment['json_metadata']: + comment['community'] = comment['json_metadata']['community'] + + parse_int = [ + "author_reputation", + ] + for p in parse_int: + if p in comment and isinstance(comment.get(p), string_types): + comment[p] = int(comment.get(p, "0")) + + if "active_votes" in comment: + new_active_votes = [] + for vote in comment["active_votes"]: + if 'time' in vote and isinstance(vote.get('time'), string_types): + vote['time'] = formatTimeString(vote.get('time', "1970-01-01T00:00:00")) + parse_int = [ + "rshares", "reputation", + ] + for p in parse_int: + if p in vote and isinstance(vote.get(p), string_types): + vote[p] = int(vote.get(p, "0")) + new_active_votes.append(vote) + comment["active_votes"] = new_active_votes + return comment + + def refresh(self): + if self.identifier == "": + return + if not self.dpay.is_connected(): + return + [author, permlink] = resolve_authorperm(self.identifier) + self.dpay.rpc.set_next_node_on_empty_reply(True) + if self.dpay.rpc.get_use_appbase(): + content = self.dpay.rpc.get_discussion({'author': author, 'permlink': permlink}, api="tags") + else: + content = self.dpay.rpc.get_content(author, permlink) + if not content or not content['author'] or not content['permlink']: + raise ContentDoesNotExistsException(self.identifier) + content = self._parse_json_data(content) + content["authorperm"] = construct_authorperm(content['author'], content['permlink']) + super(Comment, self).__init__(content, id_item="authorperm", lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + + def json(self): + output = self.copy() + if "authorperm" in output: + output.pop("authorperm") + if 'json_metadata' in output: + output["json_metadata"] = json.dumps(output["json_metadata"], separators=[',', ':']) + if "tags" in output: + output.pop("tags") + if "community" in output: + output.pop("community") + parse_times = [ + "active", "cashout_time", "created", "last_payout", "last_update", + "max_cashout_time" + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + bbd_amounts = [ + "total_payout_value", + "max_accepted_payout", + "pending_payout_value", + "curator_payout_value", + "total_pending_payout_value", + "promoted", + ] + for p in bbd_amounts: + if p in output and isinstance(output[p], Amount): + output[p] = output[p].json() + parse_int = [ + "author_reputation", + ] + for p in parse_int: + if p in output and isinstance(output[p], integer_types): + output[p] = str(output[p]) + if "active_votes" in output: + new_active_votes = [] + for vote in output["active_votes"]: + if 'time' in vote: + p_date = vote.get('time', datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + vote['time'] = formatTimeString(p_date) + else: + vote['time'] = p_date + parse_int = [ + "rshares", "reputation", + ] + for p in parse_int: + if p in vote and isinstance(vote[p], integer_types): + vote[p] = str(vote[p]) + new_active_votes.append(vote) + output["active_votes"] = new_active_votes + return json.loads(str(json.dumps(output))) + + @property + def id(self): + return self["id"] + + @property + def author(self): + return self["author"] + + @property + def permlink(self): + return self["permlink"] + + @property + def authorperm(self): + return construct_authorperm(self["author"], self["permlink"]) + + @property + def category(self): + if "category" in self: + return self["category"] + else: + return "" + + @property + def parent_author(self): + return self["parent_author"] + + @property + def parent_permlink(self): + return self["parent_permlink"] + + @property + def depth(self): + return self["depth"] + + @property + def title(self): + if "title" in self: + return self["title"] + else: + return "" + + @property + def body(self): + if "body" in self: + return self["body"] + else: + return "" + + @property + def json_metadata(self): + if "json_metadata" in self: + return self["json_metadata"] + else: + return {} + + def is_main_post(self): + """ Returns True if main post, and False if this is a comment (reply). + """ + return self['depth'] == 0 + + def is_comment(self): + """ Returns True if post is a comment + """ + return self['depth'] > 0 + + @property + def reward(self): + """ Return the estimated total BBD reward. + """ + a_zero = Amount(0, self.dpay.bbd_symbol, dpay_instance=self.dpay) + total = Amount(self.get("total_payout_value", a_zero), dpay_instance=self.dpay) + pending = Amount(self.get("pending_payout_value", a_zero), dpay_instance=self.dpay) + return total + pending + + def is_pending(self): + """ Return if the payout is pending (the post/comment + is younger than 7 days) + """ + a_zero = Amount(0, self.dpay.bbd_symbol, dpay_instance=self.dpay) + total = Amount(self.get("total_payout_value", a_zero), dpay_instance=self.dpay) + post_age_days = self.time_elapsed().total_seconds() / 60 / 60 / 24 + return post_age_days < 7.0 and float(total) == 0 + + def time_elapsed(self): + """Return a timedelta on how old the post is. + """ + utc = pytz.timezone('UTC') + return utc.localize(datetime.utcnow()) - self['created'] + + def curation_penalty_compensation_BBD(self): + """ Returns The required post payout amount after 15 minutes + which will compentsate the curation penalty, if voting earlier than 15 minutes + """ + self.refresh() + if self.dpay.hardfork >= 20: + reverse_auction_window_seconds = DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20 + else: + reverse_auction_window_seconds = DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6 + return self.reward * reverse_auction_window_seconds / ((self.time_elapsed()).total_seconds() / 60) ** 2 + + def estimate_curation_BBD(self, vote_value_BBD, estimated_value_BBD=None): + """ Estimates curation reward + + :param float vote_value_BBD: The vote value in BBD for which the curation + should be calculated + :param float estimated_value_BBD: When set, this value is used for calculate + the curation. When not set, the current post value is used. + """ + self.refresh() + if estimated_value_BBD is None: + estimated_value_BBD = float(self.reward) + t = 1.0 - self.get_curation_penalty() + k = vote_value_BBD / (vote_value_BBD + float(self.reward)) + K = (1 - math.sqrt(1 - k)) / 4 / k + return K * vote_value_BBD * t * math.sqrt(estimated_value_BBD) + + def get_curation_penalty(self, vote_time=None): + """ If post is less than 30 minutes old, it will incur a curation + reward penalty. + + :param datetime vote_time: A vote time can be given and the curation + penalty is calculated regarding the given time (default is None) + When set to None, the current date is used. + :returns: Float number between 0 and 1 (0.0 -> no penalty, 1.0 -> 100 % curation penalty) + :rtype: float + + """ + if vote_time is None: + elapsed_seconds = self.time_elapsed().total_seconds() + elif isinstance(vote_time, str): + elapsed_seconds = (formatTimeString(vote_time) - self["created"]).total_seconds() + elif isinstance(vote_time, (datetime, date)): + elapsed_seconds = (vote_time - self["created"]).total_seconds() + else: + raise ValueError("vote_time must be a string or a datetime") + if self.dpay.hardfork >= 20: + reward = (elapsed_seconds / DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20) + else: + reward = (elapsed_seconds / DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6) + if reward > 1: + reward = 1.0 + return 1.0 - reward + + def get_vote_with_curation(self, voter=None, raw_data=False, pending_payout_value=None): + """ Returns vote for voter. Returns None, if the voter cannot be found in `active_votes`. + + :param str voter: Voter for which the vote should be returned + :param bool raw_data: If True, the raw data are returned + :param float/str pending_payout_BBD: When not None this value instead of the current + value is used for calculating the rewards + """ + specific_vote = None + if voter is None: + voter = Account(self["author"], dpay_instance=self.dpay) + else: + voter = Account(voter, dpay_instance=self.dpay) + for vote in self["active_votes"]: + if voter["name"] == vote["voter"]: + specific_vote = vote + if specific_vote is not None and raw_data: + return specific_vote + elif specific_vote is not None: + curation_reward = self.get_curation_rewards(pending_payout_BBD=True, pending_payout_value=pending_payout_value) + specific_vote["curation_reward"] = curation_reward["active_votes"][voter["name"]] + specific_vote["ROI"] = float(curation_reward["active_votes"][voter["name"]]) / float(voter.get_voting_value_BBD(voting_weight=specific_vote["percent"] / 100)) * 100 + return specific_vote + else: + return None + + def get_beneficiaries_pct(self): + """ Returns the sum of all post beneficiaries in percentage + """ + beneficiaries = self["beneficiaries"] + weight = 0 + for b in beneficiaries: + weight += b["weight"] + return weight / 100. + + def get_rewards(self): + """ Returns the total_payout, author_payout and the curator payout in BBD. + When the payout is still pending, the estimated payout is given out. + + Example::: + + { + 'total_payout': 9.956 BBD, + 'author_payout': 7.166 BBD, + 'curator_payout': 2.790 BBD + } + + """ + if self.is_pending(): + total_payout = Amount(self["pending_payout_value"], dpay_instance=self.dpay) + author_payout = self.get_author_rewards()["total_payout_BBD"] + curator_payout = total_payout - author_payout + else: + total_payout = Amount(self["total_payout_value"], dpay_instance=self.dpay) + curator_payout = Amount(self["curator_payout_value"], dpay_instance=self.dpay) + author_payout = total_payout - curator_payout + return {"total_payout": total_payout, "author_payout": author_payout, "curator_payout": curator_payout} + + def get_author_rewards(self): + """ Returns the author rewards. + + Example::: + + { + 'pending_rewards': True, + 'payout_SP': 0.912 BEX, + 'payout_BBD': 3.583 BBD, + 'total_payout_BBD': 7.166 BBD + } + + """ + if not self.is_pending(): + total_payout = Amount(self["total_payout_value"], dpay_instance=self.dpay) + curator_payout = Amount(self["curator_payout_value"], dpay_instance=self.dpay) + author_payout = total_payout - curator_payout + return {'pending_rewards': False, + "payout_SP": Amount(0, self.dpay.dpay_symbol, dpay_instance=self.dpay), + "payout_BBD": Amount(0, self.dpay.bbd_symbol, dpay_instance=self.dpay), + "total_payout_BBD": author_payout} + + median_price = Price(self.dpay.get_current_median_history(), dpay_instance=self.dpay) + beneficiaries_pct = self.get_beneficiaries_pct() + curation_tokens = self.reward * 0.25 + author_tokens = self.reward - curation_tokens + curation_rewards = self.get_curation_rewards() + author_tokens += median_price * curation_rewards['unclaimed_rewards'] + + benefactor_tokens = author_tokens * beneficiaries_pct / 100. + author_tokens -= benefactor_tokens + + bbd_dpay = author_tokens * self["percent_dpay_dollars"] / 20000. + vesting_dpay = median_price.as_base(self.dpay.dpay_symbol) * (author_tokens - bbd_dpay) + + return {'pending_rewards': True, "payout_SP": vesting_dpay, "payout_BBD": bbd_dpay, "total_payout_BBD": author_tokens} + + def get_curation_rewards(self, pending_payout_BBD=False, pending_payout_value=None): + """ Returns the curation rewards. + + :param bool pending_payout_BBD: If True, the rewards are returned in BBD and not in BEX (default is False) + :param float/str pending_payout_value: When not None this value instead of the current + value is used for calculating the rewards + + `pending_rewards` is True when + the post is younger than 7 days. `unclaimed_rewards` is the + amount of curation_rewards that goes to the author (self-vote or votes within + the first 30 minutes). `active_votes` contains all voter with their curation reward. + + Example::: + + { + 'pending_rewards': True, 'unclaimed_rewards': 0.245 BEX, + 'active_votes': { + 'leprechaun': 0.006 BEX, 'timcliff': 0.186 BEX, + 'st3llar': 0.000 BEX, 'crokkon': 0.015 BEX, 'feedyourminnows': 0.003 BEX, + 'isnochys': 0.003 BEX, 'loshcat': 0.001 BEX, 'greenorange': 0.000 BEX, + 'qustodian': 0.123 BEX, 'jpphotography': 0.002 BEX, 'thinkingmind': 0.001 BEX, + 'oups': 0.006 BEX, 'mattockfs': 0.001 BEX, 'holger80': 0.003 BEX, 'michaelizer': 0.004 BEX, + 'flugschwein': 0.010 BEX, 'ulisessabeque': 0.000 BEX, 'hakancelik': 0.002 BEX, 'sbi2': 0.008 BEX, + 'zcool': 0.000 BEX, 'dpayhq': 0.002 BEX, 'rowdiya': 0.000 BEX, 'qurator-tier-1-2': 0.012 BEX + } + } + + """ + median_price = Price(self.dpay.get_current_median_history(), dpay_instance=self.dpay) + pending_rewards = False + total_vote_weight = self["total_vote_weight"] + if not self["allow_curation_rewards"]: + max_rewards = Amount(0, self.dpay.dpay_symbol, dpay_instance=self.dpay) + unclaimed_rewards = max_rewards.copy() + elif not self.is_pending(): + max_rewards = Amount(self["curator_payout_value"], dpay_instance=self.dpay) + unclaimed_rewards = Amount(self["total_payout_value"], dpay_instance=self.dpay) * 0.25 - max_rewards + total_vote_weight = 0 + for vote in self["active_votes"]: + total_vote_weight += int(vote["weight"]) + else: + if pending_payout_value is None: + pending_payout_value = Amount(self["pending_payout_value"], dpay_instance=self.dpay) + elif isinstance(pending_payout_value, (float, integer_types)): + pending_payout_value = Amount(pending_payout_value, self.dpay.bbd_symbol, dpay_instance=self.dpay) + elif isinstance(pending_payout_value, str): + pending_payout_value = Amount(pending_payout_value, dpay_instance=self.dpay) + if pending_payout_BBD: + max_rewards = (pending_payout_value * 0.25) + else: + max_rewards = median_price.as_base(self.dpay.dpay_symbol) * (pending_payout_value * 0.25) + unclaimed_rewards = max_rewards.copy() + pending_rewards = True + + active_votes = {} + for vote in self["active_votes"]: + if total_vote_weight > 0: + claim = max_rewards * int(vote["weight"]) / total_vote_weight + else: + claim = 0 + if claim > 0 and pending_rewards: + unclaimed_rewards -= claim + if claim > 0: + active_votes[vote["voter"]] = claim + else: + active_votes[vote["voter"]] = 0 + + return {'pending_rewards': pending_rewards, 'unclaimed_rewards': unclaimed_rewards, "active_votes": active_votes} + + def get_reblogged_by(self, identifier=None): + """Shows in which blogs this post appears""" + if not identifier: + post_author = self["author"] + post_permlink = self["permlink"] + else: + [post_author, post_permlink] = resolve_authorperm(identifier) + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + return self.dpay.rpc.get_reblogged_by({'author': post_author, 'permlink': post_permlink}, api="follow")['accounts'] + else: + return self.dpay.rpc.get_reblogged_by(post_author, post_permlink, api="follow") + + def get_replies(self, raw_data=False, identifier=None): + """ Returns content replies + + :param bool raw_data: When set to False, the replies will be returned as Comment class objects + """ + if not identifier: + post_author = self["author"] + post_permlink = self["permlink"] + else: + [post_author, post_permlink] = resolve_authorperm(identifier) + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + content_replies = self.dpay.rpc.get_content_replies({'author': post_author, 'permlink': post_permlink}, api="tags")['discussions'] + else: + content_replies = self.dpay.rpc.get_content_replies(post_author, post_permlink, api="tags") + if raw_data: + return content_replies + return [Comment(c, dpay_instance=self.dpay) for c in content_replies] + + def get_all_replies(self, parent=None): + """ Returns all content replies + """ + if parent is None: + parent = self + if parent["children"] > 0: + children = parent.get_replies() + if children is None: + return [] + for cc in children[:]: + children.extend(self.get_all_replies(parent=cc)) + return children + return [] + + def get_parent(self, children=None): + """ Returns the parent post width depth == 0""" + if children is None: + children = self + while children["depth"] > 0: + children = Comment(construct_authorperm(children["parent_author"], children["parent_permlink"]), dpay_instance=self.dpay) + return children + + def get_votes(self, raw_data=False): + """Returns all votes as ActiveVotes object""" + if raw_data: + return self["active_votes"] + from .vote import ActiveVotes + return ActiveVotes(self, lazy=False, dpay_instance=self.dpay) + + def upvote(self, weight=+100, voter=None): + """ Upvote the post + + :param float weight: (optional) Weight for posting (-100.0 - + +100.0) defaults to +100.0 + :param str voter: (optional) Voting account + + """ + last_payout = self.get('last_payout', None) + if last_payout is not None: + if formatToTimeStamp(last_payout) > 0: + raise VotingInvalidOnArchivedPost + return self.vote(weight, account=voter) + + def downvote(self, weight=-100, voter=None): + """ Downvote the post + + :param float weight: (optional) Weight for posting (-100.0 - + +100.0) defaults to -100.0 + :param str voter: (optional) Voting account + + """ + last_payout = self.get('last_payout', None) + if last_payout is not None: + if formatToTimeStamp(last_payout) > 0: + raise VotingInvalidOnArchivedPost + return self.vote(weight, account=voter) + + def vote(self, weight, account=None, identifier=None, **kwargs): + """ Vote for a post + + :param float weight: Voting weight. Range: -100.0 - +100.0. + :param str account: (optional) Account to use for voting. If + ``account`` is not defined, the ``default_account`` will be used + or a ValueError will be raised + :param str identifier: Identifier for the post to vote. Takes the + form ``@author/permlink``. + + """ + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self.dpay) + if not identifier: + post_author = self["author"] + post_permlink = self["permlink"] + else: + [post_author, post_permlink] = resolve_authorperm(identifier) + + vote_weight = int(float(weight) * DPAY_1_PERCENT) + if vote_weight > DPAY_100_PERCENT: + vote_weight = DPAY_100_PERCENT + if vote_weight < -DPAY_100_PERCENT: + vote_weight = -DPAY_100_PERCENT + + op = operations.Vote( + **{ + "voter": account["name"], + "author": post_author, + "permlink": post_permlink, + "weight": vote_weight + }) + + return self.dpay.finalizeOp(op, account, "posting", **kwargs) + + def edit(self, body, meta=None, replace=False): + """ Edit an existing post + + :param str body: Body of the reply + :param json meta: JSON meta object that can be attached to the + post. (optional) + :param bool replace: Instead of calculating a *diff*, replace + the post entirely (defaults to ``False``) + + """ + if not meta: + meta = {} + original_post = self + + if replace: + newbody = body + else: + newbody = make_patch(original_post["body"], body) + if not newbody: + log.info("No changes made! Skipping ...") + return + + reply_identifier = construct_authorperm( + original_post["parent_author"], original_post["parent_permlink"]) + + new_meta = {} + if meta is not None: + if bool(original_post["json_metadata"]): + new_meta = original_post["json_metadata"] + for key in meta: + new_meta[key] = meta[key] + else: + new_meta = meta + + return self.dpay.post( + original_post["title"], + newbody, + reply_identifier=reply_identifier, + author=original_post["author"], + permlink=original_post["permlink"], + json_metadata=new_meta, + ) + + def reply(self, body, title="", author="", meta=None): + """ Reply to an existing post + + :param str body: Body of the reply + :param str title: Title of the reply post + :param str author: Author of reply (optional) if not provided + ``default_user`` will be used, if present, else + a ``ValueError`` will be raised. + :param json meta: JSON meta object that can be attached to the + post. (optional) + + """ + return self.dpay.post( + title, + body, + json_metadata=meta, + author=author, + reply_identifier=self.identifier) + + def delete(self, account=None, identifier=None): + """ Delete an existing post/comment + + :param str account: (optional) Account to use for deletion. If + ``account`` is not defined, the ``default_account`` will be + taken or a ValueError will be raised. + + :param str identifier: (optional) Identifier for the post to delete. + Takes the form ``@author/permlink``. By default the current post + will be used. + + Note: a post/comment can only be deleted as long as it has no + replies and no positive rshares on it. + + """ + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self.dpay) + if not identifier: + post_author = self["author"] + post_permlink = self["permlink"] + else: + [post_author, post_permlink] = resolve_authorperm(identifier) + op = operations.Delete_comment( + **{"author": post_author, + "permlink": post_permlink}) + return self.dpay.finalizeOp(op, account, "posting") + + def repost(self, identifier=None, account=None): + """ Repost a post + + :param str identifier: post identifier (@/) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + """ + if not account: + account = self.dpay.configStorage.get("default_account") + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self.dpay) + if identifier is None: + identifier = self.identifier + author, permlink = resolve_authorperm(identifier) + json_body = [ + "reblog", { + "account": account["name"], + "author": author, + "permlink": permlink + } + ] + return self.dpay.custom_json( + id="follow", json_data=json_body, required_posting_auths=[account["name"]]) + + +class RecentReplies(list): + """ Obtain a list of recent replies + + :param str author: author + :param bool skip_own: (optional) Skip replies of the author to him/herself. + Default: True + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + """ + def __init__(self, author, skip_own=True, lazy=False, full=True, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(True) + state = self.dpay.rpc.get_state("/@%s/recent-replies" % author) + replies = state["accounts"][author].get("recent_replies", []) + comments = [] + for reply in replies: + post = state["content"][reply] + if skip_own and post["author"] == author: + continue + comments.append(Comment(post, lazy=lazy, full=full, dpay_instance=self.dpay)) + super(RecentReplies, self).__init__(comments) + + +class RecentByPath(list): + """ Obtain a list of votes for an account + + :param str account: Account name + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + """ + def __init__(self, path="promoted", category=None, lazy=False, full=True, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(True) + state = self.dpay.rpc.get_state("/" + path) + replies = state["discussion_idx"][''].get(path, []) + comments = [] + for reply in replies: + post = state["content"][reply] + if category is None or (category is not None and post["category"] == category): + comments.append(Comment(post, lazy=lazy, full=full, dpay_instance=self.dpay)) + super(RecentByPath, self).__init__(comments) diff --git a/dpaycli/constants.py b/dpaycli/constants.py new file mode 100755 index 0000000..adaa2dd --- /dev/null +++ b/dpaycli/constants.py @@ -0,0 +1,49 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + + +DPAY_100_PERCENT = 10000 +DPAY_1_PERCENT = 100 +DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20 = 900 +DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6 = 1800 +DPAY_VOTE_REGENERATION_SECONDS = 432000 +DPAY_VOTING_MANA_REGENERATION_SECONDS = 432000 +DPAY_VOTE_DUST_THRESHOLD = 50000000 +DPAY_ROOT_POST_PARENT = '' + +STATE_BYTES_SCALE = 10000 +STATE_TRANSACTION_BYTE_SIZE = 174 +STATE_TRANSFER_FROM_SAVINGS_BYTE_SIZE = 229 +STATE_LIMIT_ORDER_BYTE_SIZE = 1940 +RC_DEFAULT_EXEC_COST = 100000 +DPAY_RC_REGEN_TIME = 60 * 60 * 24 * 5 + +state_object_size_info = {'authority_base_size': 4 * STATE_BYTES_SCALE, + 'authority_account_member_size': 18 * STATE_BYTES_SCALE, + 'authority_key_member_size': 35 * STATE_BYTES_SCALE, + 'account_object_base_size': 480 * STATE_BYTES_SCALE, + 'account_authority_object_base_size': 40 * STATE_BYTES_SCALE, + 'account_recovery_request_object_base_size': 32 * STATE_BYTES_SCALE, + 'comment_object_base_size': 201 * STATE_BYTES_SCALE, + 'comment_object_permlink_char_size': 1 * STATE_BYTES_SCALE, + 'comment_object_parent_permlink_char_size': 2 * STATE_BYTES_SCALE, + 'comment_object_beneficiaries_member_size': 18 * STATE_BYTES_SCALE, + 'comment_vote_object_base_size': 47 * STATE_BYTES_SCALE, + 'convert_request_object_base_size': 48 * STATE_BYTES_SCALE, + 'decline_voting_rights_request_object_base_size': 28 * STATE_BYTES_SCALE, + 'escrow_object_base_size': 119 * STATE_BYTES_SCALE, + 'limit_order_object_base_size': 76 * STATE_LIMIT_ORDER_BYTE_SIZE, + 'savings_withdraw_object_byte_size': 64 * STATE_TRANSFER_FROM_SAVINGS_BYTE_SIZE, + 'transaction_object_base_size': 35 * STATE_TRANSACTION_BYTE_SIZE, + 'transaction_object_byte_size': 1 * STATE_TRANSACTION_BYTE_SIZE, + 'vesting_delegation_object_base_size': 60 * STATE_BYTES_SCALE, + 'vesting_delegation_expiration_object_base_size': 44 * STATE_BYTES_SCALE, + 'withdraw_vesting_route_object_base_size': 43 * STATE_BYTES_SCALE, + 'witness_object_base_size': 266 * STATE_BYTES_SCALE, + 'witness_object_url_char_size': 1 * STATE_BYTES_SCALE, + 'witness_vote_object_base_size': 40 * STATE_BYTES_SCALE} + +operation_exec_info = {} diff --git a/dpaycli/conveyor.py b/dpaycli/conveyor.py new file mode 100755 index 0000000..360bb98 --- /dev/null +++ b/dpaycli/conveyor.py @@ -0,0 +1,316 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import hashlib +import base64 +import json +import random +import requests +import struct +from datetime import datetime +from binascii import hexlify +from .instance import shared_dpay_instance +from .account import Account +from dpaycligraphenebase.py23 import py23_bytes +from dpaycligraphenebase.ecdsasig import sign_message +try: + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + + +class Conveyor(object): + """ Class to access DSite API instances: + https://github.com/dsites/dsite-api + + Description from the official documentation: + + * Feature flags: "Feature flags allows our apps (condenser mainly) to + hide certain features behind flags." + * User data: "Conveyor is the central point for storing sensitive user + data (email, phone, etc). No other services should store this data + and should instead query for it here every time." + * User tags: "Tagging mechanism for other services, allows defining and + assigning tags to accounts (or other identifiers) and querying for + them." + + Not contained in the documentation, but implemented and working: + + * Draft handling: saving, listing and removing post drafts + consisting of a post title and a body. + + The underlying RPC authentication and request signing procedure is + described here: https://github.com/dpays/rpc-auth + + """ + + def __init__(self, url="https://api.dsite.io", + dpay_instance=None): + """ Initialize a Conveyor instance + :param str url: (optional) URL to the Conveyor API, defaults to + https://api.dsite.io + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + """ + + self.url = url + self.dpay = dpay_instance or shared_dpay_instance() + self.id = 0 + self.ENCODING = 'utf-8' + self.TIMEFORMAT = '%Y-%m-%dT%H:%M:%S.%f' + self.K = hashlib.sha256(py23_bytes('dpay_jsonrpc_auth', + self.ENCODING)).digest() + + def prehash_message(self, timestamp, account, method, params, nonce): + """ Prepare a hash for the Conveyor API request with SHA256 according + to https://github.com/dpays/rpc-auth + Hashing of `second` is then done inside `ecdsasig.sign_message()`. + + :param str timestamp: valid iso8601 datetime ending in "Z" + :param str account: valid dPay blockchain account name + :param str method: Conveyor method name to be called + :param bytes param: base64 encoded request parameters + :param bytes nonce: random 8 bytes + + """ + first = hashlib.sha256(py23_bytes(timestamp + account + method + + params, self.ENCODING)) + return self.K + first.digest() + nonce + + def _request(self, account, method, params, key): + """Assemble the request, hash it, sign it and send it to the Conveyor + instance. Returns the server response as JSON. + + :param str account: account name + :param str method: Conveyor method name to be called + :param dict params: request parameters as `dict` + :param str key: DPay posting key for signing + + """ + params_bytes = py23_bytes(json.dumps(params), self.ENCODING) + params_enc = base64.b64encode(params_bytes).decode(self.ENCODING) + timestamp = datetime.utcnow().strftime(self.TIMEFORMAT)[:-3] + "Z" + nonce_int = random.getrandbits(64) + nonce_bytes = struct.pack('>Q', nonce_int) # 64bit ULL, big endian + nonce_str = "%016x" % (nonce_int) + + message = self.prehash_message(timestamp, account, method, + params_enc, nonce_bytes) + signature = sign_message(message, key) + signature_hex = hexlify(signature).decode(self.ENCODING) + + request = { + "jsonrpc": "2.0", + "id": self.id, + "method": method, + "params": { + "__signed": { + "account": account, + "nonce": nonce_str, + "params": params_enc, + "signatures": [signature_hex], + "timestamp": timestamp + } + } + } + r = requests.post(self.url, data=json.dumps(request)) + self.id += 1 + return r.json() + + def _conveyor_method(self, account, signing_account, method, params): + """ Wrapper function to handle account and key lookups + + :param str account: name of the addressed account + :param str signing_account: name of the account to sign the request + :param method: Conveyor method name to be called + :params dict params: request parameters as `dict` + + """ + account = Account(account, dpay_instance=self.dpay) + if signing_account is None: + signer = account + else: + signer = Account(signing_account, dpay_instance=self.dpay) + if "posting" not in signer: + signer.refresh() + if "posting" not in signer: + raise AssertionError("Could not access posting permission") + for authority in signer["posting"]["key_auths"]: + posting_wif = self.dpay.wallet.getPrivateKeyForPublicKey( + authority[0]) + return self._request(account['name'], method, params, + posting_wif) + + def get_user_data(self, account, signing_account=None): + """ Get the account's email address and phone number. The request has to be + signed by the requested account or an admin account. + + :param str account: requested account + :param str signing_account: (optional) account to sign the + request. If unset, `account` is used. + + Example: + + .. code-block:: python + + from dpaycli import DPay + from dpaycli.conveyor import Conveyor + s = DPay(keys=["5JPOSTINGKEY"]) + c = Conveyor(dpay_instance=s) + print(c.get_user_data('accountname')) + + """ + account = Account(account, dpay_instance=self.dpay) + user_data = self._conveyor_method(account, signing_account, + "conveyor.get_user_data", + [account['name']]) + if "result" in user_data: + return user_data["result"] + else: + return user_data + + def set_user_data(self, account, params, signing_account=None): + """ Set the account's email address and phone number. The request has to be + signed by an admin account. + + :param str account: requested account + :param dict param: user data to be set + :param str signing_account: (optional) account to sign the + request. If unset, `account` is used. + + Example: + + .. code-block:: python + + from dpaycli import DPay + from dpaycli.conveyor import Conveyor + s = DPay(keys=["5JADMINPOSTINGKEY"]) + c = Conveyor(dpay_instance=s) + userdata = {'email': 'foo@bar.com', 'phone':'+123456789'} + c.set_user_data('accountname', userdata, 'adminaccountname') + + """ + return self._conveyor_method(account, signing_account, + "conveyor.set_user_data", + [params]) + + def get_feature_flags(self, account, signing_account=None): + """ Get the account's feature flags. The request has to be signed by the + requested account or an admin account. + + :param str account: requested account + :param str signing_account: (optional) account to sign the + request. If unset, `account` is used. + + Example: + + .. code-block:: python + + from dpaycli import DPay + from dpaycli.conveyor import Conveyor + s = DPay(keys=["5JPOSTINGKEY"]) + c = Conveyor(dpay_instance=s) + print(c.get_feature_flags('accountname')) + + """ + account = Account(account, dpay_instance=self.dpay) + feature_flags = self._conveyor_method(account, signing_account, + "conveyor.get_feature_flags", + [account['name']]) + if "result" in feature_flags: + return feature_flags["result"] + else: + return feature_flags + + def get_feature_flag(self, account, flag, signing_account=None): + """ Test if a specific feature flag is set for an account. The request + has to be signed by the requested account or an admin account. + + :param str account: requested account + :param str flag: flag to be tested + :param str signing_account: (optional) account to sign the + request. If unset, `account` is used. + + Example: + + .. code-block:: python + + from dpaycli import DPay + from dpaycli.conveyor import Conveyor + s = DPay(keys=["5JPOSTINGKEY"]) + c = Conveyor(dpay_instance=s) + print(c.get_feature_flag('accountname', 'accepted_tos')) + + """ + account = Account(account, dpay_instance=self.dpay) + return self._conveyor_method(account, signing_account, + "conveyor.get_feature_flag", + [account['name'], flag]) + + def save_draft(self, account, title, body): + """ Save a draft in the Conveyor database + + :param str account: requested account + :param str title: draft post title + :param str body: draft post body + + """ + account = Account(account, dpay_instance=self.dpay) + draft = {'title': title, 'body': body} + return self._conveyor_method(account, None, + "conveyor.save_draft", + [account['name'], draft]) + + def list_drafts(self, account): + """ List all saved drafts from `account` + + :param str account: requested account + + Sample output: + + .. code-block:: js + + { + 'jsonrpc': '2.0', 'id': 2, 'result': [ + {'title': 'draft-title', 'body': 'draft-body', + 'uuid': '06497e1e-ac30-48cb-a069-27e1672924c9'} + ] + } + + """ + account = Account(account, dpay_instance=self.dpay) + return self._conveyor_method(account, None, + "conveyor.list_drafts", + [account['name']]) + + def remove_draft(self, account, uuid): + """ Remove a draft from the Conveyor database + + :param str account: requested account + :param str uuid: draft identifier as returned from + `list_drafts` + + """ + account = Account(account, dpay_instance=self.dpay) + return self._conveyor_method(account, None, + "conveyor.remove_draft", + [account['name'], uuid]) + + def healthcheck(self): + """ Get the Conveyor status + + Sample output: + + .. code-block:: js + + { + 'ok': True, 'version': '1.1.1-4d28e36-1528725174', + 'date': '2018-07-21T12:12:25.502Z' + } + + """ + url = urljoin(self.url, "/.well-known/healthcheck.json") + r = requests.get(url) + return r.json() diff --git a/dpaycli/discussions.py b/dpaycli/discussions.py new file mode 100755 index 0000000..9b0ef61 --- /dev/null +++ b/dpaycli/discussions.py @@ -0,0 +1,677 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from .instance import shared_dpay_instance +from .account import Account +from .comment import Comment +from .utils import resolve_authorperm +import logging +log = logging.getLogger(__name__) + + +class Query(dict): + """ Query to be used for all discussion queries + + :param int limit: limits the number of posts + :param str tag: tag query + :param int truncate_body: + :param array filter_tags: + :param array select_authors: + :param array select_tags: + :param str start_author: + :param str start_permlink: + :param str start_tag: + :param str parent_author: + :param str parent_permlink: + :param str start_parent_author: + :param str before_date: + :param str author: Author (see Discussions_by_author_before_date) + + .. testcode:: + + from dpaycli.discussions import Query + query = Query(limit=10, tag="dsocial") + + """ + def __init__(self, limit=0, tag="", truncate_body=0, + filter_tags=[], select_authors=[], select_tags=[], + start_author=None, start_permlink=None, + start_tag=None, parent_author=None, + parent_permlink=None, start_parent_author=None, + before_date=None, author=None): + self["limit"] = limit + self["truncate_body"] = truncate_body + self["tag"] = tag + self["filter_tags"] = filter_tags + self["select_authors"] = select_authors + self["select_tags"] = select_tags + self["start_author"] = start_author + self["start_permlink"] = start_permlink + self["start_tag"] = start_tag + self["parent_author"] = parent_author + self["parent_permlink"] = parent_permlink + self["start_parent_author"] = start_parent_author + self["before_date"] = before_date + self["author"] = author + + +class Discussions(object): + """ Get Discussions + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + """ + def __init__(self, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.lazy = lazy + + def get_discussions(self, discussion_type, discussion_query, limit=1000): + """ Get Discussions + + :param str discussion_type: Defines the used discussion query + :param dpaycli.discussions.Query discussion_query: + + .. testcode:: + + from dpaycli.discussions import Query, Discussions + query = Query(limit=51, tag="dsocial") + discussions = Discussions() + count = 0 + for d in discussions.get_discussions("tags", query, limit=200): + print(("%d. " % (count + 1)) + str(d)) + count += 1 + + """ + if limit >= 100 and discussion_query["limit"] == 0: + discussion_query["limit"] = 100 + elif limit < 100 and discussion_query["limit"] == 0: + discussion_query["limit"] = limit + query_count = 0 + found_more_than_start_entry = True + if "start_author" in discussion_query: + start_author = discussion_query["start_author"] + else: + start_author = None + if "start_permlink" in discussion_query: + start_permlink = discussion_query["start_permlink"] + else: + start_permlink = None + if "start_tag" in discussion_query: + start_tag = discussion_query["start_tag"] + else: + start_tag = None + if "start_parent_author" in discussion_query: + start_parent_author = discussion_query["start_parent_author"] + else: + start_parent_author = None + if not discussion_query["before_date"]: + discussion_query["before_date"] = "1970-01-01T00:00:00" + while (query_count < limit and found_more_than_start_entry): + rpc_query_count = 0 + discussion_query["start_author"] = start_author + discussion_query["start_permlink"] = start_permlink + discussion_query["start_tag"] = start_tag + discussion_query["start_parent_author"] = start_parent_author + if discussion_type == "trending": + dd = Discussions_by_trending(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "author_before_date": + dd = Discussions_by_author_before_date(author=discussion_query["author"], + start_permlink=discussion_query["start_permlink"], + before_date=discussion_query["before_date"], + limit=discussion_query["limit"], + dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "payout": + dd = Comment_discussions_by_payout(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "post_payout": + dd = Post_discussions_by_payout(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "created": + dd = Discussions_by_created(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "active": + dd = Discussions_by_active(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "cashout": + dd = Discussions_by_cashout(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "votes": + dd = Discussions_by_votes(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "children": + dd = Discussions_by_children(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "hot": + dd = Discussions_by_hot(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "feed": + dd = Discussions_by_feed(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "blog": + dd = Discussions_by_blog(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "comments": + dd = Discussions_by_comments(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "promoted": + dd = Discussions_by_promoted(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "replies": + dd = Replies_by_last_update(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + elif discussion_type == "tags": + dd = Trending_tags(discussion_query, dpay_instance=self.dpay, lazy=self.lazy) + + if not dd: + return + + for d in dd: + double_result = False + if discussion_type == "tags": + if query_count != 0 and rpc_query_count == 0 and (d["name"] == start_tag): + double_result = True + if len(dd) == 1: + found_more_than_start_entry = False + start_tag = d["name"] + elif discussion_type == "replies": + if query_count != 0 and rpc_query_count == 0 and (d["author"] == start_parent_author and d["permlink"] == start_permlink): + double_result = True + if len(dd) == 1: + found_more_than_start_entry = False + start_parent_author = d["author"] + start_permlink = d["permlink"] + else: + if query_count != 0 and rpc_query_count == 0 and (d["author"] == start_author and d["permlink"] == start_permlink): + double_result = True + if len(dd) == 1: + found_more_than_start_entry = False + start_author = d["author"] + start_permlink = d["permlink"] + rpc_query_count += 1 + if not double_result: + query_count += 1 + if query_count <= limit: + yield d + + +class Discussions_by_trending(list): + """ Get Discussions by trending + + :param dpaycli.discussions.Query discussion_query: Defines the parameter for + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_trending + q = Query(limit=10, tag="dpay") + for h in Discussions_by_trending(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_trending(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_trending(discussion_query) + super(Discussions_by_trending, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_author_before_date(list): + """ Get Discussions by author before date + + .. note:: To retrieve discussions before date, the time of creation + of the discussion @author/start_permlink must be older than + the specified before_date parameter. + + :param str author: Defines the author *(required)* + :param str start_permlink: Defines the permlink of a starting discussion + :param str before_date: Defines the before date for query + :param int limit: Defines the limit of discussions + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + from dpaycli.discussions import Query, Discussions_by_author_before_date + for h in Discussions_by_author_before_date(limit=10, author="gtg"): + print(h) + + """ + def __init__(self, author="", start_permlink="", before_date="1970-01-01T00:00:00", limit=100, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + discussion_query = {"author": author, "start_permlink": start_permlink, "before_date": before_date, "limit": limit} + posts = self.dpay.rpc.get_discussions_by_author_before_date(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_author_before_date(author, start_permlink, before_date, limit) + super(Discussions_by_author_before_date, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Comment_discussions_by_payout(list): + """ Get comment_discussions_by_payout + + :param dpaycli.discussions.Query discussion_query: Defines the parameter for + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Comment_discussions_by_payout + q = Query(limit=10) + for h in Comment_discussions_by_payout(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_comment_discussions_by_payout(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_comment_discussions_by_payout(discussion_query) + super(Comment_discussions_by_payout, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Post_discussions_by_payout(list): + """ Get post_discussions_by_payout + + :param dpaycli.discussions.Query discussion_query: Defines the parameter for + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Post_discussions_by_payout + q = Query(limit=10) + for h in Post_discussions_by_payout(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_post_discussions_by_payout(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_post_discussions_by_payout(discussion_query) + super(Post_discussions_by_payout, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_created(list): + """ Get discussions_by_created + + :param dpaycli.discussions.Query discussion_query: Defines the parameter for + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_created + q = Query(limit=10) + for h in Discussions_by_created(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_created(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_created(discussion_query) + super(Discussions_by_created, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_active(list): + """ get_discussions_by_active + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_active + q = Query(limit=10) + for h in Discussions_by_active(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_active(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_active(discussion_query) + super(Discussions_by_active, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_cashout(list): + """ Get discussions_by_cashout. This query seems to be broken at the moment. + The output is always empty. + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_cashout + q = Query(limit=10) + for h in Discussions_by_cashout(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_cashout(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_cashout(discussion_query) + super(Discussions_by_cashout, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_votes(list): + """ Get discussions_by_votes + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_votes + q = Query(limit=10) + for h in Discussions_by_votes(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_votes(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_votes(discussion_query) + super(Discussions_by_votes, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_children(list): + """ Get discussions by children + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_children + q = Query(limit=10) + for h in Discussions_by_children(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_children(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_children(discussion_query) + super(Discussions_by_children, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_hot(list): + """ Get discussions by hot + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_hot + q = Query(limit=10, tag="dpay") + for h in Discussions_by_hot(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_hot(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_hot(discussion_query) + super(Discussions_by_hot, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_feed(list): + """ Get discussions by feed + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts, tag musst be set to a username + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_feed + q = Query(limit=10, tag="dpay") + for h in Discussions_by_feed(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_feed(discussion_query, api="tags")['discussions'] + else: + # limit = discussion_query["limit"] + # account = discussion_query["tag"] + # entryId = 0 + # posts = self.dpay.rpc.get_feed(account, entryId, limit, api='follow')["comment"] + posts = self.dpay.rpc.get_discussions_by_feed(discussion_query) + super(Discussions_by_feed, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_blog(list): + """ Get discussions by blog + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts, tag musst be set to a username + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_blog + q = Query(limit=10) + for h in Discussions_by_blog(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_blog(discussion_query, api="tags")['discussions'] + else: + # limit = discussion_query["limit"] + # account = discussion_query["tag"] + # entryId = 0 + # posts = self.dpay.rpc.get_feed(account, entryId, limit, api='follow') + posts = self.dpay.rpc.get_discussions_by_blog(discussion_query) + super(Discussions_by_blog, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_comments(list): + """ Get discussions by comments + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts, start_author and start_permlink must be set. + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_comments + q = Query(limit=10, start_author="dsocial", start_permlink="firstpost") + for h in Discussions_by_comments(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_comments(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_comments(discussion_query) + super(Discussions_by_comments, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Discussions_by_promoted(list): + """ Get discussions by promoted + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Discussions_by_promoted + q = Query(limit=10, tag="dpay") + for h in Discussions_by_promoted(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_discussions_by_promoted(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_discussions_by_promoted(discussion_query) + super(Discussions_by_promoted, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Replies_by_last_update(list): + """ Returns a list of replies by last update + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts start_parent_author and start_permlink must be set. + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Replies_by_last_update + q = Query(limit=10, start_parent_author="dsocial", start_permlink="firstpost") + for h in Replies_by_last_update(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + posts = self.dpay.rpc.get_replies_by_last_update(discussion_query, api="tags")['discussions'] + else: + posts = self.dpay.rpc.get_replies_by_last_update(discussion_query["start_parent_author"], discussion_query["start_permlink"], discussion_query["limit"]) + super(Replies_by_last_update, self).__init__( + [ + Comment(x, lazy=lazy, dpay_instance=self.dpay) + for x in posts + ] + ) + + +class Trending_tags(list): + """ Returns the list of trending tags. + + :param dpaycli.discussions.Query discussion_query: Defines the parameter + searching posts, start_tag can be set. + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. testcode:: + + from dpaycli.discussions import Query, Trending_tags + q = Query(limit=10, start_tag="") + for h in Trending_tags(q): + print(h) + + """ + def __init__(self, discussion_query, lazy=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.dpay.rpc.set_next_node_on_empty_reply(self.dpay.rpc.get_use_appbase()) + if self.dpay.rpc.get_use_appbase(): + tags = self.dpay.rpc.get_trending_tags(discussion_query, api="tags")['tags'] + else: + tags = self.dpay.rpc.get_trending_tags(discussion_query["start_tag"], discussion_query["limit"], api="tags") + super(Trending_tags, self).__init__( + [ + x + for x in tags + ] + ) diff --git a/dpaycli/dpay.py b/dpaycli/dpay.py new file mode 100755 index 0000000..f2cf389 --- /dev/null +++ b/dpaycli/dpay.py @@ -0,0 +1,1948 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import object +import json +import logging +import re +import os +import math +import ast +import time +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from datetime import datetime, timedelta, date +from dpaycliapi.dpaynoderpc import DPayNodeRPC +from dpaycliapi.exceptions import NoAccessApi, NoApiWithName +from dpaycligraphenebase.account import PrivateKey, PublicKey +from dpayclibase import transactions, operations +from dpaycligraphenebase.chains import known_chains +from .account import Account +from .amount import Amount +from .price import Price +from .storage import configStorage as config +from .version import version as dpaycli_version +from .exceptions import ( + AccountExistsException, + AccountDoesNotExistsException +) +from .wallet import Wallet +from .dpayid import DPayID +from .transactionbuilder import TransactionBuilder +from .utils import formatTime, resolve_authorperm, derive_permlink, remove_from_dict, addTzInfo, formatToTimeStamp +from dpaycli.constants import DPAY_VOTE_REGENERATION_SECONDS, DPAY_100_PERCENT, DPAY_1_PERCENT, DPAY_RC_REGEN_TIME + +log = logging.getLogger(__name__) + + +class DPay(object): + """ Connect to the DPay network. + + :param str node: Node to connect to *(optional)* + :param str rpcuser: RPC user *(optional)* + :param str rpcpassword: RPC password *(optional)* + :param bool nobroadcast: Do **not** broadcast a transaction! + *(optional)* + :param bool unsigned: Do **not** sign a transaction! *(optional)* + :param bool debug: Enable Debugging *(optional)* + :param array,dict,string keys: Predefine the wif keys to shortcut the + wallet database *(optional)* + :param array,dict,string wif: Predefine the wif keys to shortcut the + wallet database *(optional)* + :param bool offline: Boolean to prevent connecting to network (defaults + to ``False``) *(optional)* + :param int expiration: Delay in seconds until transactions are supposed + to expire *(optional)* (default is 30) + :param str blocking: Wait for broadcasted transactions to be included + in a block and return full transaction (can be "head" or + "irreversible") + :param bool bundle: Do not broadcast transactions right away, but allow + to bundle operations. It is not possible to send out more than one + vote operation and more than one comment operation in a single broadcast *(optional)* + :param bool appbase: Use the new appbase rpc protocol on nodes with version + 0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower. + :param int num_retries: Set the maximum number of reconnects to the nodes before + NumRetriesReached is raised. Disabled for -1. (default is -1) + :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5) + :param int timeout: Timeout setting for https nodes (default is 60) + :param bool use_dpid: When True, a dpayid object is created. Can be used for + broadcast posting op or creating hot_links (default is False) + :param DPayID dpayid: A DPayID object can be set manually, set use_dpid to True + :param dict custom_chains: custom chain which should be added to the known chains + + Three wallet operation modes are possible: + + * **Wallet Database**: Here, the dpaylibs load the keys from the + locally stored wallet SQLite database (see ``storage.py``). + To use this mode, simply call ``DPay()`` without the + ``keys`` parameter + * **Providing Keys**: Here, you can provide the keys for + your accounts manually. All you need to do is add the wif + keys for the accounts you want to use as a simple array + using the ``keys`` parameter to ``DPay()``. + * **Force keys**: This more is for advanced users and + requires that you know what you are doing. Here, the + ``keys`` parameter is a dictionary that overwrite the + ``active``, ``owner``, ``posting`` or ``memo`` keys for + any account. This mode is only used for *foreign* + signatures! + + If no node is provided, it will connect to default nodes of + http://dpaynodes.com. Default settings can be changed with: + + .. code-block:: python + + dpay = DPay() + + where ```` starts with ``https://``, ``ws://`` or ``wss://``. + + The purpose of this class it to simplify interaction with + DPay. + + The idea is to have a class that allows to do this: + + .. code-block:: python + + >>> from dpaycli import DPay + >>> dpay = DPay() + >>> print(dpay.get_blockchain_version()) # doctest: +SKIP + + This class also deals with edits, votes and reading content. + + Example for adding a custom chain: + + .. code-block:: python + + from dpaycli import DPay + stm = DPay(node=["https://testnet.dpaydev.com"], custom_chains={"MYTESTNET": + {'chain_assets': [{'asset': 'BBD', 'id': 0, 'precision': 3, 'symbol': 'BBD'}, + {'asset': 'BEX', 'id': 1, 'precision': 3, 'symbol': 'BEX'}, + {'asset': 'VESTS', 'id': 2, 'precision': 6, 'symbol': 'VESTS'}], + 'chain_id': '79276aea5d4877d9a25892eaa01b0adf019d3e5cb12a97478df3298ccdd01674', + 'min_version': '0.0.0', + 'prefix': 'MTN'} + } + ) + + """ + + def __init__(self, + node="", + rpcuser=None, + rpcpassword=None, + debug=False, + data_refresh_time_seconds=900, + **kwargs): + """Init dpay + + :param str node: Node to connect to *(optional)* + :param str rpcuser: RPC user *(optional)* + :param str rpcpassword: RPC password *(optional)* + :param bool nobroadcast: Do **not** broadcast a transaction! + *(optional)* + :param bool unsigned: Do **not** sign a transaction! *(optional)* + :param bool debug: Enable Debugging *(optional)* + :param array,dict,string keys: Predefine the wif keys to shortcut the + wallet database *(optional)* + :param array,dict,string wif: Predefine the wif keys to shortcut the + wallet database *(optional)* + :param bool offline: Boolean to prevent connecting to network (defaults + to ``False``) *(optional)* + :param int expiration: Delay in seconds until transactions are supposed + to expire *(optional)* (default is 30) + :param str blocking: Wait for broadcast transactions to be included + in a block and return full transaction (can be "head" or + "irreversible") + :param bool bundle: Do not broadcast transactions right away, but allow + to bundle operations *(optional)* + :param bool use_condenser: Use the old condenser_api rpc protocol on nodes with version + 0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower. + :param int num_retries: Set the maximum number of reconnects to the nodes before + NumRetriesReached is raised. Disabled for -1. (default is -1) + :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5) + :param int timeout: Timeout setting for https nodes (default is 60) + :param bool use_dpid: When True, a dpayid object is created. Can be used for broadcast + posting op or creating hot_links (default is False) + :param DPayID dpayid: A DPayID object can be set manually, set use_dpid to True + + """ + + self.rpc = None + self.debug = debug + + self.offline = bool(kwargs.get("offline", False)) + self.nobroadcast = bool(kwargs.get("nobroadcast", False)) + self.unsigned = bool(kwargs.get("unsigned", False)) + self.expiration = int(kwargs.get("expiration", 30)) + self.bundle = bool(kwargs.get("bundle", False)) + self.dpayconnect = kwargs.get("dpayid", None) + self.use_dpid = bool(kwargs.get("use_dpid", False)) + self.blocking = kwargs.get("blocking", False) + self.custom_chains = kwargs.get("custom_chains", {}) + + # Store config for access through other Classes + self.config = config + + if not self.offline: + self.connect(node=node, + rpcuser=rpcuser, + rpcpassword=rpcpassword, + **kwargs) + + self.data = {'last_refresh': None, 'last_node': None, 'dynamic_global_properties': None, 'feed_history': None, + 'get_feed_history': None, 'hardfork_properties': None, + 'network': None, 'witness_schedule': None, 'reserve_ratio': None, + 'config': None, 'reward_funds': None} + self.data_refresh_time_seconds = data_refresh_time_seconds + # self.refresh_data() + + # txbuffers/propbuffer are initialized and cleared + self.clear() + + self.wallet = Wallet(dpay_instance=self, **kwargs) + + # set dpayid + if self.dpayconnect is not None and not isinstance(self.dpayconnect, DPayID): + raise ValueError("dpayid musst be DPayID object") + if self.dpayconnect is None and self.use_dpid: + self.dpayconnect = DPayID(dpay_instance=self, **kwargs) + elif self.dpayconnect is not None and not self.use_dpid: + self.use_dpid = True + + # ------------------------------------------------------------------------- + # Basic Calls + # ------------------------------------------------------------------------- + def connect(self, + node="", + rpcuser="", + rpcpassword="", + **kwargs): + """ Connect to dPay network (internal use only) + """ + if not node: + node = self.get_default_nodes() + if not bool(node): + raise ValueError("A DPay node needs to be provided!") + + if not rpcuser and "rpcuser" in config: + rpcuser = config["rpcuser"] + + if not rpcpassword and "rpcpassword" in config: + rpcpassword = config["rpcpassword"] + + self.rpc = DPayNodeRPC(node, rpcuser, rpcpassword, **kwargs) + + def is_connected(self): + """Returns if rpc is connected""" + return self.rpc is not None + + def __repr__(self): + if self.offline: + return "<%s offline=True>" % ( + self.__class__.__name__) + elif self.rpc and self.rpc.url: + return "<%s node=%s, nobroadcast=%s>" % ( + self.__class__.__name__, str(self.rpc.url), str(self.nobroadcast)) + else: + return "<%s, nobroadcast=%s>" % ( + self.__class__.__name__, str(self.nobroadcast)) + + def refresh_data(self, force_refresh=False, data_refresh_time_seconds=None): + """ Read and stores dPay blockchain parameters + If the last data refresh is older than data_refresh_time_seconds, data will be refreshed + + :param bool force_refresh: if True, a refresh of the data is enforced + :param float data_refresh_time_seconds: set a new minimal refresh time in seconds + + """ + if self.offline: + return + if data_refresh_time_seconds is not None: + self.data_refresh_time_seconds = data_refresh_time_seconds + if self.data['last_refresh'] is not None and not force_refresh and self.data["last_node"] == self.rpc.url: + if (datetime.utcnow() - self.data['last_refresh']).total_seconds() < self.data_refresh_time_seconds: + return + self.data['last_refresh'] = datetime.utcnow() + self.data["last_node"] = self.rpc.url + self.data["dynamic_global_properties"] = self.get_dynamic_global_properties(False) + try: + self.data['feed_history'] = self.get_feed_history(False) + self.data['get_feed_history'] = self.get_feed_history(False) + except: + self.data['feed_history'] = None + self.data['get_feed_history'] = None + try: + self.data['hardfork_properties'] = self.get_hardfork_properties(False) + except: + self.data['hardfork_properties'] = None + self.data['network'] = self.get_network(False) + self.data['witness_schedule'] = self.get_witness_schedule(False) + self.data['config'] = self.get_config(False) + self.data['reward_funds'] = self.get_reward_funds(False) + try: + self.data['reserve_ratio'] = self.get_reserve_ratio(False) + except: + self.data['reserve_ratio'] = None + + def get_dynamic_global_properties(self, use_stored_data=True): + """ This call returns the *dynamic global properties* + + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + + """ + if use_stored_data: + self.refresh_data() + return self.data['dynamic_global_properties'] + if self.rpc is None: + return None + self.rpc.set_next_node_on_empty_reply(True) + return self.rpc.get_dynamic_global_properties(api="database") + + def get_reserve_ratio(self, use_stored_data=True): + """ This call returns the *reserve ratio* + + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + + """ + if use_stored_data: + self.refresh_data() + return self.data['reserve_ratio'] + + if self.rpc is None: + return None + self.rpc.set_next_node_on_empty_reply(True) + if self.rpc.get_use_appbase(): + return self.rpc.get_reserve_ratio(api="witness") + else: + props = self.get_dynamic_global_properties() + # conf = self.get_config() + reserve_ratio = {'id': 0, 'average_block_size': props['average_block_size'], + 'current_reserve_ratio': props['current_reserve_ratio'], + 'max_virtual_bandwidth': props['max_virtual_bandwidth']} + return reserve_ratio + + def get_feed_history(self, use_stored_data=True): + """ Returns the feed_history + + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + + """ + if use_stored_data: + self.refresh_data() + return self.data['feed_history'] + if self.rpc is None: + return None + self.rpc.set_next_node_on_empty_reply(True) + return self.rpc.get_feed_history(api="database") + + def get_reward_funds(self, use_stored_data=True): + """ Get details for a reward fund. + + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + + """ + if use_stored_data: + self.refresh_data() + return self.data['reward_funds'] + + if self.rpc is None: + return None + ret = None + self.rpc.set_next_node_on_empty_reply(True) + if self.rpc.get_use_appbase(): + funds = self.rpc.get_reward_funds(api="database") + if funds is not None: + funds = funds['funds'] + else: + return None + if len(funds) > 0: + funds = funds[0] + ret = funds + else: + ret = self.rpc.get_reward_fund("post", api="database") + return ret + + def get_current_median_history(self, use_stored_data=True): + """ Returns the current median price + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + """ + if use_stored_data: + self.refresh_data() + if self.data['get_feed_history']: + return self.data['get_feed_history']['current_median_history'] + else: + return None + if self.rpc is None: + return None + ret = None + self.rpc.set_next_node_on_empty_reply(True) + if self.rpc.get_use_appbase(): + ret = self.rpc.get_feed_history(api="database")['current_median_history'] + else: + ret = self.rpc.get_current_median_history_price(api="database") + return ret + + def get_hardfork_properties(self, use_stored_data=True): + """ Returns Hardfork and live_time of the hardfork + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + """ + if use_stored_data: + self.refresh_data() + return self.data['hardfork_properties'] + if self.rpc is None: + return None + ret = None + self.rpc.set_next_node_on_empty_reply(True) + if self.rpc.get_use_appbase(): + ret = self.rpc.get_hardfork_properties(api="database") + else: + ret = self.rpc.get_next_scheduled_hardfork(api="database") + + return ret + + def get_network(self, use_stored_data=True): + """ Identify the network + :param bool use_stored_data: if True, stored data will be returned. If stored data are + empty or old, refresh_data() is used. + + :returns: Network parameters + :rtype: dict + """ + if use_stored_data: + self.refresh_data() + return self.data['network'] + + if self.rpc is None: + return None + try: + return self.rpc.get_network() + except: + return known_chains["DPAY"] + + def get_median_price(self, use_stored_data=True): + """ Returns the current median history price as Price + """ + median_price = self.get_current_median_history(use_stored_data=use_stored_data) + if median_price is None: + return None + a = Price( + None, + base=Amount(median_price['base'], dpay_instance=self), + quote=Amount(median_price['quote'], dpay_instance=self), + dpay_instance=self + ) + return a.as_base(self.bbd_symbol) + + def get_block_interval(self, use_stored_data=True): + """Returns the block interval in seconds""" + props = self.get_config(use_stored_data=use_stored_data) + block_interval = 3 + if props is None: + return block_interval + for key in props: + if key[-14:] == "BLOCK_INTERVAL": + block_interval = props[key] + + return block_interval + + def get_blockchain_version(self, use_stored_data=True): + """Returns the blockchain version""" + props = self.get_config(use_stored_data=use_stored_data) + blockchain_version = '0.0.0' + if props is None: + return blockchain_version + for key in props: + if key[-18:] == "BLOCKCHAIN_VERSION": + blockchain_version = props[key] + return blockchain_version + + def get_dust_threshold(self, use_stored_data=True): + """Returns the vote dust threshold""" + props = self.get_config(use_stored_data=use_stored_data) + dust_threshold = 0 + if props is None: + return dust_threshold + for key in props: + if key[-20:] == "VOTE_DUST_THRESHOLD": + dust_threshold = props[key] + return dust_threshold + + def get_resource_params(self): + """Returns the resource parameter""" + return self.rpc.get_resource_params(api="rc")["resource_params"] + + def get_resource_pool(self): + """Returns the resource pool""" + return self.rpc.get_resource_pool(api="rc")["resource_pool"] + + def get_rc_cost(self, resource_count): + """Returns the RC costs based on the resource_count""" + pools = self.get_resource_pool() + params = self.get_resource_params() + config = self.get_config() + dyn_param = self.get_dynamic_global_properties() + rc_regen = int(Amount(dyn_param["total_vesting_shares"], dpay_instance=self)) / (DPAY_RC_REGEN_TIME / config["DPAY_BLOCK_INTERVAL"]) + total_cost = 0 + if rc_regen == 0: + return total_cost + for resource_type in resource_count: + curve_params = params[resource_type]["price_curve_params"] + current_pool = int(pools[resource_type]["pool"]) + count = resource_count[resource_type] + count *= params[resource_type]["resource_dynamics_params"]["resource_unit"] + cost = self._compute_rc_cost(curve_params, current_pool, count, rc_regen) + total_cost += cost + return total_cost + + def _compute_rc_cost(self, curve_params, current_pool, resource_count, rc_regen): + """Helper function for computing the RC costs""" + num = int(rc_regen) + num *= int(curve_params['coeff_a']) + num = int(num) >> int(curve_params['shift']) + num += 1 + num *= int(resource_count) + denom = int(curve_params['coeff_b']) + if int(current_pool) > 0: + denom += int(current_pool) + num_denom = num / denom + return int(num_denom) + 1 + + def rshares_to_bbd(self, rshares, not_broadcasted_vote=False, use_stored_data=True): + """ Calculates the current BBD value of a vote + """ + payout = float(rshares) * self.get_bbd_per_rshares(use_stored_data=use_stored_data, + not_broadcasted_vote_rshares=rshares if not_broadcasted_vote else 0) + return payout + + def get_bbd_per_rshares(self, not_broadcasted_vote_rshares=0, use_stored_data=True): + """ Returns the current rshares to BBD ratio + """ + reward_fund = self.get_reward_funds(use_stored_data=use_stored_data) + reward_balance = Amount(reward_fund["reward_balance"], dpay_instance=self).amount + recent_claims = float(reward_fund["recent_claims"]) + not_broadcasted_vote_rshares + + fund_per_share = reward_balance / (recent_claims) + median_price = self.get_median_price(use_stored_data=use_stored_data) + if median_price is None: + return 0 + BBD_price = (median_price * Amount(1, self.dpay_symbol, dpay_instance=self)).amount + return fund_per_share * BBD_price + + def get_dpay_per_mvest(self, time_stamp=None, use_stored_data=True): + """ Returns the MVEST to BEX ratio + + :param int time_stamp: (optional) if set, return an estimated + BEX per MVEST ratio for the given time stamp. If unset the + current ratio is returned (default). (can also be a datetime object) + """ + if time_stamp is not None: + if isinstance(time_stamp, (datetime, date)): + time_stamp = formatToTimeStamp(time_stamp) + a = 2.1325476281078992e-05 + b = -31099.685481490847 + a2 = 2.9019227739473682e-07 + b2 = 48.41432402074669 + + if (time_stamp < (b2 - b) / (a - a2)): + return a * time_stamp + b + else: + return a2 * time_stamp + b2 + global_properties = self.get_dynamic_global_properties(use_stored_data=use_stored_data) + + return ( + Amount(global_properties['total_vesting_fund_dpay'], dpay_instance=self).amount / + (Amount(global_properties['total_vesting_shares'], dpay_instance=self).amount / 1e6) + ) + + def vests_to_sp(self, vests, timestamp=None, use_stored_data=True): + """ Converts vests to BP + + :param dpaycli.amount.Amount vests/float vests: Vests to convert + :param int timestamp: (Optional) Can be used to calculate + the conversion rate from the past + + """ + if isinstance(vests, Amount): + vests = vests.amount + return vests / 1e6 * self.get_dpay_per_mvest(timestamp, use_stored_data=use_stored_data) + + def bp_to_vests(self, bp, timestamp=None, use_stored_data=True): + """ Converts BP to vests + + :param float bp: DPay power to convert + :param datetime timestamp: (Optional) Can be used to calculate + the conversion rate from the past + """ + return bp * 1e6 / self.get_dpay_per_mvest(timestamp, use_stored_data=use_stored_data) + + def bp_to_bbd(self, bp, voting_power=DPAY_100_PERCENT, vote_pct=DPAY_100_PERCENT, not_broadcasted_vote=True, use_stored_data=True): + """ Obtain the resulting BBD vote value from DPay power + :param number dpay_power: DPay Power + :param int voting_power: voting power (100% = 10000) + :param int vote_pct: voting percentage (100% = 10000) + :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote). + Only impactful for very big votes. Slight modification to the value calculation, as the not_broadcasted + vote rshares decreases the reward pool. + """ + vesting_shares = int(self.bp_to_vests(bp, use_stored_data=use_stored_data)) + return self.vests_to_bbd(vesting_shares, voting_power=voting_power, vote_pct=vote_pct, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data) + + def vests_to_bbd(self, vests, voting_power=DPAY_100_PERCENT, vote_pct=DPAY_100_PERCENT, not_broadcasted_vote=True, use_stored_data=True): + """ Obtain the resulting BBD vote value from vests + :param number vests: vesting shares + :param int voting_power: voting power (100% = 10000) + :param int vote_pct: voting percentage (100% = 10000) + :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote). + Only impactful for very big votes. Slight modification to the value calculation, as the not_broadcasted + vote rshares decreases the reward pool. + """ + vote_rshares = self.vests_to_rshares(vests, voting_power=voting_power, vote_pct=vote_pct) + return self.rshares_to_bbd(vote_rshares, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data) + + def _max_vote_denom(self, use_stored_data=True): + # get props + global_properties = self.get_dynamic_global_properties(use_stored_data=use_stored_data) + vote_power_reserve_rate = global_properties['vote_power_reserve_rate'] + max_vote_denom = vote_power_reserve_rate * DPAY_VOTE_REGENERATION_SECONDS + return max_vote_denom + + def _calc_resulting_vote(self, voting_power=DPAY_100_PERCENT, vote_pct=DPAY_100_PERCENT, use_stored_data=True): + # determine voting power used + used_power = int((voting_power * abs(vote_pct)) / DPAY_100_PERCENT * (60 * 60 * 24)) + max_vote_denom = self._max_vote_denom(use_stored_data=use_stored_data) + used_power = int((used_power + max_vote_denom - 1) / max_vote_denom) + return used_power + + def bp_to_rshares(self, dpay_power, voting_power=DPAY_100_PERCENT, vote_pct=DPAY_100_PERCENT, use_stored_data=True): + """ Obtain the r-shares from DPay power + + :param number dpay_power: DPay Power + :param int voting_power: voting power (100% = 10000) + :param int vote_pct: voting percentage (100% = 10000) + + """ + # calculate our account voting shares (from vests) + vesting_shares = int(self.bp_to_vests(dpay_power, use_stored_data=use_stored_data)) + return self.vests_to_rshares(vesting_shares, voting_power=voting_power, vote_pct=vote_pct, use_stored_data=use_stored_data) + + def vests_to_rshares(self, vests, voting_power=DPAY_100_PERCENT, vote_pct=DPAY_100_PERCENT, subtract_dust_threshold=True, use_stored_data=True): + """ Obtain the r-shares from vests + + :param number vests: vesting shares + :param int voting_power: voting power (100% = 10000) + :param int vote_pct: voting percentage (100% = 10000) + + """ + used_power = self._calc_resulting_vote(voting_power=voting_power, vote_pct=vote_pct, use_stored_data=use_stored_data) + # calculate vote rshares + rshares = int(math.copysign(vests * 1e6 * used_power / DPAY_100_PERCENT, vote_pct)) + if subtract_dust_threshold: + if abs(rshares) <= self.get_dust_threshold(use_stored_data=use_stored_data): + return 0 + rshares -= math.copysign(self.get_dust_threshold(use_stored_data=use_stored_data), vote_pct) + return rshares + + def bbd_to_rshares(self, bbd, not_broadcasted_vote=False, use_stored_data=True): + """ Obtain the r-shares from BBD + + :param str/int/Amount bbd: BBD + :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote). + Only impactful for very high amounts of BBD. Slight modification to the value calculation, as the not_broadcasted + vote rshares decreases the reward pool. + + """ + if isinstance(bbd, Amount): + bbd = Amount(bbd, dpay_instance=self) + elif isinstance(bbd, string_types): + bbd = Amount(bbd, dpay_instance=self) + else: + bbd = Amount(bbd, self.bbd_symbol, dpay_instance=self) + if bbd['symbol'] != self.bbd_symbol: + raise AssertionError('Should input BBD, not any other asset!') + reward_pool_bbd = self.get_median_price(use_stored_data=use_stored_data) * Amount(self.get_reward_funds(use_stored_data=use_stored_data)['reward_balance']) + if bbd.amount > reward_pool_bbd.amount: + raise ValueError('Provided more BBD than available in the reward pool.') + + # If the vote was already broadcasted we can assume the blockchain values to be true + if not not_broadcasted_vote: + return bbd.amount / self.get_bbd_per_rshares(use_stored_data=use_stored_data) + + # If the vote wasn't broadcasted (yet), we have to calculate the rshares while considering + # the change our vote is causing to the recent_claims. This is more important for really + # big votes which have a significant impact on the recent_claims. + + # Get some data from the blockchain + reward_fund = self.get_reward_funds(use_stored_data=use_stored_data) + reward_balance = Amount(reward_fund["reward_balance"], dpay_instance=self).amount + recent_claims = float(reward_fund["recent_claims"]) + median_price = self.get_median_price(use_stored_data=use_stored_data) + BBD_price = (median_price * Amount(1, self.dpay_symbol, dpay_instance=self)).amount + + # This is the formular we can use to determine the "true" rshares + # We get this formular by some math magic using the previous used formulas + # FundsPerShare = (balance / (claims+newShares))*Price + # newShares = Amount / FundsPerShare + # We can now resolve both formulas for FundsPerShare and set the formulas to be equal + # (balance / (claims+newShares))*Price = Amount / newShares + # Now we resolve for newShares resulting in: + # newShares = = claims * amount / (balance*price -amount) + rshares = recent_claims * bbd.amount / ((reward_balance * BBD_price) - bbd.amount) + + return int(rshares) + + def rshares_to_vote_pct(self, rshares, dpay_power=None, vests=None, voting_power=DPAY_100_PERCENT, use_stored_data=True): + """ Obtain the voting percentage for a desired rshares value + for a given DPay Power or vesting shares and voting_power + Give either dpay_power or vests, not both. + When the output is greater than 10000 or less than -10000, + the given absolute rshares are too high + + Returns the required voting percentage (100% = 10000) + + :param number rshares: desired rshares value + :param number dpay_power: DPay Power + :param number vests: vesting shares + :param int voting_power: voting power (100% = 10000) + + """ + if dpay_power is None and vests is None: + raise ValueError("Either dpay_power or vests has to be set!") + if dpay_power is not None and vests is not None: + raise ValueError("Either dpay_power or vests has to be set. Not both!") + if dpay_power is not None: + vests = int(self.bp_to_vests(dpay_power, use_stored_data=use_stored_data) * 1e6) + + if self.hardfork >= 20: + rshares += math.copysign(self.get_dust_threshold(use_stored_data=use_stored_data), rshares) + + max_vote_denom = self._max_vote_denom(use_stored_data=use_stored_data) + + used_power = int(math.ceil(abs(rshares) * DPAY_100_PERCENT / vests)) + used_power = used_power * max_vote_denom + + vote_pct = used_power * DPAY_100_PERCENT / (60 * 60 * 24) / voting_power + return int(math.copysign(vote_pct, rshares)) + + def bbd_to_vote_pct(self, bbd, dpay_power=None, vests=None, voting_power=DPAY_100_PERCENT, not_broadcasted_vote=True, use_stored_data=True): + """ Obtain the voting percentage for a desired BBD value + for a given DPay Power or vesting shares and voting power + Give either DPay Power or vests, not both. + When the output is greater than 10000 or smaller than -10000, + the BBD value is too high. + + Returns the required voting percentage (100% = 10000) + + :param str/int/Amount bbd: desired BBD value + :param number dpay_power: DPay Power + :param number vests: vesting shares + :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote). + Only impactful for very high amounts of BBD. Slight modification to the value calculation, as the not_broadcasted + vote rshares decreases the reward pool. + + """ + if isinstance(bbd, Amount): + bbd = Amount(bbd, dpay_instance=self) + elif isinstance(bbd, string_types): + bbd = Amount(bbd, dpay_instance=self) + else: + bbd = Amount(bbd, self.bbd_symbol, dpay_instance=self) + if bbd['symbol'] != self.bbd_symbol: + raise AssertionError() + rshares = self.bbd_to_rshares(bbd, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data) + return self.rshares_to_vote_pct(rshares, dpay_power=dpay_power, vests=vests, voting_power=voting_power, use_stored_data=use_stored_data) + + def get_chain_properties(self, use_stored_data=True): + """ Return witness elected chain properties + + Properties::: + + { + 'account_creation_fee': '30.000 BEX', + 'maximum_block_size': 65536, + 'bbd_interest_rate': 250 + } + + """ + if use_stored_data: + self.refresh_data() + return self.data['witness_schedule']['median_props'] + else: + return self.get_witness_schedule(use_stored_data)['median_props'] + + def get_witness_schedule(self, use_stored_data=True): + """ Return witness elected chain properties + + """ + if use_stored_data: + self.refresh_data() + return self.data['witness_schedule'] + + if self.rpc is None: + return None + self.rpc.set_next_node_on_empty_reply(True) + return self.rpc.get_witness_schedule(api="database") + + def get_config(self, use_stored_data=True): + """ Returns internal chain configuration. + + :param bool use_stored_data: If True, the chached value is returned + """ + if use_stored_data: + self.refresh_data() + config = self.data['config'] + else: + if self.rpc is None: + return None + self.rpc.set_next_node_on_empty_reply(True) + config = self.rpc.get_config(api="database") + return config + + @property + def chain_params(self): + if self.offline or self.rpc is None: + return known_chains["DPAY"] + else: + return self.get_network() + + @property + def hardfork(self): + if self.offline or self.rpc is None: + versions = known_chains['DPAY']['min_version'] + else: + hf_prop = self.get_hardfork_properties() + if "current_hardfork_version" in hf_prop: + versions = hf_prop["current_hardfork_version"] + else: + versions = self.get_blockchain_version() + return int(versions.split('.')[1]) + + @property + def prefix(self): + return self.chain_params["prefix"] + + def set_default_account(self, account): + """ Set the default account to be used + """ + Account(account, dpay_instance=self) + config["default_account"] = account + + def set_password_storage(self, password_storage): + """ Set the password storage mode. + + When set to "no", the password has to be provided each time. + When set to "environment" the password is taken from the + UNLOCK variable + + When set to "keyring" the password is taken from the + python keyring module. A wallet password can be stored with + python -m keyring set dpaycli wallet password + + :param str password_storage: can be "no", + "keyring" or "environment" + + """ + config["password_storage"] = password_storage + + def set_default_nodes(self, nodes): + """ Set the default nodes to be used + """ + if bool(nodes): + if isinstance(nodes, list): + nodes = str(nodes) + config["node"] = nodes + else: + config.delete("node") + + def get_default_nodes(self): + """Returns the default nodes""" + if "node" in config: + nodes = config["node"] + elif "nodes" in config: + nodes = config["nodes"] + elif "default_nodes" in config and bool(config["default_nodes"]): + nodes = config["default_nodes"] + else: + nodes = [] + if isinstance(nodes, str) and nodes[0] == '[' and nodes[-1] == ']': + nodes = ast.literal_eval(nodes) + return nodes + + def move_current_node_to_front(self): + """Returns the default node list, until the first entry + is equal to the current working node url + """ + node = self.get_default_nodes() + if len(node) < 2: + return + offline = self.offline + while not offline and node[0] != self.rpc.url and len(node) > 1: + node = node[1:] + [node[0]] + self.set_default_nodes(node) + + def set_default_vote_weight(self, vote_weight): + """ Set the default vote weight to be used + """ + config["default_vote_weight"] = vote_weight + + def finalizeOp(self, ops, account, permission, **kwargs): + """ This method obtains the required private keys if present in + the wallet, finalizes the transaction, signs it and + broadacasts it + + :param operation ops: The operation (or list of operations) to + broadcast + :param operation account: The account that authorizes the + operation + :param string permission: The required permission for + signing (active, owner, posting) + :param object append_to: This allows to provide an instance of + ProposalsBuilder (see :func:`dpay.new_proposal`) or + TransactionBuilder (see :func:`dpay.new_tx()`) to specify + where to put a specific operation. + + .. note:: ``append_to`` is exposed to every method used in the + DPay class + + .. note:: If ``ops`` is a list of operation, they all need to be + signable by the same key! Thus, you cannot combine ops + that require active permission with ops that require + posting permission. Neither can you use different + accounts for different operations! + + .. note:: This uses ``dpaycli.txbuffer`` as instance of + :class:`dpaycli.transactionbuilder.TransactionBuilder`. + You may want to use your own txbuffer + """ + if self.offline: + return {} + if "append_to" in kwargs and kwargs["append_to"]: + + # Append to the append_to and return + append_to = kwargs["append_to"] + parent = append_to.get_parent() + if not isinstance(append_to, (TransactionBuilder)): + raise AssertionError() + append_to.appendOps(ops) + # Add the signer to the buffer so we sign the tx properly + parent.appendSigner(account, permission) + # This returns as we used append_to, it does NOT broadcast, or sign + return append_to.get_parent() + # Go forward to see what the other options do ... + else: + # Append to the default buffer + self.txbuffer.appendOps(ops) + + # Add signing information, signer, sign and optionally broadcast + if self.unsigned: + # In case we don't want to sign anything + self.txbuffer.addSigningInformation(account, permission) + return self.txbuffer + elif self.bundle: + # In case we want to add more ops to the tx (bundle) + self.txbuffer.appendSigner(account, permission) + return self.txbuffer.json() + else: + # default behavior: sign + broadcast + self.txbuffer.appendSigner(account, permission) + self.txbuffer.sign() + return self.txbuffer.broadcast() + + def sign(self, tx=None, wifs=[]): + """ Sign a provided transaction with the provided key(s) + + :param dict tx: The transaction to be signed and returned + :param string wifs: One or many wif keys to use for signing + a transaction. If not present, the keys will be loaded + from the wallet as defined in "missing_signatures" key + of the transactions. + """ + if tx: + txbuffer = TransactionBuilder(tx, dpay_instance=self) + else: + txbuffer = self.txbuffer + txbuffer.appendWif(wifs) + txbuffer.appendMissingSignatures() + txbuffer.sign() + return txbuffer.json() + + def broadcast(self, tx=None): + """ Broadcast a transaction to the DPay network + + :param tx tx: Signed transaction to broadcast + + """ + if tx: + # If tx is provided, we broadcast the tx + return TransactionBuilder(tx, dpay_instance=self).broadcast() + else: + return self.txbuffer.broadcast() + + def info(self, use_stored_data=True): + """ Returns the global properties + """ + return self.get_dynamic_global_properties(use_stored_data=use_stored_data) + + # ------------------------------------------------------------------------- + # Wallet stuff + # ------------------------------------------------------------------------- + def newWallet(self, pwd): + """ Create a new wallet. This method is basically only calls + :func:`dpaycli.wallet.create`. + + :param str pwd: Password to use for the new wallet + + :raises dpaycli.exceptions.WalletExists: if there is already a + wallet created + + """ + return self.wallet.create(pwd) + + def unlock(self, *args, **kwargs): + """ Unlock the internal wallet + """ + return self.wallet.unlock(*args, **kwargs) + + # ------------------------------------------------------------------------- + # Transaction Buffers + # ------------------------------------------------------------------------- + @property + def txbuffer(self): + """ Returns the currently active tx buffer + """ + return self.tx() + + def tx(self): + """ Returns the default transaction buffer + """ + return self._txbuffers[0] + + def new_tx(self, *args, **kwargs): + """ Let's obtain a new txbuffer + + :returns int txid: id of the new txbuffer + """ + builder = TransactionBuilder( + *args, + dpay_instance=self, + **kwargs + ) + self._txbuffers.append(builder) + return builder + + def clear(self): + self._txbuffers = [] + # Base/Default proposal/tx buffers + self.new_tx() + # self.new_proposal() + + # ------------------------------------------------------------------------- + # Account related calls + # ------------------------------------------------------------------------- + def claim_account(self, creator, fee=None, **kwargs): + """"Claim account for claimed account creation. + + When fee is 0 BEX a subsidized account is claimed and can be created + later with create_claimed_account. + The number of subsidized account is limited. + + :param str creator: which account should pay the registration fee (RC or BEX) + (defaults to ``default_account``) + :param str fee: when set to 0 BEX (default), claim account is paid by RC + """ + fee = fee if fee is not None else "0 %s" % (self.dpay_symbol) + if not creator and config["default_account"]: + creator = config["default_account"] + if not creator: + raise ValueError( + "Not creator account given. Define it with " + + "creator=x, or set the default_account using dpay") + creator = Account(creator, dpay_instance=self) + op = { + "fee": Amount(fee, dpay_instance=self), + "creator": creator["name"], + "prefix": self.prefix, + } + op = operations.Claim_account(**op) + return self.finalizeOp(op, creator, "active", **kwargs) + + def create_claimed_account( + self, + account_name, + creator=None, + owner_key=None, + active_key=None, + memo_key=None, + posting_key=None, + password=None, + additional_owner_keys=[], + additional_active_keys=[], + additional_posting_keys=[], + additional_owner_accounts=[], + additional_active_accounts=[], + additional_posting_accounts=[], + storekeys=True, + store_owner_key=False, + json_meta=None, + combine_with_claim_account=False, + fee=None, + **kwargs + ): + """ Create new claimed account on DPay + + The brainkey/password can be used to recover all generated keys + (see `dpaycligraphenebase.account` for more details. + + By default, this call will use ``default_account`` to + register a new name ``account_name`` with all keys being + derived from a new brain key that will be returned. The + corresponding keys will automatically be installed in the + wallet. + + .. warning:: Don't call this method unless you know what + you are doing! Be sure to understand what this + method does and where to find the private keys + for your account. + + .. note:: Please note that this imports private keys + (if password is present) into the wallet by + default when nobroadcast is set to False. + However, it **does not import the owner + key** for security reasons by default. + If you set store_owner_key to True, the + owner key is stored. + Do NOT expect to be able to recover it from + the wallet if you lose your password! + + .. note:: Account creations cost a fee that is defined by + the network. If you create an account, you will + need to pay for that fee! + + :param str account_name: (**required**) new account name + :param str json_meta: Optional meta data for the account + :param str owner_key: Main owner key + :param str active_key: Main active key + :param str posting_key: Main posting key + :param str memo_key: Main memo_key + :param str password: Alternatively to providing keys, one + can provide a password from which the + keys will be derived + :param array additional_owner_keys: Additional owner public keys + :param array additional_active_keys: Additional active public keys + :param array additional_posting_keys: Additional posting public keys + :param array additional_owner_accounts: Additional owner account + names + :param array additional_active_accounts: Additional acctive account + names + :param bool storekeys: Store new keys in the wallet (default: + ``True``) + :param bool combine_with_claim_account: When set to True, a + claim_account operation is additionally broadcasted + :param str fee: When combine_with_claim_account is set to True, + this parameter is used for the claim_account operation + + :param str creator: which account should pay the registration fee + (defaults to ``default_account``) + :raises AccountExistsException: if the account already exists on + the blockchain + + """ + fee = fee if fee is not None else "0 %s" % (self.dpay_symbol) + if not creator and config["default_account"]: + creator = config["default_account"] + if not creator: + raise ValueError( + "Not creator account given. Define it with " + + "creator=x, or set the default_account using dpay") + if password and (owner_key or active_key or memo_key): + raise ValueError( + "You cannot use 'password' AND provide keys!" + ) + + try: + Account(account_name, dpay_instance=self) + raise AccountExistsException + except AccountDoesNotExistsException: + pass + + creator = Account(creator, dpay_instance=self) + + " Generate new keys from password" + from dpaycligraphenebase.account import PasswordKey + if password: + active_key = PasswordKey(account_name, password, role="active", prefix=self.prefix) + owner_key = PasswordKey(account_name, password, role="owner", prefix=self.prefix) + posting_key = PasswordKey(account_name, password, role="posting", prefix=self.prefix) + memo_key = PasswordKey(account_name, password, role="memo", prefix=self.prefix) + active_pubkey = active_key.get_public_key() + owner_pubkey = owner_key.get_public_key() + posting_pubkey = posting_key.get_public_key() + memo_pubkey = memo_key.get_public_key() + active_privkey = active_key.get_private_key() + posting_privkey = posting_key.get_private_key() + owner_privkey = owner_key.get_private_key() + memo_privkey = memo_key.get_private_key() + # store private keys + try: + if storekeys and not self.nobroadcast: + if store_owner_key: + self.wallet.addPrivateKey(str(owner_privkey)) + self.wallet.addPrivateKey(str(active_privkey)) + self.wallet.addPrivateKey(str(memo_privkey)) + self.wallet.addPrivateKey(str(posting_privkey)) + except ValueError as e: + log.info(str(e)) + + elif (owner_key and active_key and memo_key and posting_key): + active_pubkey = PublicKey( + active_key, prefix=self.prefix) + owner_pubkey = PublicKey( + owner_key, prefix=self.prefix) + posting_pubkey = PublicKey( + posting_key, prefix=self.prefix) + memo_pubkey = PublicKey( + memo_key, prefix=self.prefix) + else: + raise ValueError( + "Call incomplete! Provide either a password or public keys!" + ) + owner = format(owner_pubkey, self.prefix) + active = format(active_pubkey, self.prefix) + posting = format(posting_pubkey, self.prefix) + memo = format(memo_pubkey, self.prefix) + + owner_key_authority = [[owner, 1]] + active_key_authority = [[active, 1]] + posting_key_authority = [[posting, 1]] + owner_accounts_authority = [] + active_accounts_authority = [] + posting_accounts_authority = [] + + # additional authorities + for k in additional_owner_keys: + owner_key_authority.append([k, 1]) + for k in additional_active_keys: + active_key_authority.append([k, 1]) + for k in additional_posting_keys: + posting_key_authority.append([k, 1]) + + for k in additional_owner_accounts: + addaccount = Account(k, dpay_instance=self) + owner_accounts_authority.append([addaccount["name"], 1]) + for k in additional_active_accounts: + addaccount = Account(k, dpay_instance=self) + active_accounts_authority.append([addaccount["name"], 1]) + for k in additional_posting_accounts: + addaccount = Account(k, dpay_instance=self) + posting_accounts_authority.append([addaccount["name"], 1]) + if combine_with_claim_account: + op = { + "fee": Amount(fee, dpay_instance=self), + "creator": creator["name"], + "prefix": self.prefix, + } + op = operations.Claim_account(**op) + ops = [op] + op = { + "creator": creator["name"], + "new_account_name": account_name, + 'owner': {'account_auths': owner_accounts_authority, + 'key_auths': owner_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'active': {'account_auths': active_accounts_authority, + 'key_auths': active_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'posting': {'account_auths': active_accounts_authority, + 'key_auths': posting_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'memo_key': memo, + "json_metadata": json_meta or {}, + "prefix": self.prefix, + } + op = operations.Create_claimed_account(**op) + if combine_with_claim_account: + ops.append(op) + return self.finalizeOp(ops, creator, "active", **kwargs) + else: + return self.finalizeOp(op, creator, "active", **kwargs) + + def create_account( + self, + account_name, + creator=None, + owner_key=None, + active_key=None, + memo_key=None, + posting_key=None, + password=None, + additional_owner_keys=[], + additional_active_keys=[], + additional_posting_keys=[], + additional_owner_accounts=[], + additional_active_accounts=[], + additional_posting_accounts=[], + storekeys=True, + store_owner_key=False, + json_meta=None, + **kwargs + ): + """ Create new account on DPay + + The brainkey/password can be used to recover all generated keys + (see `dpaycligraphenebase.account` for more details. + + By default, this call will use ``default_account`` to + register a new name ``account_name`` with all keys being + derived from a new brain key that will be returned. The + corresponding keys will automatically be installed in the + wallet. + + .. warning:: Don't call this method unless you know what + you are doing! Be sure to understand what this + method does and where to find the private keys + for your account. + + .. note:: Please note that this imports private keys + (if password is present) into the wallet by + default when nobroadcast is set to False. + However, it **does not import the owner + key** for security reasons by default. + If you set store_owner_key to True, the + owner key is stored. + Do NOT expect to be able to recover it from + the wallet if you lose your password! + + .. note:: Account creations cost a fee that is defined by + the network. If you create an account, you will + need to pay for that fee! + + :param str account_name: (**required**) new account name + :param str json_meta: Optional meta data for the account + :param str owner_key: Main owner key + :param str active_key: Main active key + :param str posting_key: Main posting key + :param str memo_key: Main memo_key + :param str password: Alternatively to providing keys, one + can provide a password from which the + keys will be derived + :param array additional_owner_keys: Additional owner public keys + :param array additional_active_keys: Additional active public keys + :param array additional_posting_keys: Additional posting public keys + :param array additional_owner_accounts: Additional owner account + names + :param array additional_active_accounts: Additional acctive account + names + :param bool storekeys: Store new keys in the wallet (default: + ``True``) + + :param str creator: which account should pay the registration fee + (defaults to ``default_account``) + :raises AccountExistsException: if the account already exists on + the blockchain + + """ + if not creator and config["default_account"]: + creator = config["default_account"] + if not creator: + raise ValueError( + "Not creator account given. Define it with " + + "creator=x, or set the default_account using dpay") + if password and (owner_key or active_key or memo_key): + raise ValueError( + "You cannot use 'password' AND provide keys!" + ) + + try: + Account(account_name, dpay_instance=self) + raise AccountExistsException + except AccountDoesNotExistsException: + pass + + creator = Account(creator, dpay_instance=self) + + " Generate new keys from password" + from dpaycligraphenebase.account import PasswordKey + if password: + active_key = PasswordKey(account_name, password, role="active", prefix=self.prefix) + owner_key = PasswordKey(account_name, password, role="owner", prefix=self.prefix) + posting_key = PasswordKey(account_name, password, role="posting", prefix=self.prefix) + memo_key = PasswordKey(account_name, password, role="memo", prefix=self.prefix) + active_pubkey = active_key.get_public_key() + owner_pubkey = owner_key.get_public_key() + posting_pubkey = posting_key.get_public_key() + memo_pubkey = memo_key.get_public_key() + active_privkey = active_key.get_private_key() + posting_privkey = posting_key.get_private_key() + owner_privkey = owner_key.get_private_key() + memo_privkey = memo_key.get_private_key() + # store private keys + try: + if storekeys and not self.nobroadcast: + if store_owner_key: + self.wallet.addPrivateKey(str(owner_privkey)) + self.wallet.addPrivateKey(str(active_privkey)) + self.wallet.addPrivateKey(str(memo_privkey)) + self.wallet.addPrivateKey(str(posting_privkey)) + except ValueError as e: + log.info(str(e)) + + elif (owner_key and active_key and memo_key and posting_key): + active_pubkey = PublicKey( + active_key, prefix=self.prefix) + owner_pubkey = PublicKey( + owner_key, prefix=self.prefix) + posting_pubkey = PublicKey( + posting_key, prefix=self.prefix) + memo_pubkey = PublicKey( + memo_key, prefix=self.prefix) + else: + raise ValueError( + "Call incomplete! Provide either a password or public keys!" + ) + owner = format(owner_pubkey, self.prefix) + active = format(active_pubkey, self.prefix) + posting = format(posting_pubkey, self.prefix) + memo = format(memo_pubkey, self.prefix) + + owner_key_authority = [[owner, 1]] + active_key_authority = [[active, 1]] + posting_key_authority = [[posting, 1]] + owner_accounts_authority = [] + active_accounts_authority = [] + posting_accounts_authority = [] + + # additional authorities + for k in additional_owner_keys: + owner_key_authority.append([k, 1]) + for k in additional_active_keys: + active_key_authority.append([k, 1]) + for k in additional_posting_keys: + posting_key_authority.append([k, 1]) + + for k in additional_owner_accounts: + addaccount = Account(k, dpay_instance=self) + owner_accounts_authority.append([addaccount["name"], 1]) + for k in additional_active_accounts: + addaccount = Account(k, dpay_instance=self) + active_accounts_authority.append([addaccount["name"], 1]) + for k in additional_posting_accounts: + addaccount = Account(k, dpay_instance=self) + posting_accounts_authority.append([addaccount["name"], 1]) + + props = self.get_chain_properties() + if self.hardfork >= 20: + required_fee_dpay = Amount(props["account_creation_fee"], dpay_instance=self) + else: + required_fee_dpay = Amount(props["account_creation_fee"], dpay_instance=self) * 30 + op = { + "fee": required_fee_dpay, + "creator": creator["name"], + "new_account_name": account_name, + 'owner': {'account_auths': owner_accounts_authority, + 'key_auths': owner_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'active': {'account_auths': active_accounts_authority, + 'key_auths': active_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'posting': {'account_auths': active_accounts_authority, + 'key_auths': posting_key_authority, + "address_auths": [], + 'weight_threshold': 1}, + 'memo_key': memo, + "json_metadata": json_meta or {}, + "prefix": self.prefix, + } + op = operations.Account_create(**op) + return self.finalizeOp(op, creator, "active", **kwargs) + + def witness_set_properties(self, wif, owner, props, use_condenser_api=True): + """ Set witness properties + + :param privkey wif: Private signing key + :param dict props: Properties + :param str owner: witness account name + + Properties::: + + { + "account_creation_fee": x, + "account_subsidy_budget": x, + "account_subsidy_decay": x, + "maximum_block_size": x, + "url": x, + "bbd_exchange_rate": x, + "bbd_interest_rate": x, + "new_signing_key": x + } + + """ + + owner = Account(owner, dpay_instance=self) + + try: + PrivateKey(wif, prefix=self.prefix) + except Exception as e: + raise e + props_list = [["key", repr(PrivateKey(wif, prefix=self.prefix).pubkey)]] + for k in props: + props_list.append([k, props[k]]) + + op = operations.Witness_set_properties({"owner": owner["name"], "props": props_list, "prefix": self.prefix}) + tb = TransactionBuilder(use_condenser_api=use_condenser_api, dpay_instance=self) + tb.appendOps([op]) + tb.appendWif(wif) + tb.sign() + return tb.broadcast() + + def witness_update(self, signing_key, url, props, account=None, **kwargs): + """ Creates/updates a witness + + :param pubkey signing_key: Public signing key + :param str url: URL + :param dict props: Properties + :param str account: (optional) witness account name + + Properties::: + + { + "account_creation_fee": "3.000 BEX", + "maximum_block_size": 65536, + "bbd_interest_rate": 0, + } + + """ + if not account and config["default_account"]: + account = config["default_account"] + if not account: + raise ValueError("You need to provide an account") + + account = Account(account, dpay_instance=self) + + try: + PublicKey(signing_key, prefix=self.prefix) + except Exception as e: + raise e + if "account_creation_fee" in props: + props["account_creation_fee"] = Amount(props["account_creation_fee"], dpay_instance=self) + op = operations.Witness_update( + **{ + "owner": account["name"], + "url": url, + "block_signing_key": signing_key, + "props": props, + "fee": Amount(0, self.dpay_symbol, dpay_instance=self), + "prefix": self.prefix, + }) + return self.finalizeOp(op, account, "active", **kwargs) + + def _test_weights_treshold(self, authority): + """ This method raises an error if the threshold of an authority cannot + be reached by the weights. + + :param dict authority: An authority of an account + :raises ValueError: if the threshold is set too high + """ + weights = 0 + for a in authority["account_auths"]: + weights += int(a[1]) + for a in authority["key_auths"]: + weights += int(a[1]) + if authority["weight_threshold"] > weights: + raise ValueError("Threshold too restrictive!") + if authority["weight_threshold"] == 0: + raise ValueError("Cannot have threshold of 0") + + def custom_json(self, + id, + json_data, + required_auths=[], + required_posting_auths=[], + **kwargs): + """ Create a custom json operation + + :param str id: identifier for the custom json (max length 32 bytes) + :param json json_data: the json data to put into the custom_json + operation + :param list required_auths: (optional) required auths + :param list required_posting_auths: (optional) posting auths + + Note: While reqired auths and required_posting_auths are both + optional, one of the two are needed in order to send the custom + json. + + .. code-block:: python + + dpay.custom_json("id", "json_data", + required_posting_auths=['account']) + + """ + account = None + if len(required_auths): + account = required_auths[0] + elif len(required_posting_auths): + account = required_posting_auths[0] + else: + raise Exception("At least one account needs to be specified") + account = Account(account, full=False, dpay_instance=self) + op = operations.Custom_json( + **{ + "json": json_data, + "required_auths": required_auths, + "required_posting_auths": required_posting_auths, + "id": id, + "prefix": self.prefix, + }) + return self.finalizeOp(op, account, "posting", **kwargs) + + def post(self, + title, + body, + author=None, + permlink=None, + reply_identifier=None, + json_metadata=None, + comment_options=None, + community=None, + app=None, + tags=None, + beneficiaries=None, + self_vote=False, + parse_body=False, + **kwargs): + """ Create a new post. + If this post is intended as a reply/comment, `reply_identifier` needs + to be set with the identifier of the parent post/comment (eg. + `@author/permlink`). + Optionally you can also set json_metadata, comment_options and upvote + the newly created post as an author. + Setting category, tags or community will override the values provided + in json_metadata and/or comment_options where appropriate. + + :param str title: Title of the post + :param str body: Body of the post/comment + :param str author: Account are you posting from + :param str permlink: Manually set the permlink (defaults to None). + If left empty, it will be derived from title automatically. + :param str reply_identifier: Identifier of the parent post/comment (only + if this post is a reply/comment). + :param str/dict json_metadata: JSON meta object that can be attached to + the post. + :param dict comment_options: JSON options object that can be + attached to the post. + + Example:: + + comment_options = { + 'max_accepted_payout': '1000000.000 BBD', + 'percent_dpay_dollars': 10000, + 'allow_votes': True, + 'allow_curation_rewards': True, + 'extensions': [[0, { + 'beneficiaries': [ + {'account': 'account1', 'weight': 5000}, + {'account': 'account2', 'weight': 5000}, + ]} + ]] + } + + :param str community: (Optional) Name of the community we are posting + into. This will also override the community specified in + `json_metadata`. + :param str app: (Optional) Name of the app which are used for posting + when not set, dpaycli/ is used + :param str/list tags: (Optional) A list of tags to go with the + post. This will also override the tags specified in + `json_metadata`. The first tag will be used as a 'category'. If + provided as a string, it should be space separated. + :param list beneficiaries: (Optional) A list of beneficiaries + for posting reward distribution. This argument overrides + beneficiaries as specified in `comment_options`. + + For example, if we would like to split rewards between account1 and + account2:: + + beneficiaries = [ + {'account': 'account1', 'weight': 5000}, + {'account': 'account2', 'weight': 5000} + ] + + :param bool self_vote: (Optional) Upvote the post as author, right after + posting. + :param bool parse_body: (Optional) When set to True, all mentioned users, + used links and images are put into users, links and images array inside + json_metadata. This will override provided links, images and users inside + json_metadata. Hashtags will added to tags until its length is below five entries. + + """ + + # prepare json_metadata + json_metadata = json_metadata or {} + if isinstance(json_metadata, str): + json_metadata = json.loads(json_metadata) + + # override the community + if community: + json_metadata.update({'community': community}) + if app: + json_metadata.update({'app': app}) + elif 'app' not in json_metadata: + json_metadata.update({'app': 'dpaycli/%s' % (dpaycli_version)}) + + if not author and config["default_account"]: + author = config["default_account"] + if not author: + raise ValueError("You need to provide an account") + account = Account(author, dpay_instance=self) + # deal with the category and tags + if isinstance(tags, str): + tags = list(set([_f for _f in (re.split("[\W_]", tags)) if _f])) + + category = None + tags = tags or json_metadata.get('tags', []) + + if parse_body: + def get_urls(mdstring): + return list(set(re.findall('http[s]*://[^\s"><\)\(]+', mdstring))) + + def get_users(mdstring): + users = [] + for u in re.findall('(^|[^a-zA-Z0-9_!#$%&*@@\/]|(^|[^a-zA-Z0-9_+~.-\/#]))[@@]([a-z][-\.a-z\d]+[a-z\d])', mdstring): + users.append(list(u)[-1]) + return users + + def get_hashtags(mdstring): + hashtags = [] + for t in re.findall('(^|\s)(#[-a-z\d]+)', mdstring): + hashtags.append(list(t)[-1]) + return hashtags + + users = [] + image = [] + links = [] + for url in get_urls(body): + img_exts = ['.jpg', '.png', '.gif', '.svg', '.jpeg'] + if os.path.splitext(url)[1].lower() in img_exts: + image.append(url) + else: + links.append(url) + users = get_users(body) + hashtags = get_hashtags(body) + users = list(set(users).difference(set([author]))) + if len(users) > 0: + json_metadata.update({"users": users}) + if len(image) > 0: + json_metadata.update({"image": image}) + if len(links) > 0: + json_metadata.update({"links": links}) + if len(tags) < 5: + for i in range(5 - len(tags)): + if len(hashtags) > i: + tags.append(hashtags[i]) + + if tags: + # first tag should be a category + category = tags[0] + json_metadata.update({"tags": tags}) + + # can't provide a category while replying to a post + if reply_identifier and category: + category = None + + # deal with replies/categories + if reply_identifier: + parent_author, parent_permlink = resolve_authorperm( + reply_identifier) + if not permlink: + permlink = derive_permlink(title, parent_permlink) + elif category: + parent_permlink = derive_permlink(category) + parent_author = "" + if not permlink: + permlink = derive_permlink(title) + else: + parent_author = "" + parent_permlink = "" + if not permlink: + permlink = derive_permlink(title) + + post_op = operations.Comment( + **{ + "parent_author": parent_author, + "parent_permlink": parent_permlink, + "author": account["name"], + "permlink": permlink, + "title": title, + "body": body, + "json_metadata": json_metadata + }) + ops = [post_op] + + # if comment_options are used, add a new op to the transaction + if comment_options or beneficiaries: + comment_op = self._build_comment_options_op(account['name'], + permlink, + comment_options, + beneficiaries) + ops.append(comment_op) + + if self_vote: + vote_op = operations.Vote( + **{ + 'voter': account["name"], + 'author': account["name"], + 'permlink': permlink, + 'weight': DPAY_100_PERCENT, + }) + ops.append(vote_op) + + return self.finalizeOp(ops, account, "posting", **kwargs) + + def comment_options(self, options, identifier, beneficiaries=[], + account=None, **kwargs): + """ Set the comment options + + :param dict options: The options to define. + :param str identifier: Post identifier + :param list beneficiaries: (optional) list of beneficiaries + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + + For the options, you have these defaults::: + + { + "author": "", + "permlink": "", + "max_accepted_payout": "1000000.000 BBD", + "percent_dpay_dollars": 10000, + "allow_votes": True, + "allow_curation_rewards": True, + } + + """ + if not account and config["default_account"]: + account = config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self) + author, permlink = resolve_authorperm(identifier) + op = self._build_comment_options_op(author, permlink, options, + beneficiaries) + return self.finalizeOp(op, account, "posting", **kwargs) + + def _build_comment_options_op(self, author, permlink, options, + beneficiaries): + options = remove_from_dict(options or {}, [ + 'max_accepted_payout', 'percent_dpay_dollars', + 'allow_votes', 'allow_curation_rewards', 'extensions' + ], keep_keys=True) + # override beneficiaries extension + if beneficiaries: + # validate schema + # or just simply vo.Schema([{'account': str, 'weight': int}]) + + weight_sum = 0 + for b in beneficiaries: + if 'account' not in b: + raise ValueError( + "beneficiaries need an account field!" + ) + if 'weight' not in b: + b['weight'] = DPAY_100_PERCENT + if len(b['account']) > 16: + raise ValueError( + "beneficiaries error, account name length >16!" + ) + if b['weight'] < 1 or b['weight'] > DPAY_100_PERCENT: + raise ValueError( + "beneficiaries error, 1<=weight<=%s!" % + (DPAY_100_PERCENT) + ) + weight_sum += b['weight'] + + if weight_sum > DPAY_100_PERCENT: + raise ValueError( + "beneficiaries exceed total weight limit %s" % + DPAY_100_PERCENT + ) + + options['beneficiaries'] = beneficiaries + + default_max_payout = "1000000.000 %s" % (self.bbd_symbol) + comment_op = operations.Comment_options( + **{ + "author": + author, + "permlink": + permlink, + "max_accepted_payout": + options.get("max_accepted_payout", default_max_payout), + "percent_dpay_dollars": + int(options.get("percent_dpay_dollars", DPAY_100_PERCENT)), + "allow_votes": + options.get("allow_votes", True), + "allow_curation_rewards": + options.get("allow_curation_rewards", True), + "extensions": + options.get("extensions", []), + "beneficiaries": + options.get("beneficiaries", []), + }) + return comment_op + + def get_api_methods(self): + """Returns all supported api methods""" + return self.rpc.get_methods(api="jsonrpc") + + def get_apis(self): + """Returns all enabled apis""" + api_methods = self.get_api_methods() + api_list = [] + for a in api_methods: + api = a.split(".")[0] + if api not in api_list: + api_list.append(api) + return api_list + + def _get_asset_symbol(self, asset_id): + """ get the asset symbol from an asset id + + :@param int asset_id: 0 -> BBD, 1 -> BEX, 2 -> VESTS + + """ + for asset in self.chain_params['chain_assets']: + if asset['id'] == asset_id: + return asset['symbol'] + + raise KeyError("asset ID not found in chain assets") + + @property + def bbd_symbol(self): + """ get the current chains symbol for BBD (e.g. "TBD" on testnet) """ + # some networks (e.g. whaleshares) do not have BBD + try: + symbol = self._get_asset_symbol(0) + except KeyError: + symbol = self._get_asset_symbol(1) + return symbol + + @property + def dpay_symbol(self): + """ get the current chains symbol for BEX (e.g. "TESTS" on testnet) """ + return self._get_asset_symbol(1) + + @property + def vests_symbol(self): + """ get the current chains symbol for VESTS """ + return self._get_asset_symbol(2) diff --git a/dpaycli/dpayid.py b/dpaycli/dpayid.py new file mode 100755 index 0000000..fc03529 --- /dev/null +++ b/dpaycli/dpayid.py @@ -0,0 +1,300 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import json +try: + from urllib.parse import urlparse, urlencode, urljoin +except ImportError: + from urlparse import urlparse, urljoin + from urllib import urlencode +import requests +from .storage import configStorage as config +from six import PY2 +from dpaycli.instance import shared_dpay_instance +from dpaycli.amount import Amount + + +class DPayID(object): + """ DPayID + + :param str scope: comma separated string with scopes + login,offline,vote,comment,delete_comment,comment_options,custom_json,claim_reward_balance + + + .. code-block:: python + + # Run the login_app in examples and login with a account + from dpaycli import DPay + from dpaycli.dpayid import DPayID + from dpaycli.comment import Comment + dpid = DPayID(client_id="dpaycli.app") + dpay = DPay(dpayid=dpid) + dpay.wallet.unlock("supersecret-passphrase") + post = Comment("author/permlink", dpay_instance=dpay) + post.upvote(voter="test") # replace "test" with your account + + Examples for creating dpayid v2 urls for broadcasting in browser: + .. testoutput:: + + from dpaycli import DPay + from dpaycli.account import Account + from dpaycli.dpayid import DPayID + from pprint import pprint + dpay = DPay(nobroadcast=True, unsigned=True) + dpid = DPayID(dpay_instance=dpay) + acc = Account("test", dpay_instance=dpay) + pprint(dpid.url_from_tx(acc.transfer("test1", 1, "BEX", "test"))) + + .. testcode:: + + 'https://go.dpayid.io/sign/transfer?from=test&to=test1&amount=1.000+BEX&memo=test' + + .. testoutput:: + + from dpaycli import DPay + from dpaycli.transactionbuilder import TransactionBuilder + from dpayclibase import operations + from dpaycli.dpayid import DPayID + from pprint import pprint + stm = DPay(nobroadcast=True, unsigned=True) + dpid = DPayID(dpay_instance=stm) + tx = TransactionBuilder(dpay_instance=stm) + op = operations.Transfer(**{"from": 'test', + "to": 'test1', + "amount": '1.000 BEX', + "memo": 'test'}) + tx.appendOps(op) + pprint(dpid.url_from_tx(tx.json())) + + .. testcode:: + + 'https://go.dpayid.io/sign/transfer?from=test&to=test1&amount=1.000+BEX&memo=test' + + """ + + def __init__(self, dpay_instance=None, *args, **kwargs): + self.dpay = dpay_instance or shared_dpay_instance() + self.access_token = None + self.get_refresh_token = kwargs.get("get_refresh_token", False) + self.hot_sign_redirect_uri = kwargs.get("hot_sign_redirect_uri", config["hot_sign_redirect_uri"]) + if self.hot_sign_redirect_uri == "": + self.hot_sign_redirect_uri = None + self.client_id = kwargs.get("client_id", config["dpid_client_id"]) + self.scope = kwargs.get("scope", "login") + self.oauth_base_url = kwargs.get("oauth_base_url", config["oauth_base_url"]) + self.dpid_api_url = kwargs.get("dpid_api_url", config["dpid_api_url"]) + + @property + def headers(self): + return {'Authorization': self.access_token} + + def get_login_url(self, redirect_uri, **kwargs): + """ Returns a login url for receiving token from dpayid + """ + client_id = kwargs.get("client_id", self.client_id) + scope = kwargs.get("scope", self.scope) + get_refresh_token = kwargs.get("get_refresh_token", self.get_refresh_token) + params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": scope, + } + if get_refresh_token: + params.update({ + "response_type": "code", + }) + if PY2: + return urljoin( + self.oauth_base_url, + "authorize?" + urlencode(params).replace('%2C', ',')) + else: + return urljoin( + self.oauth_base_url, + "authorize?" + urlencode(params, safe=",")) + + def get_access_token(self, code): + post_data = { + "grant_type": "authorization_code", + "code": code, + "client_id": self.client_id, + "client_secret": self.dpay.wallet.getTokenForAccountName(self.client_id), + } + + r = requests.post( + urljoin(self.dpid_api_url, "oauth2/token/"), + data=post_data + ) + + return r.json() + + def me(self, username=None): + """ Calls the me function from dpayid + + .. code-block:: python + + from dpaycli.dpayid import DPayID + dpid = DPayID() + dpid.dpay.wallet.unlock("supersecret-passphrase") + dpid.me(username="test") + + """ + if username: + self.set_username(username) + url = urljoin(self.dpid_api_url, "me/") + r = requests.post(url, headers=self.headers) + return r.json() + + def set_access_token(self, access_token): + """ Is needed for broadcast() and me() + """ + self.access_token = access_token + + def set_username(self, username, permission="posting"): + """ Set a username for the next broadcast() or me operation() + The necessary token is fetched from the wallet + """ + if permission != "posting": + self.access_token = None + return + self.access_token = self.dpay.wallet.getTokenForAccountName(username) + + def broadcast(self, operations, username=None): + """ Broadcast a operations + + Sample operations: + + .. code-block:: js + + [ + [ + 'vote', { + 'voter': 'gandalf', + 'author': 'gtg', + 'permlink': 'dpay-pressure-4-need-for-speed', + 'weight': 10000 + } + ] + ] + + """ + url = urljoin(self.dpid_api_url, "broadcast/") + data = { + "operations": operations, + } + if username: + self.set_username(username) + headers = self.headers.copy() + headers.update({ + "Content-Type": "application/json; charset=utf-8", + "Accept": "application/json", + }) + + r = requests.post(url, headers=headers, data=json.dumps(data)) + try: + return r.json() + except ValueError: + return r.content + + def refresh_access_token(self, code, scope): + post_data = { + "grant_type": "refresh_token", + "refresh_token": code, + "client_id": self.client_id, + "client_secret": self.dpay.wallet.getTokenForAccountName(self.client_id), + "scope": scope, + } + + r = requests.post( + urljoin(self.dpid_api_url, "oauth2/token/"), + data=post_data, + ) + + return r.json() + + def revoke_token(self, access_token): + post_data = { + "access_token": access_token, + } + + r = requests.post( + urljoin(self.dpid_api_url, "oauth2/token/revoke"), + data=post_data + ) + + return r.json() + + def update_user_metadata(self, metadata): + put_data = { + "user_metadata": metadata, + } + r = requests.put( + urljoin(self.dpid_api_url, "me/"), + data=put_data, headers=self.headers) + + return r.json() + + def url_from_tx(self, tx, redirect_uri=None): + """ Creates a link for broadcasting an operation + + :param dict tx: includes the operation, which should be broadcast + :param str redirect_uri: Redirects to this uri, when set + """ + if not isinstance(tx, dict): + tx = tx.json() + if "operations" not in tx or not tx["operations"]: + return '' + urls = [] + operations = tx["operations"] + for op in operations: + operation = op[0] + params = op[1] + for key in params: + value = params[key] + if isinstance(value, list) and len(value) == 3: + try: + amount = Amount(value, dpay_instance=self.dpay) + params[key] = str(amount) + except: + amount = None + elif isinstance(value, bool): + if value: + params[key] = 1 + else: + params[key] = 0 + urls.append(self.create_hot_sign_url(operation, params, redirect_uri=redirect_uri)) + if len(urls) == 1: + return urls[0] + else: + return urls + + def create_hot_sign_url(self, operation, params, redirect_uri=None): + """ Creates a link for broadcasting an operation + + :param str operation: operation name (e.g.: vote) + :param dict params: operation dict params + :param str redirect_uri: Redirects to this uri, when set + """ + + if not isinstance(operation, str) or not isinstance(params, dict): + raise ValueError("Invalid Request.") + + base_url = self.dpid_api_url.replace("/api", "") + if redirect_uri == "": + redirect_uri = None + + if redirect_uri is None and self.hot_sign_redirect_uri is not None: + redirect_uri = self.hot_sign_redirect_uri + if redirect_uri is not None: + params.update({"redirect_uri": redirect_uri}) + + for key in params: + if isinstance(params[key], list): + params[key] = json.dumps(params[key]) + params = urlencode(params) + url = urljoin(base_url, "sign/%s" % operation) + url += "?" + params + + return url diff --git a/dpaycli/exceptions.py b/dpaycli/exceptions.py new file mode 100755 index 0000000..bae39e2 --- /dev/null +++ b/dpaycli/exceptions.py @@ -0,0 +1,157 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + + +class WalletExists(Exception): + """ A wallet has already been created and requires a password to be + unlocked by means of :func:`dpay.wallet.unlock`. + """ + pass + + +class WalletLocked(Exception): + """ Wallet is locked + """ + pass + + +class RPCConnectionRequired(Exception): + """ An RPC connection is required + """ + pass + + +class InvalidMemoKeyException(Exception): + """ Memo key in message is invalid + """ + pass + + +class WrongMemoKey(Exception): + """ The memo provided is not equal the one on the blockchain + """ + pass + + +class OfflineHasNoRPCException(Exception): + """ When in offline mode, we don't have RPC + """ + pass + + +class AccountExistsException(Exception): + """ The requested account already exists + """ + pass + + +class AccountDoesNotExistsException(Exception): + """ The account does not exist + """ + pass + + +class AssetDoesNotExistsException(Exception): + """ The asset does not exist + """ + pass + + +class InvalidAssetException(Exception): + """ An invalid asset has been provided + """ + pass + + +class InsufficientAuthorityError(Exception): + """ The transaction requires signature of a higher authority + """ + pass + + +class VotingInvalidOnArchivedPost(Exception): + """ The transaction requires signature of a higher authority + """ + pass + + +class MissingKeyError(Exception): + """ A required key couldn't be found in the wallet + """ + pass + + +class InvalidWifError(Exception): + """ The provided private Key has an invalid format + """ + pass + + +class BlockDoesNotExistsException(Exception): + """ The block does not exist + """ + pass + + +class NoWalletException(Exception): + """ No Wallet could be found, please use :func:`dpay.wallet.create` to + create a new wallet + """ + pass + + +class WitnessDoesNotExistsException(Exception): + """ The witness does not exist + """ + pass + + +class ContentDoesNotExistsException(Exception): + """ The content does not exist + """ + pass + + +class VoteDoesNotExistsException(Exception): + """ The vote does not exist + """ + pass + + +class WrongMasterPasswordException(Exception): + """ The password provided could not properly unlock the wallet + """ + pass + + +class VestingBalanceDoesNotExistsException(Exception): + """ Vesting Balance does not exist + """ + pass + + +class InvalidMessageSignature(Exception): + """ The message signature does not fit the message + """ + pass + + +class NoWriteAccess(Exception): + """ Cannot store to sqlite3 database due to missing write access + """ + pass + + +class BatchedCallsNotSupported(Exception): + """ Batch calls do not work + """ + pass + + +class BlockWaitTimeExceeded(Exception): + """ Wait time for new block exceeded + """ + pass diff --git a/dpaycli/imageuploader.py b/dpaycli/imageuploader.py new file mode 100755 index 0000000..197e99c --- /dev/null +++ b/dpaycli/imageuploader.py @@ -0,0 +1,73 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import logging +import json +import io +import collections +import hashlib +from binascii import hexlify, unhexlify +import requests +from .instance import shared_dpay_instance +from dpaycli.account import Account +from dpaycligraphenebase.py23 import integer_types, string_types, text_type, py23_bytes +from dpaycligraphenebase.account import PrivateKey +from dpaycligraphenebase.ecdsasig import sign_message, verify_message + + +class ImageUploader(object): + def __init__( + self, + base_url="https://dsiteimages.com", + challenge="ImageSigningChallenge", + dpay_instance=None, + ): + self.challenge = challenge + self.base_url = base_url + self.dpay = dpay_instance or shared_dpay_instance() + + def upload(self, image, account, image_name=None): + """ Uploads an image + + :param str/bytes image: path to the image or image in bytes representation which should be uploaded + :param str account: Account which is used to upload. A posting key must be provided. + :param str image_name: optional + + .. code-block:: python + + from dpaycli import DPay + from dpaycli.imageuploader import ImageUploader + stm = DPay(keys=["5xxx"]) # private posting key + iu = ImageUploader(dpay_instance=stm) + iu.upload("path/to/image.png", "account_name") # "private posting key belongs to account_name + + """ + account = Account(account, dpay_instance=self.dpay) + if "posting" not in account: + account.refresh() + if "posting" not in account: + raise AssertionError("Could not access posting permission") + for authority in account["posting"]["key_auths"]: + posting_wif = self.dpay.wallet.getPrivateKeyForPublicKey(authority[0]) + + if isinstance(image, string_types): + image_data = open(image, 'rb').read() + elif isinstance(image, io.BytesIO): + image_data = image.read() + else: + image_data = image + + message = py23_bytes(self.challenge, "ascii") + image_data + signature = sign_message(message, posting_wif) + signature_in_hex = hexlify(signature).decode("ascii") + + files = {image_name or 'image': image_data} + url = "%s/%s/%s" % ( + self.base_url, + account["name"], + signature_in_hex + ) + r = requests.post(url, files=files) + return r.json() diff --git a/dpaycli/instance.py b/dpaycli/instance.py new file mode 100755 index 0000000..f725615 --- /dev/null +++ b/dpaycli/instance.py @@ -0,0 +1,65 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import object +import dpaycli as stm + + +class SharedInstance(object): + """Singelton for the DPay Instance""" + instance = None + config = {} + + +def shared_dpay_instance(): + """ This method will initialize ``SharedInstance.instance`` and return it. + The purpose of this method is to have offer single default + dpay instance that can be reused by multiple classes. + + .. code-block:: python + + from dpaycli.account import Account + from dpaycli.instance import shared_dpay_instance + + account = Account("test") + # is equivalent with + account = Account("test", dpay_instance=shared_dpay_instance()) + + """ + if not SharedInstance.instance: + clear_cache() + SharedInstance.instance = stm.DPay(**SharedInstance.config) + return SharedInstance.instance + + +def set_shared_dpay_instance(dpay_instance): + """ This method allows us to override default dpay instance for all users of + ``SharedInstance.instance``. + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + """ + clear_cache() + SharedInstance.instance = dpay_instance + + +def clear_cache(): + """ Clear Caches + """ + from .blockchainobject import BlockchainObject + BlockchainObject.clear_cache() + + +def set_shared_config(config): + """ This allows to set a config that will be used when calling + ``shared_dpay_instance`` and allows to define the configuration + without requiring to actually create an instance + """ + if not isinstance(config, dict): + raise AssertionError() + SharedInstance.config.update(config) + # if one is already set, delete + if SharedInstance.instance: + clear_cache() + SharedInstance.instance = None diff --git a/dpaycli/market.py b/dpaycli/market.py new file mode 100755 index 0000000..8ddbad3 --- /dev/null +++ b/dpaycli/market.py @@ -0,0 +1,836 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import random +import pytz +import logging +from datetime import datetime, timedelta +from dpaycli.instance import shared_dpay_instance +from .utils import ( + formatTimeFromNow, formatTime, formatTimeString, assets_from_string, parse_time, addTzInfo) +from .asset import Asset +from .amount import Amount +from .price import Price, Order, FilledOrder +from .account import Account +from dpayclibase import operations +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +REQUEST_MODULE = None +if not REQUEST_MODULE: + try: + import requests + REQUEST_MODULE = "requests" + except ImportError: + REQUEST_MODULE = None +log = logging.getLogger(__name__) + + +class Market(dict): + """ This class allows to easily access Markets on the blockchain for trading, etc. + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + :param dpaycli.asset.Asset base: Base asset + :param dpaycli.asset.Asset quote: Quote asset + :returns: Blockchain Market + :rtype: dictionary with overloaded methods + + Instances of this class are dictionaries that come with additional + methods (see below) that allow dealing with a market and its + corresponding functions. + + This class tries to identify **two** assets as provided in the + parameters in one of the following forms: + + * ``base`` and ``quote`` are valid assets (according to :class:`dpaycli.asset.Asset`) + * ``base:quote`` separated with ``:`` + * ``base/quote`` separated with ``/`` + * ``base-quote`` separated with ``-`` + + .. note:: Throughout this library, the ``quote`` symbol will be + presented first (e.g. ``BEX:BBD`` with ``BEX`` being the + quote), while the ``base`` only refers to a secondary asset + for a trade. This means, if you call + :func:`dpaycli.market.Market.sell` or + :func:`dpaycli.market.Market.buy`, you will sell/buy **only + quote** and obtain/pay **only base**. + + """ + + def __init__( + self, + base=None, + quote=None, + dpay_instance=None, + ): + """ + Init Market + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + :param dpaycli.asset.Asset base: Base asset + :param dpaycli.asset.Asset quote: Quote asset + """ + self.dpay = dpay_instance or shared_dpay_instance() + + if quote is None and isinstance(base, str): + quote_symbol, base_symbol = assets_from_string(base) + quote = Asset(quote_symbol, dpay_instance=self.dpay) + base = Asset(base_symbol, dpay_instance=self.dpay) + super(Market, self).__init__({"base": base, "quote": quote}) + elif base and quote: + quote = Asset(quote, dpay_instance=self.dpay) + base = Asset(base, dpay_instance=self.dpay) + super(Market, self).__init__({"base": base, "quote": quote}) + elif base is None and quote is None: + quote = Asset("BBD", dpay_instance=self.dpay) + base = Asset("BEX", dpay_instance=self.dpay) + super(Market, self).__init__({"base": base, "quote": quote}) + else: + raise ValueError("Unknown Market config") + + def get_string(self, separator=":"): + """ Return a formated string that identifies the market, e.g. ``BEX:BBD`` + + :param str separator: The separator of the assets (defaults to ``:``) + """ + return "%s%s%s" % (self["quote"]["symbol"], separator, self["base"]["symbol"]) + + def __eq__(self, other): + if isinstance(other, str): + quote_symbol, base_symbol = assets_from_string(other) + return ( + self["quote"]["symbol"] == quote_symbol and + self["base"]["symbol"] == base_symbol + ) or ( + self["quote"]["symbol"] == base_symbol and + self["base"]["symbol"] == quote_symbol + ) + elif isinstance(other, Market): + return ( + self["quote"]["symbol"] == other["quote"]["symbol"] and + self["base"]["symbol"] == other["base"]["symbol"] + ) + + def ticker(self, raw_data=False): + """ Returns the ticker for all markets. + + Output Parameters: + + * ``latest``: Price of the order last filled + * ``lowest_ask``: Price of the lowest ask + * ``highest_bid``: Price of the highest bid + * ``bbd_volume``: Volume of BBD + * ``dpay_volume``: Volume of BEX + * ``percent_change``: 24h change percentage (in %) + + .. note:: + Market is BEX:BBD and prices are BBD per BEX! + + Sample Output: + + .. code-block:: js + + { + 'highest_bid': 0.30100226633322913, + 'latest': 0.0, + 'lowest_ask': 0.3249636958897082, + 'percent_change': 0.0, + 'bbd_volume': 108329611.0, + 'dpay_volume': 355094043.0 + } + + """ + data = {} + # Core Exchange rate + self.dpay.rpc.set_next_node_on_empty_reply(True) + ticker = self.dpay.rpc.get_ticker(api="market_history") + + if raw_data: + return ticker + + data["highest_bid"] = Price( + ticker["highest_bid"], + base=self["base"], + quote=self["quote"], + dpay_instance=self.dpay + ) + data["latest"] = Price( + ticker["latest"], + quote=self["quote"], + base=self["base"], + dpay_instance=self.dpay + ) + data["lowest_ask"] = Price( + ticker["lowest_ask"], + base=self["base"], + quote=self["quote"], + dpay_instance=self.dpay + ) + data["percent_change"] = float(ticker["percent_change"]) + data["bbd_volume"] = Amount(ticker["bbd_volume"], dpay_instance=self.dpay) + data["dpay_volume"] = Amount(ticker["dpay_volume"], dpay_instance=self.dpay) + + return data + + def volume24h(self, raw_data=False): + """ Returns the 24-hour volume for all markets, plus totals for primary currencies. + + Sample output: + + .. code-block:: js + + { + "BEX": 361666.63617, + "BBD": 1087.0 + } + + """ + self.dpay.rpc.set_next_node_on_empty_reply(True) + volume = self.dpay.rpc.get_volume(api="market_history") + if raw_data: + return volume + return { + self["base"]["symbol"]: Amount(volume["bbd_volume"], dpay_instance=self.dpay), + self["quote"]["symbol"]: Amount(volume["dpay_volume"], dpay_instance=self.dpay) + } + + def orderbook(self, limit=25, raw_data=False): + """ Returns the order book for BBD/BEX market. + :param int limit: Limit the amount of orders (default: 25) + + Sample output (raw_data=False): + .. code-block:: js + + { + 'asks': [ + 380.510 BEX 460.291 BBD @ 1.209669 BBD/BEX, + 53.785 BEX 65.063 BBD @ 1.209687 BBD/BEX + ], + 'bids': [ + 0.292 BEX 0.353 BBD @ 1.208904 BBD/BEX, + 8.498 BEX 10.262 BBD @ 1.207578 BBD/BEX + ], + 'asks_date': [ + datetime.datetime(2018, 4, 30, 21, 7, 24, tzinfo=), + datetime.datetime(2018, 4, 30, 18, 12, 18, tzinfo=) + ], + 'bids_date': [ + datetime.datetime(2018, 4, 30, 21, 1, 21, tzinfo=), + datetime.datetime(2018, 4, 30, 20, 38, 21, tzinfo=) + ] + } + + Sample output (raw_data=True): + .. code-block:: js + + { + 'asks': [ + { + 'order_price': {'base': '8.000 BEX', 'quote': '9.618 BBD'}, + 'real_price': '1.20225000000000004', + 'dpay': 4565, + 'bbd': 5488, + 'created': '2018-04-30T21:12:45' + } + ], + 'bids': [ + { + 'order_price': {'base': '10.000 BBD', 'quote': '8.333 BEX'}, + 'real_price': '1.20004800192007677', + 'dpay': 8333, + 'bbd': 10000, + 'created': '2018-04-30T20:29:33' + } + ] + } + + .. note:: Each bid is an instance of + class:`dpaycli.price.Order` and thus carries the keys + ``base``, ``quote`` and ``price``. From those you can + obtain the actual amounts for sale + + """ + self.dpay.rpc.set_next_node_on_empty_reply(True) + if self.dpay.rpc.get_use_appbase(): + orders = self.dpay.rpc.get_order_book({'limit': limit}, api="market_history") + else: + orders = self.dpay.rpc.get_order_book(limit, api='database_api') + if raw_data: + return orders + asks = list([Order( + Amount(x["order_price"]["quote"], dpay_instance=self.dpay), + Amount(x["order_price"]["base"], dpay_instance=self.dpay), + dpay_instance=self.dpay) for x in orders["asks"]]) + bids = list([Order( + Amount(x["order_price"]["quote"], dpay_instance=self.dpay), + Amount(x["order_price"]["base"], dpay_instance=self.dpay), + dpay_instance=self.dpay).invert() for x in orders["bids"]]) + asks_date = list([formatTimeString(x["created"]) for x in orders["asks"]]) + bids_date = list([formatTimeString(x["created"]) for x in orders["bids"]]) + data = {"asks": asks, "bids": bids, "asks_date": asks_date, "bids_date": bids_date} + return data + + def recent_trades(self, limit=25, raw_data=False): + """ Returns the order book for a given market. You may also + specify "all" to get the orderbooks of all markets. + + :param int limit: Limit the amount of orders (default: 25) + :param bool raw_data: when False, FilledOrder objects will be + returned + + Sample output (raw_data=False): + + .. code-block:: js + + [ + (2018-04-30 21:00:54+00:00) 0.267 BEX 0.323 BBD @ 1.209738 BBD/BEX, + (2018-04-30 20:59:30+00:00) 0.131 BEX 0.159 BBD @ 1.213740 BBD/BEX, + (2018-04-30 20:55:45+00:00) 0.093 BEX 0.113 BBD @ 1.215054 BBD/BEX, + (2018-04-30 20:55:30+00:00) 26.501 BEX 32.058 BBD @ 1.209690 BBD/BEX, + (2018-04-30 20:55:18+00:00) 2.108 BEX 2.550 BBD @ 1.209677 BBD/BEX, + ] + + Sample output (raw_data=True): + + .. code-block:: js + + [ + {'date': '2018-04-30T21:02:45', 'current_pays': '0.235 BBD', 'open_pays': '0.194 BEX'}, + {'date': '2018-04-30T21:02:03', 'current_pays': '24.494 BBD', 'open_pays': '20.248 BEX'}, + {'date': '2018-04-30T20:48:30', 'current_pays': '175.464 BEX', 'open_pays': '211.955 BBD'}, + {'date': '2018-04-30T20:48:30', 'current_pays': '0.999 BEX', 'open_pays': '1.207 BBD'}, + {'date': '2018-04-30T20:47:54', 'current_pays': '0.273 BBD', 'open_pays': '0.225 BEX'}, + ] + + .. note:: Each bid is an instance of + class:`dpay.price.Order` and thus carries the keys + ``base``, ``quote`` and ``price``. From those you can + obtain the actual amounts for sale + + """ + self.dpay.rpc.set_next_node_on_empty_reply(limit > 0) + if self.dpay.rpc.get_use_appbase(): + orders = self.dpay.rpc.get_recent_trades({'limit': limit}, api="market_history")['trades'] + else: + orders = self.dpay.rpc.get_recent_trades(limit, api="market_history") + if raw_data: + return orders + filled_order = list([FilledOrder(x, dpay_instance=self.dpay) for x in orders]) + return filled_order + + def trade_history(self, start=None, stop=None, intervall=None, limit=25, raw_data=False): + """ Returns the trade history for the internal market + + This function allows to fetch a fixed number of trades at fixed + intervall times to reduce the call duration time. E.g. it is possible to + receive the trades from the last 7 days, by fetching 100 trades each 6 hours. + + When intervall is set to None, all trades are received between start and stop. + This can take a while. + + :param datetime start: Start date + :param datetime stop: Stop date + :param timedelta intervall: Defines the intervall + :param int limit: Defines how many trades are fetched at each intervall point + :param bool raw_data: when True, the raw data are returned + """ + utc = pytz.timezone('UTC') + if not stop: + stop = utc.localize(datetime.utcnow()) + if not start: + start = stop - timedelta(hours=1) + start = addTzInfo(start) + stop = addTzInfo(stop) + current_start = start + filled_order = [] + fo = self.trades(start=current_start, stop=stop, limit=limit, raw_data=raw_data) + if intervall is None and len(fo) > 0: + current_start = fo[-1]["date"] + filled_order += fo + elif intervall is not None: + current_start += intervall + filled_order += [fo] + last_date = fo[-1]["date"] + while (len(fo) > 0 and last_date < stop): + fo = self.trades(start=current_start, stop=stop, limit=limit, raw_data=raw_data) + if len(fo) == 0 or fo[-1]["date"] == last_date: + break + last_date = fo[-1]["date"] + if intervall is None: + current_start = last_date + filled_order += fo + else: + current_start += intervall + filled_order += [fo] + return filled_order + + def trades(self, limit=100, start=None, stop=None, raw_data=False): + """ Returns your trade history for a given market. + + :param int limit: Limit the amount of orders (default: 100) + :param datetime start: start time + :param datetime stop: stop time + + """ + # FIXME, this call should also return whether it was a buy or + # sell + utc = pytz.timezone('UTC') + if not stop: + stop = utc.localize(datetime.utcnow()) + if not start: + start = stop - timedelta(hours=24) + start = addTzInfo(start) + stop = addTzInfo(stop) + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + orders = self.dpay.rpc.get_trade_history({'start': formatTimeString(start), + 'end': formatTimeString(stop), + 'limit': limit}, api="market_history")['trades'] + else: + orders = self.dpay.rpc.get_trade_history( + formatTimeString(start), + formatTimeString(stop), + limit, api="market_history") + if raw_data: + return orders + filled_order = list([FilledOrder(x, dpay_instance=self.dpay) for x in orders]) + return filled_order + + def market_history_buckets(self): + self.dpay.rpc.set_next_node_on_empty_reply(True) + ret = self.dpay.rpc.get_market_history_buckets(api="market_history") + if self.dpay.rpc.get_use_appbase(): + return ret['bucket_sizes'] + else: + return ret + + def market_history(self, bucket_seconds=300, start_age=3600, end_age=0, raw_data=False): + """ Return the market history (filled orders). + + :param int bucket_seconds: Bucket size in seconds (see + `returnMarketHistoryBuckets()`) + :param int start_age: Age (in seconds) of the start of the + window (default: 1h/3600) + :param int end_age: Age (in seconds) of the end of the window + (default: now/0) + :param bool raw_data: (optional) returns raw data if set True + + Example: + .. code-block:: js + + { + 'close_bbd': 2493387, + 'close_dpay': 7743431, + 'high_bbd': 1943872, + 'high_dpay': 5999610, + 'id': '7.1.5252', + 'low_bbd': 534928, + 'low_dpay': 1661266, + 'open': '2016-07-08T11:25:00', + 'open_bbd': 534928, + 'open_dpay': 1661266, + 'bbd_volume': 9714435, + 'seconds': 300, + 'dpay_volume': 30088443 + } + + """ + buckets = self.market_history_buckets() + if bucket_seconds < 5 and bucket_seconds >= 0: + bucket_seconds = buckets[bucket_seconds] + else: + if bucket_seconds not in buckets: + raise ValueError("You need select the bucket_seconds from " + str(buckets)) + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + history = self.dpay.rpc.get_market_history({'bucket_seconds': bucket_seconds, + 'start': formatTimeFromNow(-start_age - end_age), + 'end': formatTimeFromNow(-end_age)}, api="market_history")['buckets'] + else: + history = self.dpay.rpc.get_market_history( + bucket_seconds, + formatTimeFromNow(-start_age - end_age), + formatTimeFromNow(-end_age), + api="market_history") + if raw_data: + return history + new_history = [] + for h in history: + if 'open' in h and isinstance(h.get('open'), string_types): + h['open'] = formatTimeString(h.get('open', "1970-01-01T00:00:00")) + new_history.append(h) + return new_history + + def accountopenorders(self, account=None, raw_data=False): + """ Returns open Orders + + :param dpay.account.Account account: Account name or instance of Account to show orders for in this market + :param bool raw_data: (optional) returns raw data if set True, + or a list of Order() instances if False (defaults to False) + """ + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, full=True, dpay_instance=self.dpay) + + r = [] + # orders = account["limit_orders"] + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + orders = self.dpay.rpc.find_limit_orders({'account': account["name"]}, api="database")['orders'] + else: + orders = self.dpay.rpc.get_open_orders(account["name"]) + if raw_data: + return orders + for o in orders: + order = {} + order["order"] = Order( + Amount(o["sell_price"]["base"], dpay_instance=self.dpay), + Amount(o["sell_price"]["quote"], dpay_instance=self.dpay), + dpay_instance=self.dpay + ) + order["orderid"] = o["orderid"] + order["created"] = formatTimeString(o["created"]) + r.append(order) + return r + + def buy( + self, + price, + amount, + expiration=None, + killfill=False, + account=None, + orderid=None, + returnOrderId=False + ): + """ Places a buy order in a given market + + :param float price: price denoted in ``base``/``quote`` + :param number amount: Amount of ``quote`` to buy + :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days) + :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False) + :param string account: Account name that executes that order + :param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in + the head/irreversible block and add the key "orderid" to the tx output + + Prices/Rates are denoted in 'base', i.e. the BBD_DPAY market + is priced in BEX per BBD. + + **Example:** in the BBD_DPAY market, a price of 300 means + a BBD is worth 300 BEX + + .. note:: + + All prices returned are in the **reversed** orientation as the + market. I.e. in the BEX/BBD market, prices are BBD per BEX. + That way you can multiply prices with `1.05` to get a +5%. + + .. warning:: + + Since buy orders are placed as + limit-sell orders for the base asset, + you may end up obtaining more of the + buy asset than you placed the order + for. Example: + + * You place and order to buy 10 BBD for 100 BEX/BBD + * This means that you actually place a sell order for 1000 BEX in order to obtain **at least** 10 BBD + * If an order on the market exists that sells BBD for cheaper, you will end up with more than 10 BBD + """ + if not expiration: + expiration = self.dpay.config["order-expiration"] + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self.dpay) + + if isinstance(price, Price): + price = price.as_base(self["base"]["symbol"]) + + if isinstance(amount, Amount): + amount = Amount(amount, dpay_instance=self.dpay) + if not amount["asset"]["symbol"] == self["quote"]["symbol"]: + raise AssertionError("Price: {} does not match amount: {}".format( + str(price), str(amount))) + elif isinstance(amount, str): + amount = Amount(amount, dpay_instance=self.dpay) + else: + amount = Amount(amount, self["quote"]["symbol"], dpay_instance=self.dpay) + + order = operations.Limit_order_create(**{ + "owner": account["name"], + "orderid": orderid or random.getrandbits(32), + "amount_to_sell": Amount( + float(amount) * float(price), + self["base"]["symbol"], + dpay_instance=self.dpay + ), + "min_to_receive": Amount( + float(amount), + self["quote"]["symbol"], + dpay_instance=self.dpay + ), + "expiration": formatTimeFromNow(expiration), + "fill_or_kill": killfill, + "prefix": self.dpay.prefix, + }) + + if returnOrderId: + # Make blocking broadcasts + prevblocking = self.dpay.blocking + self.dpay.blocking = returnOrderId + + tx = self.dpay.finalizeOp(order, account["name"], "active") + + if returnOrderId: + tx["orderid"] = tx["operation_results"][0][1] + self.dpay.blocking = prevblocking + + return tx + + def sell( + self, + price, + amount, + expiration=None, + killfill=False, + account=None, + orderid=None, + returnOrderId=False + ): + """ Places a sell order in a given market + + :param float price: price denoted in ``base``/``quote`` + :param number amount: Amount of ``quote`` to sell + :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days) + :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False) + :param string account: Account name that executes that order + :param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in + the head/irreversible block and add the key "orderid" to the tx output + + Prices/Rates are denoted in 'base', i.e. the BBD_DPAY market + is priced in BEX per BBD. + + **Example:** in the BBD_DPAY market, a price of 300 means + a BBD is worth 300 BEX + + .. note:: + + All prices returned are in the **reversed** orientation as the + market. I.e. in the BEX/BBD market, prices are BBD per BEX. + That way you can multiply prices with `1.05` to get a +5%. + """ + if not expiration: + expiration = self.dpay.config["order-expiration"] + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, dpay_instance=self.dpay) + if isinstance(price, Price): + price = price.as_base(self["base"]["symbol"]) + + if isinstance(amount, Amount): + amount = Amount(amount, dpay_instance=self.dpay) + if not amount["asset"]["symbol"] == self["quote"]["symbol"]: + raise AssertionError("Price: {} does not match amount: {}".format( + str(price), str(amount))) + elif isinstance(amount, str): + amount = Amount(amount, dpay_instance=self.dpay) + else: + amount = Amount(amount, self["quote"]["symbol"], dpay_instance=self.dpay) + + order = operations.Limit_order_create(**{ + "owner": account["name"], + "orderid": orderid or random.getrandbits(32), + "amount_to_sell": Amount( + float(amount), + self["quote"]["symbol"], + dpay_instance=self.dpay + ), + "min_to_receive": Amount( + float(amount) * float(price), + self["base"]["symbol"], + dpay_instance=self.dpay + ), + "expiration": formatTimeFromNow(expiration), + "fill_or_kill": killfill, + "prefix": self.dpay.prefix, + }) + if returnOrderId: + # Make blocking broadcasts + prevblocking = self.dpay.blocking + self.dpay.blocking = returnOrderId + + tx = self.dpay.finalizeOp(order, account["name"], "active") + + if returnOrderId: + tx["orderid"] = tx["operation_results"][0][1] + self.dpay.blocking = prevblocking + + return tx + + def cancel(self, orderNumbers, account=None, **kwargs): + """ Cancels an order you have placed in a given market. Requires + only the "orderNumbers". + + :param int/list orderNumbers: A single order number or a list of order numbers + """ + if not account: + if "default_account" in self.dpay.config: + account = self.dpay.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, full=False, dpay_instance=self.dpay) + + if not isinstance(orderNumbers, (list, set, tuple)): + orderNumbers = {orderNumbers} + + op = [] + for order in orderNumbers: + op.append( + operations.Limit_order_cancel(**{ + "owner": account["name"], + "orderid": order, + "prefix": self.dpay.prefix})) + return self.dpay.finalizeOp(op, account["name"], "active", **kwargs) + + @staticmethod + def _weighted_average(values, weights): + """ Calculates a weighted average + """ + if not (len(values) == len(weights) and len(weights) > 0): + raise AssertionError("Length of both array must be the same and greater than zero!") + return sum(x * y for x, y in zip(values, weights)) / sum(weights) + + @staticmethod + def btc_usd_ticker(verbose=False): + """ Returns the BTC/USD price from bitfinex, gdax, kraken, okcoin and bitstamp. The mean price is + weighted by the exchange volume. + """ + prices = {} + responses = [] + urls = [ + "https://api.bitfinex.com/v1/pubticker/BTCUSD", + "https://api.gdax.com/products/BTC-USD/ticker", + "https://api.kraken.com/0/public/Ticker?pair=XBTUSD", + "https://www.okcoin.com/api/v1/ticker.do?symbol=btc_usd", + "https://www.bitstamp.net/api/v2/ticker/btcusd/", + ] + try: + responses = list(requests.get(u, timeout=30) for u in urls) + except Exception as e: + log.debug(str(e)) + + for r in [x for x in responses + if hasattr(x, "status_code") and x.status_code == 200 and x.json()]: + try: + if "bitfinex" in r.url: + data = r.json() + prices['bitfinex'] = { + 'price': float(data['last_price']), + 'volume': float(data['volume'])} + elif "gdax" in r.url: + data = r.json() + prices['gdax'] = { + 'price': float(data['price']), + 'volume': float(data['volume'])} + elif "kraken" in r.url: + data = r.json()['result']['XXBTZUSD']['p'] + prices['kraken'] = { + 'price': float(data[0]), + 'volume': float(data[1])} + elif "okcoin" in r.url: + data = r.json()["ticker"] + prices['okcoin'] = { + 'price': float(data['last']), + 'volume': float(data['vol'])} + elif "bitstamp" in r.url: + data = r.json() + prices['bitstamp'] = { + 'price': float(data['last']), + 'volume': float(data['volume'])} + except KeyError as e: + log.info(str(e)) + + if verbose: + print(prices) + + if len(prices) == 0: + raise RuntimeError("Obtaining BTC/USD prices has failed from all sources.") + + # vwap + return Market._weighted_average( + [x['price'] for x in prices.values()], + [x['volume'] for x in prices.values()]) + + @staticmethod + def dpay_btc_ticker(): + """ Returns the BEX/BTC price from bittrex, binance, huobi and upbit. The mean price is + weighted by the exchange volume. + """ + prices = {} + responses = [] + urls = [ + # "https://poloniex.com/public?command=returnTicker", + "https://bittrex.com/api/v1.1/public/getmarketsummary?market=BTC-STEEM", + "https://api.binance.com/api/v1/ticker/24hr", + "https://api.huobi.pro/market/history/kline?period=1day&size=1&symbol=steembtc", + "https://crix-api.upbit.com/v1/crix/trades/ticks?code=CRIX.UPBIT.BTC-STEEM&count=1", + ] + headers = {'Content-type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36'} + try: + responses = list(requests.get(u, headers=headers, timeout=30) for u in urls) + except Exception as e: + log.debug(str(e)) + + for r in [x for x in responses + if hasattr(x, "status_code") and x.status_code == 200 and x.json()]: + try: + if "poloniex" in r.url: + data = r.json()["BTC_DPAY"] + prices['poloniex'] = { + 'price': float(data['last']), + 'volume': float(data['baseVolume'])} + elif "bittrex" in r.url: + data = r.json()["result"][0] + price = (data['Bid'] + data['Ask']) / 2 + prices['bittrex'] = {'price': price, 'volume': data['BaseVolume']} + elif "binance" in r.url: + data = [x for x in r.json() if x['symbol'] == 'BEXBTC'][0] + prices['binance'] = { + 'price': float(data['lastPrice']), + 'volume': float(data['quoteVolume'])} + elif "huobi" in r.url: + data = r.json()["data"][-1] + prices['huobi'] = { + 'price': float(data['close']), + 'volume': float(data['vol'])} + elif "upbit" in r.url: + data = r.json()[-1] + prices['upbit'] = { + 'price': float(data['tradePrice']), + 'volume': float(data['tradeVolume'])} + except KeyError as e: + log.info(str(e)) + + if len(prices) == 0: + raise RuntimeError("Obtaining BEX/BTC prices has failed from all sources.") + + return Market._weighted_average( + [x['price'] for x in prices.values()], + [x['volume'] for x in prices.values()]) + + def dpay_usd_implied(self): + """Returns the current BEX/USD market price""" + return self.dpay_btc_ticker() * self.btc_usd_ticker() diff --git a/dpaycli/memo.py b/dpaycli/memo.py new file mode 100755 index 0000000..4ad4ef9 --- /dev/null +++ b/dpaycli/memo.py @@ -0,0 +1,268 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import object +from dpaycli.instance import shared_dpay_instance +import random +from dpayclibase import memo as BtsMemo +from dpaycligraphenebase.account import PrivateKey, PublicKey +from .account import Account +from .exceptions import MissingKeyError + + +class Memo(object): + """ Deals with Memos that are attached to a transfer + + :param dpaycli.account.Account from_account: Account that has sent the memo + :param dpaycli.account.Account to_account: Account that has received the memo + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + A memo is encrypted with a shared secret derived from a private key of + the sender and a public key of the receiver. Due to the underlying + mathematics, the same shared secret can be derived by the private key + of the receiver and the public key of the sender. The encrypted message + is perturbed by a nonce that is part of the transmitted message. + + .. code-block:: python + + from dpaycli.memo import Memo + m = Memo("dpay", "dpay.foundation") + m.dpay.wallet.unlock("secret") + enc = (m.encrypt("foobar")) + print(enc) + >> {'nonce': '17329630356955254641', 'message': '8563e2bb2976e0217806d642901a2855'} + print(m.decrypt(enc)) + >> foobar + + To decrypt a memo, simply use + + .. code-block:: python + + from dpaycli.memo import Memo + m = Memo() + m.dpay.wallet.unlock("secret") + print(m.decrypt(op_data["memo"])) + + if ``op_data`` being the payload of a transfer operation. + + Memo Keys + + In DPay, memos are AES-256 encrypted with a shared secret between sender and + receiver. It is derived from the memo private key of the sender and the memo + public key of the receiver. + + In order for the receiver to decode the memo, the shared secret has to be + derived from the receiver's private key and the senders public key. + + The memo public key is part of the account and can be retrieved with the + `get_account` call: + + .. code-block:: js + + get_account + { + [...] + "options": { + "memo_key": "GPH5TPTziKkLexhVKsQKtSpo4bAv5RnB8oXcG4sMHEwCcTf3r7dqE", + [...] + }, + [...] + } + + while the memo private key can be dumped with `dump_private_keys` + + Memo Message + + The take the following form: + + .. code-block:: js + + { + "from": "GPH5mgup8evDqMnT86L7scVebRYDC2fwAWmygPEUL43LjstQegYCC", + "to": "GPH5Ar4j53kFWuEZQ9XhxbAja4YXMPJ2EnUg5QcrdeMFYUNMMNJbe", + "nonce": "13043867485137706821", + "message": "d55524c37320920844ca83bb20c8d008" + } + + The fields `from` and `to` contain the memo public key of sender and receiver. + The `nonce` is a random integer that is used for the seed of the AES encryption + of the message. + + Encrypting a memo + + The high level memo class makes use of the dpaycli wallet to obtain keys + for the corresponding accounts. + + .. code-block:: python + + from dpaycli.memo import Memo + from dpaycli.account import Account + + memoObj = Memo( + from_account=Account(from_account), + to_account=Account(to_account) + ) + encrypted_memo = memoObj.encrypt(memo) + + Decoding of a received memo + + .. code-block:: python + + from getpass import getpass + from dpaycli.block import Block + from dpaycli.memo import Memo + + # Obtain a transfer from the blockchain + block = Block(23755086) # block + transaction = block["transactions"][3] # transactions + op = transaction["operations"][0] # operation + op_id = op[0] # operation type + op_data = op[1] # operation payload + + # Instantiate Memo for decoding + memo = Memo() + + # Unlock wallet + memo.unlock_wallet(getpass()) + + # Decode memo + # Raises exception if required keys not available in the wallet + print(memo.decrypt(op_data["transfer"])) + + """ + def __init__( + self, + from_account=None, + to_account=None, + dpay_instance=None + ): + + self.dpay = dpay_instance or shared_dpay_instance() + + if to_account: + self.to_account = Account(to_account, dpay_instance=self.dpay) + if from_account: + self.from_account = Account(from_account, dpay_instance=self.dpay) + + def unlock_wallet(self, *args, **kwargs): + """ Unlock the library internal wallet + """ + self.dpay.wallet.unlock(*args, **kwargs) + return self + + def encrypt(self, memo, bts_encrypt=False): + """ Encrypt a memo + + :param str memo: clear text memo message + :returns: encrypted memo + :rtype: str + """ + if not memo: + return None + + nonce = str(random.getrandbits(64)) + memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey( + self.from_account["memo_key"] + ) + if not memo_wif: + raise MissingKeyError("Memo key for %s missing!" % self.from_account["name"]) + + if not hasattr(self, 'chain_prefix'): + self.chain_prefix = self.dpay.prefix + + if bts_encrypt: + enc = BtsMemo.encode_memo_bts( + PrivateKey(memo_wif), + PublicKey( + self.to_account["memo_key"], + prefix=self.chain_prefix + ), + nonce, + memo + ) + + return { + "message": enc, + "nonce": nonce, + "from": self.from_account["memo_key"], + "to": self.to_account["memo_key"] + } + else: + enc = BtsMemo.encode_memo( + PrivateKey(memo_wif), + PublicKey( + self.to_account["memo_key"], + prefix=self.chain_prefix + ), + nonce, + memo, + prefix=self.chain_prefix + ) + + return { + "message": enc, + "from": self.from_account["memo_key"], + "to": self.to_account["memo_key"] + } + + def decrypt(self, memo): + """ Decrypt a memo + + :param str memo: encrypted memo message + :returns: encrypted memo + :rtype: str + """ + if not memo: + return None + + # We first try to decode assuming we received the memo + if isinstance(memo, dict) and "to" in memo and "from" in memo and "memo" in memo: + memo_to = Account(memo["to"], dpay_instance=self.dpay) + memo_from = Account(memo["from"], dpay_instance=self.dpay) + message = memo["memo"] + else: + memo_to = self.to_account + memo_from = self.from_account + message = memo + if isinstance(memo, dict) and "nonce" in memo: + nonce = memo.get("nonce") + else: + nonce = "" + + try: + memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey( + memo_to["memo_key"] + ) + pubkey = memo_from["memo_key"] + except MissingKeyError: + try: + # if that failed, we assume that we have sent the memo + memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey( + memo_from["memo_key"] + ) + pubkey = memo_to["memo_key"] + except MissingKeyError: + # if all fails, raise exception + raise MissingKeyError( + "Non of the required memo keys are installed!" + "Need any of {}".format( + [memo_to["name"], memo_from["name"]])) + + if not hasattr(self, 'chain_prefix'): + self.chain_prefix = self.dpay.prefix + + if message[0] == '#': + return BtsMemo.decode_memo( + PrivateKey(memo_wif), + message + ) + else: + return BtsMemo.decode_memo_bts( + PrivateKey(memo_wif), + PublicKey(pubkey, prefix=self.chain_prefix), + nonce, + message + ) diff --git a/dpaycli/message.py b/dpaycli/message.py new file mode 100755 index 0000000..6703144 --- /dev/null +++ b/dpaycli/message.py @@ -0,0 +1,154 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import object +import re +import logging +from binascii import hexlify, unhexlify +from dpaycligraphenebase.ecdsasig import verify_message, sign_message +from dpaycligraphenebase.account import PublicKey +from dpaycli.instance import shared_dpay_instance +from dpaycli.account import Account +from .exceptions import InvalidMessageSignature +from .storage import configStorage as config + + +log = logging.getLogger(__name__) + +MESSAGE_SPLIT = ( + "-----BEGIN BEX SIGNED MESSAGE-----", + "-----BEGIN META-----", + "-----BEGIN SIGNATURE-----", + "-----END BEX SIGNED MESSAGE-----" +) + +SIGNED_MESSAGE_META = """{message} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]}""" + +SIGNED_MESSAGE_ENCAPSULATED = """ +{MESSAGE_SPLIT[0]} +{message} +{MESSAGE_SPLIT[1]} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]} +{MESSAGE_SPLIT[2]} +{signature} +{MESSAGE_SPLIT[3]} +""" + + +class Message(object): + + def __init__(self, message, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.message = message + + def sign(self, account=None, **kwargs): + """ Sign a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: the signed message encapsulated in a known format + + """ + if not account: + if "default_account" in config: + account = config["default_account"] + if not account: + raise ValueError("You need to provide an account") + + # Data for message + account = Account(account, dpay_instance=self.dpay) + info = self.dpay.info() + meta = dict( + timestamp=info["time"], + block=info["head_block_number"], + memokey=account["memo_key"], + account=account["name"]) + + # wif key + wif = self.dpay.wallet.getPrivateKeyForPublicKey( + account["memo_key"] + ) + + # signature + message = self.message.strip() + signature = hexlify(sign_message( + SIGNED_MESSAGE_META.format(**locals()), + wif + )).decode("ascii") + + message = self.message + return SIGNED_MESSAGE_ENCAPSULATED.format( + MESSAGE_SPLIT=MESSAGE_SPLIT, + **locals() + ) + + def verify(self, **kwargs): + """ Verify a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: True if the message is verified successfully + + :raises: InvalidMessageSignature if the signature is not ok + + """ + # Split message into its parts + parts = re.split("|".join(MESSAGE_SPLIT), self.message) + parts = [x for x in parts if x.strip()] + + if not len(parts) > 2: + raise AssertionError("Incorrect number of message parts") + + message = parts[0].strip() + signature = parts[2].strip() + # Parse the meta data + meta = dict(re.findall(r'(\S+)=(.*)', parts[1])) + + # Ensure we have all the data in meta + if "account" not in meta: + raise AssertionError() + if "memokey" not in meta: + raise AssertionError() + if "block" not in meta: + raise AssertionError() + if "timestamp" not in meta: + raise AssertionError() + + # Load account from blockchain + account = Account( + meta.get("account"), + dpay_instance=self.dpay) + + # Test if memo key is the same as on the blockchain + if not account["memo_key"] == meta["memokey"]: + log.error( + "Memo Key of account {} on the Blockchain".format( + account["name"]) + + "differs from memo key in the message: {} != {}".format( + account["memo_key"], meta["memokey"] + ) + ) + + # Reformat message + message = SIGNED_MESSAGE_META.format(**locals()) + + # Verify Signature + pubkey = verify_message(message, unhexlify(signature)) + + # Verify pubky + pk = PublicKey(hexlify(pubkey).decode("ascii")) + if format(pk, self.dpay.prefix) != meta["memokey"]: + raise InvalidMessageSignature + + return True diff --git a/dpaycli/nodelist.py b/dpaycli/nodelist.py new file mode 100755 index 0000000..5be0fb1 --- /dev/null +++ b/dpaycli/nodelist.py @@ -0,0 +1,159 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import next +import re +import time +import math +import json +from dpaycli.instance import shared_dpay_instance +from dpaycli.account import Account +import logging +log = logging.getLogger(__name__) + + +class NodeList(list): + """ Returns a node list + + .. code-block:: python + + from dpaycli.nodelist import NodeList + n = NodeList() + nodes_urls = n.get_nodes() + + """ + def __init__(self): + nodes = [ + { + "url": "https://greatchain.dpaynodes.com", + "version": "0.20.5", + "type": "appbase", + "owner": "dpay", + "score": 100 + }] + super(NodeList, self).__init__(nodes) + + def update_nodes(self, weights=None, dpay_instance=None): + """ Reads metadata from fullnodeupdate and recalculates the nodes score + + :params list/dict weight: can be used to weight the different benchmarks + + .. code-block:: python + from dpaycli.nodelist import NodeList + nl = NodeList() + weights = [0, 0.1, 0.2, 1] + nl.update_nodes(weights) + weights = {'block': 0.1, 'history': 0.1, 'apicall': 1, 'config': 1} + nl.update_nodes(weights) + """ + dpay = dpay_instance or shared_dpay_instance() + metadata = None + account = None + cnt = 0 + while metadata is None and cnt < 5: + cnt += 1 + try: + account = Account("fullnodeupdate", dpay_instance=dpay) + metadata = json.loads(account["json_metadata"]) + except: + dpay.rpc.next() + account = None + metadata = None + if metadata is None: + return + report = metadata["report"] + failing_nodes = metadata["failing_nodes"] + parameter = metadata["parameter"] + benchmarks = parameter["benchmarks"] + if weights is None: + weights_dict = {} + for benchmark in benchmarks: + weights_dict[benchmark] = (1. / len(benchmarks)) + elif isinstance(weights, list): + weights_dict = {} + i = 0 + weight_sum = 0 + for w in weights: + weight_sum += w + for benchmark in benchmarks: + if i < len(weights): + weights_dict[benchmark] = weights[i] / weight_sum + else: + weights_dict[benchmark] = 0. + i += 1 + elif isinstance(weights, dict): + weights_dict = {} + i = 0 + weight_sum = 0 + for w in weights: + weight_sum += weights[w] + for benchmark in benchmarks: + if benchmark in weights: + weights_dict[benchmark] = weights[benchmark] / weight_sum + else: + weights_dict[benchmark] = 0. + + max_score = len(report) + 1 + new_nodes = [] + for node in self: + new_node = node.copy() + for report_node in report: + if node["url"] == report_node["node"]: + new_node["version"] = report_node["version"] + scores = [] + for benchmark in benchmarks: + result = report_node[benchmark] + rank = result["rank"] + if not result["ok"]: + rank = max_score + 1 + score = (max_score - rank) / (max_score - 1) * 100 + weighted_score = score * weights_dict[benchmark] + scores.append(weighted_score) + sum_score = 0 + for score in scores: + sum_score += score + new_node["score"] = sum_score + for node_failing in failing_nodes: + if node["url"] == node_failing: + new_node["score"] = -1 + new_nodes.append(new_node) + super(NodeList, self).__init__(new_nodes) + + def get_nodes(self, normal=True, appbase=True, dev=False, testnet=False, testnetdev=False, wss=True, https=True, not_working=False): + """ Returns nodes as list + + :param bool normal: when True, nodes with version 0.19.5 are included + :param bool appbase: when True, nodes with version 0.19.11 are included + :param bool dev: when True, dev nodes with version 0.19.11 are included + :param bool testnet: when True, testnet nodes are included + :param bool testnetdev: When True, testnet-dev nodes are included + :param bool not_working: When True, all nodes including not working ones will be returned + + """ + node_list = [] + node_type_list = [] + if normal: + node_type_list.append("normal") + if appbase: + node_type_list.append("appbase") + if dev: + node_type_list.append("appbase-dev") + if testnet: + node_type_list.append("testnet") + if testnetdev: + node_type_list.append("testnet-dev") + for node in self: + if node["type"] in node_type_list and (node["score"] >= 0 or not_working): + if not https and node["url"][:5] == 'https': + continue + if not wss and node["url"][:3] == 'wss': + continue + node_list.append(node) + + return [node["url"] for node in sorted(node_list, key=lambda self: self['score'], reverse=True)] + + def get_testnet(self, testnet=True, testnetdev=False): + """Returns testnet nodes""" + return self.get_nodes(normal=False, appbase=False, testnet=testnet, testnetdev=testnetdev) diff --git a/dpaycli/notify.py b/dpaycli/notify.py new file mode 100755 index 0000000..8888ced --- /dev/null +++ b/dpaycli/notify.py @@ -0,0 +1,89 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import logging +from events import Events +from dpaycliapi.websocket import DPayWebsocket +from dpaycli.instance import shared_dpay_instance +from dpaycli.blockchain import Blockchain +from dpaycli.price import Order, FilledOrder +log = logging.getLogger(__name__) +# logging.basicConfig(level=logging.DEBUG) + + +class Notify(Events): + """ Notifications on Blockchain events. + + This modules allows yout to be notified of events taking place on the + blockchain. + + :param fnt on_block: Callback that will be called for each block received + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + **Example** + + .. code-block:: python + + from pprint import pprint + from dpaycli.notify import Notify + + notify = Notify( + on_block=print, + ) + notify.listen() + + """ + + __events__ = [ + 'on_block', + ] + + def __init__( + self, + # accounts=[], + on_block=None, + only_block_id=False, + dpay_instance=None, + keep_alive=25 + ): + # Events + Events.__init__(self) + self.events = Events() + + # DPay instance + self.dpay = dpay_instance or shared_dpay_instance() + + # Callbacks + if on_block: + self.on_block += on_block + + # Open the websocket + self.websocket = DPayWebsocket( + urls=self.dpay.rpc.nodes, + user=self.dpay.rpc.user, + password=self.dpay.rpc.password, + only_block_id=only_block_id, + on_block=self.process_block, + keep_alive=keep_alive + ) + + def reset_subscriptions(self, accounts=[]): + """Change the subscriptions of a running Notify instance + """ + self.websocket.reset_subscriptions(accounts) + + def close(self): + """Cleanly close the Notify instance + """ + self.websocket.close() + + def process_block(self, message): + self.on_block(message) + + def listen(self): + """ This call initiates the listening/notification process. It + behaves similar to ``run_forever()``. + """ + self.websocket.run_forever() diff --git a/dpaycli/price.py b/dpaycli/price.py new file mode 100755 index 0000000..4c8f947 --- /dev/null +++ b/dpaycli/price.py @@ -0,0 +1,521 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from future.utils import python_2_unicode_compatible +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from fractions import Fraction +from dpaycli.instance import shared_dpay_instance +from .exceptions import InvalidAssetException +from .account import Account +from .amount import Amount +from .asset import Asset +from .utils import formatTimeString +from .utils import parse_time, assets_from_string + + +@python_2_unicode_compatible +class Price(dict): + """ This class deals with all sorts of prices of any pair of assets to + simplify dealing with the tuple:: + + (quote, base) + + each being an instance of :class:`dpaycli.amount.Amount`. The + amount themselves define the price. + + .. note:: + + The price (floating) is derived as ``base/quote`` + + :param list args: Allows to deal with different representations of a price + :param dpaycli.asset.Asset base: Base asset + :param dpaycli.asset.Asset quote: Quote asset + :param dpaycli.dpay.DPay dpay_instance: DPay instance + :returns: All data required to represent a price + :rtype: dict + + Way to obtain a proper instance: + + * ``args`` is a str with a price and two assets + * ``args`` can be a floating number and ``base`` and ``quote`` being instances of :class:`dpaycli.asset.Asset` + * ``args`` can be a floating number and ``base`` and ``quote`` being instances of ``str`` + * ``args`` can be dict with keys ``price``, ``base``, and ``quote`` (*graphene balances*) + * ``args`` can be dict with keys ``base`` and ``quote`` + * ``args`` can be dict with key ``receives`` (filled orders) + * ``args`` being a list of ``[quote, base]`` both being instances of :class:`dpaycli.amount.Amount` + * ``args`` being a list of ``[quote, base]`` both being instances of ``str`` (``amount symbol``) + * ``base`` and ``quote`` being instances of :class:`dpaycli.asset.Amount` + + This allows instanciations like: + + * ``Price("0.315 BBD/BEX")`` + * ``Price(0.315, base="BBD", quote="BEX")`` + * ``Price(0.315, base=Asset("BBD"), quote=Asset("BEX"))`` + * ``Price({"base": {"amount": 1, "asset_id": "BBD"}, "quote": {"amount": 10, "asset_id": "BBD"}})`` + * ``Price(quote="10 BEX", base="1 BBD")`` + * ``Price("10 BEX", "1 BBD")`` + * ``Price(Amount("10 BEX"), Amount("1 BBD"))`` + * ``Price(1.0, "BBD/BEX")`` + + Instances of this class can be used in regular mathematical expressions + (``+-*/%``) such as: + + .. code-block:: python + + >>> from dpaycli.price import Price + >>> Price("0.3314 BBD/BEX") * 2 + 0.662804 BBD/BEX + >>> Price(0.3314, "BBD", "BEX") + 0.331402 BBD/BEX + + """ + def __init__( + self, + price=None, + base=None, + quote=None, + base_asset=None, # to identify sell/buy + dpay_instance=None + ): + + self.dpay = dpay_instance or shared_dpay_instance() + if price is "": + price = None + if (price is not None and isinstance(price, string_types) and not base and not quote): + import re + price, assets = price.split(" ") + base_symbol, quote_symbol = assets_from_string(assets) + base = Asset(base_symbol, dpay_instance=self.dpay) + quote = Asset(quote_symbol, dpay_instance=self.dpay) + frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) + self["quote"] = Amount(amount=frac.denominator, asset=quote, dpay_instance=self.dpay) + self["base"] = Amount(amount=frac.numerator, asset=base, dpay_instance=self.dpay) + + elif (price is not None and isinstance(price, dict) and + "base" in price and + "quote" in price): + if "price" in price: + raise AssertionError("You cannot provide a 'price' this way") + # Regular 'price' objects according to dpay-core + # base_id = price["base"]["asset_id"] + # if price["base"]["asset_id"] == base_id: + self["base"] = Amount(price["base"], dpay_instance=self.dpay) + self["quote"] = Amount(price["quote"], dpay_instance=self.dpay) + # else: + # self["quote"] = Amount(price["base"], dpay_instance=self.dpay) + # self["base"] = Amount(price["quote"], dpay_instance=self.dpay) + + elif (price is not None and isinstance(base, Asset) and isinstance(quote, Asset)): + frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) + self["quote"] = Amount(amount=frac.denominator, asset=quote, dpay_instance=self.dpay) + self["base"] = Amount(amount=frac.numerator, asset=base, dpay_instance=self.dpay) + + elif (price is not None and isinstance(base, string_types) and isinstance(quote, string_types)): + base = Asset(base, dpay_instance=self.dpay) + quote = Asset(quote, dpay_instance=self.dpay) + frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) + self["quote"] = Amount(amount=frac.denominator, asset=quote, dpay_instance=self.dpay) + self["base"] = Amount(amount=frac.numerator, asset=base, dpay_instance=self.dpay) + + elif (price is None and isinstance(base, string_types) and isinstance(quote, string_types)): + self["quote"] = Amount(quote, dpay_instance=self.dpay) + self["base"] = Amount(base, dpay_instance=self.dpay) + elif (price is not None and isinstance(price, string_types) and isinstance(base, string_types)): + self["quote"] = Amount(price, dpay_instance=self.dpay) + self["base"] = Amount(base, dpay_instance=self.dpay) + # len(args) > 1 + + elif isinstance(price, Amount) and isinstance(base, Amount): + self["quote"], self["base"] = price, base + + # len(args) == 0 + elif (price is None and isinstance(base, Amount) and isinstance(quote, Amount)): + self["quote"] = quote + self["base"] = base + + elif ((isinstance(price, float) or isinstance(price, integer_types)) and + isinstance(base, string_types)): + import re + base_symbol, quote_symbol = assets_from_string(base) + base = Asset(base_symbol, dpay_instance=self.dpay) + quote = Asset(quote_symbol, dpay_instance=self.dpay) + frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) + self["quote"] = Amount(amount=frac.denominator, asset=quote, dpay_instance=self.dpay) + self["base"] = Amount(amount=frac.numerator, asset=base, dpay_instance=self.dpay) + + else: + raise ValueError("Couldn't parse 'Price'.") + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + if ("quote" in self and + "base" in self and + self["base"] and self["quote"]): # don't derive price for deleted Orders + dict.__setitem__(self, "price", self._safedivide( + self["base"]["amount"], + self["quote"]["amount"])) + + def copy(self): + return Price( + None, + base=self["base"].copy(), + quote=self["quote"].copy(), + dpay_instance=self.dpay) + + def _safedivide(self, a, b): + if b != 0.0: + return a / b + else: + return float('Inf') + + def symbols(self): + return self["base"]["symbol"], self["quote"]["symbol"] + + def as_base(self, base): + """ Returns the price instance so that the base asset is ``base``. + + Note: This makes a copy of the object! + + .. code-block:: python + + >>> from dpaycli.price import Price + >>> Price("0.3314 BBD/BEX").as_base("BEX") + 3.017483 BEX/BBD + + """ + if base == self["base"]["symbol"]: + return self.copy() + elif base == self["quote"]["symbol"]: + return self.copy().invert() + else: + raise InvalidAssetException + + def as_quote(self, quote): + """ Returns the price instance so that the quote asset is ``quote``. + + Note: This makes a copy of the object! + + .. code-block:: python + + >>> from dpaycli.price import Price + >>> Price("0.3314 BBD/BEX").as_quote("BBD") + 3.017483 BEX/BBD + + """ + if quote == self["quote"]["symbol"]: + return self.copy() + elif quote == self["base"]["symbol"]: + return self.copy().invert() + else: + raise InvalidAssetException + + def invert(self): + """ Invert the price (e.g. go from ``BBD/BEX`` into ``BEX/BBD``) + + .. code-block:: python + + >>> from dpaycli.price import Price + >>> Price("0.3314 BBD/BEX").invert() + 3.017483 BEX/BBD + + """ + tmp = self["quote"] + self["quote"] = self["base"] + self["base"] = tmp + return self + + def json(self): + return { + "base": self["base"].json(), + "quote": self["quote"].json() + } + + def __repr__(self): + return "{price:.{precision}f} {base}/{quote}".format( + price=self["price"], + base=self["base"]["symbol"], + quote=self["quote"]["symbol"], + precision=( + self["base"]["asset"]["precision"] + + self["quote"]["asset"]["precision"] + ) + ) + + def __float__(self): + return self["price"] + + def _check_other(self, other): + if not other["base"]["symbol"] == self["base"]["symbol"]: + raise AssertionError() + if not other["quote"]["symbol"] == self["quote"]["symbol"]: + raise AssertionError() + + def __mul__(self, other): + a = self.copy() + if isinstance(other, Price): + # Rotate/invert other + if ( + self["quote"]["symbol"] not in other.symbols() and + self["base"]["symbol"] not in other.symbols() + ): + raise InvalidAssetException + + # base/quote = a/b + # a/b * b/c = a/c + a = self.copy() + if self["quote"]["symbol"] == other["base"]["symbol"]: + a["base"] = Amount( + float(self["base"]) * float(other["base"]), self["base"]["symbol"], + dpay_instance=self.dpay + ) + a["quote"] = Amount( + float(self["quote"]) * float(other["quote"]), other["quote"]["symbol"], + dpay_instance=self.dpay + ) + # a/b * c/a = c/b + elif self["base"]["symbol"] == other["quote"]["symbol"]: + a["base"] = Amount( + float(self["base"]) * float(other["base"]), other["base"]["symbol"], + dpay_instance=self.dpay + ) + a["quote"] = Amount( + float(self["quote"]) * float(other["quote"]), self["quote"]["symbol"], + dpay_instance=self.dpay + ) + else: + raise ValueError("Wrong rotation of prices") + elif isinstance(other, Amount): + if not other["asset"] == self["quote"]["asset"]: + raise AssertionError() + a = other.copy() * self["price"] + a["asset"] = self["base"]["asset"].copy() + a["symbol"] = self["base"]["asset"]["symbol"] + else: + a["base"] *= other + return a + + def __imul__(self, other): + if isinstance(other, Price): + tmp = self * other + self["base"] = tmp["base"] + self["quote"] = tmp["quote"] + else: + self["base"] *= other + return self + + def __div__(self, other): + a = self.copy() + if isinstance(other, Price): + # Rotate/invert other + if sorted(self.symbols()) == sorted(other.symbols()): + return float(self.as_base(self["base"]["symbol"])) / float(other.as_base(self["base"]["symbol"])) + elif self["quote"]["symbol"] in other.symbols(): + other = other.as_base(self["quote"]["symbol"]) + elif self["base"]["symbol"] in other.symbols(): + other = other.as_base(self["base"]["symbol"]) + else: + raise InvalidAssetException + a["base"] = Amount( + float(self["base"].amount / other["base"].amount), other["quote"]["symbol"], + dpay_instance=self.dpay + ) + a["quote"] = Amount( + float(self["quote"].amount / other["quote"].amount), self["quote"]["symbol"], + dpay_instance=self.dpay + ) + elif isinstance(other, Amount): + if not other["asset"] == self["quote"]["asset"]: + raise AssertionError() + a = other.copy() / self["price"] + a["asset"] = self["base"]["asset"].copy() + a["symbol"] = self["base"]["asset"]["symbol"] + else: + a["base"] /= other + return a + + def __idiv__(self, other): + if isinstance(other, Price): + tmp = self / other + self["base"] = tmp["base"] + self["quote"] = tmp["quote"] + else: + self["base"] /= other + return self + + def __floordiv__(self, other): + raise NotImplementedError("This is not possible as the price is a ratio") + + def __ifloordiv__(self, other): + raise NotImplementedError("This is not possible as the price is a ratio") + + def __lt__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] < other["price"] + else: + return self["price"] < float(other or 0) + + def __le__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] <= other["price"] + else: + return self["price"] <= float(other or 0) + + def __eq__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] == other["price"] + else: + return self["price"] == float(other or 0) + + def __ne__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] != other["price"] + else: + return self["price"] != float(other or 0) + + def __ge__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] >= other["price"] + else: + return self["price"] >= float(other or 0) + + def __gt__(self, other): + if isinstance(other, Price): + self._check_other(other) + return self["price"] > other["price"] + else: + return self["price"] > float(other or 0) + + __truediv__ = __div__ + __truemul__ = __mul__ + __str__ = __repr__ + + @property + def market(self): + """ Open the corresponding market + + :returns: Instance of :class:`dpaycli.market.Market` for the + corresponding pair of assets. + """ + from .market import Market + return Market( + base=self["base"]["asset"], + quote=self["quote"]["asset"], + dpay_instance=self.dpay + ) + + +class Order(Price): + """ This class inherits :class:`dpaycli.price.Price` but has the ``base`` + and ``quote`` Amounts not only be used to represent the price (as a + ratio of base and quote) but instead has those amounts represent the + amounts of an actual order! + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. note:: + + If an order is marked as deleted, it will carry the + 'deleted' key which is set to ``True`` and all other + data be ``None``. + """ + def __init__(self, base, quote=None, dpay_instance=None, **kwargs): + + self.dpay = dpay_instance or shared_dpay_instance() + + if ( + isinstance(base, dict) and + "sell_price" in base + ): + super(Order, self).__init__(base["sell_price"]) + self["id"] = base.get("id") + elif ( + isinstance(base, dict) and + "min_to_receive" in base and + "amount_to_sell" in base + ): + super(Order, self).__init__( + Amount(base["min_to_receive"], dpay_instance=self.dpay), + Amount(base["amount_to_sell"], dpay_instance=self.dpay), + ) + self["id"] = base.get("id") + elif isinstance(base, Amount) and isinstance(quote, Amount): + super(Order, self).__init__(None, base=base, quote=quote) + else: + raise ValueError("Unknown format to load Order") + + def __repr__(self): + if "deleted" in self and self["deleted"]: + return "deleted order %s" % self["id"] + else: + t = "" + if "time" in self and self["time"]: + t += "(%s) " % self["time"] + if "type" in self and self["type"]: + t += "%s " % str(self["type"]) + if "quote" in self and self["quote"]: + t += "%s " % str(self["quote"]) + if "base" in self and self["base"]: + t += "%s " % str(self["base"]) + return t + "@ " + Price.__repr__(self) + + __str__ = __repr__ + + +class FilledOrder(Price): + """ This class inherits :class:`dpaycli.price.Price` but has the ``base`` + and ``quote`` Amounts not only be used to represent the price (as a + ratio of base and quote) but instead has those amounts represent the + amounts of an actually filled order! + + :param dpaycli.dpay.DPay dpay_instance: DPay instance + + .. note:: Instances of this class come with an additional ``date`` key + that shows when the order has been filled! + """ + + def __init__(self, order, dpay_instance=None, **kwargs): + + self.dpay = dpay_instance or shared_dpay_instance() + if isinstance(order, dict) and "current_pays" in order and "open_pays" in order: + # filled orders from account history + if "op" in order: + order = order["op"] + + super(FilledOrder, self).__init__( + Amount(order["open_pays"], dpay_instance=self.dpay), + Amount(order["current_pays"], dpay_instance=self.dpay), + ) + if "date" in order: + self["date"] = formatTimeString(order["date"]) + + else: + raise ValueError("Couldn't parse 'Price'.") + + def json(self): + return { + "date": formatTimeString(self["date"]), + "current_pays": self["base"].json(), + "open_pays": self["quote"].json(), + } + + def __repr__(self): + t = "" + if "date" in self and self["date"]: + t += "(%s) " % self["date"] + if "type" in self and self["type"]: + t += "%s " % str(self["type"]) + if "quote" in self and self["quote"]: + t += "%s " % str(self["quote"]) + if "base" in self and self["base"]: + t += "%s " % str(self["base"]) + return t + "@ " + Price.__repr__(self) + + __str__ = __repr__ diff --git a/dpaycli/profile.py b/dpaycli/profile.py new file mode 100755 index 0000000..927794b --- /dev/null +++ b/dpaycli/profile.py @@ -0,0 +1,66 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import logging +import json +import collections + + +class DotDict(dict): + def __init__(self, *args): + """ This class simplifies the use of "."-separated + keys when defining a nested dictionary::: + + >>> from dpaycli.profile import Profile + >>> keys = ['profile.url', 'profile.img'] + >>> values = ["http:", "foobar"] + >>> p = Profile(keys, values) + >>> print(p["profile"]["url"]) + http: + + """ + if len(args) == 2: + for i, item in enumerate(args[0]): + t = self + parts = item.split('.') + for j, part in enumerate(parts): + if j < len(parts) - 1: + t = t.setdefault(part, {}) + else: + t[part] = args[1][i] + elif len(args) == 1 and isinstance(args[0], dict): + for k, v in args[0].items(): + self[k] = v + elif len(args) == 1 and isinstance(args[0], str): + for k, v in json.loads(args[0]).items(): + self[k] = v + + +class Profile(DotDict): + """ This class is a template to model a user's on-chain + profile according to + + * https://github.com/adcpm/dpayscript + """ + + def __init__(self, *args, **kwargs): + super(Profile, self).__init__(*args, **kwargs) + + def __str__(self): + return json.dumps(self) + + def update(self, u): + for k, v in u.items(): + if isinstance(v, collections.Mapping): + self.setdefault(k, {}).update(v) + else: + self[k] = u[k] + + def remove(self, key): + parts = key.split(".") + if len(parts) > 1: + self[parts[0]].pop(".".join(parts[1:])) + else: + super(Profile, self).pop(parts[0], None) diff --git a/dpaycli/rc.py b/dpaycli/rc.py new file mode 100755 index 0000000..26c5538 --- /dev/null +++ b/dpaycli/rc.py @@ -0,0 +1,187 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import logging +import json +from .instance import shared_dpay_instance +from dpaycli.constants import state_object_size_info +import hashlib +from binascii import hexlify, unhexlify +import os +from pprint import pprint +from dpaycli.amount import Amount +from dpayclibase import operations +from dpayclibase.objects import Operation +from dpayclibase.signedtransactions import Signed_Transaction +from dpaycligraphenebase.py23 import py23_bytes, bytes_types + + +class RC(object): + def __init__( + self, + dpay_instance=None, + ): + self.dpay = dpay_instance or shared_dpay_instance() + + def get_tx_size(self, op): + """Returns the tx size of an operation""" + ops = [Operation(op)] + prefix = u"BEX" + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + ref_block_num = 34294 + ref_block_prefix = 3707022213 + expiration = "2016-04-06T08:29:27" + tx = Signed_Transaction(ref_block_num=ref_block_num, + ref_block_prefix=ref_block_prefix, + expiration=expiration, + operations=ops) + tx = tx.sign([wif], chain=prefix) + txWire = hexlify(py23_bytes(tx)).decode("ascii") + tx_size = len(txWire) + return tx_size + + def get_resource_count(self, tx_size, state_bytes_count=0, new_account_op_count=0, market_op_count=0): + """Creates the resource_count dictionary based on tx_size, state_bytes_count, new_account_op_count and market_op_count""" + resource_count = {"resource_history_bytes": tx_size} + resource_count["resource_state_bytes"] = state_object_size_info["transaction_object_base_size"] + resource_count["resource_state_bytes"] += state_object_size_info["transaction_object_byte_size"] * tx_size + resource_count["resource_state_bytes"] += state_bytes_count + resource_count["resource_new_accounts"] = new_account_op_count + if market_op_count > 0: + resource_count["resource_market_bytes"] = tx_size + return resource_count + + def comment_dict(self, comment_dict): + """Calc RC costs for a comment dict object + + Example for calculating RC costs + + .. code-block:: python + + from dpaycli.rc import RC + comment_dict = { + "permlink": "test", "author": "holger80", + "body": "test", "parent_permlink": "", + "parent_author": "", "title": "test", + "json_metadata": {"foo": "bar"} + } + + rc = RC() + print(rc.comment_from_dict(comment_dict)) + + """ + op = operations.Comment(**comment_dict) + tx_size = self.get_tx_size(op) + permlink_length = len(comment_dict["permlink"]) + parent_permlink_length = len(comment_dict["parent_permlink"]) + return self.comment(tx_size=tx_size, permlink_length=permlink_length, parent_permlink_length=parent_permlink_length) + + def comment(self, tx_size=1000, permlink_length=10, parent_permlink_length=10): + """Calc RC for a comment""" + state_bytes_count = state_object_size_info["comment_object_base_size"] + state_bytes_count += state_object_size_info["comment_object_permlink_char_size"] * permlink_length + state_bytes_count += state_object_size_info["comment_object_parent_permlink_char_size"] * parent_permlink_length + resource_count = self.get_resource_count(tx_size, state_bytes_count) + return self.dpay.get_rc_cost(resource_count) + + def vote_dict(self, vote_dict): + """Calc RC costs for a vote + + Example for calculating RC costs + + .. code-block:: python + + from dpaycli.rc import RC + vote_dict = { + "voter": "foobara", "author": "foobarc", + "permlink": "foobard", "weight": 1000 + } + + rc = RC() + print(rc.comment(vote_dict)) + + """ + op = operations.Vote(**vote_dict) + tx_size = self.get_tx_size(op) + return self.vote(tx_size=tx_size) + + def vote(self, tx_size=210): + """Calc RC for a vote""" + state_bytes_count = state_object_size_info["comment_vote_object_base_size"] + resource_count = self.get_resource_count(tx_size, state_bytes_count) + return self.dpay.get_rc_cost(resource_count) + + def transfer_dict(self, transfer_dict): + """Calc RC costs for a transfer dict object + + Example for calculating RC costs + + .. code-block:: python + + from dpaycli.rc import RC + from dpaycli.amount import Amount + transfer_dict = { + "from": "foo", "to": "baar", + "amount": Amount("111.110 BEX"), + "memo": "Fooo" + } + + rc = RC() + print(rc.comment(transfer_dict)) + + """ + market_op_count = 1 + op = operations.Transfer(**transfer_dict) + tx_size = self.get_tx_size(op) + return self.transfer(tx_size=tx_size, market_op_count=market_op_count) + + def transfer(self, tx_size=290, market_op_count=1): + """Calc RC of a transfer""" + resource_count = self.get_resource_count(tx_size, market_op_count=market_op_count) + return self.dpay.get_rc_cost(resource_count) + + def custom_json_dict(self, custom_json_dict): + """Calc RC costs for a custom_json + + Example for calculating RC costs + + .. code-block:: python + + from dpaycli.rc import RC + from collections import OrderedDict + custom_json_dict = { + "json": [ + "reblog", OrderedDict([("account", "xeroc"), ("author", "chainsquad"), + ("permlink", "streemian-com-to-open-its-doors-and-offer-a-20-discount") + ]) + ], + "required_auths": [], + "required_posting_auths": ["xeroc"], + "id": "follow" + } + + rc = RC() + print(rc.comment(custom_json_dict)) + + """ + op = operations.Custom_json(**custom_json_dict) + tx_size = self.get_tx_size(op) + return self.custom_json(tx_size=tx_size) + + def custom_json(self, tx_size=444): + resource_count = self.get_resource_count(tx_size) + return self.dpay.get_rc_cost(resource_count) + + def account_update_dict(self, account_update_dict): + """Calc RC costs for account update""" + op = operations.Account_update(**account_update_dict) + tx_size = self.get_tx_size(op) + resource_count = self.get_resource_count(tx_size) + return self.dpay.get_rc_cost(resource_count) + + def claim_account(self, tx_size=300): + """Claim account""" + resource_count = self.get_resource_count(tx_size, new_account_op_count=1) + return self.dpay.get_rc_cost(resource_count) diff --git a/dpaycli/snapshot.py b/dpaycli/snapshot.py new file mode 100755 index 0000000..bc15bb5 --- /dev/null +++ b/dpaycli/snapshot.py @@ -0,0 +1,531 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +import pytz +import json +import re +from datetime import datetime, timedelta, date, time +import math +import random +import logging +from bisect import bisect_left +from dpaycli.utils import formatTimeString, formatTimedelta, remove_from_dict, reputation_to_score, addTzInfo, parse_time +from dpaycli.amount import Amount +from dpaycli.account import Account +from dpaycli.vote import Vote +from dpaycli.instance import shared_dpay_instance +from dpaycli.constants import DPAY_VOTE_REGENERATION_SECONDS, DPAY_1_PERCENT, DPAY_100_PERCENT + +log = logging.getLogger(__name__) + + +class AccountSnapshot(list): + """ This class allows to easily access Account history + + :param str account_name: Name of the account + :param dpaycli.dpay.DPay dpay_instance: DPay + instance + """ + def __init__(self, account, account_history=[], dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.account = Account(account, dpay_instance=self.dpay) + self.reset() + super(AccountSnapshot, self).__init__(account_history) + + def reset(self): + """ Resets the arrays not the stored account history + """ + self.own_vests = [Amount(0, self.dpay.vests_symbol, dpay_instance=self.dpay)] + self.own_dpay = [Amount(0, self.dpay.dpay_symbol, dpay_instance=self.dpay)] + self.own_bbd = [Amount(0, self.dpay.bbd_symbol, dpay_instance=self.dpay)] + self.delegated_vests_in = [{}] + self.delegated_vests_out = [{}] + self.timestamps = [addTzInfo(datetime(1970, 1, 1, 0, 0, 0, 0))] + import dpayclibase.operationids + self.ops_statistics = dpayclibase.operationids.operations.copy() + for key in self.ops_statistics: + self.ops_statistics[key] = 0 + self.reward_timestamps = [] + self.author_rewards = [] + self.curation_rewards = [] + self.curation_per_1000_SP_timestamp = [] + self.curation_per_1000_SP = [] + self.out_vote_timestamp = [] + self.out_vote_weight = [] + self.in_vote_timestamp = [] + self.in_vote_weight = [] + self.in_vote_rep = [] + self.in_vote_rshares = [] + self.vp = [] + self.vp_timestamp = [] + self.rep = [] + self.rep_timestamp = [] + + def search(self, search_str, start=None, stop=None, use_block_num=True): + """ Returns ops in the given range""" + ops = [] + if start is not None: + start = addTzInfo(start) + if stop is not None: + stop = addTzInfo(stop) + for op in self: + if use_block_num and start is not None and isinstance(start, int): + if op["block"] < start: + continue + elif not use_block_num and start is not None and isinstance(start, int): + if op["index"] < start: + continue + elif start is not None and isinstance(start, (datetime, date, time)): + if start > formatTimeString(op["timestamp"]): + continue + if use_block_num and stop is not None and isinstance(stop, int): + if op["block"] > stop: + continue + elif not use_block_num and stop is not None and isinstance(stop, int): + if op["index"] > stop: + continue + elif stop is not None and isinstance(stop, (datetime, date, time)): + if stop < formatTimeString(op["timestamp"]): + continue + op_string = json.dumps(list(op.values())) + if re.search(search_str, op_string): + ops.append(op) + return ops + + def get_ops(self, start=None, stop=None, use_block_num=True, only_ops=[], exclude_ops=[]): + """ Returns ops in the given range""" + if start is not None: + start = addTzInfo(start) + if stop is not None: + stop = addTzInfo(stop) + for op in self: + if use_block_num and start is not None and isinstance(start, int): + if op["block"] < start: + continue + elif not use_block_num and start is not None and isinstance(start, int): + if op["index"] < start: + continue + elif start is not None and isinstance(start, (datetime, date, time)): + if start > formatTimeString(op["timestamp"]): + continue + if use_block_num and stop is not None and isinstance(stop, int): + if op["block"] > stop: + continue + elif not use_block_num and stop is not None and isinstance(stop, int): + if op["index"] > stop: + continue + elif stop is not None and isinstance(stop, (datetime, date, time)): + if stop < formatTimeString(op["timestamp"]): + continue + if exclude_ops and op["type"] in exclude_ops: + continue + if not only_ops or op["type"] in only_ops: + yield op + + def get_data(self, timestamp=None, index=0): + """ Returns snapshot for given timestamp""" + if timestamp is None: + timestamp = datetime.utcnow() + timestamp = addTzInfo(timestamp) + # Find rightmost value less than x + i = bisect_left(self.timestamps, timestamp) + if i: + index = i - 1 + else: + return {} + ts = self.timestamps[index] + own = self.own_vests[index] + din = self.delegated_vests_in[index] + dout = self.delegated_vests_out[index] + dpay = self.own_dpay[index] + bbd = self.own_bbd[index] + sum_in = sum([din[key].amount for key in din]) + sum_out = sum([dout[key].amount for key in dout]) + bp_in = self.dpay.vests_to_sp(sum_in, timestamp=ts) + bp_out = self.dpay.vests_to_sp(sum_out, timestamp=ts) + bp_own = self.dpay.vests_to_sp(own, timestamp=ts) + bp_eff = bp_own + bp_in - bp_out + return {"timestamp": ts, "vests": own, "delegated_vests_in": din, "delegated_vests_out": dout, + "bp_own": bp_own, "bp_eff": bp_eff, "dpay": dpay, "bbd": bbd, "index": index} + + def get_account_history(self, start=None, stop=None, use_block_num=True): + """ Uses account history to fetch all related ops + + :param int/datetime start: start number/date of transactions to + return (*optional*) + :param int/datetime stop: stop number/date of transactions to + return (*optional*) + :param bool use_block_num: if true, start and stop are block numbers, + otherwise virtual OP count numbers. + + """ + super(AccountSnapshot, self).__init__( + [ + h + for h in self.account.history(start=start, stop=stop, use_block_num=use_block_num) + ] + ) + + def update_rewards(self, timestamp, curation_reward, author_vests, author_dpay, author_bbd): + self.reward_timestamps.append(timestamp) + self.curation_rewards.append(curation_reward) + self.author_rewards.append({"vests": author_vests, "whitehorse": author_dpay, "bbd": author_bbd}) + + def update_out_vote(self, timestamp, weight): + self.out_vote_timestamp.append(timestamp) + self.out_vote_weight.append(weight) + + def update_in_vote(self, timestamp, weight, op): + v = Vote(op) + try: + v.refresh() + self.in_vote_timestamp.append(timestamp) + self.in_vote_weight.append(weight) + self.in_vote_rep.append(int(v["reputation"])) + self.in_vote_rshares.append(int(v["rshares"])) + except: + print("Could not found: %s" % v) + return + + def update(self, timestamp, own, delegated_in=None, delegated_out=None, dpay=0, bbd=0): + """ Updates the internal state arrays + + :param datetime timestamp: datetime of the update + :param Amount/float own: vests + :param dict delegated_in: Incoming delegation + :param dict delegated_out: Outgoing delegation + :param Amount/float dpay: dpay + :param Amount/float bbd: bbd + + """ + self.timestamps.append(timestamp - timedelta(seconds=1)) + self.own_vests.append(self.own_vests[-1]) + self.own_dpay.append(self.own_dpay[-1]) + self.own_bbd.append(self.own_bbd[-1]) + self.delegated_vests_in.append(self.delegated_vests_in[-1]) + self.delegated_vests_out.append(self.delegated_vests_out[-1]) + + self.timestamps.append(timestamp) + self.own_vests.append(self.own_vests[-1] + own) + self.own_dpay.append(self.own_dpay[-1] + dpay) + self.own_bbd.append(self.own_bbd[-1] + bbd) + + new_deleg = dict(self.delegated_vests_in[-1]) + if delegated_in is not None and delegated_in: + if delegated_in['amount'] == 0: + del new_deleg[delegated_in['account']] + else: + new_deleg[delegated_in['account']] = delegated_in['amount'] + self.delegated_vests_in.append(new_deleg) + + new_deleg = dict(self.delegated_vests_out[-1]) + if delegated_out is not None and delegated_out: + if delegated_out['account'] is None: + # return_vesting_delegation + for delegatee in new_deleg: + if new_deleg[delegatee]['amount'] == delegated_out['amount']: + del new_deleg[delegatee] + break + + elif delegated_out['amount'] != 0: + # new or updated non-zero delegation + new_deleg[delegated_out['account']] = delegated_out['amount'] + + # skip undelegations here, wait for 'return_vesting_delegation' + # del new_deleg[delegated_out['account']] + + self.delegated_vests_out.append(new_deleg) + + def build(self, only_ops=[], exclude_ops=[], enable_rewards=False, enable_out_votes=False, enable_in_votes=False): + """ Builds the account history based on all account operations + + :param array only_ops: Limit generator by these + operations (*optional*) + :param array exclude_ops: Exclude thse operations from + generator (*optional*) + + """ + if len(self.timestamps) > 0: + start_timestamp = self.timestamps[-1] + else: + start_timestamp = None + for op in sorted(self, key=lambda k: k['timestamp']): + ts = parse_time(op['timestamp']) + if start_timestamp is not None and start_timestamp > ts: + continue + # print(op) + if op['type'] in exclude_ops: + continue + if len(only_ops) > 0 and op['type'] not in only_ops: + continue + self.ops_statistics[op['type']] += 1 + self.parse_op(op, only_ops=only_ops, enable_rewards=enable_rewards, enable_out_votes=enable_out_votes, enable_in_votes=enable_in_votes) + + def parse_op(self, op, only_ops=[], enable_rewards=False, enable_out_votes=False, enable_in_votes=False): + """ Parse account history operation""" + ts = parse_time(op['timestamp']) + + if op['type'] == "account_create": + fee_dpay = Amount(op['fee'], dpay_instance=self.dpay).amount + fee_vests = self.dpay.bp_to_vests(Amount(op['fee'], dpay_instance=self.dpay).amount, timestamp=ts) + # print(fee_vests) + if op['new_account_name'] == self.account["name"]: + self.update(ts, fee_vests, 0, 0) + return + if op['creator'] == self.account["name"]: + self.update(ts, 0, 0, 0, fee_dpay * (-1), 0) + return + + elif op['type'] == "account_create_with_delegation": + fee_dpay = Amount(op['fee'], dpay_instance=self.dpay).amount + fee_vests = self.dpay.bp_to_vests(Amount(op['fee'], dpay_instance=self.dpay).amount, timestamp=ts) + if op['new_account_name'] == self.account["name"]: + if Amount(op['delegation'], dpay_instance=self.dpay).amount > 0: + delegation = {'account': op['creator'], 'amount': + Amount(op['delegation'], dpay_instance=self.dpay)} + else: + delegation = None + self.update(ts, fee_vests, delegation, 0) + return + + if op['creator'] == self.account["name"]: + delegation = {'account': op['new_account_name'], 'amount': + Amount(op['delegation'], dpay_instance=self.dpay)} + self.update(ts, 0, 0, delegation, fee_dpay * (-1), 0) + return + + elif op['type'] == "delegate_vesting_shares": + vests = Amount(op['vesting_shares'], dpay_instance=self.dpay) + # print(op) + if op['delegator'] == self.account["name"]: + delegation = {'account': op['delegatee'], 'amount': vests} + self.update(ts, 0, 0, delegation) + return + if op['delegatee'] == self.account["name"]: + delegation = {'account': op['delegator'], 'amount': vests} + self.update(ts, 0, delegation, 0) + return + + elif op['type'] == "transfer": + amount = Amount(op['amount'], dpay_instance=self.dpay) + # print(op) + if op['from'] == self.account["name"]: + if amount.symbol == self.dpay.dpay_symbol: + self.update(ts, 0, 0, 0, amount * (-1), 0) + elif amount.symbol == self.dpay.bbd_symbol: + self.update(ts, 0, 0, 0, 0, amount * (-1)) + if op['to'] == self.account["name"]: + if amount.symbol == self.dpay.dpay_symbol: + self.update(ts, 0, 0, 0, amount, 0) + elif amount.symbol == self.dpay.bbd_symbol: + self.update(ts, 0, 0, 0, 0, amount) + # print(op, vests) + # self.update(ts, vests, 0, 0) + return + + elif op['type'] == "fill_order": + current_pays = Amount(op["current_pays"], dpay_instance=self.dpay) + open_pays = Amount(op["open_pays"], dpay_instance=self.dpay) + if op["current_owner"] == self.account["name"]: + if current_pays.symbol == self.dpay.dpay_symbol: + self.update(ts, 0, 0, 0, current_pays * (-1), open_pays) + elif current_pays.symbol == self.dpay.bbd_symbol: + self.update(ts, 0, 0, 0, open_pays, current_pays * (-1)) + if op["open_owner"] == self.account["name"]: + if current_pays.symbol == self.dpay.dpay_symbol: + self.update(ts, 0, 0, 0, current_pays, open_pays * (-1)) + elif current_pays.symbol == self.dpay.bbd_symbol: + self.update(ts, 0, 0, 0, open_pays * (-1), current_pays) + # print(op) + return + + elif op['type'] == "transfer_to_vesting": + dpay = Amount(op['amount'], dpay_instance=self.dpay) + vests = self.dpay.bp_to_vests(dpay.amount, timestamp=ts) + if op['from'] == self.account["name"]: + self.update(ts, vests, 0, 0, dpay * (-1), 0) + else: + self.update(ts, vests, 0, 0, 0, 0) + # print(op) + # print(op, vests) + return + + elif op['type'] == "fill_vesting_withdraw": + # print(op) + vests = Amount(op['withdrawn'], dpay_instance=self.dpay) + self.update(ts, vests * (-1), 0, 0) + return + + elif op['type'] == "return_vesting_delegation": + delegation = {'account': None, 'amount': + Amount(op['vesting_shares'], dpay_instance=self.dpay)} + self.update(ts, 0, 0, delegation) + return + + elif op['type'] == "claim_reward_balance": + vests = Amount(op['reward_vests'], dpay_instance=self.dpay) + dpay = Amount(op['reward_dpay'], dpay_instance=self.dpay) + bbd = Amount(op['reward_bbd'], dpay_instance=self.dpay) + self.update(ts, vests, 0, 0, dpay, bbd) + return + + elif op['type'] == "curation_reward": + if "curation_reward" in only_ops or enable_rewards: + vests = Amount(op['reward'], dpay_instance=self.dpay) + if "curation_reward" in only_ops: + self.update(ts, vests, 0, 0) + if enable_rewards: + self.update_rewards(ts, vests, 0, 0, 0) + return + + elif op['type'] == "author_reward": + if "author_reward" in only_ops or enable_rewards: + # print(op) + vests = Amount(op['vesting_payout'], dpay_instance=self.dpay) + dpay = Amount(op['dpay_payout'], dpay_instance=self.dpay) + bbd = Amount(op['bbd_payout'], dpay_instance=self.dpay) + if "author_reward" in only_ops: + self.update(ts, vests, 0, 0, dpay, bbd) + if enable_rewards: + self.update_rewards(ts, 0, vests, dpay, bbd) + return + + elif op['type'] == "producer_reward": + vests = Amount(op['vesting_shares'], dpay_instance=self.dpay) + self.update(ts, vests, 0, 0) + return + + elif op['type'] == "comment_benefactor_reward": + if op['benefactor'] == self.account["name"]: + if "reward" in op: + vests = Amount(op['reward'], dpay_instance=self.dpay) + self.update(ts, vests, 0, 0) + else: + vests = Amount(op['vesting_payout'], dpay_instance=self.dpay) + dpay = Amount(op['dpay_payout'], dpay_instance=self.dpay) + bbd = Amount(op['bbd_payout'], dpay_instance=self.dpay) + self.update(ts, vests, 0, 0, dpay, bbd) + return + else: + return + + elif op['type'] == "fill_convert_request": + amount_in = Amount(op["amount_in"], dpay_instance=self.dpay) + amount_out = Amount(op["amount_out"], dpay_instance=self.dpay) + if op["owner"] == self.account["name"]: + self.update(ts, 0, 0, 0, amount_out, amount_in * (-1)) + return + + elif op['type'] == "interest": + interest = Amount(op["interest"], dpay_instance=self.dpay) + self.update(ts, 0, 0, 0, 0, interest) + return + + elif op['type'] == "vote": + if "vote" in only_ops or enable_out_votes: + weight = int(op['weight']) + if op["voter"] == self.account["name"]: + self.update_out_vote(ts, weight) + if "vote" in only_ops or enable_in_votes and op["author"] == self.account["name"]: + weight = int(op['weight']) + self.update_in_vote(ts, weight, op) + return + + elif op['type'] in ['comment', 'feed_publish', 'shutdown_witness', + 'account_witness_vote', 'witness_update', 'custom_json', + 'limit_order_create', 'account_update', + 'account_witness_proxy', 'limit_order_cancel', 'comment_options', + 'delete_comment', 'interest', 'recover_account', 'pow', + 'fill_convert_request', 'convert', 'request_account_recovery']: + return + + # if "vests" in str(op).lower(): + # print(op) + # else: + # print(op) + + def build_bp_arrays(self): + """ Builds the own_sp and eff_sp array""" + self.own_sp = [] + self.eff_sp = [] + for (ts, own, din, dout) in zip(self.timestamps, self.own_vests, + self.delegated_vests_in, + self.delegated_vests_out): + sum_in = sum([din[key].amount for key in din]) + sum_out = sum([dout[key].amount for key in dout]) + bp_in = self.dpay.vests_to_sp(sum_in, timestamp=ts) + bp_out = self.dpay.vests_to_sp(sum_out, timestamp=ts) + bp_own = self.dpay.vests_to_sp(own, timestamp=ts) + bp_eff = bp_own + bp_in - bp_out + self.own_sp.append(bp_own) + self.eff_sp.append(bp_eff) + + def build_rep_arrays(self): + """ Build reputation arrays """ + self.rep_timestamp = [self.timestamps[1]] + self.rep = [reputation_to_score(0)] + current_reputation = 0 + for (ts, rshares, rep) in zip(self.in_vote_timestamp, self.in_vote_rshares, self.in_vote_rep): + if rep > 0: + if rshares > 0 or (rshares < 0 and rep > current_reputation): + current_reputation += rshares >> 6 + self.rep.append(reputation_to_score(current_reputation)) + self.rep_timestamp.append(ts) + + def build_vp_arrays(self): + """ Build vote power arrays""" + self.vp_timestamp = [self.timestamps[1]] + self.vp = [DPAY_100_PERCENT] + for (ts, weight) in zip(self.out_vote_timestamp, self.out_vote_weight): + self.vp.append(self.vp[-1]) + + if self.vp[-1] < DPAY_100_PERCENT: + regenerated_vp = ((ts - self.vp_timestamp[-1]).total_seconds()) * DPAY_100_PERCENT / DPAY_VOTE_REGENERATION_SECONDS + self.vp[-1] += int(regenerated_vp) + + if self.vp[-1] > DPAY_100_PERCENT: + self.vp[-1] = DPAY_100_PERCENT + self.vp[-1] -= self.dpay._calc_resulting_vote(self.vp[-1], weight) + if self.vp[-1] < 0: + self.vp[-1] = 0 + + self.vp_timestamp.append(ts) + + def build_curation_arrays(self, end_date=None, sum_days=7): + """ Build curation arrays""" + self.curation_per_1000_SP_timestamp = [] + self.curation_per_1000_SP = [] + if sum_days <= 0: + raise ValueError("sum_days must be greater than 0") + index = 0 + curation_sum = 0 + days = (self.reward_timestamps[-1] - self.reward_timestamps[0]).days // sum_days * sum_days + if end_date is None: + end_date = self.reward_timestamps[-1] - timedelta(days=days) + for (ts, vests) in zip(self.reward_timestamps, self.curation_rewards): + if vests == 0: + continue + bp = self.dpay.vests_to_sp(vests, timestamp=ts) + data = self.get_data(timestamp=ts, index=index) + index = data["index"] + if "bp_eff" in data and data["bp_eff"] > 0: + curation_1k_sp = bp / data["bp_eff"] * 1000 / sum_days * 7 + else: + curation_1k_sp = 0 + if ts < end_date: + curation_sum += curation_1k_sp + else: + self.curation_per_1000_SP_timestamp.append(end_date) + self.curation_per_1000_SP.append(curation_sum) + end_date = end_date + timedelta(days=sum_days) + curation_sum = 0 + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, str(self.account["name"])) diff --git a/dpaycli/storage.py b/dpaycli/storage.py new file mode 100755 index 0000000..ca29ec6 --- /dev/null +++ b/dpaycli/storage.py @@ -0,0 +1,676 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import object +from dpaycligraphenebase.py23 import py23_bytes, bytes_types +import shutil +import time +import os +import sqlite3 +from .aes import AESCipher +from appdirs import user_data_dir +from datetime import datetime +import logging +from binascii import hexlify +import random +import hashlib +from .exceptions import WrongMasterPasswordException, NoWriteAccess +from .nodelist import NodeList +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) +log.addHandler(logging.StreamHandler()) + +timeformat = "%Y%m%d-%H%M%S" + + +class DataDir(object): + """ This class ensures that the user's data is stored in its OS + preotected user directory: + + **OSX:** + + * `~/Library/Application Support/` + + **Windows:** + + * `C:\\Documents and Settings\\\\Application Data\\Local Settings\\\\` + * `C:\\Documents and Settings\\\\Application Data\\\\` + + **Linux:** + + * `~/.local/share/` + + Furthermore, it offers an interface to generated backups + in the `backups/` directory every now and then. + """ + appname = "dpaycli" + appauthor = "dpaycli" + storageDatabase = "dpaycli.sqlite" + + data_dir = user_data_dir(appname, appauthor) + sqlDataBaseFile = os.path.join(data_dir, storageDatabase) + + def __init__(self): + #: Storage + self.mkdir_p() + + def mkdir_p(self): + """ Ensure that the directory in which the data is stored + exists + """ + if os.path.isdir(self.data_dir): + return + else: + try: + os.makedirs(self.data_dir) + except FileExistsError: + self.sqlDataBaseFile = ":memory:" + return + except OSError: + self.sqlDataBaseFile = ":memory:" + return + + def sqlite3_backup(self, backupdir): + """ Create timestamped database copy + """ + if self.sqlDataBaseFile == ":memory:": + return + if not os.path.isdir(backupdir): + os.mkdir(backupdir) + backup_file = os.path.join( + backupdir, + os.path.basename(self.storageDatabase) + + datetime.utcnow().strftime("-" + timeformat)) + self.sqlite3_copy(self.sqlDataBaseFile, backup_file) + configStorage["lastBackup"] = datetime.utcnow().strftime(timeformat) + + def sqlite3_copy(self, src, dst): + """Copy sql file from src to dst""" + if self.sqlDataBaseFile == ":memory:": + return + if not os.path.isfile(src): + return + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + # Lock database before making a backup + cursor.execute('begin immediate') + # Make new backup file + shutil.copyfile(src, dst) + log.info("Creating {}...".format(dst)) + # Unlock database + connection.rollback() + + def recover_with_latest_backup(self, backupdir="backups"): + """ Replace database with latest backup""" + file_date = 0 + if self.sqlDataBaseFile == ":memory:": + return + if not os.path.isdir(backupdir): + backupdir = os.path.join(self.data_dir, backupdir) + if not os.path.isdir(backupdir): + return + newest_backup_file = None + for filename in os.listdir(backupdir): + backup_file = os.path.join(backupdir, filename) + if os.stat(backup_file).st_ctime > file_date: + if os.path.isfile(backup_file): + file_date = os.stat(backup_file).st_ctime + newest_backup_file = backup_file + if newest_backup_file is not None: + self.sqlite3_copy(newest_backup_file, self.sqlDataBaseFile) + + def clean_data(self): + """ Delete files older than 70 days + """ + if self.sqlDataBaseFile == ":memory:": + return + log.info("Cleaning up old backups") + for filename in os.listdir(self.data_dir): + backup_file = os.path.join(self.data_dir, filename) + if os.stat(backup_file).st_ctime < (time.time() - 70 * 86400): + if os.path.isfile(backup_file): + os.remove(backup_file) + log.info("Deleting {}...".format(backup_file)) + + def refreshBackup(self): + """ Make a new backup + """ + backupdir = os.path.join(self.data_dir, "backups") + self.sqlite3_backup(backupdir) + self.clean_data() + + +class Key(DataDir): + """ This is the key storage that stores the public key and the + (possibly encrypted) private key in the `keys` table in the + SQLite3 database. + """ + __tablename__ = 'keys' + + def __init__(self): + super(Key, self).__init__() + + def exists_table(self): + """ Check if the database table exists + """ + query = ("SELECT name FROM sqlite_master " + "WHERE type='table' AND name=?", (self.__tablename__, )) + try: + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + return True if cursor.fetchone() else False + except sqlite3.OperationalError: + self.sqlDataBaseFile = ":memory:" + log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) + return True + + def create_table(self): + """ Create the new table in the SQLite database + """ + query = ("CREATE TABLE {0} (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "pub STRING(256)," + "wif STRING(256))".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + connection.commit() + + def getPublicKeys(self): + """ Returns the public keys stored in the database + """ + query = ("SELECT pub from {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(query) + results = cursor.fetchall() + return [x[0] for x in results] + except sqlite3.OperationalError: + return [] + + def getPrivateKeyForPublicKey(self, pub): + """ Returns the (possibly encrypted) private key that + corresponds to a public key + + :param str pub: Public key + + The encryption scheme is BIP38 + """ + query = ("SELECT wif from {0} WHERE pub=?".format(self.__tablename__), (pub,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + key = cursor.fetchone() + if key: + return key[0] + else: + return None + + def updateWif(self, pub, wif): + """ Change the wif to a pubkey + + :param str pub: Public key + :param str wif: Private key + """ + query = ("UPDATE {0} SET wif=? WHERE pub=?".format(self.__tablename__), (wif, pub)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def add(self, wif, pub): + """ Add a new public/private key pair (correspondence has to be + checked elsewhere!) + + :param str pub: Public key + :param str wif: Private key + """ + if self.getPrivateKeyForPublicKey(pub): + raise ValueError("Key already in storage") + query = ("INSERT INTO {0} (pub, wif) VALUES (?, ?)".format(self.__tablename__), (pub, wif)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def delete(self, pub): + """ Delete the key identified as `pub` + + :param str pub: Public key + """ + query = ("DELETE FROM {0} WHERE pub=?".format(self.__tablename__), (pub,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def wipe(self, sure=False): + """Purge the entire wallet. No keys will survive this!""" + if not sure: + log.error( + "You need to confirm that you are sure " + "and understand the implications of " + "wiping your wallet!" + ) + return + else: + query = ("DELETE FROM {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + connection.commit() + + +class Token(DataDir): + """ This is the token storage that stores the public username and the + (possibly encrypted) token in the `token` table in the + SQLite3 database. + """ + __tablename__ = 'token' + + def __init__(self): + super(Token, self).__init__() + + def exists_table(self): + """ Check if the database table exists + """ + query = ("SELECT name FROM sqlite_master " + "WHERE type='table' AND name=?", (self.__tablename__, )) + try: + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + return True if cursor.fetchone() else False + except sqlite3.OperationalError: + self.sqlDataBaseFile = ":memory:" + log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) + return True + + def create_table(self): + """ Create the new table in the SQLite database + """ + query = ("CREATE TABLE {0} (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name STRING(256)," + "token STRING(256))".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + connection.commit() + + def getPublicNames(self): + """ Returns the public names stored in the database + """ + query = ("SELECT name from {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(query) + results = cursor.fetchall() + return [x[0] for x in results] + except sqlite3.OperationalError: + return [] + + def getTokenForPublicName(self, name): + """ Returns the (possibly encrypted) private token that + corresponds to a public name + + :param str pub: Public name + + The encryption scheme is BIP38 + """ + query = ("SELECT token from {0} WHERE name=?".format(self.__tablename__), (name,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + token = cursor.fetchone() + if token: + return token[0] + else: + return None + + def updateToken(self, name, token): + """ Change the token to a name + + :param str name: Public name + :param str token: Private token + """ + query = ("UPDATE {0} SET token=? WHERE name=?".format(self.__tablename__), (token, name)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def add(self, name, token): + """ Add a new public/private token pair (correspondence has to be + checked elsewhere!) + + :param str name: Public name + :param str token: Private token + """ + if self.getTokenForPublicName(name): + raise ValueError("Key already in storage") + query = ("INSERT INTO {0} (name, token) VALUES (?, ?)".format(self.__tablename__), (name, token)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def delete(self, name): + """ Delete the key identified as `name` + + :param str name: Public name + """ + query = ("DELETE FROM {0} WHERE name=?".format(self.__tablename__), (name,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + + def wipe(self, sure=False): + """Purge the entire wallet. No keys will survive this!""" + if not sure: + log.error( + "You need to confirm that you are sure " + "and understand the implications of " + "wiping your wallet!" + ) + return + else: + query = ("DELETE FROM {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + connection.commit() + + +class Configuration(DataDir): + """ This is the configuration storage that stores key/value + pairs in the `config` table of the SQLite3 database. + """ + __tablename__ = "config" + + #: Default configuration + nodelist = NodeList() + nodes = nodelist.get_nodes(normal=True, appbase=True, dev=False, testnet=False) + config_defaults = { + "node": nodes, + "password_storage": "environment", + "rpcpassword": "", + "rpcuser": "", + "order-expiration": 7 * 24 * 60 * 60, + "client_id": "", + "hot_sign_redirect_uri": None, + "dpid_api_url": "https://go.dpayid.io/api/", + "oauth_base_url": "https://go.dpayid.io/oauth2/"} + + def __init__(self): + super(Configuration, self).__init__() + + def exists_table(self): + """ Check if the database table exists + """ + query = ("SELECT name FROM sqlite_master " + "WHERE type='table' AND name=?", (self.__tablename__,)) + try: + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(*query) + return True if cursor.fetchone() else False + except sqlite3.OperationalError: + self.sqlDataBaseFile = ":memory:" + log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) + return True + + def create_table(self): + """ Create the new table in the SQLite database + """ + query = ("CREATE TABLE {0} (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "key STRING(256)," + "value STRING(256))".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(query) + connection.commit() + except sqlite3.OperationalError: + log.error("Could not write to database: %s" % (self.__tablename__)) + raise NoWriteAccess("Could not write to database: %s" % (self.__tablename__)) + + def checkBackup(self): + """ Backup the SQL database every 7 days + """ + if ("lastBackup" not in configStorage or + configStorage["lastBackup"] == ""): + print("No backup has been created yet!") + self.refreshBackup() + try: + if ( + datetime.utcnow() - + datetime.strptime(configStorage["lastBackup"], + timeformat) + ).days > 7: + print("Backups older than 7 days!") + self.refreshBackup() + except: + self.refreshBackup() + + def _haveKey(self, key): + """ Is the key `key` available int he configuration? + """ + query = ("SELECT value FROM {0} WHERE key=?".format(self.__tablename__), (key,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(*query) + return True if cursor.fetchone() else False + except sqlite3.OperationalError: + log.warning("Could not read %s (database: %s)" % (str(key), self.__tablename__)) + return False + + def __getitem__(self, key): + """ This method behaves differently from regular `dict` in that + it returns `None` if a key is not found! + """ + query = ("SELECT value FROM {0} WHERE key=?".format(self.__tablename__), (key,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(*query) + result = cursor.fetchone() + if result: + return result[0] + else: + if key in self.config_defaults: + return self.config_defaults[key] + else: + return None + except sqlite3.OperationalError: + log.warning("Could not read %s (database: %s)" % (str(key), self.__tablename__)) + if key in self.config_defaults: + return self.config_defaults[key] + else: + return None + + def get(self, key, default=None): + """ Return the key if exists or a default value + """ + if key in self: + return self.__getitem__(key) + else: + return default + + def __contains__(self, key): + if self._haveKey(key) or key in self.config_defaults: + return True + else: + return False + + def __setitem__(self, key, value): + if self._haveKey(key): + query = ("UPDATE {0} SET value=? WHERE key=?".format(self.__tablename__), (value, key)) + else: + query = ("INSERT INTO {0} (key, value) VALUES (?, ?)".format(self.__tablename__), (key, value)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(*query) + connection.commit() + except sqlite3.OperationalError: + log.error("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) + raise NoWriteAccess("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) + + def delete(self, key): + """ Delete a key from the configuration store + """ + query = ("DELETE FROM {0} WHERE key=?".format(self.__tablename__), (key,)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + try: + cursor.execute(*query) + connection.commit() + except sqlite3.OperationalError: + log.error("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) + raise NoWriteAccess("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) + + def __iter__(self): + return iter(list(self.items())) + + def items(self): + query = ("SELECT key, value from {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + r = {} + for key, value in cursor.fetchall(): + r[key] = value + return r + + def __len__(self): + query = ("SELECT id from {0} ".format(self.__tablename__)) + connection = sqlite3.connect(self.sqlDataBaseFile) + cursor = connection.cursor() + cursor.execute(query) + return len(cursor.fetchall()) + + +class MasterPassword(object): + """ The keys are encrypted with a Masterpassword that is stored in + the configurationStore. It has a checksum to verify correctness + of the password + """ + + password = "" # nosec + decrypted_master = "" + + #: This key identifies the encrypted master password stored in the confiration + config_key = "encrypted_master_password" + + def __init__(self, password): + """ The encrypted private keys in `keys` are encrypted with a + random encrypted masterpassword that is stored in the + configuration. + + The password is used to encrypt this masterpassword. To + decrypt the keys stored in the keys database, one must use + BIP38, decrypt the masterpassword from the configuration + store with the user password, and use the decrypted + masterpassword to decrypt the BIP38 encrypted private keys + from the keys storage! + + :param str password: Password to use for en-/de-cryption + """ + self.password = password + if self.config_key not in configStorage: + self.newMaster() + self.saveEncrytpedMaster() + else: + self.decryptEncryptedMaster() + + def decryptEncryptedMaster(self): + """ Decrypt the encrypted masterpassword + """ + aes = AESCipher(self.password) + checksum, encrypted_master = configStorage[self.config_key].split("$") + try: + decrypted_master = aes.decrypt(encrypted_master) + except: + raise WrongMasterPasswordException + if checksum != self.deriveChecksum(decrypted_master): + raise WrongMasterPasswordException + self.decrypted_master = decrypted_master + + def saveEncrytpedMaster(self): + """ Store the encrypted master password in the configuration + store + """ + configStorage[self.config_key] = self.getEncryptedMaster() + + def newMaster(self): + """ Generate a new random masterpassword + """ + # make sure to not overwrite an existing key + if (self.config_key in configStorage and + configStorage[self.config_key]): + return + self.decrypted_master = hexlify(os.urandom(32)).decode("ascii") + + def deriveChecksum(self, s): + """ Derive the checksum + """ + checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest() + return checksum[:4] + + def getEncryptedMaster(self): + """ Obtain the encrypted masterkey + """ + if not self.decrypted_master: + raise Exception("master not decrypted") + aes = AESCipher(self.password) + return "{}${}".format(self.deriveChecksum(self.decrypted_master), + aes.encrypt(self.decrypted_master)) + + def changePassword(self, newpassword): + """ Change the password + """ + self.password = newpassword + self.saveEncrytpedMaster() + + @staticmethod + def wipe(sure=False): + """Remove all keys from configStorage""" + if not sure: + log.error( + "You need to confirm that you are sure " + "and understand the implications of " + "wiping your wallet!" + ) + return + else: + configStorage.delete(MasterPassword.config_key) + + +# Create keyStorage +keyStorage = Key() +tokenStorage = Token() +configStorage = Configuration() + +# Create Tables if database is brand new +if not configStorage.exists_table(): + configStorage.create_table() + +newKeyStorage = False +if not keyStorage.exists_table(): + newKeyStorage = True + keyStorage.create_table() + +newTokenStorage = False +if not tokenStorage.exists_table(): + newTokenStorage = True + tokenStorage.create_table() diff --git a/dpaycli/transactionbuilder.py b/dpaycli/transactionbuilder.py new file mode 100755 index 0000000..5f0a389 --- /dev/null +++ b/dpaycli/transactionbuilder.py @@ -0,0 +1,498 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from future.utils import python_2_unicode_compatible +import logging +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from .account import Account +from .utils import formatTimeFromNow +from .dpayid import DPayID +from dpayclibase.objects import Operation +from dpaycligraphenebase.account import PrivateKey, PublicKey +from dpayclibase.signedtransactions import Signed_Transaction +from dpayclibase import transactions, operations +from .exceptions import ( + InsufficientAuthorityError, + MissingKeyError, + InvalidWifError, + WalletLocked, + OfflineHasNoRPCException +) +from dpaycli.instance import shared_dpay_instance +log = logging.getLogger(__name__) + + +@python_2_unicode_compatible +class TransactionBuilder(dict): + """ This class simplifies the creation of transactions by adding + operations and signers. + To build your own transactions and sign them + + :param dict tx: transaction (Optional). If not set, the new transaction is created. + :param int expiration: Delay in seconds until transactions are supposed + to expire *(optional)* (default is 30) + :param DPay dpay_instance: If not set, shared_dpay_instance() is used + + .. testcode:: + + from dpaycli.transactionbuilder import TransactionBuilder + from dpayclibase.operations import Transfer + from dpaycli import DPay + wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + stm = DPay(nobroadcast=True, keys={'active': wif}) + tx = TransactionBuilder(dpay_instance=stm) + transfer = {"from": "test", "to": "test1", "amount": "1 BEX", "memo": ""} + tx.appendOps(Transfer(transfer)) + tx.appendSigner("test", "active") # or tx.appendWif(wif) + signed_tx = tx.sign() + broadcast_tx = tx.broadcast() + + """ + def __init__( + self, + tx={}, + use_condenser_api=True, + dpay_instance=None, + **kwargs + ): + self.dpay = dpay_instance or shared_dpay_instance() + self.clear() + if tx and isinstance(tx, dict): + super(TransactionBuilder, self).__init__(tx) + # Load operations + self.ops = tx["operations"] + self._require_reconstruction = False + else: + self._require_reconstruction = True + self._use_condenser_api = use_condenser_api + self.set_expiration(kwargs.get("expiration", self.dpay.expiration)) + + def set_expiration(self, p): + """Set expiration date""" + self.expiration = p + + def is_empty(self): + """Check if ops is empty""" + return not (len(self.ops) > 0) + + def list_operations(self): + """List all ops""" + if self.dpay.is_connected() and self.dpay.rpc.get_use_appbase(): + # appbase disabled by now + appbase = not self._use_condenser_api + else: + appbase = False + return [Operation(o, appbase=appbase, prefix=self.dpay.prefix) for o in self.ops] + + def _is_signed(self): + """Check if signatures exists""" + return "signatures" in self and bool(self["signatures"]) + + def _is_constructed(self): + """Check if tx is already constructed""" + return "expiration" in self and bool(self["expiration"]) + + def _is_require_reconstruction(self): + return self._require_reconstruction + + def _set_require_reconstruction(self): + self._require_reconstruction = True + + def _unset_require_reconstruction(self): + self._require_reconstruction = False + + def __repr__(self): + return str(self) + + def __str__(self): + return str(self.json()) + + def __getitem__(self, key): + if key not in self: + self.constructTx() + return dict(self).__getitem__(key) + + def get_parent(self): + """ TransactionBuilders don't have parents, they are their own parent + """ + return self + + def json(self, with_prefix=False): + """ Show the transaction as plain json + """ + if not self._is_constructed() or self._is_require_reconstruction(): + self.constructTx() + json_dict = dict(self) + if with_prefix: + json_dict["prefix"] = self.dpay.prefix + return json_dict + + def appendOps(self, ops, append_to=None): + """ Append op(s) to the transaction builder + + :param list ops: One or a list of operations + """ + if isinstance(ops, list): + self.ops.extend(ops) + else: + self.ops.append(ops) + self._set_require_reconstruction() + + def appendSigner(self, account, permission): + """ Try to obtain the wif key from the wallet by telling which account + and permission is supposed to sign the transaction + It is possible to add more than one signer. + """ + if not self.dpay.is_connected(): + return + if permission not in ["active", "owner", "posting"]: + raise AssertionError("Invalid permission") + account = Account(account, dpay_instance=self.dpay) + if permission not in account: + account = Account(account, dpay_instance=self.dpay, lazy=False, full=True) + account.clear_cache() + account.refresh() + if permission not in account: + account = Account(account, dpay_instance=self.dpay) + if permission not in account: + raise AssertionError("Could not access permission") + + required_treshold = account[permission]["weight_threshold"] + if self.dpay.wallet.locked(): + raise WalletLocked() + if self.dpay.use_dpid: + self.dpay.dpayid.set_username(account["name"], permission) + return + + def fetchkeys(account, perm, level=0): + if level > 2: + return [] + r = [] + for authority in account[perm]["key_auths"]: + try: + wif = self.dpay.wallet.getPrivateKeyForPublicKey( + authority[0]) + if wif: + r.append([wif, authority[1]]) + except ValueError: + pass + except MissingKeyError: + pass + + if sum([x[1] for x in r]) < required_treshold: + # go one level deeper + for authority in account[perm]["account_auths"]: + auth_account = Account( + authority[0], dpay_instance=self.dpay) + r.extend(fetchkeys(auth_account, perm, level + 1)) + + return r + + if account["name"] not in self.signing_accounts: + # is the account an instance of public key? + if isinstance(account, PublicKey): + self.wifs.add( + self.dpay.wallet.getPrivateKeyForPublicKey( + str(account) + ) + ) + else: + if permission not in account: + raise AssertionError("Could not access permission") + required_treshold = account[permission]["weight_threshold"] + keys = fetchkeys(account, permission) + # If keys are empty, try again with active key + if not keys and permission == "posting": + _keys = fetchkeys(account, "active") + keys.extend(_keys) + # If keys are empty, try again with owner key + if not keys and permission != "owner": + _keys = fetchkeys(account, "owner") + keys.extend(_keys) + for x in keys: + self.wifs.add(x[0]) + + self.signing_accounts.append(account["name"]) + + def appendWif(self, wif): + """ Add a wif that should be used for signing of the transaction. + + :param string wif: One wif key to use for signing + a transaction. + """ + if wif: + try: + PrivateKey(wif, prefix=self.dpay.prefix) + self.wifs.add(wif) + except: + raise InvalidWifError + + def clearWifs(self): + """Clear all stored wifs""" + self.wifs = set() + + def constructTx(self, ref_block_num=None, ref_block_prefix=None): + """ Construct the actual transaction and store it in the class's dict + store + + """ + ops = list() + if self.dpay.is_connected() and self.dpay.rpc.get_use_appbase(): + # appbase disabled by now + # broadcasting does not work at the moment + appbase = not self._use_condenser_api + else: + appbase = False + for op in self.ops: + # otherwise, we simply wrap ops into Operations + ops.extend([Operation(op, appbase=appbase, prefix=self.dpay.prefix)]) + + # We no wrap everything into an actual transaction + expiration = formatTimeFromNow( + self.expiration or self.dpay.expiration + ) + if ref_block_num is None or ref_block_prefix is None: + ref_block_num, ref_block_prefix = transactions.getBlockParams( + self.dpay.rpc) + self.tx = Signed_Transaction( + ref_block_prefix=ref_block_prefix, + expiration=expiration, + operations=ops, + ref_block_num=ref_block_num, + custom_chains=self.dpay.custom_chains, + prefix=self.dpay.prefix + ) + + super(TransactionBuilder, self).update(self.tx.json()) + self._unset_require_reconstruction() + + def sign(self, reconstruct_tx=True): + """ Sign a provided transaction with the provided key(s) + One or many wif keys to use for signing a transaction. + The wif keys can be provided by "appendWif" or the + signer can be defined "appendSigner". The wif keys + from all signer that are defined by "appendSigner + will be loaded from the wallet. + + :param bool reconstruct_tx: when set to False and tx + is already contructed, it will not reconstructed + and already added signatures remain + + """ + if not self._is_constructed() or (self._is_constructed() and reconstruct_tx): + self.constructTx() + if "operations" not in self or not self["operations"]: + return + if self.dpay.use_dpid: + return + # We need to set the default prefix, otherwise pubkeys are + # presented wrongly! + if self.dpay.rpc is not None: + operations.default_prefix = ( + self.dpay.chain_params["prefix"]) + elif "blockchain" in self: + operations.default_prefix = self["blockchain"]["prefix"] + + try: + signedtx = Signed_Transaction(**self.json(with_prefix=True)) + signedtx.add_custom_chains(self.dpay.custom_chains) + except: + raise ValueError("Invalid TransactionBuilder Format") + + if not any(self.wifs): + raise MissingKeyError + + signedtx.sign(self.wifs, chain=self.dpay.chain_params) + self["signatures"].extend(signedtx.json().get("signatures")) + return signedtx + + def verify_authority(self): + """ Verify the authority of the signed transaction + """ + try: + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + args = {'trx': self.json()} + else: + args = self.json() + ret = self.dpay.rpc.verify_authority(args, api="database") + if not ret: + raise InsufficientAuthorityError + elif isinstance(ret, dict) and "valid" in ret and not ret["valid"]: + raise InsufficientAuthorityError + except Exception as e: + raise e + + def get_potential_signatures(self): + """ Returns public key from signature + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + args = {'trx': self.json()} + else: + args = self.json() + ret = self.dpay.rpc.get_potential_signatures(args, api="database") + if 'keys' in ret: + ret = ret["keys"] + return ret + + def get_transaction_hex(self): + """ Returns a hex value of the transaction + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + args = {'trx': self.json()} + else: + args = self.json() + ret = self.dpay.rpc.get_transaction_hex(args, api="database") + if 'hex' in ret: + ret = ret["hex"] + return ret + + def get_required_signatures(self, available_keys=list()): + """ Returns public key from signature + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + args = {'trx': self.json(), 'available_keys': available_keys} + ret = self.dpay.rpc.get_required_signatures(args, api="database") + else: + ret = self.dpay.rpc.get_required_signatures(self.json(), available_keys, api="database") + + return ret + + def broadcast(self, max_block_age=-1): + """ Broadcast a transaction to the dPay network + Returns the signed transaction and clears itself + after broadast + + Clears itself when broadcast was not successfully. + + :param int max_block_age: paramerter only used + for appbase ready nodes + + """ + # Cannot broadcast an empty transaction + if not self._is_signed(): + self.sign() + + if "operations" not in self or not self["operations"]: + return + ret = self.json() + if self.dpay.is_connected() and self.dpay.rpc.get_use_appbase(): + # Returns an internal Error at the moment + if not self._use_condenser_api: + args = {'trx': self.json(), 'max_block_age': max_block_age} + broadcast_api = "network_broadcast" + else: + args = self.json() + broadcast_api = "condenser" + else: + args = self.json() + broadcast_api = "network_broadcast" + + if self.dpay.nobroadcast: + log.info("Not broadcasting anything!") + self.clear() + return ret + # Broadcast + try: + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.use_dpid: + ret = self.dpay.dpayid.broadcast(self["operations"]) + elif self.dpay.blocking: + ret = self.dpay.rpc.broadcast_transaction_synchronous( + args, api=broadcast_api) + if "trx" in ret: + ret.update(**ret.get("trx")) + else: + self.dpay.rpc.broadcast_transaction( + args, api=broadcast_api) + except Exception as e: + # log.error("Could Not broadcasting anything!") + self.clear() + raise e + + self.clear() + return ret + + def clear(self): + """ Clear the transaction builder and start from scratch + """ + self.ops = [] + self.wifs = set() + self.signing_accounts = [] + # This makes sure that _is_constructed will return False afterwards + self["expiration"] = None + super(TransactionBuilder, self).__init__({}) + + def addSigningInformation(self, account, permission, reconstruct_tx=False): + """ This is a private method that adds side information to a + unsigned/partial transaction in order to simplify later + signing (e.g. for multisig or coldstorage) + + Not needed when "appendWif" was already or is going to be used + + FIXME: Does not work with owner keys! + + :param bool reconstruct_tx: when set to False and tx + is already contructed, it will not reconstructed + and already added signatures remain + + """ + if not self._is_constructed() or (self._is_constructed() and reconstruct_tx): + self.constructTx() + self["blockchain"] = self.dpay.chain_params + + if isinstance(account, PublicKey): + self["missing_signatures"] = [ + str(account) + ] + else: + accountObj = Account(account, dpay_instance=self.dpay) + authority = accountObj[permission] + # We add a required_authorities to be able to identify + # how to sign later. This is an array, because we + # may later want to allow multiple operations per tx + self.update({"required_authorities": { + accountObj["name"]: authority + }}) + for account_auth in authority["account_auths"]: + account_auth_account = Account(account_auth[0], dpay_instance=self.dpay) + self["required_authorities"].update({ + account_auth[0]: account_auth_account.get(permission) + }) + + # Try to resolve required signatures for offline signing + self["missing_signatures"] = [ + x[0] for x in authority["key_auths"] + ] + # Add one recursion of keys from account_auths: + for account_auth in authority["account_auths"]: + account_auth_account = Account(account_auth[0], dpay_instance=self.dpay) + self["missing_signatures"].extend( + [x[0] for x in account_auth_account[permission]["key_auths"]] + ) + + def appendMissingSignatures(self): + """ Store which accounts/keys are supposed to sign the transaction + + This method is used for an offline-signer! + """ + missing_signatures = self.get("missing_signatures", []) + for pub in missing_signatures: + try: + wif = self.dpay.wallet.getPrivateKeyForPublicKey(pub) + if wif: + self.appendWif(wif) + except MissingKeyError: + wif = None diff --git a/dpaycli/utils.py b/dpaycli/utils.py new file mode 100755 index 0000000..04302f9 --- /dev/null +++ b/dpaycli/utils.py @@ -0,0 +1,290 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import next +import re +import time as timenow +import math +from datetime import datetime, tzinfo, timedelta, date, time +import pytz +import difflib + +timeFormat = '%Y-%m-%dT%H:%M:%S' +# https://github.com/matiasb/python-unidiff/blob/master/unidiff/constants.py#L37 +# @@ (source offset, length) (target offset, length) @@ (section header) +RE_HUNK_HEADER = re.compile( + r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?\ @@[ ]?(.*)$", + flags=re.MULTILINE) + + +def formatTime(t): + """ Properly Format Time for permlinks + """ + if isinstance(t, float): + return datetime.utcfromtimestamp(t).strftime("%Y%m%dt%H%M%S%Z") + if isinstance(t, (datetime, date, time)): + return t.strftime("%Y%m%dt%H%M%S%Z") + + +def addTzInfo(t, timezone='UTC'): + """Returns a datetime object with tzinfo added""" + if t and isinstance(t, (datetime, date, time)) and t.tzinfo is None: + utc = pytz.timezone(timezone) + t = utc.localize(t) + return t + + +def formatTimeString(t): + """ Properly Format Time for permlinks + """ + if isinstance(t, (datetime, date, time)): + return t.strftime(timeFormat) + return addTzInfo(datetime.strptime(t, timeFormat)) + + +def formatToTimeStamp(t): + """ Returns a timestamp integer + + :param datetime t: datetime object + :return: Timestamp as integer + """ + if isinstance(t, (datetime, date, time)): + t = addTzInfo(t) + else: + t = formatTimeString(t) + epoch = addTzInfo(datetime(1970, 1, 1)) + return int((t - epoch).total_seconds()) + + +def formatTimeFromNow(secs=0): + """ Properly Format Time that is `x` seconds in the future + + :param int secs: Seconds to go in the future (`x>0`) or the + past (`x<0`) + :return: Properly formated time for Graphene (`%Y-%m-%dT%H:%M:%S`) + :rtype: str + + """ + return datetime.utcfromtimestamp( + timenow.time() + int(secs)).strftime(timeFormat) + + +def formatTimedelta(td): + """Format timedelta to String + """ + if not isinstance(td, timedelta): + return "" + days, seconds = td.days, td.seconds + hours = days * 24 + seconds // 3600 + minutes = (seconds % 3600) // 60 + seconds = (seconds % 60) + return "%d:%s:%s" % (hours, str(minutes).zfill(2), str(seconds).zfill(2)) + + +def parse_time(block_time): + """Take a string representation of time from the blockchain, and parse it + into datetime object. + """ + utc = pytz.timezone('UTC') + return utc.localize(datetime.strptime(block_time, timeFormat)) + + +def assets_from_string(text): + """Correctly split a string containing an asset pair. + + Splits the string into two assets with the separator being on of the + following: ``:``, ``/``, or ``-``. + """ + return re.split(r'[\-:/]', text) + + +def sanitize_permlink(permlink): + permlink = permlink.strip() + permlink = re.sub("_|\s|\.", "-", permlink) + permlink = re.sub("[^\w-]", "", permlink) + permlink = re.sub("[^a-zA-Z0-9-]", "", permlink) + permlink = permlink.lower() + return permlink + + +def derive_permlink(title, parent_permlink=None, parent_author=None): + permlink = "" + + if parent_permlink and parent_author: + permlink += "re-" + permlink += parent_author.replace("@", "") + permlink += "-" + permlink += parent_permlink + permlink += "-" + formatTime(timenow.time()) + "z" + elif parent_permlink: + permlink += "re-" + permlink += parent_permlink + permlink += "-" + formatTime(timenow.time()) + "z" + else: + permlink += title + + return sanitize_permlink(permlink) + + +def resolve_authorperm(identifier): + """Correctly split a string containing an authorperm. + + Splits the string into author and permlink with the + following separator: ``/``. + + Examples: + + .. code-block:: python + + >>> from dpaycli.utils import resolve_authorperm + >>> author, permlink = resolve_authorperm('https://dvideo.io/#!/v/pottlund/m5cqkd1a') + >>> author, permlink = resolve_authorperm("https://dsite.io/witness-category/@gtg/24lfrm-gtg-witness-log") + >>> author, permlink = resolve_authorperm("@gtg/24lfrm-gtg-witness-log") + >>> author, permlink = resolve_authorperm("https://dsocial.io/@gtg/24lfrm-gtg-witness-log") + + """ + # without any http(s) + match = re.match("@?([\w\-\.]*)/([\w\-]*)", identifier) + if hasattr(match, "group"): + return match.group(1), match.group(2) + # dtube url + match = re.match("([\w\-\.]+[^#?\s]+)/#!/v/?([\w\-\.]*)/([\w\-]*)", identifier) + if hasattr(match, "group"): + return match.group(2), match.group(3) + # url + match = re.match("([\w\-\.]+[^#?\s]+)/@?([\w\-\.]*)/([\w\-]*)", identifier) + if not hasattr(match, "group"): + raise ValueError("Invalid identifier") + return match.group(2), match.group(3) + + +def construct_authorperm(*args): + """ Create a post identifier from comment/post object or arguments. + Examples: + + .. code-block:: python + + >>> from dpaycli.utils import construct_authorperm + >>> print(construct_authorperm('username', 'permlink')) + @username/permlink + >>> print(construct_authorperm({'author': 'username', 'permlink': 'permlink'})) + @username/permlink + + """ + username_prefix = '@' + if len(args) == 1: + op = args[0] + author, permlink = op['author'], op['permlink'] + elif len(args) == 2: + author, permlink = args + else: + raise ValueError( + 'construct_identifier() received unparsable arguments') + + fields = dict(prefix=username_prefix, author=author, permlink=permlink) + return "{prefix}{author}/{permlink}".format(**fields) + + +def resolve_root_identifier(url): + m = re.match("/([^/]*)/@([^/]*)/([^#]*).*", url) + if not m: + return "", "" + else: + category = m.group(1) + author = m.group(2) + permlink = m.group(3) + return construct_authorperm(author, permlink), category + + +def resolve_authorpermvoter(identifier): + """Correctly split a string containing an authorpermvoter. + + Splits the string into author and permlink with the + following separator: ``/`` and ``|``. + """ + pos = identifier.find("|") + if pos < 0: + raise ValueError("Invalid identifier") + [author, permlink] = resolve_authorperm(identifier[:pos]) + return author, permlink, identifier[pos + 1:] + + +def construct_authorpermvoter(*args): + """ Create a vote identifier from vote object or arguments. + Examples: + + .. code-block:: python + + >>> from dpaycli.utils import construct_authorpermvoter + >>> print(construct_authorpermvoter('username', 'permlink', 'voter')) + @username/permlink|voter + >>> print(construct_authorpermvoter({'author': 'username', 'permlink': 'permlink', 'voter': 'voter'})) + @username/permlink|voter + + """ + username_prefix = '@' + if len(args) == 1: + op = args[0] + if "authorperm" in op: + authorperm, voter = op['authorperm'], op['voter'] + [author, permlink] = resolve_authorperm(authorperm) + else: + author, permlink, voter = op['author'], op['permlink'], op['voter'] + elif len(args) == 2: + authorperm, voter = args + [author, permlink] = resolve_authorperm(authorperm) + elif len(args) == 3: + author, permlink, voter = args + else: + raise ValueError( + 'construct_identifier() received unparsable arguments') + + fields = dict(prefix=username_prefix, author=author, permlink=permlink, voter=voter) + return "{prefix}{author}/{permlink}|{voter}".format(**fields) + + +def reputation_to_score(rep): + """Converts the account reputation value into the reputation score""" + if isinstance(rep, str): + rep = int(rep) + if rep == 0: + return 25. + score = max([math.log10(abs(rep)) - 9, 0]) + if rep < 0: + score *= -1 + score = (score * 9.) + 25. + return score + + +def remove_from_dict(obj, keys=list(), keep_keys=True): + """ Prune a class or dictionary of all but keys (keep_keys=True). + Prune a class or dictionary of specified keys.(keep_keys=False). + """ + if type(obj) == dict: + items = list(obj.items()) + elif isinstance(obj, dict): + items = list(obj.items()) + else: + items = list(obj.__dict__.items()) + if keep_keys: + return {k: v for k, v in items if k in keys} + else: + return {k: v for k, v in items if k not in keys} + + +def make_patch(a, b, n=3): + # _no_eol = '\n' + "\ No newline at end of file" + '\n' + _no_eol = '\n' + diffs = difflib.unified_diff(a.splitlines(True), b.splitlines(True), n=n) + try: + _, _ = next(diffs), next(diffs) + del _ + except StopIteration: + pass + return ''.join([d if d[-1] == '\n' else d + _no_eol for d in diffs]) + + +def findall_patch_hunks(body=None): + return RE_HUNK_HEADER.findall(body) diff --git a/dpaycli/version.py b/dpaycli/version.py new file mode 100755 index 0000000..f69b1b5 --- /dev/null +++ b/dpaycli/version.py @@ -0,0 +1,2 @@ +"""THIS FILE IS GENERATED FROM dpaycli SETUP.PY.""" +version = '0.02.0' diff --git a/dpaycli/vote.py b/dpaycli/vote.py new file mode 100755 index 0000000..9c9cff6 --- /dev/null +++ b/dpaycli/vote.py @@ -0,0 +1,409 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import json +import math +import pytz +import logging +from prettytable import PrettyTable +from datetime import datetime, date +from dpaycligraphenebase.py23 import integer_types, string_types, text_type +from .instance import shared_dpay_instance +from .account import Account +from .exceptions import VoteDoesNotExistsException +from .utils import resolve_authorperm, resolve_authorpermvoter, construct_authorpermvoter, construct_authorperm, formatTimeString, addTzInfo, reputation_to_score +from .blockchainobject import BlockchainObject +from .comment import Comment +from dpaycliapi.exceptions import UnkownKey + +log = logging.getLogger(__name__) + + +class Vote(BlockchainObject): + """ Read data about a Vote in the chain + + :param str authorperm: perm link to post/comment + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + + .. code-block:: python + + >>> from dpaycli.vote import Vote + >>> v = Vote("@dsocial/dpay-pressure-4-need-for-speed|gandalf") + + """ + type_id = 11 + + def __init__( + self, + voter, + authorperm=None, + full=False, + lazy=False, + dpay_instance=None + ): + self.full = full + self.lazy = lazy + self.dpay = dpay_instance or shared_dpay_instance() + if isinstance(voter, string_types) and authorperm is not None: + [author, permlink] = resolve_authorperm(authorperm) + self["voter"] = voter + self["author"] = author + self["permlink"] = permlink + authorpermvoter = construct_authorpermvoter(author, permlink, voter) + self["authorpermvoter"] = authorpermvoter + elif isinstance(voter, dict) and "author" in voter and "permlink" in voter and "voter" in voter: + authorpermvoter = voter + authorpermvoter["authorpermvoter"] = construct_authorpermvoter(voter["author"], voter["permlink"], voter["voter"]) + authorpermvoter = self._parse_json_data(authorpermvoter) + elif isinstance(voter, dict) and "authorperm" in voter and authorperm is not None: + [author, permlink] = resolve_authorperm(voter["authorperm"]) + authorpermvoter = voter + authorpermvoter["voter"] = authorperm + authorpermvoter["author"] = author + authorpermvoter["permlink"] = permlink + authorpermvoter["authorpermvoter"] = construct_authorpermvoter(author, permlink, authorperm) + authorpermvoter = self._parse_json_data(authorpermvoter) + elif isinstance(voter, dict) and "voter" in voter and authorperm is not None: + [author, permlink] = resolve_authorperm(authorperm) + authorpermvoter = voter + authorpermvoter["author"] = author + authorpermvoter["permlink"] = permlink + authorpermvoter["authorpermvoter"] = construct_authorpermvoter(author, permlink, voter["voter"]) + authorpermvoter = self._parse_json_data(authorpermvoter) + else: + authorpermvoter = voter + [author, permlink, voter] = resolve_authorpermvoter(authorpermvoter) + self["author"] = author + self["permlink"] = permlink + + super(Vote, self).__init__( + authorpermvoter, + id_item="authorpermvoter", + lazy=lazy, + full=full, + dpay_instance=dpay_instance + ) + + def refresh(self): + if self.identifier is None: + return + if not self.dpay.is_connected(): + return + [author, permlink, voter] = resolve_authorpermvoter(self.identifier) + try: + self.dpay.rpc.set_next_node_on_empty_reply(True) + if self.dpay.rpc.get_use_appbase(): + votes = self.dpay.rpc.get_active_votes({'author': author, 'permlink': permlink}, api="tags")['votes'] + else: + votes = self.dpay.rpc.get_active_votes(author, permlink, api="database_api") + except UnkownKey: + raise VoteDoesNotExistsException(self.identifier) + + vote = None + for x in votes: + if x["voter"] == voter: + vote = x + if not vote: + raise VoteDoesNotExistsException(self.identifier) + vote = self._parse_json_data(vote) + vote["authorpermvoter"] = construct_authorpermvoter(author, permlink, voter) + super(Vote, self).__init__(vote, id_item="authorpermvoter", lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + + def _parse_json_data(self, vote): + parse_int = [ + "rshares", "reputation", + ] + for p in parse_int: + if p in vote and isinstance(vote.get(p), string_types): + vote[p] = int(vote.get(p, "0")) + + if "time" in vote and isinstance(vote.get("time"), string_types) and vote.get("time") != '': + vote["time"] = formatTimeString(vote.get("time", "1970-01-01T00:00:00")) + elif "timestamp" in vote and isinstance(vote.get("timestamp"), string_types) and vote.get("timestamp") != '': + vote["time"] = formatTimeString(vote.get("timestamp", "1970-01-01T00:00:00")) + else: + vote["time"] = formatTimeString("1970-01-01T00:00:00") + return vote + + def json(self): + output = self.copy() + if "author" in output: + output.pop("author") + if "permlink" in output: + output.pop("permlink") + parse_times = [ + "time" + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + parse_int = [ + "rshares", "reputation", + ] + for p in parse_int: + if p in output and isinstance(output[p], integer_types): + output[p] = str(output[p]) + return json.loads(str(json.dumps(output))) + + @property + def voter(self): + return self["voter"] + + @property + def authorperm(self): + if "authorperm" in self: + return self["authorperm"] + elif "authorpermvoter" in self: + [author, permlink, voter] = resolve_authorpermvoter(self["authorpermvoter"]) + return construct_authorperm(author, permlink) + elif "author" in self and "permlink" in self: + return construct_authorperm(self["author"], self["permlink"]) + else: + return "" + + @property + def votee(self): + votee = '' + authorperm = self.get("authorperm", "") + authorpermvoter = self.get("authorpermvoter", "") + if authorperm != "": + votee = resolve_authorperm(authorperm)[0] + elif authorpermvoter != "": + votee = resolve_authorpermvoter(authorpermvoter)[0] + return votee + + @property + def weight(self): + return self["weight"] + + @property + def bbd(self): + return self.dpay.rshares_to_bbd(int(self.get("rshares", 0))) + + @property + def rshares(self): + return int(self.get("rshares", 0)) + + @property + def percent(self): + return self.get("percent", 0) + + @property + def reputation(self): + return self.get("reputation", 0) + + @property + def rep(self): + return reputation_to_score(int(self.reputation)) + + @property + def time(self): + return self["time"] + + +class VotesObject(list): + def get_sorted_list(self, sort_key="time", reverse=True): + utc = pytz.timezone('UTC') + + if sort_key == 'bbd': + sortedList = sorted(self, key=lambda self: self.rshares, reverse=reverse) + elif sort_key == 'time': + sortedList = sorted(self, key=lambda self: (utc.localize(datetime.utcnow()) - self.time).total_seconds(), reverse=reverse) + elif sort_key == 'votee': + sortedList = sorted(self, key=lambda self: self.votee, reverse=reverse) + elif sort_key in ['voter', 'rshares', 'percent', 'weight']: + sortedList = sorted(self, key=lambda self: self[sort_key], reverse=reverse) + else: + sortedList = self + return sortedList + + def printAsTable(self, voter=None, votee=None, start=None, stop=None, start_percent=None, stop_percent=None, sort_key="time", reverse=True, allow_refresh=True, return_str=False, **kwargs): + utc = pytz.timezone('UTC') + table_header = ["Voter", "Votee", "BBD", "Time", "Rshares", "Percent", "Weight"] + t = PrettyTable(table_header) + t.align = "l" + start = addTzInfo(start) + stop = addTzInfo(stop) + for vote in self.get_sorted_list(sort_key=sort_key, reverse=reverse): + if not allow_refresh: + vote.cached = True + + d_time = vote.time + if d_time != formatTimeString("1970-01-01T00:00:00"): + td = utc.localize(datetime.utcnow()) - d_time + timestr = str(td.days) + " days " + str(td.seconds // 3600) + ":" + str((td.seconds // 60) % 60) + else: + start = None + stop = None + timestr = '' + + percent = vote.get('percent', '') + if percent == '': + start_percent = None + stop_percent = None + if (start is None or d_time >= start) and (stop is None or d_time <= stop) and\ + (start_percent is None or percent >= start_percent) and (stop_percent is None or percent <= stop_percent) and\ + (voter is None or vote["voter"] == voter) and (votee is None or vote.votee == votee): + t.add_row([vote['voter'], + vote.votee, + str(round(vote.bbd, 2)).ljust(5) + "$", + timestr, + vote.get("rshares", ""), + str(vote.get('percent', '')), + str(vote['weight'])]) + + if return_str: + return t.get_string(**kwargs) + else: + print(t.get_string(**kwargs)) + + def get_list(self, var="voter", voter=None, votee=None, start=None, stop=None, start_percent=None, stop_percent=None, sort_key="time", reverse=True): + vote_list = [] + start = addTzInfo(start) + stop = addTzInfo(stop) + for vote in self.get_sorted_list(sort_key=sort_key, reverse=reverse): + d_time = vote.time + if d_time != formatTimeString("1970-01-01T00:00:00"): + start = None + stop = None + percent = vote.get('percent', '') + if percent == '': + start_percent = None + stop_percent = None + if (start is None or d_time >= start) and (stop is None or d_time <= stop) and\ + (start_percent is None or percent >= start_percent) and (stop_percent is None or percent <= stop_percent) and\ + (voter is None or vote["voter"] == voter) and (votee is None or vote.votee == votee): + v = '' + if var == "voter": + v = vote["voter"] + elif var == "votee": + v = vote.votee + elif var == "bbd": + v = vote.bbd + elif var == "time": + v = d_time + elif var == "rshares": + v = vote.get("rshares", 0) + elif var == "percent": + v = percent + elif var == "weight": + v = vote['weight'] + vote_list.append(v) + return vote_list + + def print_stats(self, return_str=False, **kwargs): + # utc = pytz.timezone('UTC') + table_header = ["voter", "votee", "bbd", "time", "rshares", "percent", "weight"] + t = PrettyTable(table_header) + t.align = "l" + + def __contains__(self, item): + if isinstance(item, Account): + name = item["name"] + authorperm = "" + elif isinstance(item, Comment): + authorperm = item.authorperm + name = "" + else: + name = item + authorperm = item + + return ( + any([name == x.voter for x in self]) or + any([name == x.votee for x in self]) or + any([authorperm == x.authorperm for x in self]) + ) + + def __str__(self): + return self.printAsTable(return_str=True) + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, str(self.identifier)) + + +class ActiveVotes(VotesObject): + """ Obtain a list of votes for a post + + :param str authorperm: authorperm link + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + """ + def __init__(self, authorperm, lazy=False, full=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + votes = None + if not self.dpay.is_connected(): + return None + self.dpay.rpc.set_next_node_on_empty_reply(False) + if isinstance(authorperm, Comment): + if 'active_votes' in authorperm and len(authorperm["active_votes"]) > 0: + votes = authorperm["active_votes"] + elif self.dpay.rpc.get_use_appbase(): + self.dpay.rpc.set_next_node_on_empty_reply(True) + votes = self.dpay.rpc.get_active_votes({'author': authorperm["author"], + 'permlink': authorperm["permlink"]}, + api="tags")['votes'] + else: + votes = self.dpay.rpc.get_active_votes(authorperm["author"], authorperm["permlink"]) + authorperm = authorperm["authorperm"] + elif isinstance(authorperm, string_types): + [author, permlink] = resolve_authorperm(authorperm) + if self.dpay.rpc.get_use_appbase(): + self.dpay.rpc.set_next_node_on_empty_reply(True) + votes = self.dpay.rpc.get_active_votes({'author': author, + 'permlink': permlink}, + api="tags")['votes'] + else: + votes = self.dpay.rpc.get_active_votes(author, permlink) + elif isinstance(authorperm, list): + votes = authorperm + authorperm = None + elif isinstance(authorperm, dict): + votes = authorperm["active_votes"] + authorperm = authorperm["authorperm"] + if votes is None: + return + self.identifier = authorperm + super(ActiveVotes, self).__init__( + [ + Vote(x, authorperm=authorperm, lazy=lazy, full=full, dpay_instance=self.dpay) + for x in votes + ] + ) + + +class AccountVotes(VotesObject): + """ Obtain a list of votes for an account + Lists the last 100+ votes on the given account. + + :param str account: Account name + :param dpay dpay_instance: DPay() instance to use when accesing a RPC + """ + def __init__(self, account, start=None, stop=None, lazy=False, full=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + start = addTzInfo(start) + stop = addTzInfo(stop) + account = Account(account, dpay_instance=self.dpay) + votes = account.get_account_votes() + self.identifier = account["name"] + vote_list = [] + if votes is None: + votes = [] + for x in votes: + time = x.get("time", "") + if time != "" and isinstance(time, string_types): + d_time = formatTimeString(time) + elif isinstance(time, datetime): + d_time = time + else: + d_time = addTzInfo(datetime(1970, 1, 1, 0, 0, 0)) + if (start is None or d_time >= start) and (stop is None or d_time <= stop): + vote_list.append(Vote(x, authorperm=account["name"], lazy=lazy, full=full, dpay_instance=self.dpay)) + + super(AccountVotes, self).__init__(vote_list) diff --git a/dpaycli/wallet.py b/dpaycli/wallet.py new file mode 100755 index 0000000..69feb2b --- /dev/null +++ b/dpaycli/wallet.py @@ -0,0 +1,688 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str, bytes +from builtins import object +import logging +import os +import hashlib +from dpaycligraphenebase import bip38 +from dpaycligraphenebase.account import PrivateKey +from dpaycli.instance import shared_dpay_instance +from .account import Account +from .aes import AESCipher +from .exceptions import ( + MissingKeyError, + InvalidWifError, + WalletExists, + WalletLocked, + WrongMasterPasswordException, + NoWalletException, + OfflineHasNoRPCException, + AccountDoesNotExistsException, +) +from dpaycliapi.exceptions import NoAccessApi +from dpaycligraphenebase.py23 import py23_bytes +from .storage import configStorage as config +try: + import keyring + if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): + KEYRING_AVAILABLE = True + else: + KEYRING_AVAILABLE = False +except ImportError: + KEYRING_AVAILABLE = False + +log = logging.getLogger(__name__) + + +class Wallet(object): + """ The wallet is meant to maintain access to private keys for + your accounts. It either uses manually provided private keys + or uses a SQLite database managed by storage.py. + + :param DPayNodeRPC rpc: RPC connection to a DPay node + :param array,dict,string keys: Predefine the wif keys to shortcut the + wallet database + + Three wallet operation modes are possible: + + * **Wallet Database**: Here, dpaycli loads the keys from the + locally stored wallet SQLite database (see ``storage.py``). + To use this mode, simply call ``DPay()`` without the + ``keys`` parameter + * **Providing Keys**: Here, you can provide the keys for + your accounts manually. All you need to do is add the wif + keys for the accounts you want to use as a simple array + using the ``keys`` parameter to ``DPay()``. + * **Force keys**: This more is for advanced users and + requires that you know what you are doing. Here, the + ``keys`` parameter is a dictionary that overwrite the + ``active``, ``owner``, ``posting`` or ``memo`` keys for + any account. This mode is only used for *foreign* + signatures! + + A new wallet can be created by using: + + .. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.wipe(True) + dpay.wallet.create("supersecret-passphrase") + + This will raise an exception if you already have a wallet installed. + + + The wallet can be unlocked for signing using + + .. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("supersecret-passphrase") + + A private key can be added by using the + :func:`dpay.wallet.Wallet.addPrivateKey` method that is available + **after** unlocking the wallet with the correct passphrase: + + .. code-block:: python + + from dpaycli import DPay + dpay = DPay() + dpay.wallet.unlock("supersecret-passphrase") + dpay.wallet.addPrivateKey("5xxxxxxxxxxxxxxxxxxxx") + + .. note:: The private key has to be either in hexadecimal or in wallet + import format (wif) (starting with a ``5``). + + """ + masterpassword = None + + # Keys from database + configStorage = None + MasterPassword = None + keyStorage = None + tokenStorage = None + + # Manually provided keys + keys = {} # struct with pubkey as key and wif as value + token = {} + keyMap = {} # type:wif pairs to force certain keys + + def __init__(self, dpay_instance=None, *args, **kwargs): + self.dpay = dpay_instance or shared_dpay_instance() + + # Compatibility after name change from wif->keys + if "wif" in kwargs and "keys" not in kwargs: + kwargs["keys"] = kwargs["wif"] + master_password_set = False + if "keys" in kwargs: + self.setKeys(kwargs["keys"]) + else: + """ If no keys are provided manually we load the SQLite + keyStorage + """ + from .storage import (keyStorage, + MasterPassword) + self.MasterPassword = MasterPassword + master_password_set = True + self.keyStorage = keyStorage + + if "token" in kwargs: + self.setToken(kwargs["token"]) + else: + """ If no keys are provided manually we load the SQLite + keyStorage + """ + from .storage import tokenStorage + if not master_password_set: + from .storage import MasterPassword + self.MasterPassword = MasterPassword + self.tokenStorage = tokenStorage + + @property + def prefix(self): + if self.dpay.is_connected(): + prefix = self.dpay.prefix + else: + # If not connected, load prefix from config + prefix = config["prefix"] + return prefix or "DWB" # default prefix is DWB + + @property + def rpc(self): + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + return self.dpay.rpc + + def setKeys(self, loadkeys): + """ This method is strictly only for in memory keys that are + passed to Wallet/DPay with the ``keys`` argument + """ + log.debug( + "Force setting of private keys. Not using the wallet database!") + self.clear_local_keys() + if isinstance(loadkeys, dict): + Wallet.keyMap = loadkeys + loadkeys = list(loadkeys.values()) + elif not isinstance(loadkeys, list): + loadkeys = [loadkeys] + + for wif in loadkeys: + pub = self._get_pub_from_wif(wif) + Wallet.keys[pub] = str(wif) + + def setToken(self, loadtoken): + """ This method is strictly only for in memory token that are + passed to Wallet/DPay with the ``token`` argument + """ + log.debug( + "Force setting of private token. Not using the wallet database!") + self.clear_local_token() + if isinstance(loadtoken, dict): + Wallet.token = loadtoken + else: + raise ValueError("token must be a dict variable!") + + def unlock(self, pwd=None): + """ Unlock the wallet database + """ + if not self.created(): + raise NoWalletException + + if not pwd: + self.tryUnlockFromEnv() + else: + if (self.masterpassword is None and + config[self.MasterPassword.config_key]): + self.masterpwd = self.MasterPassword(pwd) + self.masterpassword = self.masterpwd.decrypted_master + + def tryUnlockFromEnv(self): + """ Try to fetch the unlock password from UNLOCK environment variable and keyring when no password is given. + """ + password_storage = self.dpay.config["password_storage"] + if password_storage == "environment" and "UNLOCK" in os.environ: + log.debug("Trying to use environmental variable to unlock wallet") + pwd = os.environ.get("UNLOCK") + self.unlock(pwd) + elif password_storage == "keyring" and KEYRING_AVAILABLE: + log.debug("Trying to use keyring to unlock wallet") + pwd = keyring.get_password("dpaycli", "wallet") + self.unlock(pwd) + else: + raise WrongMasterPasswordException + + def lock(self): + """ Lock the wallet database + """ + self.masterpassword = None + + def unlocked(self): + """ Is the wallet database unlocked? + """ + return not self.locked() + + def locked(self): + """ Is the wallet database locked? + """ + if Wallet.keys: # Keys have been manually provided! + return False + try: + self.tryUnlockFromEnv() + except WrongMasterPasswordException: + pass + return not bool(self.masterpassword) + + def changePassphrase(self, new_pwd): + """ Change the passphrase for the wallet database + """ + if self.locked(): + raise AssertionError() + self.masterpwd.changePassword(new_pwd) + + def created(self): + """ Do we have a wallet database already? + """ + if len(self.getPublicKeys()): + # Already keys installed + return True + elif self.MasterPassword.config_key in config: + # no keys but a master password + return True + else: + return False + + def create(self, pwd): + """ Alias for newWallet() + """ + self.newWallet(pwd) + + def newWallet(self, pwd): + """ Create a new wallet database + """ + if self.created(): + raise WalletExists("You already have created a wallet!") + self.masterpwd = self.MasterPassword(pwd) + self.masterpassword = self.masterpwd.decrypted_master + self.masterpwd.saveEncrytpedMaster() + + def wipe(self, sure=False): + """ Purge all data in wallet database + """ + if not sure: + log.error( + "You need to confirm that you are sure " + "and understand the implications of " + "wiping your wallet!" + ) + return + else: + from .storage import ( + keyStorage, + tokenStorage, + MasterPassword + ) + MasterPassword.wipe(sure) + keyStorage.wipe(sure) + tokenStorage.wipe(sure) + self.clear_local_keys() + + def clear_local_keys(self): + """Clear all manually provided keys""" + Wallet.keys = {} + Wallet.keyMap = {} + + def clear_local_token(self): + """Clear all manually provided token""" + Wallet.token = {} + + def encrypt_wif(self, wif): + """ Encrypt a wif key + """ + if self.locked(): + raise AssertionError() + return format( + bip38.encrypt(PrivateKey(wif, prefix=self.prefix), self.masterpassword), "encwif") + + def decrypt_wif(self, encwif): + """ decrypt a wif key + """ + try: + # Try to decode as wif + PrivateKey(encwif, prefix=self.prefix) + return encwif + except (ValueError, AssertionError): + pass + if self.locked(): + raise AssertionError() + return format(bip38.decrypt(encwif, self.masterpassword), "wif") + + def deriveChecksum(self, s): + """ Derive the checksum + """ + checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest() + return checksum[:4] + + def encrypt_token(self, token): + """ Encrypt a token key + """ + if self.locked(): + raise AssertionError() + aes = AESCipher(self.masterpassword) + return "{}${}".format(self.deriveChecksum(token), aes.encrypt(token)) + + def decrypt_token(self, enctoken): + """ decrypt a wif key + """ + if self.locked(): + raise AssertionError() + aes = AESCipher(self.masterpassword) + checksum, encrypted_token = enctoken.split("$") + try: + decrypted_token = aes.decrypt(encrypted_token) + except: + raise WrongMasterPasswordException + if checksum != self.deriveChecksum(decrypted_token): + raise WrongMasterPasswordException + return decrypted_token + + def _get_pub_from_wif(self, wif): + """ Get the pubkey as string, from the wif key as string + """ + # it could be either graphenebase or dpay so we can't check + # the type directly + if isinstance(wif, PrivateKey): + wif = str(wif) + try: + return format(PrivateKey(wif).pubkey, self.prefix) + except: + raise InvalidWifError( + "Invalid Private Key Format. Please use WIF!") + + def addToken(self, name, token): + if self.tokenStorage: + if not self.created(): + raise NoWalletException + self.tokenStorage.add(name, self.encrypt_token(token)) + + def getTokenForAccountName(self, name): + """ Obtain the private token for a given public name + + :param str name: Public name + """ + if(Wallet.token): + if name in Wallet.token: + return Wallet.token[name] + else: + raise MissingKeyError("No private token for {} found".format(name)) + else: + # Test if wallet exists + if not self.created(): + raise NoWalletException + + if not self.unlocked(): + raise WalletLocked + + enctoken = self.tokenStorage.getTokenForPublicName(name) + if not enctoken: + raise MissingKeyError("No private token for {} found".format(name)) + return self.decrypt_token(enctoken) + + def removeTokenFromPublicName(self, name): + """ Remove a token from the wallet database + + :param str name: token to be removed + """ + if self.tokenStorage: + # Test if wallet exists + if not self.created(): + raise NoWalletException + self.tokenStorage.delete(name) + + def addPrivateKey(self, wif): + """Add a private key to the wallet database + + :param str wif: Private key + """ + pub = self._get_pub_from_wif(wif) + if isinstance(wif, PrivateKey): + wif = str(wif) + if self.keyStorage: + # Test if wallet exists + if not self.created(): + raise NoWalletException + self.keyStorage.add(self.encrypt_wif(wif), pub) + + def getPrivateKeyForPublicKey(self, pub): + """ Obtain the private key for a given public key + + :param str pub: Public Key + """ + if(Wallet.keys): + if pub in Wallet.keys: + return Wallet.keys[pub] + else: + raise MissingKeyError("No private key for {} found".format(pub)) + else: + # Test if wallet exists + if not self.created(): + raise NoWalletException + + if not self.unlocked(): + raise WalletLocked + + encwif = self.keyStorage.getPrivateKeyForPublicKey(pub) + if not encwif: + raise MissingKeyError("No private key for {} found".format(pub)) + return self.decrypt_wif(encwif) + + def removePrivateKeyFromPublicKey(self, pub): + """ Remove a key from the wallet database + + :param str pub: Public key + """ + if self.keyStorage: + # Test if wallet exists + if not self.created(): + raise NoWalletException + self.keyStorage.delete(pub) + + def removeAccount(self, account): + """ Remove all keys associated with a given account + + :param str account: name of account to be removed + """ + accounts = self.getAccounts() + for a in accounts: + if a["name"] == account: + self.removePrivateKeyFromPublicKey(a["pubkey"]) + + def getKeyForAccount(self, name, key_type): + """ Obtain `key_type` Private Key for an account from the wallet database + + :param str name: Account name + :param str key_type: key type, has to be one of "owner", "active", + "posting" or "memo" + """ + if key_type not in ["owner", "active", "posting", "memo"]: + raise AssertionError("Wrong key type") + if key_type in Wallet.keyMap: + return Wallet.keyMap.get(key_type) + else: + if self.rpc.get_use_appbase(): + account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] + else: + account = self.rpc.get_account(name) + if not account: + return + if len(account) == 0: + return + if key_type == "memo": + key = self.getPrivateKeyForPublicKey( + account[0]["memo_key"]) + if key: + return key + else: + key = None + for authority in account[0][key_type]["key_auths"]: + try: + key = self.getPrivateKeyForPublicKey(authority[0]) + if key: + return key + except MissingKeyError: + key = None + if key is None: + raise MissingKeyError("No private key for {} found".format(name)) + return + + def getKeysForAccount(self, name, key_type): + """ Obtain a List of `key_type` Private Keys for an account from the wallet database + + :param str name: Account name + :param str key_type: key type, has to be one of "owner", "active", + "posting" or "memo" + """ + if key_type not in ["owner", "active", "posting", "memo"]: + raise AssertionError("Wrong key type") + if key_type in Wallet.keyMap: + return Wallet.keyMap.get(key_type) + else: + if self.rpc.get_use_appbase(): + account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] + else: + account = self.rpc.get_account(name) + if not account: + return + if len(account) == 0: + return + if key_type == "memo": + key = self.getPrivateKeyForPublicKey( + account[0]["memo_key"]) + if key: + return [key] + else: + keys = [] + key = None + for authority in account[0][key_type]["key_auths"]: + try: + key = self.getPrivateKeyForPublicKey(authority[0]) + if key: + keys.append(key) + except MissingKeyError: + key = None + if key is None: + raise MissingKeyError("No private key for {} found".format(name)) + return keys + return + + def getOwnerKeyForAccount(self, name): + """ Obtain owner Private Key for an account from the wallet database + """ + return self.getKeyForAccount(name, "owner") + + def getMemoKeyForAccount(self, name): + """ Obtain owner Memo Key for an account from the wallet database + """ + return self.getKeyForAccount(name, "memo") + + def getActiveKeyForAccount(self, name): + """ Obtain owner Active Key for an account from the wallet database + """ + return self.getKeyForAccount(name, "active") + + def getPostingKeyForAccount(self, name): + """ Obtain owner Posting Key for an account from the wallet database + """ + return self.getKeyForAccount(name, "posting") + + def getOwnerKeysForAccount(self, name): + """ Obtain list of all owner Private Keys for an account from the wallet database + """ + return self.getKeysForAccount(name, "owner") + + def getActiveKeysForAccount(self, name): + """ Obtain list of all owner Active Keys for an account from the wallet database + """ + return self.getKeysForAccount(name, "active") + + def getPostingKeysForAccount(self, name): + """ Obtain list of all owner Posting Keys for an account from the wallet database + """ + return self.getKeysForAccount(name, "posting") + + def getAccountFromPrivateKey(self, wif): + """ Obtain account name from private key + """ + pub = self._get_pub_from_wif(wif) + return self.getAccountFromPublicKey(pub) + + def getAccountsFromPublicKey(self, pub): + """ Obtain all account names associated with a public key + + :param str pub: Public key + """ + if not self.dpay.is_connected(): + raise OfflineHasNoRPCException("No RPC available in offline mode!") + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + names = self.dpay.rpc.get_key_references({'keys': [pub]}, api="account_by_key")["accounts"] + else: + names = self.dpay.rpc.get_key_references([pub], api="account_by_key") + for name in names: + for i in name: + yield i + + def getAccountFromPublicKey(self, pub): + """ Obtain the first account name from public key + + :param str pub: Public key + + Note: this returns only the first account with the given key. To + get all accounts associated with a given public key, use + ``getAccountsFromPublicKey``. + """ + names = list(self.getAccountsFromPublicKey(pub)) + if not names: + return None + else: + return names[0] + + def getAllAccounts(self, pub): + """ Get the account data for a public key (all accounts found for this + public key) + + :param str pub: Public key + """ + for name in self.getAccountsFromPublicKey(pub): + try: + account = Account(name, dpay_instance=self.dpay) + except AccountDoesNotExistsException: + continue + yield {"name": account["name"], + "account": account, + "type": self.getKeyType(account, pub), + "pubkey": pub} + + def getAccount(self, pub): + """ Get the account data for a public key (first account found for this + public key) + + :param str pub: Public key + """ + name = self.getAccountFromPublicKey(pub) + if not name: + return {"name": None, "type": None, "pubkey": pub} + else: + try: + account = Account(name, dpay_instance=self.dpay) + except: + return + return {"name": account["name"], + "account": account, + "type": self.getKeyType(account, pub), + "pubkey": pub} + + def getKeyType(self, account, pub): + """ Get key type + + :param dpaycli.account.Account/dict account: Account data + :param str pub: Public key + + """ + for authority in ["owner", "active", "posting"]: + for key in account[authority]["key_auths"]: + if pub == key[0]: + return authority + if pub == account["memo_key"]: + return "memo" + return None + + def getAccounts(self): + """ Return all accounts installed in the wallet database + """ + pubkeys = self.getPublicKeys() + accounts = [] + for pubkey in pubkeys: + # Filter those keys not for our network + if pubkey[:len(self.prefix)] == self.prefix: + accounts.extend(self.getAllAccounts(pubkey)) + return accounts + + def getPublicKeys(self): + """ Return all installed public keys + """ + if self.keyStorage: + return self.keyStorage.getPublicKeys() + else: + return list(Wallet.keys.keys()) + + def getPublicNames(self): + """ Return all installed public token + """ + if self.tokenStorage: + return self.tokenStorage.getPublicNames() + else: + return list(Wallet.token.keys()) diff --git a/dpaycli/witness.py b/dpaycli/witness.py new file mode 100755 index 0000000..d1aea67 --- /dev/null +++ b/dpaycli/witness.py @@ -0,0 +1,431 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import json +from dpaycli.instance import shared_dpay_instance +from dpaycligraphenebase.py23 import bytes_types, integer_types, string_types, text_type +from .account import Account +from .amount import Amount +from .exceptions import WitnessDoesNotExistsException +from .blockchainobject import BlockchainObject +from .utils import formatTimeString +from datetime import datetime, timedelta, date +from dpayclibase import transactions, operations +from dpaycligraphenebase.account import PrivateKey, PublicKey +import pytz +from prettytable import PrettyTable + + +class Witness(BlockchainObject): + """ Read data about a witness in the chain + + :param str account_name: Name of the witness + :param dpay dpay_instance: DPay() instance to use when + accesing a RPC + + .. code-block:: python + + >>> from dpaycli.witness import Witness + >>> Witness("gtg") + + + """ + type_id = 3 + + def __init__( + self, + owner, + full=False, + lazy=False, + dpay_instance=None + ): + self.full = full + self.lazy = lazy + self.dpay = dpay_instance or shared_dpay_instance() + if isinstance(owner, dict): + owner = self._parse_json_data(owner) + super(Witness, self).__init__( + owner, + lazy=lazy, + full=full, + id_item="owner", + dpay_instance=dpay_instance + ) + + def refresh(self): + if not self.identifier: + return + if not self.dpay.is_connected(): + return + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + witness = self.dpay.rpc.find_witnesses({'owners': [self.identifier]}, api="database")['witnesses'] + if len(witness) > 0: + witness = witness[0] + else: + witness = self.dpay.rpc.get_witness_by_account(self.identifier) + if not witness: + raise WitnessDoesNotExistsException(self.identifier) + witness = self._parse_json_data(witness) + super(Witness, self).__init__(witness, id_item="owner", lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + + def _parse_json_data(self, witness): + parse_times = [ + "created", "last_bbd_exchange_update", "hardfork_time_vote", + ] + for p in parse_times: + if p in witness and isinstance(witness.get(p), string_types): + witness[p] = formatTimeString(witness.get(p, "1970-01-01T00:00:00")) + parse_int = [ + "votes", "virtual_last_update", "virtual_position", "virtual_scheduled_time", + ] + for p in parse_int: + if p in witness and isinstance(witness.get(p), string_types): + witness[p] = int(witness.get(p, "0")) + return witness + + def json(self): + output = self.copy() + parse_times = [ + "created", "last_bbd_exchange_update", "hardfork_time_vote", + ] + for p in parse_times: + if p in output: + p_date = output.get(p, datetime(1970, 1, 1, 0, 0)) + if isinstance(p_date, (datetime, date)): + output[p] = formatTimeString(p_date) + else: + output[p] = p_date + parse_int = [ + "votes", "virtual_last_update", "virtual_position", "virtual_scheduled_time", + ] + for p in parse_int: + if p in output and isinstance(output[p], integer_types): + output[p] = str(output[p]) + return json.loads(str(json.dumps(output))) + + @property + def account(self): + return Account(self["owner"], dpay_instance=self.dpay) + + @property + def is_active(self): + return len(self['signing_key']) > 3 and self['signing_key'][3:] != '1111111111111111111111111111111114T1Anm' + + def feed_publish(self, + base, + quote=None, + account=None): + """ Publish a feed price as a witness. + :param float base: USD Price of BEX in BBD (implied price) + :param float quote: (optional) Quote Price. Should be 1.000 (default), unless + we are adjusting the feed to support the peg. + :param str account: (optional) the source account for the transfer + if not self["owner"] + """ + quote = quote if quote is not None else "1.000 %s" % (self.dpay.symbol) + if not account: + account = self["owner"] + if not account: + raise ValueError("You need to provide an account") + + account = Account(account, dpay_instance=self.dpay) + if isinstance(base, Amount): + base = Amount(base, dpay_instance=self.dpay) + elif isinstance(base, string_types): + base = Amount(base, dpay_instance=self.dpay) + else: + base = Amount(base, self.dpay.bbd_symbol, dpay_instance=self.dpay) + + if isinstance(quote, Amount): + quote = Amount(quote, dpay_instance=self.dpay) + elif isinstance(quote, string_types): + quote = Amount(quote, dpay_instance=self.dpay) + else: + quote = Amount(quote, self.dpay.dpay_symbol, dpay_instance=self.dpay) + + if not base.symbol == self.dpay.bbd_symbol: + raise AssertionError() + if not quote.symbol == self.dpay.dpay_symbol: + raise AssertionError() + + op = operations.Feed_publish( + **{ + "publisher": account["name"], + "exchange_rate": { + "base": base, + "quote": quote, + }, + "prefix": self.dpay.prefix, + }) + return self.dpay.finalizeOp(op, account, "active") + + def update(self, signing_key, url, props, account=None): + """ Update witness + + :param pubkey signing_key: Signing key + :param str url: URL + :param dict props: Properties + :param str account: (optional) witness account name + + Properties::: + + { + "account_creation_fee": x, + "maximum_block_size": x, + "bbd_interest_rate": x, + } + + """ + if not account: + account = self["owner"] + return self.dpay.witness_update(signing_key, url, props, account=account) + + +class WitnessesObject(list): + def printAsTable(self, sort_key="votes", reverse=True, return_str=False, **kwargs): + utc = pytz.timezone('UTC') + table_header = ["Name", "Votes [PV]", "Disabled", "Missed", "Feed base", "Feed quote", "Feed update", "Fee", "Size", "Interest", "Version"] + t = PrettyTable(table_header) + t.align = "l" + if sort_key == 'base': + sortedList = sorted(self, key=lambda self: self['bbd_exchange_rate']['base'], reverse=reverse) + elif sort_key == 'quote': + sortedList = sorted(self, key=lambda self: self['bbd_exchange_rate']['quote'], reverse=reverse) + elif sort_key == 'last_bbd_exchange_update': + sortedList = sorted(self, key=lambda self: (utc.localize(datetime.utcnow()) - self['last_bbd_exchange_update']).total_seconds(), reverse=reverse) + elif sort_key == 'account_creation_fee': + sortedList = sorted(self, key=lambda self: self['props']['account_creation_fee'], reverse=reverse) + elif sort_key == 'bbd_interest_rate': + sortedList = sorted(self, key=lambda self: self['props']['bbd_interest_rate'], reverse=reverse) + elif sort_key == 'maximum_block_size': + sortedList = sorted(self, key=lambda self: self['props']['maximum_block_size'], reverse=reverse) + elif sort_key == 'votes': + sortedList = sorted(self, key=lambda self: int(self[sort_key]), reverse=reverse) + else: + sortedList = sorted(self, key=lambda self: self[sort_key], reverse=reverse) + for witness in sortedList: + td = utc.localize(datetime.utcnow()) - witness['last_bbd_exchange_update'] + disabled = "" + if not witness.is_active: + disabled = "yes" + t.add_row([witness['owner'], + str(round(int(witness['votes']) / 1e15, 2)), + disabled, + str(witness['total_missed']), + str(Amount(witness['bbd_exchange_rate']['base'], dpay_instance=self.dpay)), + str(Amount(witness['bbd_exchange_rate']['quote'], dpay_instance=self.dpay)), + str(td.days) + " days " + str(td.seconds // 3600) + ":" + str((td.seconds // 60) % 60), + str(witness['props']['account_creation_fee']), + str(witness['props']['maximum_block_size']), + str(witness['props']['bbd_interest_rate'] / 100) + " %", + witness['running_version']]) + if return_str: + return t.get_string(**kwargs) + else: + print(t.get_string(**kwargs)) + + def get_votes_sum(self): + vote_sum = 0 + for witness in self: + vote_sum += int(witness['votes']) + return vote_sum + + def __contains__(self, item): + from .account import Account + if isinstance(item, Account): + name = item["name"] + elif self.dpay: + account = Account(item, dpay_instance=self.dpay) + name = account["name"] + + return ( + any([name == x["owner"] for x in self]) + ) + + def __str__(self): + return self.printAsTable(return_str=True) + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, str(self.identifier)) + + +class Witnesses(WitnessesObject): + """ Obtain a list of **active** witnesses and the current schedule + + :param dpay dpay_instance: DPay() instance to use when + accesing a RPC + + .. code-block:: python + + >>> from dpaycli.witness import Witnesses + >>> Witnesses() + + + """ + def __init__(self, lazy=False, full=True, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.lazy = lazy + self.full = full + self.refresh() + + def refresh(self): + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + self.active_witnessess = self.dpay.rpc.get_active_witnesses(api="database")['witnesses'] + self.schedule = self.dpay.rpc.get_witness_schedule(api="database") + self.witness_count = self.dpay.rpc.get_witness_count(api="condenser") + else: + self.active_witnessess = self.dpay.rpc.get_active_witnesses() + self.schedule = self.dpay.rpc.get_witness_schedule() + self.witness_count = self.dpay.rpc.get_witness_count() + self.current_witness = self.dpay.get_dynamic_global_properties(use_stored_data=False)["current_witness"] + self.identifier = "" + super(Witnesses, self).__init__( + [ + Witness(x, lazy=self.lazy, full=self.full, dpay_instance=self.dpay) + for x in self.active_witnessess + ] + ) + + +class WitnessesVotedByAccount(WitnessesObject): + """ Obtain a list of witnesses which have been voted by an account + + :param str account: Account name + :param dpay dpay_instance: DPay() instance to use when + accesing a RPC + + .. code-block:: python + + >>> from dpaycli.witness import WitnessesVotedByAccount + >>> WitnessesVotedByAccount("gtg") + + + """ + def __init__(self, account, lazy=False, full=True, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.account = Account(account, full=True, dpay_instance=self.dpay) + account_name = self.account["name"] + self.identifier = account_name + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + if "witnesses_voted_for" not in self.account: + return + limit = self.account["witnesses_voted_for"] + witnessess_dict = self.dpay.rpc.list_witness_votes({'start': [account_name], 'limit': limit, 'order': 'by_account_witness'}, api="database")['votes'] + witnessess = [] + for w in witnessess_dict: + witnessess.append(w["witness"]) + else: + if "witness_votes" not in self.account: + return + witnessess = self.account["witness_votes"] + + super(WitnessesVotedByAccount, self).__init__( + [ + Witness(x, lazy=lazy, full=full, dpay_instance=self.dpay) + for x in witnessess + ] + ) + + +class WitnessesRankedByVote(WitnessesObject): + """ Obtain a list of witnesses ranked by Vote + + :param str from_account: Witness name from which the lists starts (default = "") + :param int limit: Limits the number of shown witnesses (default = 100) + :param dpay dpay_instance: DPay() instance to use when + accesing a RPC + + .. code-block:: python + + >>> from dpaycli.witness import WitnessesRankedByVote + >>> WitnessesRankedByVote(limit=100) + + + """ + def __init__(self, from_account="", limit=100, lazy=False, full=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + witnessList = [] + last_limit = limit + self.identifier = "" + use_condenser = True + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase() and not use_condenser: + query_limit = 1000 + else: + query_limit = 100 + if self.dpay.rpc.get_use_appbase() and not use_condenser and from_account == "": + last_account = None + elif self.dpay.rpc.get_use_appbase() and not use_condenser: + last_account = Witness(from_account, dpay_instance=self.dpay)["votes"] + else: + last_account = from_account + if limit > query_limit: + while last_limit > query_limit: + tmpList = WitnessesRankedByVote(last_account, query_limit) + if (last_limit < limit): + witnessList.extend(tmpList[1:]) + last_limit -= query_limit - 1 + else: + witnessList.extend(tmpList) + last_limit -= query_limit + if self.dpay.rpc.get_use_appbase(): + last_account = witnessList[-1]["votes"] + else: + last_account = witnessList[-1]["owner"] + if (last_limit < limit): + last_limit += 1 + if self.dpay.rpc.get_use_appbase() and not use_condenser: + witnessess = self.dpay.rpc.list_witnesses({'start': [last_account], 'limit': last_limit, 'order': 'by_vote_name'}, api="database")['witnesses'] + elif self.dpay.rpc.get_use_appbase() and use_condenser: + witnessess = self.dpay.rpc.get_witnesses_by_vote(last_account, last_limit, api="condenser") + else: + witnessess = self.dpay.rpc.get_witnesses_by_vote(last_account, last_limit) + # self.witness_count = len(self.voted_witnessess) + if (last_limit < limit): + witnessess = witnessess[1:] + if len(witnessess) > 0: + for x in witnessess: + witnessList.append(Witness(x, lazy=lazy, full=full, dpay_instance=self.dpay)) + if len(witnessList) == 0: + return + super(WitnessesRankedByVote, self).__init__(witnessList) + + +class ListWitnesses(WitnessesObject): + """ List witnesses ranked by name + + :param str from_account: Witness name from which the lists starts (default = "") + :param int limit: Limits the number of shown witnesses (default = 100) + :param dpay dpay_instance: DPay() instance to use when + accesing a RPC + + .. code-block:: python + + >>> from dpaycli.witness import ListWitnesses + >>> ListWitnesses(from_account="gtg", limit=100) + + + """ + def __init__(self, from_account="", limit=100, lazy=False, full=False, dpay_instance=None): + self.dpay = dpay_instance or shared_dpay_instance() + self.identifier = from_account + self.dpay.rpc.set_next_node_on_empty_reply(False) + if self.dpay.rpc.get_use_appbase(): + witnessess = self.dpay.rpc.list_witnesses({'start': from_account, 'limit': limit, 'order': 'by_name'}, api="database")['witnesses'] + else: + witnessess = self.dpay.rpc.lookup_witness_accounts(from_account, limit) + if len(witnessess) == 0: + return + super(ListWitnesses, self).__init__( + [ + Witness(x, lazy=lazy, full=full, dpay_instance=self.dpay) + for x in witnessess + ] + ) diff --git a/dpaycliapi/__init__.py b/dpaycliapi/__init__.py new file mode 100755 index 0000000..7dc7469 --- /dev/null +++ b/dpaycliapi/__init__.py @@ -0,0 +1,10 @@ +""" dpaycliapi.""" +from .version import version as __version__ +__all__ = [ + "dpaynoderpc", + "exceptions", + "websocket", + "rpcutils", + "graphenerpc", + "node", +] diff --git a/dpaycliapi/dpaynoderpc.py b/dpaycliapi/dpaynoderpc.py new file mode 100755 index 0000000..1bf1714 --- /dev/null +++ b/dpaycliapi/dpaynoderpc.py @@ -0,0 +1,187 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +import re +import sys +from .graphenerpc import GrapheneRPC +from . import exceptions +import logging +log = logging.getLogger(__name__) + + +class DPayNodeRPC(GrapheneRPC): + """ This class allows to call API methods exposed by the witness node via + websockets / rpc-json. + + :param str urls: Either a single Websocket/Http URL, or a list of URLs + :param str user: Username for Authentication + :param str password: Password for Authentication + :param int num_retries: Try x times to num_retries to a node on disconnect, -1 for indefinitely + :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5) + :param int timeout: Timeout setting for https nodes (default is 60) + :param bool use_condenser: Use the old condenser_api rpc protocol on nodes with version + 0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower. + + """ + + def __init__(self, *args, **kwargs): + """ Init DPayNodeRPC + + :param str urls: Either a single Websocket/Http URL, or a list of URLs + :param str user: Username for Authentication + :param str password: Password for Authentication + :param int num_retries: Try x times to num_retries to a node on disconnect, -1 for indefinitely + :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5) + :param int timeout: Timeout setting for https nodes (default is 60) + + """ + super(DPayNodeRPC, self).__init__(*args, **kwargs) + self.next_node_on_empty_reply = False + + def set_next_node_on_empty_reply(self, next_node_on_empty_reply=True): + """Switch to next node on empty reply for the next rpc call""" + self.next_node_on_empty_reply = next_node_on_empty_reply + + def rpcexec(self, payload): + """ Execute a call by sending the payload. + It makes use of the GrapheneRPC library. + In here, we mostly deal with DPay specific error handling + + :param json payload: Payload data + :raises ValueError: if the server does not respond in proper JSON format + :raises RPCError: if the server returns an error + """ + if self.url is None: + raise exceptions.RPCConnection("RPC is not connected!") + doRetry = True + maxRetryCountReached = False + while doRetry and not maxRetryCountReached: + doRetry = False + try: + # Forward call to GrapheneWebsocketRPC and catch+evaluate errors + reply = super(DPayNodeRPC, self).rpcexec(payload) + if self.next_node_on_empty_reply and not bool(reply) and self.nodes.working_nodes_count > 1: + self._retry_on_next_node("Empty Reply") + doRetry = True + self.next_node_on_empty_reply = True + else: + self.next_node_on_empty_reply = False + return reply + except exceptions.RPCErrorDoRetry as e: + msg = exceptions.decodeRPCErrorMsg(e).strip() + try: + self.nodes.sleep_and_check_retries(str(msg), call_retry=True) + doRetry = True + except exceptions.CallRetriesReached: + if self.nodes.working_nodes_count > 1: + self._retry_on_next_node(msg) + doRetry = True + else: + self.next_node_on_empty_reply = False + raise exceptions.CallRetriesReached + except exceptions.RPCError as e: + try: + doRetry = self._check_error_message(e, self.error_cnt_call) + except exceptions.CallRetriesReached: + msg = exceptions.decodeRPCErrorMsg(e).strip() + if self.nodes.working_nodes_count > 1: + self._retry_on_next_node(msg) + doRetry = True + else: + self.next_node_on_empty_reply = False + raise exceptions.CallRetriesReached + except Exception as e: + self.next_node_on_empty_reply = False + raise e + maxRetryCountReached = self.nodes.num_retries_call_reached + self.next_node_on_empty_reply = False + + def _retry_on_next_node(self, error_msg): + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(error_msg, sleep=False, call_retry=False) + self.next() + + def _check_error_message(self, e, cnt): + """Check error message and decide what to do""" + doRetry = False + msg = exceptions.decodeRPCErrorMsg(e).strip() + if re.search("missing required active authority", msg): + raise exceptions.MissingRequiredActiveAuthority + elif re.search("missing required active authority", msg): + raise exceptions.MissingRequiredActiveAuthority + elif re.match("^no method with name.*", msg): + raise exceptions.NoMethodWithName(msg) + elif re.search("Could not find method", msg): + raise exceptions.NoMethodWithName(msg) + elif re.search("Could not find API", msg): + if self._check_api_name(msg): + # self._switch_to_next_node(msg, "ApiNotSupported") + raise exceptions.ApiNotSupported(msg) + else: + raise exceptions.NoApiWithName(msg) + elif re.search("irrelevant signature included", msg): + raise exceptions.UnnecessarySignatureDetected(msg) + elif re.search("WinError", msg): + raise exceptions.RPCError(msg) + elif re.search("Unable to acquire database lock", msg): + self.nodes.sleep_and_check_retries(str(msg), call_retry=True) + doRetry = True + elif re.search("Request Timeout", msg): + self.nodes.sleep_and_check_retries(str(msg), call_retry=True) + doRetry = True + elif re.search("Bad or missing upstream response", msg): + self.nodes.sleep_and_check_retries(str(msg), call_retry=True) + doRetry = True + elif re.search("Internal Error", msg) or re.search("Unknown exception", msg): + self.nodes.sleep_and_check_retries(str(msg), call_retry=True) + doRetry = True + elif re.search("!check_max_block_age", str(e)): + self._switch_to_next_node(str(e)) + doRetry = True + elif re.search("out_of_rangeEEEE: unknown key", msg) or re.search("unknown key:unknown key", msg): + raise exceptions.UnkownKey(msg) + elif re.search("Assert Exception:v.is_object(): Input data have to treated as object", msg): + raise exceptions.UnhandledRPCError("Use Operation(op, appbase=True) to prevent error: " + msg) + # elif re.search("Client returned invalid format. Expected JSON!", msg): + # self._switch_to_next_node(msg) + # doRetry = True + elif msg: + raise exceptions.UnhandledRPCError(msg) + else: + raise e + return doRetry + + def _switch_to_next_node(self, msg, error_type="UnhandledRPCError"): + if self.nodes.working_nodes_count == 1: + if error_type == "UnhandledRPCError": + raise exceptions.UnhandledRPCError(msg) + elif error_type == "ApiNotSupported": + raise exceptions.ApiNotSupported(msg) + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(str(msg), sleep=False) + self.next() + + def _check_api_name(self, msg): + error_start = "Could not find API" + known_apis = ['account_history_api', 'tags_api', + 'database_api', 'market_history_api', + 'block_api', 'account_by_key_api', 'chain_api', + 'follow_api', 'condenser_api', 'debug_node_api', + 'witness_api', 'test_api', + 'network_broadcast_api'] + for api in known_apis: + if re.search(error_start + " " + api, msg): + return True + if msg[-18:] == error_start: + return True + return False + + def get_account(self, name, **kwargs): + """ Get full account details from account name + + :param str name: Account name + """ + if isinstance(name, str): + return self.get_accounts([name], **kwargs) diff --git a/dpaycliapi/exceptions.py b/dpaycliapi/exceptions.py new file mode 100755 index 0000000..b9f1911 --- /dev/null +++ b/dpaycliapi/exceptions.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import re + + +def decodeRPCErrorMsg(e): + """ Helper function to decode the raised Exception and give it a + python Exception class + """ + found = re.search( + ( + "(10 assert_exception: Assert Exception\n|" + "3030000 tx_missing_posting_auth)" + ".*: (.*)\n" + ), + str(e), + flags=re.M) + if found: + return found.group(2).strip() + else: + return str(e) + + +class UnauthorizedError(Exception): + """UnauthorizedError Exception.""" + + pass + + +class RPCConnection(Exception): + """RPCConnection Exception.""" + + pass + + +class RPCError(Exception): + """RPCError Exception.""" + + pass + + +class RPCErrorDoRetry(Exception): + """RPCErrorDoRetry Exception.""" + + pass + + +class NumRetriesReached(Exception): + """NumRetriesReached Exception.""" + + pass + + +class CallRetriesReached(Exception): + """CallRetriesReached Exception. Only for internal use""" + + pass + + +class MissingRequiredActiveAuthority(RPCError): + pass + + +class UnkownKey(RPCError): + pass + + +class NoMethodWithName(RPCError): + pass + + +class NoApiWithName(RPCError): + pass + + +class ApiNotSupported(RPCError): + pass + + +class UnhandledRPCError(RPCError): + pass + + +class NoAccessApi(RPCError): + pass + + +class InvalidEndpointUrl(Exception): + pass + + +class UnnecessarySignatureDetected(Exception): + pass + + +class WorkingNodeMissing(Exception): + pass + + +class TimeoutException(Exception): + pass diff --git a/dpaycliapi/graphenerpc.py b/dpaycliapi/graphenerpc.py new file mode 100755 index 0000000..9419246 --- /dev/null +++ b/dpaycliapi/graphenerpc.py @@ -0,0 +1,477 @@ +"""graphennewsrpc.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import next +from builtins import str +from builtins import object +from itertools import cycle +import threading +import sys +import json +import signal +import logging +import ssl +import re +import time +import warnings +import six +from .exceptions import ( + UnauthorizedError, RPCConnection, RPCError, RPCErrorDoRetry, NumRetriesReached, CallRetriesReached, WorkingNodeMissing, TimeoutException +) +from .rpcutils import ( + is_network_appbase_ready, + get_api_name, get_query +) +from .node import Nodes +from dpaycligraphenebase.version import version as dpaycli_version +from dpaycligraphenebase.chains import known_chains +if sys.version_info[0] < 3: + from thread import interrupt_main +else: + from _thread import interrupt_main +WEBSOCKET_MODULE = None +if not WEBSOCKET_MODULE: + try: + import websocket + from websocket._exceptions import WebSocketConnectionClosedException, WebSocketTimeoutException + WEBSOCKET_MODULE = "websocket" + except ImportError: + WEBSOCKET_MODULE = None +REQUEST_MODULE = None +if not REQUEST_MODULE: + try: + import requests + from requests.adapters import HTTPAdapter + from requests.packages.urllib3.util.retry import Retry + from requests.exceptions import ConnectionError + REQUEST_MODULE = "requests" + except ImportError: + REQUEST_MODULE = None + +log = logging.getLogger(__name__) + + +class SessionInstance(object): + """Singelton for the Session Instance""" + instance = None + + +def set_session_instance(instance): + """Set session instance""" + SessionInstance.instance = instance + + +def shared_session_instance(): + """Get session instance""" + if REQUEST_MODULE is None: + raise Exception() + if not SessionInstance.instance: + SessionInstance.instance = requests.Session() + return SessionInstance.instance + + +def create_ws_instance(use_ssl=True, enable_multithread=True): + """Get websocket instance""" + if WEBSOCKET_MODULE is None: + raise Exception() + if use_ssl: + ssl_defaults = ssl.get_default_verify_paths() + sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} + return websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=enable_multithread) + else: + return websocket.WebSocket(enable_multithread=enable_multithread) + + +class GrapheneRPC(object): + """ + This class allows to call API methods synchronously, without callbacks. + + It logs warnings and errors. + + :param str urls: Either a single Websocket/Http URL, or a list of URLs + :param str user: Username for Authentication + :param str password: Password for Authentication + :param int num_retries: Try x times to num_retries to a node on disconnect, -1 for indefinitely + :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5) + :param int timeout: Timeout setting for https nodes (default is 60) + :param bool autoconnect: When set to false, connection is performed on the first rpc call (default is True) + :param bool use_condenser: Use the old condenser_api rpc protocol on nodes with version + 0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower. + :param dict custom_chains: custom chain which should be added to the known chains + + Available APIs: + + * database + * network_node + * network_broadcast + + Usage: + + .. code-block:: python + + from dpaycliapi.graphenerpc import GrapheneRPC + ws = GrapheneRPC("wss://greatchain.dpaynodes.com","","") + print(ws.get_account_count()) + + ws = GrapheneRPC("https://api.dpays.io","","") + print(ws.get_account_count()) + + .. note:: This class allows to call methods available via + websocket. If you want to use the notification + subsystem, please use ``GrapheneWebsocket`` instead. + + """ + + def __init__(self, urls, user=None, password=None, **kwargs): + """Init.""" + self.rpc_methods = {'offline': -1, 'ws': 0, 'jsonrpc': 1, 'wsappbase': 2, 'appbase': 3} + self.current_rpc = self.rpc_methods["ws"] + self._request_id = 0 + self.timeout = kwargs.get('timeout', 60) + num_retries = kwargs.get("num_retries", -1) + num_retries_call = kwargs.get("num_retries_call", 5) + self.use_condenser = kwargs.get("use_condenser", False) + self.disable_chain_detection = kwargs.get("disable_chain_detection", False) + self.known_chains = known_chains + custom_chain = kwargs.get("custom_chains", {}) + if len(custom_chain) > 0: + for c in custom_chain: + if c not in self.known_chains: + self.known_chains[c] = custom_chain[c] + + self.nodes = Nodes(urls, num_retries, num_retries_call) + if self.nodes.working_nodes_count == 0: + self.current_rpc = self.rpc_methods["offline"] + + self.user = user + self.password = password + self.ws = None + self.url = None + self.session = None + self.rpc_queue = [] + if kwargs.get("autoconnect", True): + self.rpcconnect() + + @property + def num_retries(self): + return self.nodes.num_retries + + @property + def num_retries_call(self): + return self.nodes.num_retries_call + + @property + def error_cnt_call(self): + return self.nodes.error_cnt_call + + @property + def error_cnt(self): + return self.nodes.error_cnt + + def get_request_id(self): + """Get request id.""" + self._request_id += 1 + return self._request_id + + def next(self): + """Switches to the next node url""" + if self.ws: + try: + self.rpcclose() + except Exception as e: + log.warning(str(e)) + self.rpcconnect() + + def is_appbase_ready(self): + """Check if node is appbase ready""" + return self.current_rpc in [self.rpc_methods['wsappbase'], self.rpc_methods['appbase']] + + def get_use_appbase(self): + """Returns True if appbase ready and appbase calls are set""" + return not self.use_condenser and self.is_appbase_ready() + + def rpcconnect(self, next_url=True): + """Connect to next url in a loop.""" + if self.nodes.working_nodes_count == 0: + return + while True: + if next_url: + self.url = next(self.nodes) + self.nodes.reset_error_cnt_call() + log.debug("Trying to connect to node %s" % self.url) + if self.url[:3] == "wss": + self.ws = create_ws_instance(use_ssl=True) + self.ws.settimeout(self.timeout) + self.current_rpc = self.rpc_methods["ws"] + elif self.url[:2] == "ws": + self.ws = create_ws_instance(use_ssl=False) + self.ws.settimeout(self.timeout) + self.current_rpc = self.rpc_methods["ws"] + else: + self.ws = None + self.session = shared_session_instance() + self.current_rpc = self.rpc_methods["jsonrpc"] + self.headers = {'User-Agent': 'dpaycli v%s' % (dpaycli_version), + 'content-type': 'application/json'} + try: + if self.ws: + self.ws.connect(self.url) + self.rpclogin(self.user, self.password) + if self.disable_chain_detection: + # Set to appbase rpc format + if self.current_rpc == self.rpc_methods['ws']: + self.current_rpc = self.rpc_methods['wsappbase'] + else: + self.current_rpc = self.rpc_methods['appbase'] + break + try: + props = None + if not self.use_condenser: + props = self.get_config(api="database") + else: + props = self.get_config() + except Exception as e: + if re.search("Bad Cast:Invalid cast from type", str(e)): + # retry with appbase + if self.current_rpc == self.rpc_methods['ws']: + self.current_rpc = self.rpc_methods['wsappbase'] + else: + self.current_rpc = self.rpc_methods['appbase'] + props = self.get_config(api="database") + if props is None: + raise RPCError("Could not receive answer for get_config") + if is_network_appbase_ready(props): + if self.ws: + self.current_rpc = self.rpc_methods["wsappbase"] + else: + self.current_rpc = self.rpc_methods["appbase"] + break + except KeyboardInterrupt: + raise + except Exception as e: + self.nodes.increase_error_cnt() + do_sleep = not next_url or (next_url and self.nodes.working_nodes_count == 1) + self.nodes.sleep_and_check_retries(str(e), sleep=do_sleep) + next_url = True + + def rpclogin(self, user, password): + """Login into Websocket""" + if self.ws and self.current_rpc == self.rpc_methods['ws'] and user and password: + self.login(user, password, api="login_api") + + def rpcclose(self): + """Close Websocket""" + if self.ws is None: + return + # if self.ws.connected: + self.ws.close() + + def request_send(self, payload): + if self.user is not None and self.password is not None: + response = self.session.post(self.url, + data=payload, + headers=self.headers, + timeout=self.timeout, + auth=(self.user, self.password)) + else: + response = self.session.post(self.url, + data=payload, + headers=self.headers, + timeout=self.timeout) + if response.status_code == 401: + raise UnauthorizedError + return response.text + + def ws_send(self, payload): + if self.ws is None: + raise RPCConnection("No websocket available!") + self.ws.send(payload) + reply = self.ws.recv() + return reply + + def version_string_to_int(self, network_version): + version_list = network_version.split('.') + return int(int(version_list[0]) * 1e8 + int(version_list[1]) * 1e4 + int(version_list[2])) + + def get_network(self, props=None): + """ Identify the connected network. This call returns a + dictionary with keys chain_id, core_symbol and prefix + """ + if props is None: + props = self.get_config(api="database") + chain_id = None + network_version = None + for key in props: + if key[-8:] == "CHAIN_ID": + chain_id = props[key] + elif key[-18:] == "BLOCKCHAIN_VERSION": + network_version = props[key] + + if chain_id is None: + raise("Connecting to unknown network!") + highest_version_chain = None + for k, v in list(self.known_chains.items()): + if v["chain_id"] == chain_id and self.version_string_to_int(v["min_version"]) <= self.version_string_to_int(network_version): + if highest_version_chain is None: + highest_version_chain = v + elif v["min_version"] == '0.19.5' and self.use_condenser: + highest_version_chain = v + elif v["min_version"] == '0.0.0' and self.use_condenser: + highest_version_chain = v + elif self.version_string_to_int(v["min_version"]) > self.version_string_to_int(highest_version_chain["min_version"]) and not self.use_condenser: + highest_version_chain = v + if highest_version_chain is None: + raise("Connecting to unknown network!") + else: + return highest_version_chain + + def _check_for_server_error(self, reply): + """Checks for server error message in reply""" + if re.search("Internal Server Error", reply) or re.search("500", reply): + raise RPCErrorDoRetry("Internal Server Error") + elif re.search("Not Implemented", reply) or re.search("501", reply): + raise RPCError("Not Implemented") + elif re.search("Bad Gateway", reply) or re.search("502", reply): + raise RPCErrorDoRetry("Bad Gateway") + elif re.search("Service Temporarily Unavailable", reply) or re.search("Service Unavailable", reply) or re.search("503", reply): + raise RPCErrorDoRetry("Service Temporarily Unavailable") + elif re.search("Gateway Time-out", reply) or re.search("Gateway Timeout", reply) or re.search("504", reply): + raise RPCErrorDoRetry("Gateway Time-out") + elif re.search("HTTP Version not supported", reply) or re.search("505", reply): + raise RPCError("HTTP Version not supported") + elif re.search("Variant Also Negotiates", reply) or re.search("506", reply): + raise RPCError("Variant Also Negotiates") + elif re.search("Insufficient Storage", reply) or re.search("507", reply): + raise RPCError("Insufficient Storage") + elif re.search("Loop Detected", reply) or re.search("508", reply): + raise RPCError("Loop Detected") + elif re.search("Bandwidth Limit Exceeded", reply) or re.search("509", reply): + raise RPCError("Bandwidth Limit Exceeded") + elif re.search("Not Extended", reply) or re.search("510", reply): + raise RPCError("Not Extended") + elif re.search("Network Authentication Required", reply) or re.search("511", reply): + raise RPCError("Network Authentication Required") + else: + raise RPCError("Client returned invalid format. Expected JSON!") + + def rpcexec(self, payload): + """ + Execute a call by sending the payload. + + :param json payload: Payload data + :raises ValueError: if the server does not respond in proper JSON format + :raises RPCError: if the server returns an error + """ + log.debug(json.dumps(payload)) + if self.nodes.working_nodes_count == 0: + raise WorkingNodeMissing + if self.url is None: + raise RPCConnection("RPC is not connected!") + reply = {} + while True: + self.nodes.increase_error_cnt_call() + try: + if self.current_rpc == self.rpc_methods['ws'] or \ + self.current_rpc == self.rpc_methods['wsappbase']: + reply = self.ws_send(json.dumps(payload, ensure_ascii=False).encode('utf8')) + else: + reply = self.request_send(json.dumps(payload, ensure_ascii=False).encode('utf8')) + if not bool(reply): + try: + self.nodes.sleep_and_check_retries("Empty Reply", call_retry=True) + except CallRetriesReached: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries("Empty Reply", sleep=False, call_retry=False) + self.rpcconnect() + else: + break + except KeyboardInterrupt: + raise + except WebSocketConnectionClosedException as e: + if self.nodes.num_retries_call_reached: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False) + self.rpcconnect() + else: + # self.nodes.sleep_and_check_retries(str(e), sleep=True, call_retry=True) + self.rpcconnect(next_url=False) + except ConnectionError as e: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False) + self.rpcconnect() + except WebSocketTimeoutException as e: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False) + self.rpcconnect() + except Exception as e: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False) + self.rpcconnect() + + ret = {} + try: + ret = json.loads(reply, strict=False) + except ValueError: + self._check_for_server_error(reply) + + log.debug(json.dumps(reply)) + + if isinstance(ret, dict) and 'error' in ret: + if 'detail' in ret['error']: + raise RPCError(ret['error']['detail']) + else: + raise RPCError(ret['error']['message']) + else: + if isinstance(ret, list): + ret_list = [] + for r in ret: + if isinstance(r, dict) and 'error' in r: + if 'detail' in r['error']: + raise RPCError(r['error']['detail']) + else: + raise RPCError(r['error']['message']) + elif isinstance(r, dict) and "result" in r: + ret_list.append(r["result"]) + else: + ret_list.append(r) + self.nodes.reset_error_cnt_call() + return ret_list + elif isinstance(ret, dict) and "result" in ret: + self.nodes.reset_error_cnt_call() + return ret["result"] + elif isinstance(ret, int): + raise RPCError("Client returned invalid format. Expected JSON! Output: %s" % (str(ret))) + else: + self.nodes.reset_error_cnt_call() + return ret + return ret + + # End of Deprecated methods + #################################################################### + def __getattr__(self, name): + """Map all methods to RPC calls and pass through the arguments.""" + def method(*args, **kwargs): + + api_name = get_api_name(self.is_appbase_ready(), *args, **kwargs) + if self.is_appbase_ready() and self.use_condenser: + api_name = "condenser_api" + + # let's be able to define the num_retries per query + stored_num_retries_call = self.nodes.num_retries_call + self.nodes.num_retries_call = kwargs.get("num_retries_call", stored_num_retries_call) + add_to_queue = kwargs.get("add_to_queue", False) + query = get_query(self.is_appbase_ready() and not self.use_condenser, self.get_request_id(), api_name, name, args) + if add_to_queue: + self.rpc_queue.append(query) + self.nodes.num_retries_call = stored_num_retries_call + return None + elif len(self.rpc_queue) > 0: + self.rpc_queue.append(query) + query = self.rpc_queue + self.rpc_queue = [] + r = self.rpcexec(query) + self.nodes.num_retries_call = stored_num_retries_call + return r + return method diff --git a/dpaycliapi/node.py b/dpaycliapi/node.py new file mode 100755 index 0000000..e570f4a --- /dev/null +++ b/dpaycliapi/node.py @@ -0,0 +1,171 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import json +import re +import time +import logging +from .exceptions import ( + UnauthorizedError, RPCConnection, RPCError, NumRetriesReached, CallRetriesReached +) +log = logging.getLogger(__name__) + + +class Node(object): + def __init__( + self, + url + ): + self.url = url + self.error_cnt = 0 + self.error_cnt_call = 0 + + def __repr__(self): + return self.url + + +class Nodes(list): + """Stores Node URLs and error counts""" + def __init__(self, urls, num_retries, num_retries_call): + if isinstance(urls, str): + url_list = re.split(r",|;", urls) + if url_list is None: + url_list = [urls] + elif isinstance(urls, Nodes): + url_list = [urls[i].url for i in range(len(urls))] + elif isinstance(urls, (list, tuple, set)): + url_list = urls + elif urls is not None: + url_list = [urls] + else: + url_list = [] + super(Nodes, self).__init__([Node(x) for x in url_list]) + self.num_retries = num_retries + self.num_retries_call = num_retries_call + self.current_node_index = -1 + self.freeze_current_node = False + + def __iter__(self): + return self + + def __next__(self): + next_node_count = 0 + if self.freeze_current_node: + return self.url + while next_node_count == 0 and (self.num_retries < 0 or self.node.error_cnt < self.num_retries): + self.current_node_index += 1 + if self.current_node_index >= self.working_nodes_count: + self.current_node_index = 0 + next_node_count += 1 + if next_node_count > self.working_nodes_count + 1: + raise StopIteration + return self.url + + next = __next__ # Python 2 + + def export_working_nodes(self): + nodes_list = [] + for i in range(len(self)): + if self.num_retries < 0 or self[i].error_cnt <= self.num_retries: + nodes_list.append(self[i].url) + return nodes_list + + def __repr__(self): + nodes_list = self.export_working_nodes() + return str(nodes_list) + + @property + def working_nodes_count(self): + n = 0 + if self.freeze_current_node: + i = self.current_node_index + if self.current_node_index < 0: + i = 0 + if self.num_retries < 0 or self[i].error_cnt <= self.num_retries: + n += 1 + return n + for i in range(len(self)): + if self.num_retries < 0 or self[i].error_cnt <= self.num_retries: + n += 1 + return n + + @property + def url(self): + if self.node is None: + return '' + return self.node.url + + @property + def node(self): + if self.current_node_index < 0: + return self[0] + return self[self.current_node_index] + + @property + def error_cnt(self): + if self.node is None: + return 0 + return self.node.error_cnt + + @property + def error_cnt_call(self): + if self.node is None: + return 0 + return self.node.error_cnt_call + + @property + def num_retries_call_reached(self): + return self.error_cnt_call >= self.num_retries_call + + def increase_error_cnt(self): + """Increase node error count for current node""" + if self.node is not None: + self.node.error_cnt += 1 + + def increase_error_cnt_call(self): + """Increase call error count for current node""" + if self.node is not None: + self.node.error_cnt_call += 1 + + def reset_error_cnt_call(self): + """Set call error count for current node to zero""" + if self.node is not None: + self.node.error_cnt_call = 0 + + def reset_error_cnt(self): + """Set node error count for current node to zero""" + if self.node is not None: + self.node.error_cnt = 0 + + def sleep_and_check_retries(self, errorMsg=None, sleep=True, call_retry=False, showMsg=True): + """Sleep and check if num_retries is reached""" + if errorMsg: + log.warning("Error: {}".format(errorMsg)) + if call_retry: + cnt = self.error_cnt_call + if (self.num_retries_call >= 0 and self.error_cnt_call > self.num_retries_call): + raise CallRetriesReached() + else: + cnt = self.error_cnt + if (self.num_retries >= 0 and self.error_cnt > self.num_retries): + raise NumRetriesReached() + + if showMsg: + if call_retry: + log.warning("Retry RPC Call on node: %s (%d/%d) \n" % (self.url, cnt, self.num_retries_call)) + else: + log.warning("Lost connection or internal error on node: %s (%d/%d) \n" % (self.url, cnt, self.num_retries)) + if not sleep: + return + if cnt < 1: + sleeptime = 0 + elif cnt < 10: + sleeptime = (cnt - 1) * 1.5 + 0.5 + else: + sleeptime = 10 + if sleeptime: + log.warning("Retrying in %d seconds\n" % sleeptime) + time.sleep(sleeptime) diff --git a/dpaycliapi/rpcutils.py b/dpaycliapi/rpcutils.py new file mode 100755 index 0000000..1885ad1 --- /dev/null +++ b/dpaycliapi/rpcutils.py @@ -0,0 +1,82 @@ +"""graphennewsrpc.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import time +import json +import logging +from .exceptions import ( + UnauthorizedError, RPCConnection, RPCError, NumRetriesReached, CallRetriesReached +) +from .node import Nodes + +log = logging.getLogger(__name__) + + +def is_network_appbase_ready(props): + """Checks if the network is appbase ready""" + if "DPAY_BLOCKCHAIN_VERSION" in props: + return False + elif "DPAY_BLOCKCHAIN_VERSION" in props: + return True + + +def get_query(appbase, request_id, api_name, name, args): + query = [] + if not appbase or api_name == "condenser_api": + query = {"method": "call", + "params": [api_name, name, list(args)], + "jsonrpc": "2.0", + "id": request_id} + else: + args = json.loads(json.dumps(args)) + # print(args) + if len(args) > 0 and isinstance(args, list) and isinstance(args[0], dict): + query = {"method": api_name + "." + name, + "params": args[0], + "jsonrpc": "2.0", + "id": request_id} + elif len(args) > 0 and isinstance(args, list) and isinstance(args[0], list) and len(args[0]) > 0 and isinstance(args[0][0], dict): + for a in args[0]: + query.append({"method": api_name + "." + name, + "params": a, + "jsonrpc": "2.0", + "id": request_id}) + request_id += 1 + elif args: + query = {"method": "call", + "params": [api_name, name, list(args)], + "jsonrpc": "2.0", + "id": request_id} + request_id += 1 + elif api_name == "condenser_api": + query = {"method": api_name + "." + name, + "jsonrpc": "2.0", + "params": [], + "id": request_id} + else: + query = {"method": api_name + "." + name, + "jsonrpc": "2.0", + "params": {}, + "id": request_id} + return query + + +def get_api_name(appbase, *args, **kwargs): + if not appbase: + # Sepcify the api to talk to + if ("api" in kwargs) and len(kwargs["api"]) > 0: + api_name = kwargs["api"].replace("_api", "") + "_api" + else: + api_name = None + else: + # Sepcify the api to talk to + if ("api" in kwargs) and len(kwargs["api"]) > 0: + if kwargs["api"] not in ["jsonrpc", "hive"]: + api_name = kwargs["api"].replace("_api", "") + "_api" + else: + api_name = kwargs["api"] + else: + api_name = "condenser_api" + return api_name diff --git a/dpaycliapi/version.py b/dpaycliapi/version.py new file mode 100755 index 0000000..f69b1b5 --- /dev/null +++ b/dpaycliapi/version.py @@ -0,0 +1,2 @@ +"""THIS FILE IS GENERATED FROM dpaycli SETUP.PY.""" +version = '0.02.0' diff --git a/dpaycliapi/websocket.py b/dpaycliapi/websocket.py new file mode 100755 index 0000000..b11167e --- /dev/null +++ b/dpaycliapi/websocket.py @@ -0,0 +1,300 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import next +from builtins import str +import traceback +import threading +import ssl +import time +import json +import logging +import websocket +from itertools import cycle +from threading import Thread +from dpaycliapi.rpcutils import ( + is_network_appbase_ready, + get_api_name, get_query, UnauthorizedError, + RPCConnection, RPCError, NumRetriesReached +) +from dpaycliapi.node import Nodes +from events import Events + +log = logging.getLogger(__name__) +# logging.basicConfig(level=logging.DEBUG) + + +class DPayWebsocket(Events): + """ Create a websocket connection and request push notifications + + :param str urls: Either a single Websocket URL, or a list of URLs + :param str user: Username for Authentication + :param str password: Password for Authentication + :param int keep_alive: seconds between a ping to the backend (defaults to 25seconds) + + After instanciating this class, you can add event slots for: + + * ``on_block`` + + which will be called accordingly with the notification + message received from the DPay node: + + .. code-block:: python + + ws = DPayWebsocket( + "wss://gtg.dpay.house:8090", + ) + ws.on_block += print + ws.run_forever() + + """ + __events__ = [ + 'on_block', + ] + + def __init__( + self, + urls, + user="", + password="", + only_block_id=False, + on_block=None, + keep_alive=25, + num_retries=-1, + timeout=60, + *args, + **kwargs + ): + + self.num_retries = num_retries + self.keepalive = None + self._request_id = 0 + self.ws = None + self.user = user + self.password = password + self.keep_alive = keep_alive + self.run_event = threading.Event() + self.only_block_id = only_block_id + self.nodes = Nodes(urls, num_retries, 5) + + # Instantiate Events + Events.__init__(self) + self.events = Events() + + # Store the objects we are interested in + # self.subscription_accounts = accounts + + if on_block: + self.on_block += on_block + # if on_account: + # self.on_account += on_account + + def cancel_subscriptions(self): + """cancel_all_subscriptions removed from api""" + # self.cancel_all_subscriptions() + # api call removed in 0.19.1 + log.exception("cancel_all_subscriptions removed from api") + + def on_open(self, ws): + """ This method will be called once the websocket connection is + established. It will + + * login, + * register to the database api, and + * subscribe to the objects defined if there is a + callback/slot available for callbacks + """ + self.login(self.user, self.password, api_id=1) + # self.database(api_id=1) + self.__set_subscriptions() + self.keepalive = threading.Thread( + target=self._ping, + ) + self.keepalive.start() + + def reset_subscriptions(self, accounts=[]): + """Reset subscriptions""" + # self.subscription_accounts = accounts + self.__set_subscriptions() + + def __set_subscriptions(self): + """set subscriptions ot on_block function""" + # self.cancel_all_subscriptions() + # Subscribe to events on the Backend and give them a + # callback number that allows us to identify the event + # set_pending_transaction_callback is removed from api + # api call removed in 0.19.1 + + if len(self.on_block): + self.set_block_applied_callback( + self.__events__.index('on_block')) + + def _ping(self): + """Send keep_alive request""" + # We keep the connetion alive by requesting a short object + def ping(self): + while not self.run_event.wait(self.keep_alive): + log.debug('Sending ping') + self.get_config() + + def process_block(self, data): + """ This method is called on notices that need processing. Here, + we call the ``on_block`` slot. + """ + # id = data["id"] + if "result" not in data: + return + block = data["result"] + if block is None: + return + if isinstance(block, (bool, int)): + return + if "previous" in block: + block_id = block["previous"] + block_number = int(block_id[:8], base=16) + block["id"] = block_number + # print(result) + # print(block_number) + # self.get_block(block_number) + self.on_block(block) + + def on_message(self, ws, reply, *args): + """ This method is called by the websocket connection on every + message that is received. If we receive a ``notice``, we + hand over post-processing and signalling of events to + ``process_notice``. + """ + log.debug("Received message: %s" % str(reply)) + data = {} + try: + data = json.loads(reply, strict=False) + except ValueError: + raise ValueError("API node returned invalid format. Expected JSON!") + + if "method" in data and data.get("method") == "notice": + id = data["params"][0] + # print(data) + + if id >= len(self.__events__): + log.critical( + "Received an id that is out of range\n\n" + + str(data) + ) + return + + # This is a "general" object change notification + if id == self.__events__.index('on_block'): + # Let's see if a specific object has changed + for new_block in data["params"][1]: + if not new_block: + continue + block_id = new_block["previous"] + block_number = int(block_id[:8], base=16) + # print(new_block) + # print(block_number) + if self.only_block_id: + self.on_block(block_number) + else: + self.get_block(block_number) + else: + # print(data) + try: + callbackname = self.__events__[id] + log.debug("Patching through to call %s" % callbackname) + [getattr(self.events, callbackname)(x) for x in data["params"][1]] + except Exception as e: + log.critical("Error in {}: {}\n\n{}".format( + callbackname, str(e), traceback.format_exc())) + else: + self.process_block(data) + + def on_error(self, ws, error): + """ Called on websocket errors + """ + print(error) + log.exception(error) + + def on_close(self, ws): + """ Called when websocket connection is closed + """ + log.debug('Closing WebSocket connection with {}'.format(self.url)) + if self.keepalive and self.keepalive.is_alive(): + self.keepalive.do_run = False + self.keepalive.join() + + def run_forever(self): + """ This method is used to run the websocket app continuously. + It will execute callbacks as defined and try to stay + connected with the provided APIs + """ + while not self.run_event.is_set(): + self.url = next(self.nodes) + log.debug("Trying to connect to node %s" % self.url) + try: + # websocket.enableTrace(True) + self.ws = websocket.WebSocketApp( + self.url, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + on_open=self.on_open, + ) + self.ws.run_forever() + except websocket.WebSocketException as exc: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries() + except websocket.WebSocketTimeoutException as exc: + self.nodes.increase_error_cnt() + self.nodes.sleep_and_check_retries() + except KeyboardInterrupt: + self.ws.keep_running = False + raise + + except Exception as e: + log.critical("{}\n\n{}".format(str(e), traceback.format_exc())) + + def get_request_id(self): + """Generates next request id""" + self._request_id += 1 + return self._request_id + + def stop(self): + """Stop running Websocket""" + self.ws.keep_running = False + self.close() + + def close(self): + """ Closes the websocket connection and waits for the ping thread to close + """ + self.run_event.set() + self.ws.close() + + if self.keepalive and self.keepalive.is_alive(): + self.keepalive.join() + + def rpcexec(self, payload): + """ + Execute a call by sending the payload. + + :param json payload: Payload data + :raises ValueError: if the server does not respond in proper JSON format + :raises RPCError: if the server returns an error + """ + log.debug(json.dumps(payload)) + self.ws.send(json.dumps(payload, ensure_ascii=False).encode('utf8')) + + def __getattr__(self, name): + """ Map all methods to RPC calls and pass through the arguments + """ + if name in self.__events__: + return getattr(self.events, name) + + def method(*args, **kwargs): + api_name = get_api_name(False, *args, **kwargs) + # let's be able to define the num_retries per query + self.num_retries = kwargs.get("num_retries", self.num_retries) + query = get_query(False, self.get_request_id(), api_name, name, args) + r = self.rpcexec(query) + return r + return method diff --git a/dpayclibase/__init__.py b/dpayclibase/__init__.py new file mode 100755 index 0000000..63f7d4e --- /dev/null +++ b/dpayclibase/__init__.py @@ -0,0 +1,11 @@ +""" dpayclibase.""" +from .version import version as __version__ +__all__ = [ + 'memo', + 'objects', + 'objecttypes', + 'operationids', + 'operations', + 'signedtransactions', + 'transactions', +] diff --git a/dpayclibase/memo.py b/dpayclibase/memo.py new file mode 100755 index 0000000..bba7d2b --- /dev/null +++ b/dpayclibase/memo.py @@ -0,0 +1,232 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +from dpaycligraphenebase.py23 import py23_bytes, bytes_types +from dpaycligraphenebase.base58 import base58encode, base58decode +import sys +import hashlib +from binascii import hexlify, unhexlify +try: + from Cryptodome.Cipher import AES +except ImportError: + try: + from Crypto.Cipher import AES + except ImportError: + raise ImportError("Missing dependency: pyCryptodome") +from dpaycligraphenebase.account import PrivateKey, PublicKey +from .objects import Memo +import struct +default_prefix = "DWB" + + +def get_shared_secret(priv, pub): + """ Derive the share secret between ``priv`` and ``pub`` + + :param `Base58` priv: Private Key + :param `Base58` pub: Public Key + :return: Shared secret + :rtype: hex + + The shared secret is generated such that:: + + Pub(Alice) * Priv(Bob) = Pub(Bob) * Priv(Alice) + + """ + pub_point = pub.point() + priv_point = int(repr(priv), 16) + res = pub_point * priv_point + res_hex = '%032x' % res.x() + # Zero padding + res_hex = '0' * (64 - len(res_hex)) + res_hex + return res_hex + + +def init_aes_bts(shared_secret, nonce): + """ Initialize AES instance + + :param hex shared_secret: Shared Secret to use as encryption key + :param int nonce: Random nonce + :return: AES instance + :rtype: AES + + """ + # Shared Secret + ss = hashlib.sha512(unhexlify(shared_secret)).digest() + # Seed + seed = py23_bytes(str(nonce), 'ascii') + hexlify(ss) + seed_digest = hexlify(hashlib.sha512(seed).digest()).decode('ascii') + # Check'sum' + check = hashlib.sha256(unhexlify(seed_digest)).digest() + check = struct.unpack_from(" 32: + raise Exception("'id' too long") + + super(Custom_json, self).__init__( + OrderedDict([ + ('required_auths', + Array([String(o) for o in kwargs["required_auths"]])), + ('required_posting_auths', + Array([ + String(o) for o in kwargs["required_posting_auths"] + ])), + ('id', String(kwargs["id"])), + ('json', String(js)), + ])) + + +class Comment_options(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + + # handle beneficiaries + if "beneficiaries" in kwargs and kwargs['beneficiaries']: + kwargs['extensions'] = [[0, {'beneficiaries': kwargs['beneficiaries']}]] + + extensions = Array([]) + if "extensions" in kwargs and kwargs["extensions"]: + extensions = Array([CommentOptionExtensions(o) for o in kwargs["extensions"]]) + + super(Comment_options, self).__init__( + OrderedDict([ + ('author', String(kwargs["author"])), + ('permlink', String(kwargs["permlink"])), + ('max_accepted_payout', + Amount(kwargs["max_accepted_payout"], prefix=prefix)), + ('percent_dpay_dollars', + Uint16(int(kwargs["percent_dpay_dollars"]))), + ('allow_votes', Bool(bool(kwargs["allow_votes"]))), + ('allow_curation_rewards', + Bool(bool(kwargs["allow_curation_rewards"]))), + ('extensions', extensions), + ])) + + +class Delete_comment(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Delete_comment, self).__init__( + OrderedDict([ + ('author', String(kwargs["author"])), + ('permlink', String(kwargs["permlink"])), + ])) + + +class Feed_publish(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + if 'prefix' not in kwargs['exchange_rate']: + kwargs['exchange_rate']['prefix'] = prefix + super(Feed_publish, self).__init__( + OrderedDict([ + ('publisher', String(kwargs["publisher"])), + ('exchange_rate', ExchangeRate(kwargs["exchange_rate"])), + ])) + + +class Convert(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Convert, self).__init__( + OrderedDict([ + ('owner', String(kwargs["owner"])), + ('requestid', Uint32(kwargs["requestid"])), + ('amount', Amount(kwargs["amount"], prefix=prefix)), + ])) + + +class Set_withdraw_vesting_route(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Set_withdraw_vesting_route, self).__init__( + OrderedDict([ + ('from_account', String(kwargs["from_account"])), + ('to_account', String(kwargs["to_account"])), + ('percent', Uint16((kwargs["percent"]))), + ('auto_vest', Bool(kwargs["auto_vest"])), + ])) + + +class Limit_order_cancel(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Limit_order_cancel, self).__init__( + OrderedDict([ + ('owner', String(kwargs["owner"])), + ('orderid', Uint32(kwargs["orderid"])), + ])) + + +class Claim_account(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Claim_account, self).__init__( + OrderedDict([ + ('creator', String(kwargs["creator"])), + ('fee', Amount(kwargs["fee"], prefix=prefix)), + ('extensions', Array([])), + ])) + + +class Create_claimed_account(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + + if not len(kwargs["new_account_name"]) <= 16: + raise AssertionError("Account name must be at most 16 chars long") + + meta = "" + if "json_metadata" in kwargs and kwargs["json_metadata"]: + if isinstance(kwargs["json_metadata"], dict): + meta = json.dumps(kwargs["json_metadata"]) + else: + meta = kwargs["json_metadata"] + + super(Create_claimed_account, self).__init__( + OrderedDict([ + ('creator', String(kwargs["creator"])), + ('new_account_name', String(kwargs["new_account_name"])), + ('owner', Permission(kwargs["owner"], prefix=prefix)), + ('active', Permission(kwargs["active"], prefix=prefix)), + ('posting', Permission(kwargs["posting"], prefix=prefix)), + ('memo_key', PublicKey(kwargs["memo_key"], prefix=prefix)), + ('json_metadata', String(meta)), + ('extensions', Array([])), + ])) + + +class Delegate_vesting_shares(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Delegate_vesting_shares, self).__init__( + OrderedDict([ + ('delegator', String(kwargs["delegator"])), + ('delegatee', String(kwargs["delegatee"])), + ('vesting_shares', Amount(kwargs["vesting_shares"], prefix=prefix)), + ])) + + +class Limit_order_create(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Limit_order_create, self).__init__( + OrderedDict([ + ('owner', String(kwargs["owner"])), + ('orderid', Uint32(kwargs["orderid"])), + ('amount_to_sell', Amount(kwargs["amount_to_sell"], prefix=prefix)), + ('min_to_receive', Amount(kwargs["min_to_receive"], prefix=prefix)), + ('fill_or_kill', Bool(kwargs["fill_or_kill"])), + ('expiration', PointInTime(kwargs["expiration"])), + ])) + + +class Limit_order_create2(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + if 'prefix' not in kwargs['exchange_rate']: + kwargs['exchange_rate']['prefix'] = prefix + super(Limit_order_create2, self).__init__( + OrderedDict([ + ('owner', String(kwargs["owner"])), + ('orderid', Uint32(kwargs["orderid"])), + ('amount_to_sell', Amount(kwargs["amount_to_sell"], prefix=prefix)), + ('fill_or_kill', Bool(kwargs["fill_or_kill"])), + ('exchange_rate', ExchangeRate(kwargs["exchange_rate"])), + ('expiration', PointInTime(kwargs["expiration"])), + ])) + + +class Change_recovery_account(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Change_recovery_account, self).__init__( + OrderedDict([ + ('account_to_recover', String(kwargs["account_to_recover"])), + ('new_recovery_account', String(kwargs["new_recovery_account"])), + ('extensions', Array([])), + ])) + + +class Transfer_from_savings(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + if "memo" not in kwargs: + kwargs["memo"] = "" + + super(Transfer_from_savings, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('request_id', Uint32(kwargs["request_id"])), + ('to', String(kwargs["to"])), + ('amount', Amount(kwargs["amount"], prefix=prefix)), + ('memo', String(kwargs["memo"])), + ])) + + +class Cancel_transfer_from_savings(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Cancel_transfer_from_savings, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('request_id', Uint32(kwargs["request_id"])), + ])) + + +class Claim_reward_balance(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Claim_reward_balance, self).__init__( + OrderedDict([ + ('account', String(kwargs["account"])), + ('reward_dpay', Amount(kwargs["reward_dpay"], prefix=prefix)), + ('reward_bbd', Amount(kwargs["reward_bbd"], prefix=prefix)), + ('reward_vests', Amount(kwargs["reward_vests"], prefix=prefix)), + ])) + + +class Transfer_to_savings(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + if "memo" not in kwargs: + kwargs["memo"] = "" + super(Transfer_to_savings, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('to', String(kwargs["to"])), + ('amount', Amount(kwargs["amount"], prefix=prefix)), + ('memo', String(kwargs["memo"])), + ])) + + +class Request_account_recovery(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + new_owner = Permission(kwargs["new_owner_authority"], prefix=prefix) + super(Request_account_recovery, self).__init__( + OrderedDict([ + ('recovery_account', String(kwargs["recovery_account"])), + ('account_to_recover', String(kwargs["account_to_recover"])), + ('new_owner_authority', new_owner), + ('extensions', Array([])), + ])) + + +class Recover_account(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + new_owner = Permission(kwargs["new_owner_authority"], prefix=prefix) + recent_owner = Permission(kwargs["recent_owner_authority"], prefix=prefix) + super(Recover_account, self).__init__( + OrderedDict([ + ('account_to_recover', String(kwargs["account_to_recover"])), + ('new_owner_authority', new_owner), + ('recent_owner_authority', recent_owner), + ('extensions', Array([])), + ])) + + +class Escrow_transfer(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + meta = "" + if "json_meta" in kwargs and kwargs["json_meta"]: + if (isinstance(kwargs["json_meta"], dict) or isinstance(kwargs["json_meta"], list)): + meta = json.dumps(kwargs["json_meta"]) + else: + meta = kwargs["json_meta"] + super(Escrow_transfer, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('to', String(kwargs["to"])), + ('agent', String(kwargs["agent"])), + ('escrow_id', Uint32(kwargs["escrow_id"])), + ('bbd_amount', Amount(kwargs["bbd_amount"], prefix=prefix)), + ('dpay_amount', Amount(kwargs["dpay_amount"], prefix=prefix)), + ('fee', Amount(kwargs["fee"], prefix=prefix)), + ('ratification_deadline', PointInTime(kwargs["ratification_deadline"])), + ('escrow_expiration', PointInTime(kwargs["escrow_expiration"])), + ('json_meta', String(meta)), + ])) + + +class Escrow_dispute(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Escrow_dispute, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('to', String(kwargs["to"])), + ('who', String(kwargs["who"])), + ('escrow_id', Uint32(kwargs["escrow_id"])), + ])) + + +class Escrow_release(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) + super(Escrow_release, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('to', String(kwargs["to"])), + ('who', String(kwargs["who"])), + ('escrow_id', Uint32(kwargs["escrow_id"])), + ('bbd_amount', Amount(kwargs["bbd_amount"], prefix=prefix)), + ('dpay_amount', Amount(kwargs["dpay_amount"], prefix=prefix)), + ])) + + +class Escrow_approve(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Escrow_approve, self).__init__( + OrderedDict([ + ('from', String(kwargs["from"])), + ('to', String(kwargs["to"])), + ('agent', String(kwargs["agent"])), + ('who', String(kwargs["who"])), + ('escrow_id', Uint32(kwargs["escrow_id"])), + ('approve', Bool(kwargs["approve"])), + ])) + + +class Decline_voting_rights(GrapheneObject): + def __init__(self, *args, **kwargs): + if check_for_class(self, args): + return + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Decline_voting_rights, self).__init__( + OrderedDict([ + ('account', String(kwargs["account"])), + ('decline', Bool(kwargs["decline"])), + ])) diff --git a/dpayclibase/signedtransactions.py b/dpayclibase/signedtransactions.py new file mode 100755 index 0000000..2917e09 --- /dev/null +++ b/dpayclibase/signedtransactions.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import int, str +from dpaycligraphenebase.signedtransactions import Signed_Transaction as GrapheneSigned_Transaction +from .operations import Operation +from dpaycligraphenebase.chains import known_chains +import logging +log = logging.getLogger(__name__) + + +class Signed_Transaction(GrapheneSigned_Transaction): + """ Create a signed transaction and offer method to create the + signature + + :param num refNum: parameter ref_block_num (see ``getBlockParams``) + :param num refPrefix: parameter ref_block_prefix (see ``getBlockParams``) + :param str expiration: expiration date + :param Array operations: array of operations + :param dict custom_chains: custom chain which should be added to the known chains + """ + def __init__(self, *args, **kwargs): + self.known_chains = known_chains + custom_chain = kwargs.get("custom_chains", {}) + if len(custom_chain) > 0: + for c in custom_chain: + if c not in self.known_chains: + self.known_chains[c] = custom_chain[c] + super(Signed_Transaction, self).__init__(*args, **kwargs) + + def add_custom_chains(self, custom_chain): + if len(custom_chain) > 0: + for c in custom_chain: + if c not in self.known_chains: + self.known_chains[c] = custom_chain[c] + + def sign(self, wifkeys, chain=u"BEX"): + return super(Signed_Transaction, self).sign(wifkeys, chain) + + def verify(self, pubkeys=[], chain=u"BEX", recover_parameter=False): + return super(Signed_Transaction, self).verify(pubkeys, chain, recover_parameter) + + def getOperationKlass(self): + return Operation + + def getKnownChains(self): + return self.known_chains diff --git a/dpayclibase/transactions.py b/dpayclibase/transactions.py new file mode 100755 index 0000000..608f7be --- /dev/null +++ b/dpayclibase/transactions.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from binascii import hexlify, unhexlify +import struct +from dpaycligraphenebase.account import PublicKey +from .signedtransactions import Signed_Transaction +from .operations import ( + Op_wrapper, + Account_create, +) + + +def getBlockParams(ws): + """ Auxiliary method to obtain ``ref_block_num`` and + ``ref_block_prefix``. Requires a websocket connection to a + witness node! + """ + dynBCParams = ws.get_dynamic_global_properties() + ref_block_num = dynBCParams["head_block_number"] & 0xFFFF + ref_block_prefix = struct.unpack_from("` according to ``_format`` """ + return format(self._pk, _format) + + def __bytes__(self): + """ Returns the raw public key (has length 33)""" + return py23_bytes(self._pk) + + +@python_2_unicode_compatible +class PrivateKey(PublicKey): + """ Derives the compressed and uncompressed public keys and + constructs two instances of ``PublicKey``: + + :param str wif: Base58check-encoded wif key + :param str prefix: Network prefix (defaults to ``DWB``) + + Example::: + + PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd") + + Compressed vs. Uncompressed: + + * ``PrivateKey("w-i-f").pubkey``: + Instance of ``PublicKey`` using compressed key. + * ``PrivateKey("w-i-f").pubkey.address``: + Instance of ``Address`` using compressed key. + * ``PrivateKey("w-i-f").uncompressed``: + Instance of ``PublicKey`` using uncompressed key. + * ``PrivateKey("w-i-f").uncompressed.address``: + Instance of ``Address`` using uncompressed key. + + """ + def __init__(self, wif=None, prefix="DWB"): + if wif is None: + self._wif = Base58(hexlify(os.urandom(32)).decode('ascii'), prefix=prefix) + elif isinstance(wif, Base58): + self._wif = wif + else: + self._wif = Base58(wif, prefix=prefix) + # compress pubkeys only + self._pubkeyhex, self._pubkeyuncompressedhex = self.compressedpubkey() + self.pubkey = PublicKey(self._pubkeyhex, prefix=prefix) + self.uncompressed = PublicKey(self._pubkeyuncompressedhex, prefix=prefix) + self.uncompressed.address = Address(pubkey=self._pubkeyuncompressedhex, prefix=prefix) + self.address = Address(pubkey=self._pubkeyhex, prefix=prefix) + + def get_public_key(self): + """Returns the pubkey""" + return self.pubkey + + def compressedpubkey(self): + """ Derive uncompressed public key """ + secret = unhexlify(repr(self._wif)) + if not len(secret) == ecdsa.SECP256k1.baselen: + raise ValueError("{} != {}".format(len(secret), ecdsa.SECP256k1.baselen)) + order = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).curve.generator.order() + p = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).verifying_key.pubkey.point + x_str = ecdsa.util.number_to_string(p.x(), order) + y_str = ecdsa.util.number_to_string(p.y(), order) + compressed = hexlify(py23_bytes(chr(2 + (p.y() & 1)), 'ascii') + x_str).decode('ascii') + uncompressed = hexlify(py23_bytes(chr(4), 'ascii') + x_str + y_str).decode('ascii') + return([compressed, uncompressed]) + + def get_secret(self): + """ Get sha256 digest of the wif key. + """ + return hashlib.sha256(py23_bytes(self)).digest() + + def derive_private_key(self, sequence): + """ Derive new private key from this private key and an arbitrary + sequence number + """ + encoded = "%s %d" % (str(self), sequence) + a = py23_bytes(encoded, 'ascii') + s = hashlib.sha256(hashlib.sha512(a).digest()).digest() + return PrivateKey(hexlify(s).decode('ascii'), prefix=self.pubkey.prefix) + + def child(self, offset256): + """ Derive new private key from this key and a sha256 "offset" + """ + pubkey = self.get_public_key() + a = py23_bytes(pubkey) + offset256 + s = hashlib.sha256(a).digest() + return self.derive_from_seed(s) + + def derive_from_seed(self, offset): + """ Derive private key using "generate_from_seed" method. + Here, the key itself serves as a `seed`, and `offset` + is expected to be a sha256 digest. + """ + seed = int(hexlify(py23_bytes(self)).decode('ascii'), 16) + z = int(hexlify(offset).decode('ascii'), 16) + order = ecdsa.SECP256k1.order + + secexp = (seed + z) % order + + secret = "%0x" % secexp + return PrivateKey(secret, prefix=self.pubkey.prefix) + + def __format__(self, _format): + """ Formats the instance of:doc:`Base58 ` according to + ``_format`` + """ + return format(self._wif, _format) + + def __repr__(self): + """ Gives the hex representation of the Graphene private key.""" + return repr(self._wif) + + def __str__(self): + """ Returns the readable (uncompressed wif format) Graphene private key. This + call is equivalent to ``format(PrivateKey, "WIF")`` + """ + return format(self._wif, "WIF") + + def __bytes__(self): + """ Returns the raw private key """ + return py23_bytes(self._wif) diff --git a/dpaycligraphenebase/base58.py b/dpaycligraphenebase/base58.py new file mode 100755 index 0000000..a948f06 --- /dev/null +++ b/dpaycligraphenebase/base58.py @@ -0,0 +1,212 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import object +from builtins import chr +from future.utils import python_2_unicode_compatible +from binascii import hexlify, unhexlify +from .py23 import py23_bytes, py23_chr, bytes_types, integer_types, string_types, text_type +import hashlib +import string +import logging +log = logging.getLogger(__name__) + +""" Default Prefix """ +PREFIX = "GPH" + +known_prefixes = [ + PREFIX, + "BTS", + "MUSE", + "TEST", + "TST", + "DWB", + "STX", + "GLX", + "GLS", + "EOS", + "VIT", + "WKA", + "EUR", + "WLS", +] + + +@python_2_unicode_compatible +class Base58(object): + """Base58 base class + + This class serves as an abstraction layer to deal with base58 encoded + strings and their corresponding hex and binary representation throughout the + library. + + :param data: Data to initialize object, e.g. pubkey data, address data, ... + :type data: hex, wif, bip38 encrypted wif, base58 string + :param str prefix: Prefix to use for Address/PubKey strings (defaults to ``GPH``) + :return: Base58 object initialized with ``data`` + :rtype: Base58 + :raises ValueError: if data cannot be decoded + + * ``bytes(Base58)``: Returns the raw data + * ``str(Base58)``: Returns the readable ``Base58CheckEncoded`` data. + * ``repr(Base58)``: Gives the hex representation of the data. + * ``format(Base58,_format)`` Formats the instance according to ``_format``: + * ``"btc"``: prefixed with ``0x80``. Yields a valid btc address + * ``"wif"``: prefixed with ``0x00``. Yields a valid wif key + * ``"bts"``: prefixed with ``BTS`` + * etc. + + """ + def __init__(self, data, prefix=PREFIX): + self._prefix = prefix + if isinstance(data, Base58): + data = repr(data) + if all(c in string.hexdigits for c in data): + self._hex = data + elif data[0] == "5" or data[0] == "6": + self._hex = base58CheckDecode(data) + elif data[0] == "K" or data[0] == "L": + self._hex = base58CheckDecode(data)[:-2] + elif data[:len(self._prefix)] == self._prefix: + self._hex = gphBase58CheckDecode(data[len(self._prefix):]) + else: + raise ValueError("Error loading Base58 object") + + def __format__(self, _format): + """ Format output according to argument _format (wif,btc,...) + + :param str _format: Format to use + :return: formatted data according to _format + :rtype: str + + """ + if _format.upper() == "WIF": + return base58CheckEncode(0x80, self._hex) + elif _format.upper() == "ENCWIF": + return base58encode(self._hex) + elif _format.upper() == "BTC": + return base58CheckEncode(0x00, self._hex) + elif _format.upper() in known_prefixes: + return _format.upper() + str(self) + else: + log.warn("Format %s unknown. You've been warned!\n" % _format) + return _format.upper() + str(self) + + def __repr__(self): + """ Returns hex value of object + + :return: Hex string of instance's data + :rtype: hex string + """ + return self._hex + + def __str__(self): + """ Return graphene-base58CheckEncoded string of data + + :return: Base58 encoded data + :rtype: str + """ + return gphBase58CheckEncode(self._hex) + + def __bytes__(self): + """ Return raw bytes + + :return: Raw bytes of instance + :rtype: bytes + + """ + return unhexlify(self._hex) + + +# https://github.com/tochev/python3-cryptocoins/raw/master/cryptocoins/base58.py +BASE58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +def base58decode(base58_str): + base58_text = py23_bytes(base58_str, "ascii") + n = 0 + leading_zeroes_count = 0 + for b in base58_text: + if isinstance(b, integer_types): + n = n * 58 + BASE58_ALPHABET.find(py23_chr(b)) + else: + n = n * 58 + BASE58_ALPHABET.find(b) + if n == 0: + leading_zeroes_count += 1 + res = bytearray() + while n >= 256: + div, mod = divmod(n, 256) + res.insert(0, mod) + n = div + else: + res.insert(0, n) + return hexlify(bytearray(1) * leading_zeroes_count + res).decode('ascii') + + +def base58encode(hexstring): + byteseq = py23_bytes(unhexlify(py23_bytes(hexstring, 'ascii'))) + n = 0 + leading_zeroes_count = 0 + for c in byteseq: + n = n * 256 + c + if n == 0: + leading_zeroes_count += 1 + res = bytearray() + while n >= 58: + div, mod = divmod(n, 58) + res.insert(0, BASE58_ALPHABET[mod]) + n = div + else: + res.insert(0, BASE58_ALPHABET[n]) + return (BASE58_ALPHABET[0:1] * leading_zeroes_count + res).decode('ascii') + + +def ripemd160(s): + ripemd160 = hashlib.new('ripemd160') + ripemd160.update(unhexlify(s)) + return ripemd160.digest() + + +def doublesha256(s): + return hashlib.sha256(hashlib.sha256(unhexlify(s)).digest()).digest() + + +def b58encode(v): + return base58encode(v) + + +def b58decode(v): + return base58decode(v) + + +def base58CheckEncode(version, payload): + s = ('%.2x' % version) + payload + checksum = doublesha256(s)[:4] + result = s + hexlify(checksum).decode('ascii') + return base58encode(result) + + +def base58CheckDecode(s): + s = unhexlify(base58decode(s)) + dec = hexlify(s[:-4]).decode('ascii') + checksum = doublesha256(dec)[:4] + if not (s[-4:] == checksum): + raise AssertionError() + return dec[2:] + + +def gphBase58CheckEncode(s): + checksum = ripemd160(s)[:4] + result = s + hexlify(checksum).decode('ascii') + return base58encode(result) + + +def gphBase58CheckDecode(s): + s = unhexlify(base58decode(s)) + dec = hexlify(s[:-4]).decode('ascii') + checksum = ripemd160(dec)[:4] + if not (s[-4:] == checksum): + raise AssertionError() + return dec diff --git a/dpaycligraphenebase/bip38.py b/dpaycligraphenebase/bip38.py new file mode 100755 index 0000000..6bccbde --- /dev/null +++ b/dpaycligraphenebase/bip38.py @@ -0,0 +1,133 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str +import sys +import logging +import hashlib +from binascii import hexlify, unhexlify +from .account import PrivateKey +from .base58 import Base58, base58decode +from .py23 import py23_bytes, bytes_types, integer_types, string_types, text_type +log = logging.getLogger(__name__) + +try: + from Cryptodome.Cipher import AES +except ImportError: + try: + from Crypto.Cipher import AES + except ImportError: + raise ImportError("Missing dependency: pyCryptodome") + +SCRYPT_MODULE = None +if not SCRYPT_MODULE: + try: + import scrypt + SCRYPT_MODULE = "scrypt" + except ImportError: + try: + import pylibscrypt as scrypt + SCRYPT_MODULE = "pylibscrypt" + except ImportError: + raise ImportError( + "Missing dependency: scrypt or pylibscrypt" + ) + +log.debug("Using scrypt module: %s" % SCRYPT_MODULE) + + +class SaltException(Exception): + pass + + +def _encrypt_xor(a, b, aes): + """ Returns encrypt(a ^ b). """ + a = unhexlify('%0.32x' % (int((a), 16) ^ int(hexlify(b), 16))) + return aes.encrypt(a) + + +def encrypt(privkey, passphrase): + """ BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted privkey. + + :param privkey: Private key + :type privkey: Base58 + :param str passphrase: UTF-8 encoded passphrase for encryption + :return: BIP0038 non-ec-multiply encrypted wif key + :rtype: Base58 + + """ + privkeyhex = repr(privkey) # hex + addr = format(privkey.uncompressed.address, "BTC") + a = py23_bytes(addr, 'ascii') + salt = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4] + if sys.version < '3': + if isinstance(passphrase, text_type): + passphrase = passphrase.encode("utf-8") + + if SCRYPT_MODULE == "scrypt": + key = scrypt.hash(passphrase, salt, 16384, 8, 8) + elif SCRYPT_MODULE == "pylibscrypt": + key = scrypt.scrypt(py23_bytes(passphrase, "utf-8"), salt, 16384, 8, 8) + else: + raise ValueError("No scrypt module loaded") + (derived_half1, derived_half2) = (key[:32], key[32:]) + aes = AES.new(derived_half2, AES.MODE_ECB) + encrypted_half1 = _encrypt_xor(privkeyhex[:32], derived_half1[:16], aes) + encrypted_half2 = _encrypt_xor(privkeyhex[32:], derived_half1[16:], aes) + " flag byte is forced 0xc0 because Graphene only uses compressed keys " + payload = (b'\x01' + b'\x42' + b'\xc0' + + salt + encrypted_half1 + encrypted_half2) + " Checksum " + checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] + privatkey = hexlify(payload + checksum).decode('ascii') + return Base58(privatkey) + + +def decrypt(encrypted_privkey, passphrase): + """BIP0038 non-ec-multiply decryption. Returns WIF privkey. + + :param Base58 encrypted_privkey: Private key + :param str passphrase: UTF-8 encoded passphrase for decryption + :return: BIP0038 non-ec-multiply decrypted key + :rtype: Base58 + :raises SaltException: if checksum verification failed (e.g. wrong password) + + """ + + d = unhexlify(base58decode(encrypted_privkey)) + d = d[2:] # remove trailing 0x01 and 0x42 + flagbyte = d[0:1] # get flag byte + d = d[1:] # get payload + if not flagbyte == b'\xc0': + raise AssertionError("Flagbyte has to be 0xc0") + salt = d[0:4] + d = d[4:-4] + if sys.version < '3': + if isinstance(passphrase, text_type): + passphrase = passphrase.encode("utf-8") + if SCRYPT_MODULE == "scrypt": + key = scrypt.hash(passphrase, salt, 16384, 8, 8) + elif SCRYPT_MODULE == "pylibscrypt": + key = scrypt.scrypt(py23_bytes(passphrase, "utf-8"), salt, 16384, 8, 8) + else: + raise ValueError("No scrypt module loaded") + derivedhalf1 = key[0:32] + derivedhalf2 = key[32:64] + encryptedhalf1 = d[0:16] + encryptedhalf2 = d[16:32] + aes = AES.new(derivedhalf2, AES.MODE_ECB) + decryptedhalf2 = aes.decrypt(encryptedhalf2) + decryptedhalf1 = aes.decrypt(encryptedhalf1) + privraw = decryptedhalf1 + decryptedhalf2 + privraw = ('%064x' % (int(hexlify(privraw), 16) ^ + int(hexlify(derivedhalf1), 16))) + wif = Base58(privraw) + """ Verify Salt """ + privkey = PrivateKey(format(wif, "wif")) + addr = format(privkey.uncompressed.address, "BTC") + a = py23_bytes(addr, 'ascii') + saltverify = hashlib.sha256(hashlib.sha256(a).digest()).digest()[0:4] + if saltverify != salt: + raise SaltException('checksum verification failed! Password may be incorrect.') + return wif diff --git a/dpaycligraphenebase/chains.py b/dpaycligraphenebase/chains.py new file mode 100755 index 0000000..6aefc71 --- /dev/null +++ b/dpaycligraphenebase/chains.py @@ -0,0 +1,48 @@ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +default_prefix = "DWB" +known_chains = { + "DPAY": { + "chain_id": "38f14b346eb697ba04ae0f5adcfaa0a437ed3711197704aa256a14cb9b4a8f26", + "min_version": '0.19.6', + "prefix": "DWB", + "chain_assets": [ + {"asset": "BBD", "symbol": "BBD", "precision": 3, "id": 0}, + {"asset": "BEX", "symbol": "BEX", "precision": 3, "id": 1}, + {"asset": "VESTS", "symbol": "VESTS", "precision": 6, "id": 2} + ], + }, + "DPAYAPPBASE": { + "chain_id": "38f14b346eb697ba04ae0f5adcfaa0a437ed3711197704aa256a14cb9b4a8f26", + "min_version": '0.19.10', + "prefix": "DWB", + "chain_assets": [ + {"asset": "@@000000013", "symbol": "BBD", "precision": 3, "id": 0}, + {"asset": "@@000000021", "symbol": "BEX", "precision": 3, "id": 1}, + {"asset": "@@000000037", "symbol": "VESTS", "precision": 6, "id": 2} + ], + }, + "DPAYZERO": { + "chain_id": "38f14b346eb697ba04ae0f5adcfaa0a437ed3711197704aa256a14cb9b4a8f26", + "min_version": '0.0.0', + "prefix": "DWB", + "chain_assets": [ + {"asset": "BBD", "symbol": "BBD", "precision": 3, "id": 0}, + {"asset": "BEX", "symbol": "BEX", "precision": 3, "id": 1}, + {"asset": "VESTS", "symbol": "VESTS", "precision": 6, "id": 2} + ], + }, + "TESTNET": { + "chain_id": "79276aea5d4877d9a25892eaa01b0adf019d3e5cb12a97478df3298ccdd01673", + "min_version": '0.0.0', + "prefix": "DWT", + "chain_assets": [ + {"asset": "BBD", "symbol": "BBD", "precision": 3, "id": 0}, + {"asset": "BET", "symbol": "BET", "precision": 3, "id": 1}, + {"asset": "VESTS", "symbol": "VESTS", "precision": 6, "id": 2} + ], + } +} diff --git a/dpaycligraphenebase/dictionary.py b/dpaycligraphenebase/dictionary.py new file mode 100755 index 0000000..6b95456 --- /dev/null +++ b/dpaycligraphenebase/dictionary.py @@ -0,0 +1,2 @@ +# This Python file uses the following encoding: utf-8 +words = "a,aa,aal,aalii,aam,aba,abac,abaca,abacate,abacay,abacist,aback,abactor,abacus,abaff,abaft,abaiser,abalone,abandon,abas,abase,abased,abaser,abash,abashed,abasia,abasic,abask,abate,abater,abatis,abaton,abator,abature,abave,abaxial,abaxile,abaze,abb,abbacy,abbas,abbasi,abbassi,abbess,abbey,abbot,abbotcy,abdal,abdat,abdest,abdomen,abduce,abduct,abeam,abear,abed,abeigh,abele,abelite,abet,abettal,abettor,abey,abeyant,abfarad,abhenry,abhor,abidal,abide,abider,abidi,abiding,abietic,abietin,abigail,abigeat,abigeus,abilao,ability,abilla,abilo,abiosis,abiotic,abir,abiston,abiuret,abject,abjoint,abjudge,abjure,abjurer,abkar,abkari,ablach,ablare,ablate,ablator,ablaut,ablaze,able,ableeze,abler,ablest,ablins,abloom,ablow,ablude,abluent,ablush,ably,abmho,abnet,aboard,abode,abody,abohm,aboil,abolish,abolla,aboma,abomine,aboon,aborad,aboral,abord,abort,aborted,abortin,abortus,abound,about,abouts,above,abox,abrade,abrader,abraid,abrasax,abrase,abrash,abraum,abraxas,abreact,abreast,abret,abrico,abridge,abrim,abrin,abroach,abroad,abrook,abrupt,abscess,abscind,abscise,absciss,abscond,absence,absent,absit,absmho,absohm,absolve,absorb,absorpt,abstain,absume,absurd,absvolt,abthain,abu,abucco,abulia,abulic,abuna,abura,aburban,aburst,aburton,abuse,abusee,abuser,abusion,abusive,abut,abuttal,abutter,abuzz,abvolt,abwab,aby,abysm,abysmal,abyss,abyssal,acaciin,acacin,academe,academy,acajou,acaleph,acana,acanth,acantha,acapnia,acapu,acara,acardia,acari,acarian,acarid,acarine,acaroid,acarol,acate,acatery,acaudal,acca,accede,acceder,accend,accent,accept,accerse,access,accidia,accidie,accinge,accite,acclaim,accloy,accoast,accoil,accolle,accompt,accord,accost,account,accoy,accrete,accrual,accrue,accruer,accurse,accusal,accuse,accused,accuser,ace,acedia,acedy,acephal,acerate,acerb,acerbic,acerdol,acerin,acerose,acerous,acerra,aceship,acetal,acetate,acetic,acetify,acetin,acetize,acetoin,acetol,acetone,acetose,acetous,acetum,acetyl,ach,achage,achar,achate,ache,achene,acher,achete,achieve,achigan,achill,achime,aching,achira,acholia,acholic,achor,achree,achroma,achtel,achy,achylia,achymia,acicula,acid,acider,acidic,acidify,acidite,acidity,acidize,acidly,acidoid,acidyl,acier,aciform,acinar,acinary,acinic,acinose,acinous,acinus,aciurgy,acker,ackey,ackman,acknow,acle,aclinal,aclinic,acloud,aclys,acmatic,acme,acmic,acmite,acne,acnemia,acnodal,acnode,acock,acocotl,acoin,acoine,acold,acology,acolous,acolyte,acoma,acomia,acomous,acone,aconic,aconin,aconine,aconite,acopic,acopon,acor,acorea,acoria,acorn,acorned,acosmic,acouasm,acouchi,acouchy,acoupa,acquest,acquire,acquist,acquit,acracy,acraein,acrasia,acratia,acrawl,acraze,acre,acreage,acreak,acream,acred,acreman,acrid,acridan,acridic,acridly,acridyl,acrinyl,acrisia,acritan,acrite,acritol,acroama,acrobat,acrogen,acron,acronyc,acronym,acronyx,acrook,acrose,across,acrotic,acryl,acrylic,acrylyl,act,acta,actable,actify,actin,actinal,actine,acting,actinic,actinon,action,active,activin,actless,acton,actor,actress,actu,actual,actuary,acture,acuate,acuity,aculea,aculeus,acumen,acushla,acutate,acute,acutely,acutish,acyclic,acyesis,acyetic,acyl,acylate,acyloin,acyloxy,acystia,ad,adactyl,adad,adage,adagial,adagio,adamant,adamas,adamine,adamite,adance,adangle,adapid,adapt,adapter,adaptor,adarme,adat,adati,adatom,adaunt,adaw,adawe,adawlut,adawn,adaxial,aday,adays,adazzle,adcraft,add,adda,addable,addax,added,addedly,addend,addenda,adder,addible,addict,addle,addlins,address,addrest,adduce,adducer,adduct,ade,adead,adeem,adeep,adeling,adelite,adenase,adenia,adenine,adenoid,adenoma,adenose,adenyl,adept,adermia,adermin,adet,adevism,adfix,adhaka,adharma,adhere,adherer,adhibit,adiate,adicity,adieu,adieux,adinole,adion,adipate,adipic,adipoid,adipoma,adipose,adipous,adipsia,adipsic,adipsy,adipyl,adit,adital,aditus,adjag,adject,adjiger,adjoin,adjoint,adjourn,adjudge,adjunct,adjure,adjurer,adjust,adlay,adless,adlet,adman,admi,admiral,admire,admired,admirer,admit,admix,adnate,adnex,adnexal,adnexed,adnoun,ado,adobe,adonin,adonite,adonize,adopt,adopted,adoptee,adopter,adoral,adorant,adore,adorer,adorn,adorner,adossed,adoulie,adown,adoxy,adoze,adpao,adpress,adread,adream,adreamt,adrenal,adrenin,adrift,adrip,adroit,adroop,adrop,adrowse,adrue,adry,adsbud,adsmith,adsorb,adtevac,adular,adulate,adult,adulter,adunc,adusk,adust,advance,advene,adverb,adverse,advert,advice,advisal,advise,advised,advisee,adviser,advisor,advowee,ady,adynamy,adyta,adyton,adytum,adz,adze,adzer,adzooks,ae,aecial,aecium,aedile,aedilic,aefald,aefaldy,aefauld,aegis,aenach,aenean,aeneous,aeolid,aeolina,aeoline,aeon,aeonial,aeonian,aeonist,aer,aerage,aerate,aerator,aerial,aeric,aerical,aerie,aeried,aerify,aero,aerobe,aerobic,aerobus,aerogel,aerogen,aerogun,aeronat,aeronef,aerose,aerosol,aerugo,aery,aes,aevia,aface,afaint,afar,afara,afear,afeard,afeared,afernan,afetal,affa,affable,affably,affair,affaite,affect,affeer,affeir,affiant,affinal,affine,affined,affirm,affix,affixal,affixer,afflict,afflux,afforce,afford,affray,affront,affuse,affy,afghani,afield,afire,aflame,aflare,aflat,aflaunt,aflight,afloat,aflow,aflower,aflush,afoam,afoot,afore,afoul,afraid,afreet,afresh,afret,afront,afrown,aft,aftaba,after,aftergo,aftmost,aftosa,aftward,aga,again,against,agal,agalaxy,agalite,agallop,agalma,agama,agamete,agami,agamian,agamic,agamid,agamoid,agamont,agamous,agamy,agape,agapeti,agar,agaric,agarita,agarwal,agasp,agate,agathin,agatine,agatize,agatoid,agaty,agavose,agaze,agazed,age,aged,agedly,agee,ageless,agelong,agen,agency,agenda,agendum,agent,agentry,ager,ageusia,ageusic,agger,aggrade,aggrate,aggress,aggroup,aggry,aggur,agha,aghanee,aghast,agile,agilely,agility,aging,agio,agist,agistor,agitant,agitate,agla,aglance,aglare,agleaf,agleam,aglet,agley,aglint,aglow,aglucon,agnail,agname,agnamed,agnate,agnatic,agnel,agnize,agnomen,agnosia,agnosis,agnosy,agnus,ago,agog,agoge,agogic,agogics,agoho,agoing,agon,agonal,agone,agonic,agonied,agonist,agonium,agonize,agony,agora,agouara,agouta,agouti,agpaite,agrah,agral,agre,agree,agreed,agreer,agrege,agria,agrin,agrise,agrito,agroan,agrom,agroof,agrope,aground,agrufe,agruif,agsam,agua,ague,aguey,aguish,agunah,agush,agust,agy,agynary,agynous,agyrate,agyria,ah,aha,ahaaina,ahaunch,ahead,aheap,ahem,ahey,ahimsa,ahind,ahint,ahmadi,aho,ahong,ahorse,ahoy,ahsan,ahu,ahuatle,ahull,ahum,ahungry,ahunt,ahura,ahush,ahwal,ahypnia,ai,aid,aidable,aidance,aidant,aide,aider,aidful,aidless,aiel,aiglet,ail,ailanto,aile,aileron,ailette,ailing,aillt,ailment,ailsyte,ailuro,ailweed,aim,aimara,aimer,aimful,aiming,aimless,ainaleh,ainhum,ainoi,ainsell,aint,aion,aionial,air,airable,airampo,airan,aircrew,airdock,airdrop,aire,airer,airfoil,airhead,airily,airing,airish,airless,airlift,airlike,airmail,airman,airmark,airpark,airport,airship,airsick,airt,airward,airway,airy,aisle,aisled,aisling,ait,aitch,aitesis,aition,aiwan,aizle,ajaja,ajangle,ajar,ajari,ajava,ajhar,ajivika,ajog,ajoint,ajowan,ak,aka,akala,akaroa,akasa,akazga,akcheh,ake,akeake,akebi,akee,akeki,akeley,akepiro,akerite,akey,akhoond,akhrot,akhyana,akia,akimbo,akin,akindle,akinete,akmudar,aknee,ako,akoasm,akoasma,akonge,akov,akpek,akra,aku,akule,akund,al,ala,alacha,alack,alada,alaihi,alaite,alala,alalite,alalus,alameda,alamo,alamoth,alan,aland,alangin,alani,alanine,alannah,alantic,alantin,alantol,alanyl,alar,alares,alarm,alarmed,alarum,alary,alas,alate,alated,alatern,alation,alb,alba,alban,albarco,albata,albe,albedo,albee,albeit,albetad,albify,albinal,albinic,albino,albite,albitic,albugo,album,albumen,albumin,alburn,albus,alcaide,alcalde,alcanna,alcazar,alchemy,alchera,alchimy,alchymy,alcine,alclad,alco,alcoate,alcogel,alcohol,alcosol,alcove,alcyon,aldane,aldazin,aldehol,alder,aldern,aldim,aldime,aldine,aldol,aldose,ale,aleak,alec,alecize,alecost,alecup,alee,alef,aleft,alegar,alehoof,alem,alemana,alembic,alemite,alemmal,alen,aleph,alephs,alepole,alepot,alerce,alerse,alert,alertly,alesan,aletap,alette,alevin,alewife,alexia,alexic,alexin,aleyard,alf,alfa,alfaje,alfalfa,alfaqui,alfet,alfiona,alfonso,alforja,alga,algae,algal,algalia,algate,algebra,algedo,algesia,algesic,algesis,algetic,algic,algid,algific,algin,algine,alginic,algist,algoid,algor,algosis,algous,algum,alhenna,alias,alibi,alible,alichel,alidade,alien,aliency,alienee,aliener,alienor,alif,aliform,alight,align,aligner,aliipoe,alike,alima,aliment,alimony,alin,aliofar,alipata,aliped,aliptes,aliptic,aliquot,alish,alisier,alismad,alismal,aliso,alison,alisp,alist,alit,alite,aliunde,alive,aliyah,alizari,aljoba,alk,alkali,alkalic,alkamin,alkane,alkanet,alkene,alkenna,alkenyl,alkide,alkine,alkool,alkoxy,alkoxyl,alky,alkyd,alkyl,alkylic,alkyne,all,allan,allay,allayer,allbone,allege,alleger,allegro,allele,allelic,allene,aller,allergy,alley,alleyed,allgood,allheal,allice,allied,allies,allness,allonym,alloquy,allose,allot,allotee,allover,allow,allower,alloxan,alloy,allseed,alltud,allude,allure,allurer,alluvia,allwork,ally,allyl,allylic,alma,almadia,almadie,almagra,almanac,alme,almemar,almique,almirah,almoign,almon,almond,almondy,almoner,almonry,almost,almous,alms,almsful,almsman,almuce,almud,almude,almug,almuten,aln,alnage,alnager,alnein,alnico,alnoite,alnuin,alo,alochia,alod,alodial,alodian,alodium,alody,aloe,aloed,aloesol,aloetic,aloft,alogia,alogism,alogy,aloid,aloin,aloma,alone,along,alongst,aloof,aloofly,aloose,alop,alopeke,alose,aloud,alow,alowe,alp,alpaca,alpeen,alpha,alphol,alphorn,alphos,alphyl,alpieu,alpine,alpist,alquier,alraun,already,alright,alroot,alruna,also,alsoon,alt,altaite,altar,altared,alter,alterer,altern,alterne,althea,althein,altho,althorn,altilik,altin,alto,altoun,altrose,altun,aludel,alula,alular,alulet,alum,alumic,alumina,alumine,alumish,alumite,alumium,alumna,alumnae,alumnal,alumni,alumnus,alunite,alupag,alure,aluta,alvar,alveary,alveloz,alveola,alveole,alveoli,alveus,alvine,alvite,alvus,alway,always,aly,alypin,alysson,am,ama,amaas,amadou,amaga,amah,amain,amakebe,amala,amalaka,amalgam,amaltas,amamau,amandin,amang,amani,amania,amanori,amanous,amapa,amar,amarin,amarine,amarity,amaroid,amass,amasser,amastia,amasty,amateur,amative,amatol,amatory,amaze,amazed,amazia,amazing,amba,ambage,ambalam,amban,ambar,ambaree,ambary,ambash,ambassy,ambatch,ambay,ambeer,amber,ambery,ambiens,ambient,ambier,ambit,ambital,ambitty,ambitus,amble,ambler,ambling,ambo,ambon,ambos,ambrain,ambrein,ambrite,ambroid,ambrose,ambry,ambsace,ambury,ambush,amchoor,ame,ameed,ameen,amelia,amellus,amelu,amelus,amen,amend,amende,amender,amends,amene,amenia,amenity,ament,amental,amentia,amentum,amerce,amercer,amerism,amesite,ametria,amgarn,amhar,amhran,ami,amiable,amiably,amianth,amic,amical,amice,amiced,amicron,amid,amidase,amidate,amide,amidic,amidid,amidide,amidin,amidine,amido,amidol,amidon,amidoxy,amidst,amil,amimia,amimide,amin,aminate,amine,amini,aminic,aminity,aminize,amino,aminoid,amir,amiray,amiss,amity,amixia,amla,amli,amlikar,amlong,amma,amman,ammelin,ammer,ammeter,ammine,ammo,ammonal,ammonia,ammonic,ammono,ammu,amnesia,amnesic,amnesty,amnia,amniac,amnic,amnion,amniote,amober,amobyr,amoeba,amoebae,amoeban,amoebic,amoebid,amok,amoke,amole,amomal,amomum,among,amongst,amor,amorado,amoraic,amoraim,amoral,amoret,amorism,amorist,amoroso,amorous,amorphy,amort,amotion,amotus,amount,amour,amove,ampalea,amper,ampere,ampery,amphid,amphide,amphora,amphore,ample,amplify,amply,ampoule,ampul,ampulla,amputee,ampyx,amra,amreeta,amrita,amsath,amsel,amt,amtman,amuck,amuguis,amula,amulet,amulla,amunam,amurca,amuse,amused,amusee,amuser,amusia,amusing,amusive,amutter,amuyon,amuyong,amuze,amvis,amy,amyelia,amyelic,amygdal,amyl,amylan,amylase,amylate,amylene,amylic,amylin,amylo,amyloid,amylom,amylon,amylose,amylum,amyous,amyrin,amyrol,amyroot,an,ana,anabata,anabo,anabong,anacara,anacard,anacid,anadem,anadrom,anaemia,anaemic,anagap,anagep,anagoge,anagogy,anagram,anagua,anahau,anal,analav,analgen,analgia,analgic,anally,analogy,analyse,analyst,analyze,anam,anama,anamite,anan,anana,ananas,ananda,ananym,anaphia,anapnea,anapsid,anaqua,anarch,anarchy,anareta,anarya,anatase,anatifa,anatine,anatomy,anatox,anatron,anaudia,anaxial,anaxon,anaxone,anay,anba,anbury,anchor,anchovy,ancient,ancile,ancilla,ancon,anconad,anconal,ancone,ancony,ancora,ancoral,and,anda,andante,andirin,andiron,andric,android,androl,andron,anear,aneath,anele,anemia,anemic,anemone,anemony,anend,anenst,anent,anepia,anergia,anergic,anergy,anerly,aneroid,anes,anesis,aneuria,aneuric,aneurin,anew,angaria,angary,angekok,angel,angelet,angelic,angelin,angelot,anger,angerly,angeyok,angico,angild,angili,angina,anginal,angioid,angioma,angle,angled,angler,angling,angloid,ango,angolar,angor,angrily,angrite,angry,angst,angster,anguid,anguine,anguis,anguish,angula,angular,anguria,anhang,anhima,anhinga,ani,anicut,anidian,aniente,anigh,anight,anights,anil,anilao,anilau,anile,anilic,anilid,anilide,aniline,anility,anilla,anima,animal,animate,anime,animi,animism,animist,animize,animous,animus,anion,anionic,anis,anisal,anisate,anise,aniseed,anisic,anisil,anisoin,anisole,anisoyl,anisum,anisyl,anither,anjan,ankee,anker,ankh,ankle,anklet,anklong,ankus,ankusha,anlace,anlaut,ann,anna,annal,annale,annals,annat,annates,annatto,anneal,annelid,annet,annex,annexa,annexal,annexer,annite,annona,annoy,annoyer,annual,annuary,annuent,annuity,annul,annular,annulet,annulus,anoa,anodal,anode,anodic,anodize,anodos,anodyne,anoesia,anoesis,anoetic,anoil,anoine,anoint,anole,anoli,anolian,anolyte,anomaly,anomite,anomy,anon,anonang,anonol,anonym,anonyma,anopia,anopsia,anorak,anorexy,anormal,anorth,anosmia,anosmic,another,anotia,anotta,anotto,anotus,anounou,anoxia,anoxic,ansa,ansar,ansate,ansu,answer,ant,anta,antacid,antal,antapex,antdom,ante,anteact,anteal,antefix,antenna,antes,antewar,anthela,anthem,anthema,anthemy,anther,anthill,anthine,anthoid,anthood,anthrax,anthrol,anthryl,anti,antiae,antiar,antic,antical,anticly,anticor,anticum,antifat,antigen,antigod,antihum,antiqua,antique,antired,antirun,antisun,antitax,antiwar,antiwit,antler,antlia,antling,antoeci,antonym,antra,antral,antre,antrin,antrum,antship,antu,antwise,anubing,anuloma,anuran,anuria,anuric,anurous,anury,anus,anusim,anvil,anxiety,anxious,any,anybody,anyhow,anyone,anyway,anyways,anywhen,anywhy,anywise,aogiri,aonach,aorist,aorta,aortal,aortic,aortism,aosmic,aoudad,apa,apace,apache,apadana,apagoge,apaid,apalit,apandry,apar,aparejo,apart,apasote,apatan,apathic,apathy,apatite,ape,apeak,apedom,apehood,apeiron,apelet,apelike,apeling,apepsia,apepsy,apeptic,aper,aperch,aperea,apert,apertly,apery,apetaly,apex,apexed,aphagia,aphakia,aphakic,aphasia,aphasic,aphemia,aphemic,aphesis,apheta,aphetic,aphid,aphides,aphidid,aphodal,aphodus,aphonia,aphonic,aphony,aphoria,aphotic,aphrite,aphtha,aphthic,aphylly,aphyric,apian,apiary,apiator,apicad,apical,apices,apicula,apiece,apieces,apii,apiin,apilary,apinch,aping,apinoid,apio,apioid,apiole,apiolin,apionol,apiose,apish,apishly,apism,apitong,apitpat,aplanat,aplasia,aplenty,aplite,aplitic,aplomb,aplome,apnea,apneal,apneic,apocarp,apocha,apocope,apod,apodal,apodan,apodema,apodeme,apodia,apodous,apogamy,apogeal,apogean,apogee,apogeic,apogeny,apohyal,apoise,apojove,apokrea,apolar,apology,aponia,aponic,apoop,apoplex,apopyle,aporia,aporose,aport,aposia,aposoro,apostil,apostle,apothem,apotome,apotype,apout,apozem,apozema,appall,apparel,appay,appeal,appear,appease,append,appet,appete,applaud,apple,applied,applier,applot,apply,appoint,apport,appose,apposer,apprend,apprise,apprize,approof,approve,appulse,apraxia,apraxic,apricot,apriori,apron,apropos,apse,apsidal,apsides,apsis,apt,apteral,apteran,aptly,aptness,aptote,aptotic,apulse,apyonin,apyrene,apyrexy,apyrous,aqua,aquabib,aquage,aquaria,aquatic,aquavit,aqueous,aquifer,aquiver,aquo,aquose,ar,ara,araba,araban,arabana,arabin,arabit,arable,araca,aracari,arachic,arachin,arad,arado,arain,arake,araliad,aralie,aralkyl,aramina,araneid,aranein,aranga,arango,arar,arara,ararao,arariba,araroba,arati,aration,aratory,arba,arbacin,arbalo,arbiter,arbor,arboral,arbored,arboret,arbute,arbutin,arbutus,arc,arca,arcade,arcana,arcanal,arcane,arcanum,arcate,arch,archae,archaic,arche,archeal,arched,archer,archery,arches,archeus,archfoe,archgod,archil,arching,archive,archly,archon,archont,archsee,archsin,archspy,archwag,archway,archy,arcing,arcked,arcking,arctian,arctic,arctiid,arctoid,arcual,arcuale,arcuate,arcula,ardeb,ardella,ardency,ardent,ardish,ardoise,ardor,ardri,ardu,arduous,are,area,areach,aread,areal,arear,areaway,arecain,ared,areek,areel,arefact,areito,arena,arenae,arend,areng,arenoid,arenose,arent,areola,areolar,areole,areolet,arete,argal,argala,argali,argans,argasid,argeers,argel,argenol,argent,arghan,arghel,arghool,argil,argo,argol,argolet,argon,argosy,argot,argotic,argue,arguer,argufy,argute,argyria,argyric,arhar,arhat,aria,aribine,aricine,arid,aridge,aridian,aridity,aridly,ariel,arienzo,arietta,aright,arigue,aril,ariled,arillus,ariose,arioso,ariot,aripple,arisard,arise,arisen,arist,arista,arite,arjun,ark,arkite,arkose,arkosic,arles,arm,armada,armbone,armed,armer,armet,armful,armhole,armhoop,armied,armiger,armil,armilla,arming,armless,armlet,armload,armoire,armor,armored,armorer,armory,armpit,armrack,armrest,arms,armscye,armure,army,arn,arna,arnee,arni,arnica,arnotta,arnotto,arnut,aroar,aroast,arock,aroeira,aroid,aroint,arolium,arolla,aroma,aroon,arose,around,arousal,arouse,arouser,arow,aroxyl,arpen,arpent,arrack,arrah,arraign,arrame,arrange,arrant,arras,arrased,arratel,arrau,array,arrayal,arrayer,arrear,arrect,arrent,arrest,arriage,arriba,arride,arridge,arrie,arriere,arrimby,arris,arrish,arrival,arrive,arriver,arroba,arrope,arrow,arrowed,arrowy,arroyo,arse,arsenal,arsenic,arseno,arsenyl,arses,arsheen,arshin,arshine,arsine,arsinic,arsino,arsis,arsle,arsoite,arson,arsonic,arsono,arsyl,art,artaba,artabe,artal,artar,artel,arterin,artery,artful,artha,arthel,arthral,artiad,article,artisan,artist,artiste,artless,artlet,artlike,artware,arty,aru,arui,aruke,arumin,arupa,arusa,arusha,arustle,arval,arvel,arx,ary,aryl,arylate,arzan,arzun,as,asaddle,asak,asale,asana,asaphia,asaphid,asaprol,asarite,asaron,asarone,asbest,asbolin,ascan,ascare,ascarid,ascaron,ascend,ascent,ascetic,ascham,asci,ascian,ascii,ascites,ascitic,asclent,ascoma,ascon,ascot,ascribe,ascript,ascry,ascula,ascus,asdic,ase,asearch,aseethe,aseity,asem,asemia,asepsis,aseptic,aseptol,asexual,ash,ashake,ashame,ashamed,ashamnu,ashcake,ashen,asherah,ashery,ashes,ashet,ashily,ashine,ashiver,ashkoko,ashlar,ashless,ashling,ashman,ashore,ashpan,ashpit,ashraf,ashrafi,ashur,ashweed,ashwort,ashy,asialia,aside,asideu,asiento,asilid,asimen,asimmer,asinego,asinine,asitia,ask,askable,askance,askant,askar,askari,asker,askew,askip,asklent,askos,aslant,aslaver,asleep,aslop,aslope,asmack,asmalte,asmear,asmile,asmoke,asnort,asoak,asocial,asok,asoka,asonant,asonia,asop,asor,asouth,asp,aspace,aspect,aspen,asper,asperge,asperse,asphalt,asphyxy,aspic,aspire,aspirer,aspirin,aspish,asport,aspout,asprawl,aspread,aspring,asprout,asquare,asquat,asqueal,asquint,asquirm,ass,assacu,assagai,assai,assail,assapan,assart,assary,assate,assault,assaut,assay,assayer,assbaa,asse,assegai,asself,assent,assert,assess,asset,assets,assever,asshead,assi,assify,assign,assilag,assis,assise,assish,assist,assize,assizer,assizes,asslike,assman,assoil,assort,assuade,assuage,assume,assumed,assumer,assure,assured,assurer,assurge,ast,asta,astalk,astare,astart,astasia,astatic,astay,asteam,asteep,asteer,asteism,astelic,astely,aster,asteria,asterin,astern,astheny,asthma,asthore,astilbe,astint,astir,astite,astomia,astony,astoop,astor,astound,astrain,astral,astrand,astray,astream,astrer,astrict,astride,astrier,astrild,astroid,astrut,astute,astylar,asudden,asunder,aswail,aswarm,asway,asweat,aswell,aswim,aswing,aswirl,aswoon,asyla,asylum,at,atabal,atabeg,atabek,atactic,atafter,ataman,atangle,atap,ataraxy,ataunt,atavi,atavic,atavism,atavist,atavus,ataxia,ataxic,ataxite,ataxy,atazir,atbash,ate,atebrin,atechny,ateeter,atef,atelets,atelier,atelo,ates,ateuchi,athanor,athar,atheism,atheist,atheize,athelia,athenee,athenor,atheous,athing,athirst,athlete,athodyd,athort,athrill,athrive,athrob,athrong,athwart,athymia,athymic,athymy,athyria,athyrid,atilt,atimon,atinga,atingle,atinkle,atip,atis,atlas,atlatl,atle,atlee,atloid,atma,atman,atmid,atmo,atmos,atocha,atocia,atokal,atoke,atokous,atoll,atom,atomerg,atomic,atomics,atomism,atomist,atomity,atomize,atomy,atonal,atone,atoner,atonia,atonic,atony,atop,atophan,atopic,atopite,atopy,atour,atoxic,atoxyl,atrail,atrepsy,atresia,atresic,atresy,atretic,atria,atrial,atrip,atrium,atrocha,atropal,atrophy,atropia,atropic,atrous,atry,atta,attacco,attach,attache,attack,attacus,attagen,attain,attaint,attaleh,attar,attask,attempt,attend,attent,atter,attern,attery,attest,attic,attid,attinge,attire,attired,attirer,attorn,attract,attrap,attrist,attrite,attune,atule,atumble,atune,atwain,atweel,atween,atwin,atwirl,atwist,atwitch,atwixt,atwo,atypic,atypy,auantic,aube,aubrite,auburn,auca,auchlet,auction,aucuba,audible,audibly,audient,audile,audio,audion,audit,auditor,auge,augen,augend,auger,augerer,augh,aught,augite,augitic,augment,augur,augural,augury,august,auh,auhuhu,auk,auklet,aula,aulae,auld,auletai,aulete,auletes,auletic,aulic,auloi,aulos,aulu,aum,aumaga,aumail,aumbry,aumery,aumil,aumous,aumrie,auncel,aune,aunt,auntie,auntish,auntly,aupaka,aura,aurae,aural,aurally,aurar,aurate,aurated,aureate,aureity,aurelia,aureola,aureole,aureous,auresca,aureus,auric,auricle,auride,aurific,aurify,aurigal,aurin,aurir,aurist,aurite,aurochs,auronal,aurora,aurorae,auroral,aurore,aurous,aurum,aurure,auryl,auscult,auslaut,auspex,auspice,auspicy,austere,austral,ausu,ausubo,autarch,autarky,aute,autecy,autem,author,autism,autist,auto,autobus,autocab,autocar,autoecy,autoist,automa,automat,autonym,autopsy,autumn,auxesis,auxetic,auxin,auxinic,auxotox,ava,avadana,avahi,avail,aval,avalent,avania,avarice,avast,avaunt,ave,avellan,aveloz,avenage,avener,avenge,avenger,avenin,avenous,avens,avenue,aver,avera,average,averah,averil,averin,averral,averse,avert,averted,averter,avian,aviary,aviate,aviatic,aviator,avichi,avicide,avick,avid,avidity,avidly,avidous,avidya,avigate,avijja,avine,aviso,avital,avitic,avives,avo,avocado,avocate,avocet,avodire,avoid,avoider,avolate,avouch,avow,avowal,avowant,avowed,avower,avowry,avoyer,avulse,aw,awa,awabi,awaft,awag,await,awaiter,awake,awaken,awald,awalim,awalt,awane,awapuhi,award,awarder,aware,awash,awaste,awat,awatch,awater,awave,away,awber,awd,awe,aweary,aweband,awee,aweek,aweel,aweigh,awesome,awest,aweto,awfu,awful,awfully,awheel,awheft,awhet,awhile,awhir,awhirl,awide,awiggle,awin,awing,awink,awiwi,awkward,awl,awless,awlwort,awmous,awn,awned,awner,awning,awnless,awnlike,awny,awoke,awork,awreck,awrist,awrong,awry,ax,axal,axe,axed,axenic,axes,axfetch,axhead,axial,axially,axiate,axiform,axil,axile,axilla,axillae,axillar,axine,axinite,axiom,axion,axis,axised,axite,axle,axled,axmaker,axman,axogamy,axoid,axolotl,axon,axonal,axonost,axseed,axstone,axtree,axunge,axweed,axwise,axwort,ay,ayah,aye,ayelp,ayin,ayless,aylet,ayllu,ayond,ayont,ayous,ayu,azafrin,azalea,azarole,azelaic,azelate,azide,azilut,azimene,azimide,azimine,azimino,azimuth,azine,aziola,azo,azoch,azofier,azofy,azoic,azole,azon,azonal,azonic,azonium,azophen,azorite,azotate,azote,azoted,azoth,azotic,azotine,azotite,azotize,azotous,azox,azoxime,azoxine,azoxy,azteca,azulene,azulite,azulmic,azumbre,azure,azurean,azured,azurine,azurite,azurous,azury,azygos,azygous,azyme,azymite,azymous,b,ba,baa,baal,baar,baba,babai,babasco,babassu,babbitt,babble,babbler,babbly,babby,babe,babelet,babery,babiche,babied,babish,bablah,babloh,baboen,baboo,baboon,baboot,babroot,babu,babudom,babuina,babuism,babul,baby,babydom,babyish,babyism,bac,bacaba,bacach,bacalao,bacao,bacca,baccae,baccara,baccate,bacchar,bacchic,bacchii,bach,bache,bachel,bacilli,back,backage,backcap,backed,backen,backer,backet,backie,backing,backjaw,backlet,backlog,backrun,backsaw,backset,backup,backway,baclin,bacon,baconer,bacony,bacula,bacule,baculi,baculum,baculus,bacury,bad,badan,baddish,baddock,bade,badge,badger,badiaga,badian,badious,badland,badly,badness,bae,baetuli,baetyl,bafaro,baff,baffeta,baffle,baffler,baffy,baft,bafta,bag,baga,bagani,bagasse,bagel,bagful,baggage,baggala,bagged,bagger,baggie,baggily,bagging,baggit,baggy,baglike,bagman,bagnio,bagnut,bago,bagonet,bagpipe,bagre,bagreef,bagroom,bagwig,bagworm,bagwyn,bah,bahan,bahar,bahay,bahera,bahisti,bahnung,baho,bahoe,bahoo,baht,bahur,bahut,baignet,baikie,bail,bailage,bailee,bailer,bailey,bailie,bailiff,bailor,bain,bainie,baioc,baiocco,bairagi,bairn,bairnie,bairnly,baister,bait,baiter,baith,baittle,baize,bajada,bajan,bajra,bajree,bajri,bajury,baka,bakal,bake,baked,baken,bakepan,baker,bakerly,bakery,bakie,baking,bakli,baktun,baku,bakula,bal,balafo,balagan,balai,balance,balanic,balanid,balao,balas,balata,balboa,balcony,bald,balden,balder,baldish,baldly,baldrib,baldric,baldy,bale,baleen,baleful,balei,baleise,baler,balete,bali,baline,balita,balk,balker,balky,ball,ballad,ballade,ballam,ballan,ballant,ballast,ballata,ballate,balldom,balled,baller,ballet,balli,ballist,ballium,balloon,ballot,ballow,ballup,bally,balm,balmily,balmony,balmy,balneal,balonea,baloney,baloo,balow,balsa,balsam,balsamo,balsamy,baltei,balter,balteus,balu,balut,balza,bam,bamban,bambini,bambino,bamboo,bamoth,ban,banaba,banago,banak,banal,banally,banana,banat,banc,banca,bancal,banchi,banco,bancus,band,banda,bandage,bandaka,bandala,bandar,bandbox,bande,bandeau,banded,bander,bandhu,bandi,bandie,banding,bandit,bandle,bandlet,bandman,bando,bandog,bandore,bandrol,bandy,bane,baneful,bang,banga,bange,banger,banghy,banging,bangkok,bangle,bangled,bani,banian,banig,banilad,banish,baniwa,baniya,banjo,banjore,banjuke,bank,banked,banker,bankera,banket,banking,bankman,banky,banner,bannet,banning,bannock,banns,bannut,banquet,banshee,bant,bantam,bantay,banteng,banter,bantery,banty,banuyo,banya,banyan,banzai,baobab,bap,baptism,baptize,bar,bara,barad,barauna,barb,barbal,barbary,barbas,barbate,barbe,barbed,barbel,barber,barbet,barbion,barblet,barbone,barbudo,barbule,bard,bardane,bardash,bardel,bardess,bardic,bardie,bardily,barding,bardish,bardism,bardlet,bardo,bardy,bare,bareca,barefit,barely,barer,baresma,baretta,barff,barfish,barfly,barful,bargain,barge,bargee,bargeer,barger,bargh,bargham,bari,baria,baric,barid,barie,barile,barilla,baring,baris,barish,barit,barite,barium,bark,barken,barker,barkery,barkey,barkhan,barking,barkle,barky,barless,barley,barling,barlock,barlow,barm,barmaid,barman,barmkin,barmote,barmy,barn,barnard,barney,barnful,barnman,barny,baroi,barolo,baron,baronet,barong,baronry,barony,baroque,baroto,barpost,barra,barrack,barrad,barrage,barras,barred,barrel,barren,barrer,barret,barrico,barrier,barring,barrio,barroom,barrow,barruly,barry,barse,barsom,barter,barth,barton,baru,baruria,barvel,barwal,barway,barways,barwise,barwood,barye,baryta,barytes,barytic,baryton,bas,basal,basale,basalia,basally,basalt,basaree,bascule,base,based,basely,baseman,basenji,bases,bash,bashaw,bashful,bashlyk,basial,basiate,basic,basidia,basify,basil,basilar,basilic,basin,basined,basinet,basion,basis,bask,basker,basket,basoid,bason,basos,basote,basque,basqued,bass,bassan,bassara,basset,bassie,bassine,bassist,basso,bassoon,bassus,bast,basta,bastard,baste,basten,baster,bastide,basting,bastion,bastite,basto,baston,bat,bataan,batad,batakan,batara,batata,batch,batcher,bate,batea,bateau,bateaux,bated,batel,bateman,bater,batfish,batfowl,bath,bathe,bather,bathic,bathing,bathman,bathmic,bathos,bathtub,bathyal,batik,batiker,bating,batino,batiste,batlan,batlike,batling,batlon,batman,batoid,baton,batonne,bats,batsman,batster,batt,batta,battel,batten,batter,battery,battik,batting,battish,battle,battled,battler,battue,batty,batule,batwing,batz,batzen,bauble,bauch,bauchle,bauckie,baud,baul,bauleah,baun,bauno,bauson,bausond,bauta,bauxite,bavaroy,bavary,bavian,baviere,bavin,bavoso,baw,bawbee,bawcock,bawd,bawdily,bawdry,bawl,bawler,bawley,bawn,bawtie,baxter,baxtone,bay,baya,bayal,bayamo,bayard,baybolt,baybush,baycuru,bayed,bayeta,baygall,bayhead,bayish,baylet,baylike,bayman,bayness,bayok,bayonet,bayou,baywood,bazaar,baze,bazoo,bazooka,bazzite,bdellid,be,beach,beached,beachy,beacon,bead,beaded,beader,beadily,beading,beadle,beadlet,beadman,beadrow,beady,beagle,beak,beaked,beaker,beakful,beaky,beal,beala,bealing,beam,beamage,beamed,beamer,beamful,beamily,beaming,beamish,beamlet,beamman,beamy,bean,beanbag,beancod,beanery,beanie,beano,beant,beany,bear,beard,bearded,bearder,beardie,beardom,beardy,bearer,bearess,bearing,bearish,bearlet,bearm,beast,beastie,beastly,beat,beata,beatae,beatee,beaten,beater,beath,beatify,beating,beatus,beau,beaufin,beauish,beauism,beauti,beauty,beaux,beaver,beavery,beback,bebait,bebang,bebar,bebaron,bebaste,bebat,bebathe,bebay,bebeast,bebed,bebeeru,bebilya,bebite,beblain,beblear,bebled,bebless,beblood,bebloom,bebog,bebop,beboss,bebotch,bebrave,bebrine,bebrush,bebump,bebusy,becall,becalm,becap,becard,becarve,becater,because,becense,bechalk,becharm,bechase,becheck,becher,bechern,bechirp,becivet,beck,becker,becket,beckon,beclad,beclang,beclart,beclasp,beclaw,becloak,beclog,becloud,beclout,beclown,becolme,becolor,become,becomes,becomma,becoom,becost,becovet,becram,becramp,becrawl,becreep,becrime,becroak,becross,becrowd,becrown,becrush,becrust,becry,becuiba,becuna,becurl,becurry,becurse,becut,bed,bedad,bedamn,bedamp,bedare,bedark,bedash,bedaub,bedawn,beday,bedaze,bedbug,bedcap,bedcase,bedcord,bedded,bedder,bedding,bedead,bedeaf,bedebt,bedeck,bedel,beden,bedene,bedevil,bedew,bedewer,bedfast,bedfoot,bedgery,bedgoer,bedgown,bedight,bedikah,bedim,bedin,bedip,bedirt,bedirty,bedizen,bedkey,bedlam,bedlar,bedless,bedlids,bedman,bedmate,bedog,bedolt,bedot,bedote,bedouse,bedown,bedoyo,bedpan,bedpost,bedrail,bedral,bedrape,bedress,bedrid,bedrift,bedrip,bedrock,bedroll,bedroom,bedrop,bedrown,bedrug,bedsick,bedside,bedsite,bedsock,bedsore,bedtick,bedtime,bedub,beduck,beduke,bedull,bedumb,bedunce,bedunch,bedung,bedur,bedusk,bedust,bedwarf,bedway,bedways,bedwell,bedye,bee,beearn,beech,beechen,beechy,beedged,beedom,beef,beefer,beefily,beefin,beefish,beefy,beehead,beeherd,beehive,beeish,beek,beekite,beelbow,beelike,beeline,beelol,dpayclian,been,beennut,beer,beerage,beerily,beerish,beery,bees,beest,beeswax,beet,beeth,beetle,beetled,beetler,beety,beeve,beevish,beeware,beeway,beeweed,beewise,beewort,befall,befame,befan,befancy,befavor,befilch,befile,befilth,befire,befist,befit,beflag,beflap,beflea,befleck,beflour,beflout,beflum,befoam,befog,befool,befop,before,befoul,befret,befrill,befriz,befume,beg,begad,begall,begani,begar,begari,begash,begat,begaud,begaudy,begay,begaze,begeck,begem,beget,beggar,beggary,begging,begift,begild,begin,begird,beglad,beglare,beglic,beglide,begloom,begloze,begluc,beglue,begnaw,bego,begob,begobs,begohm,begone,begonia,begorra,begorry,begoud,begowk,begrace,begrain,begrave,begray,begreen,begrett,begrim,begrime,begroan,begrown,beguard,beguess,beguile,beguine,begulf,begum,begun,begunk,begut,behale,behalf,behap,behave,behead,behear,behears,behedge,beheld,behelp,behen,behenic,behest,behind,behint,behn,behold,behoney,behoof,behoot,behoove,behorn,behowl,behung,behymn,beice,beige,being,beinked,beira,beisa,bejade,bejan,bejant,bejazz,bejel,bejewel,bejig,bekah,bekick,beking,bekiss,bekko,beknave,beknit,beknow,beknown,bel,bela,belabor,belaced,beladle,belady,belage,belah,belam,belanda,belar,belard,belash,belate,belated,belaud,belay,belayer,belch,belcher,beld,beldam,beleaf,beleap,beleave,belee,belfry,belga,belibel,belick,belie,belief,belier,believe,belight,beliked,belion,belite,belive,bell,bellboy,belle,belled,bellhop,bellied,belling,bellite,bellman,bellote,bellow,bellows,belly,bellyer,beloam,beloid,belong,belonid,belord,belout,belove,beloved,below,belsire,belt,belted,belter,beltie,beltine,belting,beltman,belton,beluga,belute,belve,bely,belying,bema,bemad,bemadam,bemail,bemaim,beman,bemar,bemask,bemat,bemata,bemaul,bemazed,bemeal,bemean,bemercy,bemire,bemist,bemix,bemoan,bemoat,bemock,bemoil,bemole,bemolt,bemoon,bemotto,bemoult,bemouth,bemuck,bemud,bemuddy,bemuse,bemused,bemusk,ben,bena,benab,bename,benami,benasty,benben,bench,bencher,benchy,bencite,bend,benda,bended,bender,bending,bendlet,bendy,bene,beneath,benefic,benefit,benempt,benet,beng,beni,benight,benign,benison,benj,benjy,benmost,benn,benne,bennel,bennet,benny,beno,benorth,benote,bensel,bensh,benshea,benshee,benshi,bent,bentang,benthal,benthic,benthon,benthos,benting,benty,benumb,benward,benweed,benzal,benzein,benzene,benzil,benzine,benzo,benzoic,benzoid,benzoin,benzol,benzole,benzoxy,benzoyl,benzyl,beode,bepaid,bepale,bepaper,beparch,beparse,bepart,bepaste,bepat,bepaw,bepearl,bepelt,bepen,bepewed,bepiece,bepile,bepill,bepinch,bepity,beprank,bepray,bepress,bepride,beprose,bepuff,bepun,bequalm,bequest,bequote,ber,berain,berakah,berake,berapt,berat,berate,beray,bere,bereave,bereft,berend,beret,berg,berger,berglet,bergut,bergy,bergylt,berhyme,beride,berinse,berith,berley,berlin,berline,berm,berne,berobed,beroll,beround,berret,berri,berried,berrier,berry,berseem,berserk,berth,berthed,berther,bertram,bertrum,berust,bervie,berycid,beryl,bes,besa,besagne,besaiel,besaint,besan,besauce,bescab,bescarf,bescent,bescorn,bescour,bescurf,beseam,besee,beseech,beseem,beseen,beset,beshade,beshag,beshake,beshame,beshear,beshell,beshine,beshlik,beshod,beshout,beshow,beshrew,beside,besides,besiege,besigh,besin,besing,besiren,besit,beslab,beslap,beslash,beslave,beslime,beslow,beslur,besmear,besmell,besmile,besmoke,besmut,besnare,besneer,besnow,besnuff,besogne,besoil,besom,besomer,besoot,besot,besoul,besour,bespate,bespawl,bespeak,besped,bespeed,bespell,bespend,bespete,bespew,bespice,bespill,bespin,bespit,besplit,bespoke,bespot,bespout,bespray,bespy,besquib,besra,best,bestab,bestain,bestamp,bestar,bestare,bestay,bestead,besteer,bester,bestial,bestick,bestill,bestink,bestir,bestock,bestore,bestorm,bestove,bestow,bestraw,bestrew,bestuck,bestud,besugar,besuit,besully,beswarm,beswim,bet,beta,betag,betail,betaine,betalk,betask,betaxed,betear,beteela,beteem,betel,beth,bethel,bethink,bethumb,bethump,betide,betimes,betinge,betire,betis,betitle,betoil,betoken,betone,betony,betoss,betowel,betrace,betrail,betrap,betray,betread,betrend,betrim,betroth,betrunk,betso,betted,better,betters,betting,bettong,bettor,betty,betulin,betutor,between,betwine,betwit,betwixt,beveil,bevel,beveled,beveler,bevenom,bever,beverse,beveto,bevined,bevomit,bevue,bevy,bewail,bewall,beware,bewash,bewaste,bewater,beweary,beweep,bewept,bewest,bewet,bewhig,bewhite,bewidow,bewig,bewired,bewitch,bewith,bework,beworm,beworn,beworry,bewrap,bewray,bewreck,bewrite,bey,beydom,beylic,beyond,beyship,bezant,bezanty,bezel,bezetta,bezique,bezoar,bezzi,bezzle,bezzo,bhabar,bhakta,bhakti,bhalu,bhandar,bhang,bhangi,bhara,bharal,bhat,bhava,bheesty,bhikku,bhikshu,bhoosa,bhoy,bhungi,bhut,biabo,biacid,biacuru,bialate,biallyl,bianco,biarchy,bias,biaxal,biaxial,bib,bibasic,bibb,bibber,bibble,bibbler,bibbons,bibcock,bibi,bibiri,bibless,biblus,bice,biceps,bicetyl,bichir,bichord,bichy,bick,bicker,bickern,bicolor,bicone,biconic,bicorn,bicorne,bicron,bicycle,bicyclo,bid,bidar,bidarka,bidcock,bidder,bidding,biddy,bide,bident,bider,bidet,biding,bidri,biduous,bield,bieldy,bien,bienly,biennia,bier,bietle,bifara,bifer,biff,biffin,bifid,bifidly,bifilar,biflex,bifocal,bifoil,bifold,bifolia,biform,bifront,big,biga,bigamic,bigamy,bigener,bigeye,bigg,biggah,biggen,bigger,biggest,biggin,biggish,bigha,bighead,bighorn,bight,biglot,bigness,bignou,bigot,bigoted,bigotry,bigotty,bigroot,bigwig,bija,bijasal,bijou,bijoux,bike,bikh,bikini,bilabe,bilalo,bilbie,bilbo,bilby,bilch,bilcock,bildar,bilders,bile,bilge,bilgy,biliary,biliate,bilic,bilify,bilimbi,bilio,bilious,bilith,bilk,bilker,bill,billa,billbug,billed,biller,billet,billety,billian,billing,billion,billman,billon,billot,billow,billowy,billy,billyer,bilo,bilobe,bilobed,bilsh,bilsted,biltong,bimalar,bimanal,bimane,bimasty,bimbil,bimeby,bimodal,bin,binal,binary,binate,bind,binder,bindery,binding,bindle,bindlet,bindweb,bine,bing,binge,bingey,binghi,bingle,bingo,bingy,binh,bink,binman,binna,binning,binnite,bino,binocle,binodal,binode,binotic,binous,bint,binukau,biod,biodyne,biogen,biogeny,bioherm,biolith,biology,biome,bion,bionomy,biopsic,biopsy,bioral,biorgan,bios,biose,biosis,biota,biotaxy,biotic,biotics,biotin,biotite,biotome,biotomy,biotope,biotype,bioxide,bipack,biparty,biped,bipedal,biphase,biplane,bipod,bipolar,biprism,biprong,birch,birchen,bird,birddom,birdeen,birder,birdie,birding,birdlet,birdman,birdy,bireme,biretta,biri,biriba,birk,birken,birkie,birl,birle,birler,birlie,birlinn,birma,birn,birny,birr,birse,birsle,birsy,birth,birthy,bis,bisabol,bisalt,biscuit,bisect,bisexed,bisext,bishop,bismar,bismite,bismuth,bisnaga,bison,bispore,bisque,bissext,bisson,bistate,bister,bisti,bistort,bistro,bit,bitable,bitch,bite,biter,biti,biting,bitless,bito,bitolyl,bitt,bitted,bitten,bitter,bittern,bitters,bittie,bittock,bitty,bitume,bitumed,bitumen,bitwise,bityite,bitypic,biune,biunial,biunity,biurate,biurea,biuret,bivalve,bivinyl,bivious,bivocal,bivouac,biwa,bixin,biz,bizarre,bizet,bizonal,bizone,bizz,blab,blabber,black,blacken,blacker,blackey,blackie,blackit,blackly,blacky,blad,bladder,blade,bladed,blader,blading,bladish,blady,blae,blaff,blaflum,blah,blain,blair,blake,blame,blamed,blamer,blaming,blan,blanc,blanca,blanch,blanco,bland,blanda,blandly,blank,blanked,blanket,blankly,blanky,blanque,blare,blarney,blarnid,blarny,blart,blas,blase,blash,blashy,blast,blasted,blaster,blastid,blastie,blasty,blat,blatant,blate,blately,blather,blatta,blatter,blatti,blattid,blaubok,blaver,blaw,blawort,blay,blaze,blazer,blazing,blazon,blazy,bleach,bleak,bleakly,bleaky,blear,bleared,bleary,bleat,bleater,bleaty,bleb,blebby,bleck,blee,bleed,bleeder,bleery,bleeze,bleezy,blellum,blemish,blench,blend,blende,blended,blender,blendor,blenny,blent,bleo,blesbok,bless,blessed,blesser,blest,blet,blewits,blibe,blick,blickey,blight,blighty,blimp,blimy,blind,blinded,blinder,blindly,blink,blinked,blinker,blinks,blinky,blinter,blintze,blip,bliss,blissom,blister,blite,blithe,blithen,blither,blitter,blitz,blizz,blo,bloat,bloated,bloater,blob,blobbed,blobber,blobby,bloc,block,blocked,blocker,blocky,blodite,bloke,blolly,blonde,blood,blooded,bloody,blooey,bloom,bloomer,bloomy,bloop,blooper,blore,blosmy,blossom,blot,blotch,blotchy,blotter,blotto,blotty,blouse,bloused,blout,blow,blowen,blower,blowfly,blowgun,blowing,blown,blowoff,blowout,blowth,blowup,blowy,blowze,blowzed,blowzy,blub,blubber,blucher,blue,bluecap,bluecup,blueing,blueleg,bluely,bluer,blues,bluet,bluetop,bluey,bluff,bluffer,bluffly,bluffy,bluggy,bluing,bluish,bluism,blunder,blunge,blunger,blunk,blunker,blunks,blunnen,blunt,blunter,bluntie,bluntly,blup,blur,blurb,blurred,blurrer,blurry,blurt,blush,blusher,blushy,bluster,blype,bo,boa,boagane,boar,board,boarder,boardly,boardy,boarish,boast,boaster,boat,boatage,boater,boatful,boatie,boating,boatlip,boatly,boatman,bob,boba,bobac,bobbed,bobber,bobbery,bobbin,bobbing,bobbish,bobble,bobby,bobcat,bobcoat,bobeche,bobfly,bobo,bobotie,bobsled,bobstay,bobtail,bobwood,bocal,bocardo,bocca,boccale,boccaro,bocce,boce,bocher,bock,bocking,bocoy,bod,bodach,bode,bodeful,bodega,boden,boder,bodge,bodger,bodgery,bodhi,bodice,bodiced,bodied,bodier,bodikin,bodily,boding,bodkin,bodle,bodock,body,bog,boga,bogan,bogard,bogart,bogey,boggart,boggin,boggish,boggle,boggler,boggy,boghole,bogie,bogier,bogland,bogle,boglet,bogman,bogmire,bogo,bogong,bogtrot,bogue,bogum,bogus,bogway,bogwood,bogwort,bogy,bogydom,bogyism,bohawn,bohea,boho,bohor,bohunk,boid,boil,boiled,boiler,boilery,boiling,boily,boist,bojite,bojo,bokadam,bokard,bokark,boke,bokom,bola,bolar,bold,bolden,boldine,boldly,boldo,bole,boled,boleite,bolero,bolete,bolide,bolimba,bolis,bolivar,bolivia,bolk,boll,bollard,bolled,boller,bolling,bollock,bolly,bolo,boloman,boloney,bolson,bolster,bolt,boltage,boltant,boltel,bolter,bolti,bolting,bolus,bom,boma,bomb,bombard,bombast,bombed,bomber,bombo,bombola,bombous,bon,bonaci,bonagh,bonaght,bonair,bonally,bonang,bonanza,bonasus,bonbon,bonce,bond,bondage,bondar,bonded,bonder,bonding,bondman,bonduc,bone,boned,bonedog,bonelet,boner,boneset,bonfire,bong,bongo,boniata,bonify,bonito,bonk,bonnaz,bonnet,bonnily,bonny,bonsai,bonus,bonxie,bony,bonze,bonzer,bonzery,bonzian,boo,boob,boobery,boobily,boobook,booby,bood,boodie,boodle,boodler,boody,boof,booger,boohoo,boojum,book,bookdom,booked,booker,bookery,bookful,bookie,booking,bookish,bookism,booklet,bookman,booky,bool,booly,boolya,boom,boomage,boomah,boomdas,boomer,booming,boomlet,boomy,boon,boonk,boopis,boor,boorish,boort,boose,boost,booster,boosy,boot,bootboy,booted,bootee,booter,bootery,bootful,booth,boother,bootied,booting,bootleg,boots,booty,booze,boozed,boozer,boozily,boozy,bop,bopeep,boppist,bopyrid,bor,bora,borable,boracic,borage,borak,boral,borasca,borate,borax,bord,bordage,bordar,bordel,border,bordure,bore,boread,boreal,borean,boredom,boree,boreen,boregat,boreism,borele,borer,borg,borgh,borh,boric,boride,borine,boring,borish,borism,bority,borize,borlase,born,borne,borneol,borning,bornite,bornyl,boro,boron,boronic,borough,borrel,borrow,borsch,borscht,borsht,bort,bortsch,borty,bortz,borwort,boryl,borzoi,boscage,bosch,bose,boser,bosh,bosher,bosk,bosker,bosket,bosky,bosn,bosom,bosomed,bosomer,bosomy,boss,bossage,bossdom,bossed,bosser,bosset,bossing,bossism,bosslet,bossy,boston,bostryx,bosun,bot,bota,botanic,botany,botargo,botch,botched,botcher,botchka,botchy,bote,botella,boterol,botfly,both,bother,bothros,bothway,bothy,botonee,botong,bott,bottine,bottle,bottled,bottler,bottom,botulin,bouchal,bouche,boucher,boud,boudoir,bougar,bouge,bouget,bough,boughed,bought,boughy,bougie,bouk,boukit,boulder,boule,boultel,boulter,boun,bounce,bouncer,bound,bounded,bounden,bounder,boundly,bounty,bouquet,bourbon,bourd,bourder,bourdon,bourg,bourn,bourock,bourse,bouse,bouser,bousy,bout,boutade,bouto,bouw,bovate,bovid,bovine,bovoid,bow,bowable,bowback,bowbent,bowboy,bowed,bowel,boweled,bowels,bower,bowery,bowet,bowfin,bowhead,bowie,bowing,bowk,bowkail,bowker,bowknot,bowl,bowla,bowleg,bowler,bowless,bowlful,bowlike,bowline,bowling,bowls,bowly,bowman,bowpin,bowshot,bowwood,bowwort,bowwow,bowyer,boxbush,boxcar,boxen,boxer,boxfish,boxful,boxhaul,boxhead,boxing,boxlike,boxman,boxty,boxwood,boxwork,boxy,boy,boyang,boyar,boyard,boycott,boydom,boyer,boyhood,boyish,boyism,boyla,boylike,boyship,boza,bozal,bozo,bozze,bra,brab,brabant,brabble,braca,braccia,braccio,brace,braced,bracer,bracero,braces,brach,brachet,bracing,brack,bracken,bracker,bracket,bracky,bract,bractea,bracted,brad,bradawl,bradsot,brae,braeman,brag,braggat,bragger,bragget,bragite,braid,braided,braider,brail,brain,brainer,brainge,brains,brainy,braird,brairo,braise,brake,braker,brakie,braky,bramble,brambly,bran,branch,branchi,branchy,brand,branded,brander,brandy,brangle,branial,brank,brankie,branle,branner,branny,bransle,brant,brash,brashy,brasque,brass,brasse,brasser,brasset,brassic,brassie,brassy,brat,brattie,brattle,brauna,bravade,bravado,brave,bravely,braver,bravery,braving,bravish,bravo,bravura,braw,brawl,brawler,brawly,brawlys,brawn,brawned,brawner,brawny,braws,braxy,bray,brayer,brayera,braza,braze,brazen,brazer,brazera,brazier,brazil,breach,breachy,bread,breaden,breadth,breaghe,break,breakax,breaker,breakup,bream,breards,breast,breath,breathe,breathy,breba,breccia,brecham,breck,brecken,bred,brede,bredi,bree,breech,breed,breeder,breedy,breek,breeze,breezy,bregma,brehon,brei,brekkle,brelaw,breme,bremely,brent,brephic,bret,breth,brett,breva,breve,brevet,brevier,brevit,brevity,brew,brewage,brewer,brewery,brewing,brewis,brewst,brey,briar,bribe,bribee,briber,bribery,brichen,brick,brickel,bricken,brickle,brickly,bricky,bricole,bridal,bridale,bride,bridely,bridge,bridged,bridger,bridle,bridled,bridler,bridoon,brief,briefly,briefs,brier,briered,briery,brieve,brig,brigade,brigand,bright,brill,brills,brim,brimful,briming,brimmed,brimmer,brin,brine,briner,bring,bringal,bringer,brinish,brinjal,brink,briny,brioche,brique,brisk,brisken,brisket,briskly,brisque,briss,bristle,bristly,brisure,brit,brith,brither,britska,britten,brittle,brizz,broach,broad,broadax,broaden,broadly,brob,brocade,brocard,broch,brochan,broche,brocho,brock,brocked,brocket,brockle,brod,brodder,brog,brogan,brogger,broggle,brogue,broguer,broider,broigne,broil,broiler,brokage,broke,broken,broker,broking,brolga,broll,brolly,broma,bromal,bromate,brome,bromic,bromide,bromine,bromism,bromite,bromize,bromoil,bromol,bromous,bronc,bronchi,bronco,bronk,bronze,bronzed,bronzen,bronzer,bronzy,broo,brooch,brood,brooder,broody,brook,brooked,brookie,brooky,brool,broom,broomer,broomy,broon,broose,brose,brosot,brosy,brot,brotan,brotany,broth,brothel,brother,brothy,brough,brought,brow,browden,browed,browis,browman,brown,browner,brownie,brownly,browny,browse,browser,browst,bruang,brucia,brucina,brucine,brucite,bruckle,brugh,bruin,bruise,bruiser,bruit,bruiter,bruke,brulee,brulyie,brumal,brumby,brume,brumous,brunch,brunet,brunt,bruscus,brush,brushed,brusher,brushes,brushet,brushy,brusque,brustle,brut,brutage,brutal,brute,brutely,brutify,bruting,brutish,brutism,brutter,bruzz,bryonin,bryony,bu,bual,buaze,bub,buba,bubal,bubalis,bubble,bubbler,bubbly,bubby,bubinga,bubo,buboed,bubonic,bubukle,bucare,bucca,buccal,buccan,buccate,buccina,buccula,buchite,buchu,buck,bucked,buckeen,bucker,bucket,buckety,buckeye,buckie,bucking,buckish,buckle,buckled,buckler,bucklum,bucko,buckpot,buckra,buckram,bucksaw,bucky,bucolic,bucrane,bud,buda,buddage,budder,buddhi,budding,buddle,buddler,buddy,budge,budger,budget,budless,budlet,budlike,budmash,budtime,budwood,budworm,budzat,bufagin,buff,buffalo,buffed,buffer,buffet,buffing,buffle,buffont,buffoon,buffy,bufidin,bufo,bug,bugaboo,bugan,bugbane,bugbear,bugbite,bugdom,bugfish,bugger,buggery,buggy,bughead,bugle,bugled,bugler,buglet,bugloss,bugre,bugseed,bugweed,bugwort,buhl,buhr,build,builder,buildup,built,buirdly,buisson,buist,bukh,bukshi,bulak,bulb,bulbar,bulbed,bulbil,bulblet,bulbose,bulbous,bulbul,bulbule,bulby,bulchin,bulge,bulger,bulgy,bulimia,bulimic,bulimy,bulk,bulked,bulker,bulkily,bulkish,bulky,bull,bulla,bullace,bullan,bullary,bullate,bullbat,bulldog,buller,bullet,bullety,bulling,bullion,bullish,bullism,bullit,bullnut,bullock,bullous,bullule,bully,bulrush,bulse,bult,bulter,bultey,bultong,bultow,bulwand,bulwark,bum,bumbaze,bumbee,bumble,bumbler,bumbo,bumboat,bumicky,bummalo,bummed,bummer,bummie,bumming,bummler,bummock,bump,bumpee,bumper,bumpily,bumping,bumpkin,bumpy,bumtrap,bumwood,bun,buna,buncal,bunce,bunch,buncher,bunchy,bund,bunder,bundle,bundler,bundlet,bundook,bundy,bung,bungee,bungey,bungfu,bungle,bungler,bungo,bungy,bunion,bunk,bunker,bunkery,bunkie,bunko,bunkum,bunnell,bunny,bunt,buntal,bunted,bunter,bunting,bunton,bunty,bunya,bunyah,bunyip,buoy,buoyage,buoyant,bur,buran,burao,burbank,burbark,burble,burbler,burbly,burbot,burbush,burd,burden,burdie,burdock,burdon,bure,bureau,bureaux,burel,burele,buret,burette,burfish,burg,burgage,burgall,burgee,burgeon,burgess,burgh,burghal,burgher,burglar,burgle,burgoo,burgul,burgus,burhead,buri,burial,burian,buried,burier,burin,burion,buriti,burka,burke,burker,burl,burlap,burled,burler,burlet,burlily,burly,burmite,burn,burned,burner,burnet,burnie,burning,burnish,burnous,burnout,burnt,burnut,burny,buro,burp,burr,burrah,burred,burrel,burrer,burring,burrish,burrito,burro,burrow,burry,bursa,bursal,bursar,bursary,bursate,burse,burseed,burst,burster,burt,burton,burucha,burweed,bury,burying,bus,busby,buscarl,bush,bushed,bushel,busher,bushful,bushi,bushily,bushing,bushlet,bushwa,bushy,busied,busily,busine,busk,busked,busker,busket,buskin,buskle,busky,busman,buss,busser,bussock,bussu,bust,bustard,busted,bustee,buster,bustic,bustle,bustled,bustler,busy,busying,busyish,but,butanal,butane,butanol,butch,butcher,butein,butene,butenyl,butic,butine,butler,butlery,butment,butoxy,butoxyl,butt,butte,butter,buttery,butting,buttle,buttock,button,buttons,buttony,butty,butyl,butylic,butyne,butyr,butyral,butyric,butyrin,butyryl,buxerry,buxom,buxomly,buy,buyable,buyer,buzane,buzz,buzzard,buzzer,buzzies,buzzing,buzzle,buzzwig,buzzy,by,bycoket,bye,byee,byeman,byepath,byerite,bygane,bygo,bygoing,bygone,byhand,bylaw,byname,byon,byous,byously,bypass,bypast,bypath,byplay,byre,byreman,byrlaw,byrnie,byroad,byrrus,bysen,byspell,byssal,byssin,byssine,byssoid,byssus,byth,bytime,bywalk,byway,bywoner,byword,bywork,c,ca,caam,caama,caaming,caapeba,cab,caba,cabaan,caback,cabaho,cabal,cabala,cabalic,caban,cabana,cabaret,cabas,cabbage,cabbagy,cabber,cabble,cabbler,cabby,cabda,caber,cabezon,cabin,cabinet,cabio,cable,cabled,cabler,cablet,cabling,cabman,cabob,cabocle,cabook,caboose,cabot,cabree,cabrit,cabuya,cacam,cacao,cachaza,cache,cachet,cachexy,cachou,cachrys,cacique,cack,cackle,cackler,cacodyl,cacoepy,caconym,cacoon,cacti,cactoid,cacur,cad,cadamba,cadaver,cadbait,cadbit,cadbote,caddice,caddie,caddis,caddish,caddle,caddow,caddy,cade,cadelle,cadence,cadency,cadent,cadenza,cader,caderas,cadet,cadetcy,cadette,cadew,cadge,cadger,cadgily,cadgy,cadi,cadism,cadjan,cadlock,cadmia,cadmic,cadmide,cadmium,cados,cadrans,cadre,cadua,caduac,caduca,cadus,cadweed,caeca,caecal,caecum,caeoma,caesura,cafeneh,cafenet,caffa,caffeic,caffeol,caffiso,caffle,caffoy,cafh,cafiz,caftan,cag,cage,caged,cageful,cageman,cager,cagey,caggy,cagily,cagit,cagmag,cahiz,cahoot,cahot,cahow,caickle,caid,caiman,caimito,cain,caique,caird,cairn,cairned,cairny,caisson,caitiff,cajeput,cajole,cajoler,cajuela,cajun,cajuput,cake,cakebox,caker,cakette,cakey,caky,cal,calaba,calaber,calade,calais,calalu,calamus,calash,calcar,calced,calcic,calcify,calcine,calcite,calcium,calculi,calden,caldron,calean,calends,calepin,calf,calfish,caliber,calibre,calices,calicle,calico,calid,caliga,caligo,calinda,calinut,calipee,caliper,caliph,caliver,calix,calk,calkage,calker,calkin,calking,call,callant,callboy,caller,callet,calli,callid,calling,callo,callose,callous,callow,callus,calm,calmant,calmer,calmly,calmy,calomba,calomel,calool,calor,caloric,calorie,caloris,calotte,caloyer,calp,calpac,calpack,caltrap,caltrop,calumba,calumet,calumny,calve,calved,calver,calves,calvish,calvity,calvous,calx,calyces,calycle,calymma,calypso,calyx,cam,camaca,camagon,camail,caman,camansi,camara,camass,camata,camb,cambaye,camber,cambial,cambism,cambist,cambium,cambrel,cambuca,came,cameist,camel,camelry,cameo,camera,cameral,camilla,camion,camise,camisia,camlet,cammed,cammock,camoodi,camp,campana,campane,camper,campho,camphol,camphor,campion,cample,campo,campody,campoo,campus,camus,camused,camwood,can,canaba,canada,canadol,canal,canamo,canape,canard,canari,canarin,canary,canasta,canaut,cancan,cancel,cancer,canch,cancrum,cand,candela,candent,candid,candied,candier,candify,candiru,candle,candler,candock,candor,candroy,candy,candys,cane,canel,canella,canelo,caner,canette,canful,cangan,cangia,cangle,cangler,cangue,canhoop,canid,canille,caninal,canine,caninus,canions,canjac,cank,canker,cankery,canman,canna,cannach,canned,cannel,canner,cannery,cannet,cannily,canning,cannon,cannot,cannula,canny,canoe,canon,canonic,canonry,canopic,canopy,canroy,canso,cant,cantala,cantar,cantara,cantaro,cantata,canted,canteen,canter,canthal,canthus,cantic,cantico,cantily,cantina,canting,cantion,cantish,cantle,cantlet,canto,canton,cantoon,cantor,cantred,cantref,cantrip,cantus,canty,canun,canvas,canvass,cany,canyon,canzon,caoba,cap,capable,capably,capanna,capanne,capax,capcase,cape,caped,capel,capelet,capelin,caper,caperer,capes,capful,caph,caphar,caphite,capias,capicha,capital,capitan,capivi,capkin,capless,caplin,capman,capmint,capomo,capon,caporal,capot,capote,capped,capper,cappie,capping,capple,cappy,caprate,capreol,capric,caprice,caprid,caprin,caprine,caproic,caproin,caprone,caproyl,capryl,capsa,capsid,capsize,capstan,capsula,capsule,captain,caption,captive,captor,capture,capuche,capulet,capulin,car,carabao,carabid,carabin,carabus,caracal,caracol,caract,carafe,caraibe,caraipi,caramba,caramel,caranda,carane,caranna,carapax,carapo,carat,caratch,caravan,caravel,caraway,carbarn,carbeen,carbene,carbide,carbine,carbo,carbon,carbona,carbora,carboxy,carboy,carbro,carbure,carbyl,carcake,carcass,carceag,carcel,carcoon,card,cardecu,carded,cardel,carder,cardia,cardiac,cardial,cardin,carding,cardo,cardol,cardon,cardona,cardoon,care,careen,career,careful,carene,carer,caress,carest,caret,carfare,carfax,carful,carga,cargo,carhop,cariama,caribou,carid,caries,carina,carinal,cariole,carious,cark,carking,carkled,carl,carless,carlet,carlie,carlin,carline,carling,carlish,carload,carlot,carls,carman,carmele,carmine,carmot,carnage,carnal,carnate,carneol,carney,carnic,carnify,carnose,carnous,caroa,carob,caroba,caroche,carol,caroler,caroli,carolin,carolus,carom,carone,caronic,caroome,caroon,carotic,carotid,carotin,carouse,carp,carpal,carpale,carpel,carpent,carper,carpet,carpid,carping,carpium,carport,carpos,carpus,carr,carrack,carrel,carrick,carried,carrier,carrion,carrizo,carroch,carrot,carroty,carrow,carry,carse,carshop,carsick,cart,cartage,carte,cartel,carter,cartful,cartman,carton,cartoon,cartway,carty,carua,carucal,carval,carve,carvel,carven,carvene,carver,carving,carvol,carvone,carvyl,caryl,casaba,casabe,casal,casalty,casate,casaun,casava,casave,casavi,casbah,cascade,cascado,cascara,casco,cascol,case,casease,caseate,casebox,cased,caseful,casefy,caseic,casein,caseose,caseous,caser,casern,caseum,cash,casha,cashaw,cashbox,cashboy,cashel,cashew,cashier,casing,casino,casiri,cask,casket,casking,casque,casqued,casquet,cass,cassady,casse,cassena,cassia,cassie,cassina,cassine,cassino,cassis,cassock,casson,cassoon,cast,caste,caster,castice,casting,castle,castled,castlet,castock,castoff,castor,castory,castra,castral,castrum,castuli,casual,casuary,casuist,casula,cat,catalpa,catan,catapan,cataria,catarrh,catasta,catbird,catboat,catcall,catch,catcher,catchup,catchy,catclaw,catdom,cate,catechu,catella,catena,catenae,cater,cateran,caterer,caterva,cateye,catface,catfall,catfish,catfoot,catgut,cathead,cathect,catheti,cathin,cathine,cathion,cathode,cathole,cathood,cathop,cathro,cation,cativo,catjang,catkin,catlap,catlike,catlin,catling,catmint,catnip,catpipe,catskin,catstep,catsup,cattabu,cattail,cattalo,cattery,cattily,catting,cattish,cattle,catty,catvine,catwalk,catwise,catwood,catwort,caubeen,cauboge,cauch,caucho,caucus,cauda,caudad,caudae,caudal,caudata,caudate,caudex,caudle,caught,cauk,caul,cauld,caules,cauline,caulis,caulome,caulote,caum,cauma,caunch,caup,caupo,caurale,causal,causate,cause,causer,causey,causing,causse,causson,caustic,cautel,cauter,cautery,caution,cautivo,cava,cavae,caval,cavalla,cavalry,cavate,cave,caveat,cavel,cavelet,cavern,cavetto,caviar,cavie,cavil,caviler,caving,cavings,cavish,cavity,caviya,cavort,cavus,cavy,caw,cawk,cawky,cawney,cawquaw,caxiri,caxon,cay,cayenne,cayman,caza,cazimi,ce,cearin,cease,ceasmic,cebell,cebian,cebid,cebil,cebine,ceboid,cebur,cecils,cecity,cedar,cedared,cedarn,cedary,cede,cedent,ceder,cedilla,cedrat,cedrate,cedre,cedrene,cedrin,cedrine,cedrium,cedrol,cedron,cedry,cedula,cee,ceibo,ceil,ceile,ceiler,ceilidh,ceiling,celadon,celemin,celery,celesta,celeste,celiac,celite,cell,cella,cellae,cellar,celled,cellist,cello,celloid,cellose,cellule,celsian,celt,celtium,celtuce,cembalo,cement,cenacle,cendre,cenoby,cense,censer,censive,censor,censual,censure,census,cent,centage,cental,centare,centaur,centavo,centena,center,centiar,centile,centime,centimo,centner,cento,centrad,central,centric,centrum,centry,centum,century,ceorl,cep,cepa,cepe,cephid,ceps,ceptor,cequi,cerago,ceral,ceramal,ceramic,ceras,cerasin,cerata,cerate,cerated,cercal,cerci,cercus,cere,cereal,cerebra,cered,cereous,cerer,ceresin,cerevis,ceria,ceric,ceride,cerillo,ceriman,cerin,cerine,ceriops,cerise,cerite,cerium,cermet,cern,cero,ceroma,cerote,cerotic,cerotin,cerous,cerrero,cerrial,cerris,certain,certie,certify,certis,certy,cerule,cerumen,ceruse,cervid,cervine,cervix,cervoid,ceryl,cesious,cesium,cess,cesser,cession,cessor,cesspit,cest,cestode,cestoid,cestrum,cestus,cetane,cetene,ceti,cetic,cetin,cetyl,cetylic,cevine,cha,chaa,chab,chabot,chabouk,chabuk,chacate,chack,chacker,chackle,chacma,chacona,chacte,chad,chaeta,chafe,chafer,chafery,chaff,chaffer,chaffy,chaft,chafted,chagan,chagrin,chaguar,chagul,chahar,chai,chain,chained,chainer,chainon,chair,chairer,chais,chaise,chaitya,chaja,chaka,chakar,chakari,chakazi,chakdar,chakobu,chakra,chakram,chaksi,chal,chalaco,chalana,chalaza,chalaze,chalcid,chalcon,chalcus,chalder,chalet,chalice,chalk,chalker,chalky,challah,challie,challis,chalmer,chalon,chalone,chalque,chalta,chalutz,cham,chamal,chamar,chamber,chambul,chamfer,chamiso,chamite,chamma,chamois,champ,champac,champer,champy,chance,chancel,chancer,chanche,chanco,chancre,chancy,chandam,chandi,chandoo,chandu,chandul,chang,changa,changar,change,changer,chank,channel,channer,chanson,chanst,chant,chanter,chantey,chantry,chao,chaos,chaotic,chap,chapah,chape,chapeau,chaped,chapel,chapin,chaplet,chapman,chapped,chapper,chappie,chappin,chappow,chappy,chaps,chapt,chapter,char,charac,charade,charas,charbon,chard,chare,charer,charet,charge,chargee,charger,charier,charily,chariot,charism,charity,chark,charka,charkha,charm,charmel,charmer,charnel,charpit,charpoy,charqui,charr,charry,chart,charter,charuk,chary,chase,chaser,chasing,chasm,chasma,chasmal,chasmed,chasmic,chasmy,chasse,chassis,chaste,chasten,chat,chataka,chateau,chati,chatta,chattel,chatter,chatty,chauk,chaus,chaute,chauth,chavish,chaw,chawan,chawer,chawk,chawl,chay,chaya,chayote,chazan,che,cheap,cheapen,cheaply,cheat,cheatee,cheater,chebec,chebel,chebog,chebule,check,checked,checker,checkup,checky,cheder,chee,cheecha,cheek,cheeker,cheeky,cheep,cheeper,cheepy,cheer,cheered,cheerer,cheerio,cheerly,cheery,cheese,cheeser,cheesy,cheet,cheetah,cheeter,cheetie,chef,chegoe,chegre,cheir,chekan,cheke,cheki,chekmak,chela,chelate,chelem,chelide,chello,chelone,chelp,chelys,chemic,chemis,chemise,chemism,chemist,chena,chende,cheng,chenica,cheque,cherem,cherish,cheroot,cherry,chert,cherte,cherty,cherub,chervil,cheson,chess,chessel,chesser,chest,chester,chesty,cheth,chettik,chetty,chevage,cheval,cheve,cheven,chevin,chevise,chevon,chevron,chevy,chew,chewer,chewink,chewy,cheyney,chhatri,chi,chia,chiasm,chiasma,chiaus,chibouk,chibrit,chic,chicane,chichi,chick,chicken,chicker,chicky,chicle,chico,chicory,chicot,chicote,chid,chidden,chide,chider,chiding,chidra,chief,chiefly,chield,chien,chiffer,chiffon,chiggak,chigger,chignon,chigoe,chih,chihfu,chikara,chil,child,childe,childed,childly,chile,chili,chiliad,chill,chilla,chilled,chiller,chillo,chillum,chilly,chiloma,chilver,chimble,chime,chimer,chimera,chimney,chin,china,chinar,chinch,chincha,chinche,chine,chined,ching,chingma,chinik,chinin,chink,chinker,chinkle,chinks,chinky,chinnam,chinned,chinny,chino,chinoa,chinol,chinse,chint,chintz,chip,chiplet,chipped,chipper,chippy,chips,chiral,chirata,chiripa,chirk,chirm,chiro,chirp,chirper,chirpy,chirr,chirrup,chisel,chit,chitak,chital,chitin,chiton,chitose,chitra,chitter,chitty,chive,chivey,chkalik,chlamyd,chlamys,chlor,chloral,chlore,chloric,chloryl,cho,choana,choate,choaty,chob,choca,chocard,chocho,chock,chocker,choel,choenix,choffer,choga,chogak,chogset,choice,choicy,choil,choiler,choir,chokage,choke,choker,choking,chokra,choky,chol,chola,cholane,cholate,chold,choleic,choler,cholera,choli,cholic,choline,cholla,choller,cholum,chomp,chondre,chonta,choop,choose,chooser,choosy,chop,chopa,chopin,chopine,chopped,chopper,choppy,choragy,choral,chord,chorda,chordal,chorded,chore,chorea,choreal,choree,choregy,choreic,choreus,chorial,choric,chorine,chorion,chorism,chorist,chorogi,choroid,chorook,chort,chorten,chortle,chorus,choryos,chose,chosen,chott,chough,chouka,choup,chous,chouse,chouser,chow,chowder,chowk,chowry,choya,chria,chrism,chrisma,chrisom,chroma,chrome,chromic,chromid,chromo,chromy,chromyl,chronal,chronic,chrotta,chrysal,chrysid,chrysin,chub,chubbed,chubby,chuck,chucker,chuckle,chucky,chuddar,chufa,chuff,chuffy,chug,chugger,chuhra,chukar,chukker,chukor,chulan,chullpa,chum,chummer,chummy,chump,chumpy,chun,chunari,chunga,chunk,chunky,chunner,chunnia,chunter,chupak,chupon,church,churchy,churel,churl,churled,churly,churm,churn,churr,churrus,chut,chute,chuter,chutney,chyack,chyak,chyle,chylify,chyloid,chylous,chymase,chyme,chymia,chymic,chymify,chymous,chypre,chytra,chytrid,cibol,cibory,ciboule,cicad,cicada,cicadid,cicala,cicely,cicer,cichlid,cidarid,cidaris,cider,cig,cigala,cigar,cigua,cilia,ciliary,ciliate,cilice,cilium,cimbia,cimelia,cimex,cimicid,cimline,cinch,cincher,cinclis,cinct,cinder,cindery,cine,cinel,cinema,cinene,cineole,cinerea,cingle,cinnyl,cinque,cinter,cinuran,cion,cipher,cipo,cipolin,cippus,circa,circle,circled,circler,circlet,circuit,circus,circusy,cirque,cirrate,cirri,cirrose,cirrous,cirrus,cirsoid,ciruela,cisco,cise,cisele,cissing,cissoid,cist,cista,cistae,cisted,cistern,cistic,cit,citable,citadel,citator,cite,citee,citer,citess,cithara,cither,citied,citify,citizen,citole,citral,citrate,citrean,citrene,citric,citril,citrin,citrine,citron,citrous,citrus,cittern,citua,city,citydom,cityful,cityish,cive,civet,civic,civics,civil,civilly,civism,civvy,cixiid,clabber,clachan,clack,clacker,clacket,clad,cladine,cladode,cladose,cladus,clag,claggum,claggy,claim,claimer,clairce,claith,claiver,clam,clamant,clamb,clamber,clame,clamer,clammed,clammer,clammy,clamor,clamp,clamper,clan,clang,clangor,clank,clanned,clap,clapnet,clapped,clapper,clapt,claque,claquer,clarain,claret,clarify,clarin,clarion,clarity,clark,claro,clart,clarty,clary,clash,clasher,clashy,clasp,clasper,claspt,class,classed,classer,classes,classic,classis,classy,clastic,clat,clatch,clatter,clatty,claught,clausal,clause,claut,clava,claval,clavate,clave,clavel,claver,clavial,clavier,claviol,clavis,clavola,clavus,clavy,claw,clawed,clawer,clawk,clawker,clay,clayen,clayer,clayey,clayish,clayman,claypan,cleach,clead,cleaded,cleam,cleamer,clean,cleaner,cleanly,cleanse,cleanup,clear,clearer,clearly,cleat,cleave,cleaver,cleche,cleck,cled,cledge,cledgy,clee,cleek,cleeked,cleeky,clef,cleft,clefted,cleg,clem,clement,clench,cleoid,clep,clergy,cleric,clerid,clerisy,clerk,clerkly,cleruch,cletch,cleuch,cleve,clever,clevis,clew,cliack,cliche,click,clicker,clicket,clicky,cliency,client,cliff,cliffed,cliffy,clift,clifty,clima,climata,climate,climath,climax,climb,climber,clime,clinal,clinch,cline,cling,clinger,clingy,clinia,clinic,clinium,clink,clinker,clinkum,clinoid,clint,clinty,clip,clipei,clipeus,clipped,clipper,clips,clipse,clipt,clique,cliquy,clisere,clit,clitch,clite,clites,clithe,clitia,clition,clitter,clival,clive,clivers,clivis,clivus,cloaca,cloacal,cloak,cloaked,cloam,cloamen,cloamer,clobber,clochan,cloche,clocher,clock,clocked,clocker,clod,clodder,cloddy,clodlet,cloff,clog,clogger,cloggy,cloghad,clogwyn,cloit,clomb,clomben,clonal,clone,clonic,clonism,clonus,cloof,cloop,cloot,clootie,clop,close,closed,closely,closen,closer,closet,closh,closish,closter,closure,clot,clotbur,clote,cloth,clothe,clothes,clothy,clotter,clotty,cloture,cloud,clouded,cloudy,clough,clour,clout,clouted,clouter,clouty,clove,cloven,clovene,clover,clovery,clow,clown,cloy,cloyer,cloying,club,clubbed,clubber,clubby,clubdom,clubman,cluck,clue,cluff,clump,clumpy,clumse,clumsy,clunch,clung,clunk,clupeid,cluster,clutch,cluther,clutter,cly,clyer,clype,clypeal,clypeus,clysis,clysma,clysmic,clyster,cnemial,cnemis,cnicin,cnida,coabode,coach,coachee,coacher,coachy,coact,coactor,coadapt,coadmit,coadore,coaged,coagent,coagula,coaid,coaita,coak,coakum,coal,coalbag,coalbin,coalbox,coaler,coalify,coalize,coalpit,coaly,coaming,coannex,coapt,coarb,coarse,coarsen,coast,coastal,coaster,coat,coated,coatee,coater,coati,coatie,coating,coax,coaxal,coaxer,coaxial,coaxing,coaxy,cob,cobaea,cobalt,cobang,cobbed,cobber,cobbing,cobble,cobbler,cobbly,cobbra,cobby,cobcab,cobego,cobhead,cobia,cobiron,coble,cobless,cobloaf,cobnut,cobola,cobourg,cobra,coburg,cobweb,cobwork,coca,cocaine,cocash,cocause,coccal,cocci,coccid,cocco,coccoid,coccous,coccule,coccus,coccyx,cochal,cochief,cochlea,cock,cockade,cockal,cocked,cocker,cocket,cockeye,cockily,cocking,cockish,cockle,cockled,cockler,cocklet,cockly,cockney,cockpit,cockshy,cockup,cocky,coco,cocoa,cocoach,coconut,cocoon,cocotte,coctile,coction,cocuisa,cocullo,cocuyo,cod,coda,codbank,codder,codding,coddle,coddler,code,codeine,coder,codex,codfish,codger,codhead,codical,codices,codicil,codify,codilla,codille,codist,codling,codman,codo,codol,codon,codworm,coe,coecal,coecum,coed,coelar,coelder,coelect,coelho,coelia,coeliac,coelian,coelin,coeline,coelom,coeloma,coempt,coenact,coenjoy,coenobe,coequal,coerce,coercer,coetus,coeval,coexert,coexist,coff,coffee,coffer,coffin,coffle,coffret,coft,cog,cogence,cogency,cogener,cogent,cogged,cogger,coggie,cogging,coggle,coggly,coghle,cogman,cognac,cognate,cognize,cogon,cogonal,cograil,cogroad,cogue,cogway,cogwood,cohabit,coheir,cohere,coherer,cohibit,coho,cohoba,cohol,cohort,cohosh,cohune,coif,coifed,coign,coigue,coil,coiled,coiler,coiling,coin,coinage,coiner,coinfer,coining,cointer,coiny,coir,coital,coition,coiture,coitus,cojudge,cojuror,coke,cokeman,coker,cokery,coking,coky,col,cola,colane,colarin,colate,colauxe,colback,cold,colder,coldish,coldly,cole,coletit,coleur,coli,colibri,colic,colical,colicky,colima,colin,coling,colitic,colitis,colk,coll,collage,collar,collard,collare,collate,collaud,collect,colleen,college,collery,collet,colley,collide,collie,collied,collier,collin,colline,colling,collins,collock,colloid,collop,collude,collum,colly,collyba,colmar,colobin,colon,colonel,colonic,colony,color,colored,colorer,colorin,colors,colory,coloss,colossi,colove,colp,colpeo,colport,colpus,colt,colter,coltish,colugo,columbo,column,colunar,colure,coly,colyone,colytic,colyum,colza,coma,comaker,comal,comamie,comanic,comart,comate,comb,combat,combed,comber,combine,combing,comble,comboy,combure,combust,comby,come,comedic,comedo,comedy,comely,comenic,comer,comes,comet,cometic,comfit,comfort,comfrey,comfy,comic,comical,comicry,coming,comino,comism,comital,comitia,comity,comma,command,commend,comment,commie,commit,commix,commixt,commode,common,commons,commot,commove,communa,commune,commute,comoid,comose,comourn,comous,compact,company,compare,compart,compass,compear,compeer,compel,compend,compete,compile,complex,complin,complot,comply,compo,compoer,compole,compone,compony,comport,compos,compose,compost,compote,compreg,compter,compute,comrade,con,conacre,conal,conamed,conatus,concave,conceal,concede,conceit,concent,concept,concern,concert,conch,concha,conchal,conche,conched,concher,conchy,concile,concise,concoct,concord,concupy,concur,concuss,cond,condemn,condign,condite,condole,condone,condor,conduce,conduct,conduit,condyle,cone,coned,coneen,coneine,conelet,coner,cones,confab,confact,confect,confess,confide,confine,confirm,confix,conflow,conflux,conform,confuse,confute,conga,congeal,congee,conger,congest,congius,congou,conic,conical,conicle,conics,conidia,conifer,conima,conin,conine,conject,conjoin,conjure,conjury,conk,conker,conkers,conky,conn,connach,connate,connect,conner,connex,conning,connive,connote,conoid,conopid,conquer,conred,consent,consign,consist,consol,console,consort,conspue,constat,consul,consult,consume,consute,contact,contain,conte,contect,contemn,content,conter,contest,context,contise,conto,contort,contour,contra,control,contund,contuse,conure,conus,conusee,conusor,conuzee,conuzor,convect,convene,convent,convert,conveth,convex,convey,convict,convive,convoke,convoy,cony,coo,cooba,coodle,cooee,cooer,coof,cooing,cooja,cook,cookdom,cookee,cooker,cookery,cooking,cookish,cookout,cooky,cool,coolant,coolen,cooler,coolie,cooling,coolish,coolly,coolth,coolung,cooly,coom,coomb,coomy,coon,cooncan,coonily,coontie,coony,coop,cooper,coopery,cooree,coorie,cooser,coost,coot,cooter,coothay,cootie,cop,copa,copable,copaene,copaiba,copaiye,copal,copalm,copart,coparty,cope,copei,copeman,copen,copepod,coper,coperta,copied,copier,copilot,coping,copious,copis,copist,copita,copolar,copped,copper,coppery,coppet,coppice,coppin,copping,copple,coppled,coppy,copr,copra,coprose,copse,copsing,copsy,copter,copula,copular,copus,copy,copycat,copyism,copyist,copyman,coque,coquet,coquina,coquita,coquito,cor,cora,corach,coracle,corah,coraise,coral,coraled,coram,coranto,corban,corbeau,corbeil,corbel,corbie,corbula,corcass,corcir,cord,cordage,cordant,cordate,cordax,corded,cordel,corder,cordial,cordies,cording,cordite,cordoba,cordon,cordy,cordyl,core,corebel,cored,coreid,coreign,corella,corer,corf,corge,corgi,corial,coriin,coring,corinne,corium,cork,corkage,corke,corked,corker,corking,corkish,corkite,corky,corm,cormel,cormoid,cormous,cormus,corn,cornage,cornbin,corncob,cornea,corneal,cornein,cornel,corner,cornet,corneum,cornic,cornice,cornin,corning,cornu,cornual,cornule,cornute,cornuto,corny,coroa,corody,corol,corolla,corona,coronad,coronae,coronal,coroner,coronet,corozo,corp,corpora,corps,corpse,corpus,corrade,corral,correal,correct,corrie,corrige,corrode,corrupt,corsac,corsage,corsair,corse,corset,corsie,corsite,corta,cortege,cortex,cortez,cortin,cortina,coruco,coruler,corupay,corver,corvina,corvine,corvoid,coryl,corylin,corymb,coryza,cos,cosaque,coscet,coseat,cosec,cosech,coseism,coset,cosh,cosher,coshery,cosily,cosine,cosmic,cosmism,cosmist,cosmos,coss,cossas,cosse,cosset,cossid,cost,costa,costal,costar,costard,costate,costean,coster,costing,costive,costly,costrel,costula,costume,cosy,cot,cotch,cote,coteful,coterie,coth,cothe,cothish,cothon,cothurn,cothy,cotidal,cotise,cotland,cotman,coto,cotoin,cotoro,cotrine,cotset,cotta,cottage,cotte,cotted,cotter,cottid,cottier,cottoid,cotton,cottony,cotty,cotuit,cotula,cotutor,cotwin,cotwist,cotyla,cotylar,cotype,couac,coucal,couch,couched,couchee,coucher,couchy,coude,coudee,coue,cougar,cough,cougher,cougnar,coul,could,coulee,coulomb,coulure,couma,coumara,council,counite,counsel,count,counter,countor,country,county,coup,coupage,coupe,couped,coupee,couper,couple,coupled,coupler,couplet,coupon,coupure,courage,courant,courap,courb,courge,courida,courier,couril,courlan,course,coursed,courser,court,courter,courtin,courtly,cousin,cousiny,coutel,couter,couth,couthie,coutil,couvade,couxia,covado,cove,coved,covent,cover,covered,coverer,covert,covet,coveter,covey,covid,covin,coving,covisit,covite,cow,cowal,coward,cowardy,cowbane,cowbell,cowbind,cowbird,cowboy,cowdie,coween,cower,cowfish,cowgate,cowgram,cowhage,cowheel,cowherb,cowherd,cowhide,cowhorn,cowish,cowitch,cowl,cowle,cowled,cowlick,cowlike,cowling,cowman,cowpath,cowpea,cowpen,cowpock,cowpox,cowrie,cowroid,cowshed,cowskin,cowslip,cowtail,cowweed,cowy,cowyard,cox,coxa,coxal,coxcomb,coxite,coxitis,coxy,coy,coyan,coydog,coyish,coyly,coyness,coynye,coyo,coyol,coyote,coypu,coyure,coz,coze,cozen,cozener,cozier,cozily,cozy,crab,crabbed,crabber,crabby,craber,crablet,crabman,crack,cracked,cracker,crackle,crackly,cracky,craddy,cradge,cradle,cradler,craft,crafty,crag,craggan,cragged,craggy,craichy,crain,craisey,craizey,crajuru,crake,crakow,cram,crambe,crambid,cramble,crambly,crambo,crammer,cramp,cramped,cramper,crampet,crampon,crampy,cran,cranage,crance,crane,craner,craney,crania,craniad,cranial,cranian,cranic,cranium,crank,cranked,cranker,crankle,crankly,crankum,cranky,crannog,cranny,crants,crap,crapaud,crape,crappie,crappin,crapple,crappo,craps,crapy,crare,crash,crasher,crasis,crass,crassly,cratch,crate,crater,craunch,cravat,crave,craven,craver,craving,cravo,craw,crawdad,crawful,crawl,crawler,crawley,crawly,crawm,crawtae,crayer,crayon,craze,crazed,crazily,crazy,crea,creagh,creaght,creak,creaker,creaky,cream,creamer,creamy,creance,creant,crease,creaser,creasy,creat,create,creatic,creator,creche,credent,credit,cree,creed,creedal,creeded,creek,creeker,creeky,creel,creeler,creem,creen,creep,creeper,creepie,creepy,creese,creesh,creeshy,cremate,cremone,cremor,cremule,crena,crenate,crenel,crenele,crenic,crenula,creole,creosol,crepe,crepine,crepon,crept,crepy,cresol,cresoxy,cress,cressed,cresset,cresson,cressy,crest,crested,cresyl,creta,cretic,cretify,cretin,cretion,crevice,crew,crewel,crewer,crewman,crib,cribber,cribble,cribo,cribral,cric,crick,cricket,crickey,crickle,cricoid,cried,crier,criey,crig,crile,crime,crimine,crimp,crimper,crimple,crimpy,crimson,crin,crinal,crine,crined,crinet,cringe,cringer,cringle,crinite,crink,crinkle,crinkly,crinoid,crinose,crinula,cripes,cripple,cripply,crises,crisic,crisis,crisp,crisped,crisper,crisply,crispy,criss,crissal,crissum,crista,critch,crith,critic,crizzle,cro,croak,croaker,croaky,croc,crocard,croceic,crocein,croche,crochet,croci,crocin,crock,crocker,crocket,crocky,crocus,croft,crofter,crome,crone,cronet,cronish,cronk,crony,crood,croodle,crook,crooked,crooken,crookle,crool,croon,crooner,crop,cropman,croppa,cropper,croppie,croppy,croquet,crore,crosa,crosier,crosnes,cross,crosse,crossed,crosser,crossly,crotal,crotalo,crotch,crotchy,crotin,crottle,crotyl,crouch,croup,croupal,croupe,croupy,crouse,crout,croute,crouton,crow,crowbar,crowd,crowded,crowder,crowdy,crower,crowhop,crowing,crowl,crown,crowned,crowner,crowtoe,croy,croyden,croydon,croze,crozer,crozzle,crozzly,crubeen,cruce,cruces,cruche,crucial,crucian,crucify,crucily,cruck,crude,crudely,crudity,cruel,cruelly,cruels,cruelty,cruent,cruet,cruety,cruise,cruiser,cruive,cruller,crum,crumb,crumber,crumble,crumbly,crumby,crumen,crumlet,crummie,crummy,crump,crumper,crumpet,crumple,crumply,crumpy,crunch,crunchy,crunk,crunkle,crunode,crunt,cruor,crupper,crural,crureus,crus,crusade,crusado,cruse,crush,crushed,crusher,crusie,crusily,crust,crusta,crustal,crusted,cruster,crusty,crutch,cruth,crutter,crux,cry,cryable,crybaby,crying,cryogen,cryosel,crypt,crypta,cryptal,crypted,cryptic,crystal,crystic,csardas,ctene,ctenoid,cuadra,cuarta,cub,cubage,cubbing,cubbish,cubby,cubdom,cube,cubeb,cubelet,cuber,cubhood,cubi,cubic,cubica,cubical,cubicle,cubicly,cubism,cubist,cubit,cubital,cubited,cubito,cubitus,cuboid,cuck,cuckold,cuckoo,cuculla,cud,cudava,cudbear,cudden,cuddle,cuddly,cuddy,cudgel,cudweed,cue,cueball,cueca,cueist,cueman,cuerda,cuesta,cuff,cuffer,cuffin,cuffy,cuinage,cuir,cuirass,cuisine,cuisse,cuissen,cuisten,cuke,culbut,culebra,culet,culeus,culgee,culicid,cull,culla,cullage,culler,cullet,culling,cullion,cullis,cully,culm,culmen,culmy,culotte,culpa,culpose,culprit,cult,cultch,cultic,cultish,cultism,cultist,cultual,culture,cultus,culver,culvert,cum,cumal,cumay,cumbent,cumber,cumbha,cumbly,cumbre,cumbu,cumene,cumenyl,cumhal,cumic,cumidin,cumin,cuminal,cuminic,cuminol,cuminyl,cummer,cummin,cumol,cump,cumshaw,cumular,cumuli,cumulus,cumyl,cuneal,cuneate,cunette,cuneus,cunila,cunjah,cunjer,cunner,cunning,cunye,cuorin,cup,cupay,cupcake,cupel,cupeler,cupful,cuphead,cupidon,cupless,cupman,cupmate,cupola,cupolar,cupped,cupper,cupping,cuppy,cuprene,cupric,cupride,cuprite,cuproid,cuprose,cuprous,cuprum,cupseed,cupula,cupule,cur,curable,curably,curacao,curacy,curare,curate,curatel,curatic,curator,curb,curber,curbing,curby,curcas,curch,curd,curdle,curdler,curdly,curdy,cure,curer,curette,curfew,curial,curiate,curie,curin,curine,curing,curio,curiosa,curioso,curious,curite,curium,curl,curled,curler,curlew,curlike,curlily,curling,curly,curn,curney,curnock,curple,curr,currach,currack,curragh,currant,current,curried,currier,currish,curry,cursal,curse,cursed,curser,curship,cursive,cursor,cursory,curst,curstly,cursus,curt,curtail,curtain,curtal,curtate,curtesy,curtly,curtsy,curua,curuba,curule,cururo,curvant,curvate,curve,curved,curver,curvet,curvity,curvous,curvy,cuscus,cusec,cush,cushag,cushat,cushaw,cushion,cushy,cusie,cusk,cusp,cuspal,cuspate,cusped,cuspid,cuspule,cuss,cussed,cusser,cusso,custard,custody,custom,customs,cut,cutaway,cutback,cutch,cutcher,cute,cutely,cutheal,cuticle,cutie,cutin,cutis,cutitis,cutlass,cutler,cutlery,cutlet,cutling,cutlips,cutoff,cutout,cutover,cuttage,cuttail,cutted,cutter,cutting,cuttle,cuttler,cuttoo,cutty,cutup,cutweed,cutwork,cutworm,cuvette,cuvy,cuya,cwierc,cwm,cyan,cyanate,cyanean,cyanic,cyanide,cyanin,cyanine,cyanite,cyanize,cyanol,cyanole,cyanose,cyanus,cyath,cyathos,cyathus,cycad,cyclane,cyclar,cyclas,cycle,cyclene,cycler,cyclian,cyclic,cyclide,cycling,cyclism,cyclist,cyclize,cycloid,cyclone,cyclope,cyclopy,cyclose,cyclus,cyesis,cygnet,cygnine,cyke,cylix,cyma,cymar,cymba,cymbal,cymbalo,cymbate,cyme,cymelet,cymene,cymling,cymoid,cymose,cymous,cymule,cynebot,cynic,cynical,cynipid,cynism,cynoid,cyp,cypre,cypres,cypress,cyprine,cypsela,cyrus,cyst,cystal,cysted,cystic,cystid,cystine,cystis,cystoid,cystoma,cystose,cystous,cytase,cytasic,cytitis,cytode,cytoid,cytoma,cyton,cytost,cytula,czar,czardas,czardom,czarian,czaric,czarina,czarish,czarism,czarist,d,da,daalder,dab,dabb,dabba,dabber,dabble,dabbler,dabby,dablet,daboia,daboya,dabster,dace,dacite,dacitic,dacker,dacoit,dacoity,dacryon,dactyl,dad,dada,dadap,dadder,daddle,daddock,daddy,dade,dado,dae,daedal,daemon,daemony,daer,daff,daffery,daffing,daffish,daffle,daffy,daft,daftly,dag,dagaba,dagame,dagassa,dagesh,dagga,dagger,daggers,daggle,daggly,daggy,daghesh,daglock,dagoba,dags,dah,dahoon,daidle,daidly,daiker,daikon,daily,daimen,daimio,daimon,dain,daincha,dainty,daira,dairi,dairy,dais,daisied,daisy,daitya,daiva,dak,daker,dakir,dal,dalar,dale,daleman,daler,daleth,dali,dalk,dallack,dalle,dalles,dallier,dally,dalt,dalteen,dalton,dam,dama,damage,damager,damages,daman,damask,damasse,dambose,dambrod,dame,damiana,damie,damier,damine,damlike,dammar,damme,dammer,dammish,damn,damned,damner,damnify,damning,damnous,damp,dampang,damped,dampen,damper,damping,dampish,damply,dampy,damsel,damson,dan,danaid,danaide,danaine,danaite,dance,dancer,dancery,dancing,dand,danda,dander,dandify,dandily,dandle,dandler,dandy,dang,danger,dangle,dangler,danglin,danio,dank,dankish,dankly,danli,danner,dannock,dansant,danta,danton,dao,daoine,dap,daphnin,dapicho,dapico,dapifer,dapper,dapple,dappled,dar,darac,daraf,darat,darbha,darby,dardaol,dare,dareall,dareful,darer,daresay,darg,dargah,darger,dargue,dari,daribah,daric,daring,dariole,dark,darken,darkful,darkish,darkle,darkly,darky,darling,darn,darned,darnel,darner,darnex,darning,daroga,daroo,darr,darrein,darst,dart,dartars,darter,darting,dartle,dartman,dartoic,dartoid,dartos,dartre,darts,darzee,das,dash,dashed,dashee,dasheen,dasher,dashing,dashpot,dashy,dasi,dasnt,dassie,dassy,dastard,dastur,dasturi,dasyure,data,datable,datably,dataria,datary,datch,datcha,date,dater,datil,dating,dation,datival,dative,dattock,datum,daturic,daub,daube,dauber,daubery,daubing,dauby,daud,daunch,dauncy,daunt,daunter,daunton,dauphin,daut,dautie,dauw,davach,daven,daver,daverdy,davit,davoch,davy,davyne,daw,dawdle,dawdler,dawdy,dawish,dawkin,dawn,dawning,dawny,dawtet,dawtit,dawut,day,dayal,daybeam,daybook,daydawn,dayfly,dayless,daylit,daylong,dayman,daymare,daymark,dayroom,days,daysman,daystar,daytale,daytide,daytime,dayward,daywork,daywrit,daze,dazed,dazedly,dazy,dazzle,dazzler,de,deacon,dead,deaden,deader,deadeye,deading,deadish,deadly,deadman,deadpan,deadpay,deaf,deafen,deafish,deafly,deair,deal,dealate,dealer,dealing,dealt,dean,deaner,deanery,deaness,dear,dearie,dearly,dearth,deary,deash,deasil,death,deathin,deathly,deathy,deave,deavely,deb,debacle,debadge,debar,debark,debase,debaser,debate,debater,debauch,debby,debeige,deben,debile,debind,debit,debord,debosh,debouch,debride,debrief,debris,debt,debtee,debtful,debtor,debunk,debus,debut,decad,decadal,decade,decadic,decafid,decagon,decal,decamp,decan,decanal,decane,decani,decant,decap,decapod,decarch,decare,decart,decast,decate,decator,decatyl,decay,decayed,decayer,decease,deceit,deceive,decence,decency,decene,decent,decenyl,decern,decess,deciare,decibel,decide,decided,decider,decidua,decil,decile,decima,decimal,deck,decke,decked,deckel,decker,deckie,decking,deckle,declaim,declare,declass,decline,declive,decoat,decoct,decode,decoic,decoke,decolor,decorum,decoy,decoyer,decream,decree,decreer,decreet,decrete,decrew,decrial,decried,decrier,decrown,decry,decuman,decuple,decuria,decurve,decury,decus,decyl,decylic,decyne,dedimus,dedo,deduce,deduct,dee,deed,deedbox,deedeed,deedful,deedily,deedy,deem,deemer,deemie,deep,deepen,deeping,deepish,deeply,deer,deerdog,deerlet,deevey,deface,defacer,defalk,defame,defamed,defamer,defassa,defat,default,defease,defeat,defect,defence,defend,defense,defer,defial,defiant,defiber,deficit,defier,defile,defiled,defiler,define,defined,definer,deflate,deflect,deflesh,deflex,defog,deforce,deform,defoul,defraud,defray,defrock,defrost,deft,deftly,defunct,defuse,defy,deg,degas,degauss,degerm,degged,degger,deglaze,degorge,degrade,degrain,degree,degu,degum,degust,dehair,dehisce,dehorn,dehors,dehort,dehull,dehusk,deice,deicer,deicide,deictic,deific,deifier,deiform,deify,deign,deink,deinos,deiseal,deism,deist,deistic,deity,deject,dejecta,dejeune,dekko,dekle,delaine,delapse,delate,delater,delator,delawn,delay,delayer,dele,delead,delenda,delete,delf,delft,delible,delict,delight,delime,delimit,delint,deliver,dell,deloul,delouse,delta,deltaic,deltal,deltic,deltoid,delude,deluder,deluge,deluxe,delve,delver,demagog,demal,demand,demarch,demark,demast,deme,demean,demency,dement,demerit,demesne,demi,demibob,demidog,demigod,demihag,demiman,demiowl,demiox,demiram,demirep,demise,demiss,demit,demivol,demob,demoded,demoid,demon,demonic,demonry,demos,demote,demotic,demount,demulce,demure,demy,den,denaro,denary,denat,denda,dendral,dendric,dendron,dene,dengue,denial,denier,denim,denizen,dennet,denote,dense,densely,densen,densher,densify,density,dent,dental,dentale,dentary,dentata,dentate,dentel,denter,dentex,dentil,dentile,dentin,dentine,dentist,dentoid,denture,denty,denude,denuder,deny,deodand,deodara,deota,depa,depaint,depark,depart,depas,depass,depend,depeter,dephase,depict,deplane,deplete,deplore,deploy,deplume,deplump,depoh,depone,deport,deposal,depose,deposer,deposit,depot,deprave,depress,deprint,deprive,depside,depth,depthen,depute,deputy,dequeen,derah,deraign,derail,derange,derat,derate,derater,deray,derby,dere,dereism,deric,deride,derider,derival,derive,derived,deriver,derm,derma,dermad,dermal,dermic,dermis,dermoid,dermol,dern,dernier,derout,derrick,derride,derries,derry,dertrum,derust,dervish,desalt,desand,descale,descant,descend,descent,descort,descry,deseed,deseret,desert,deserve,desex,desi,desight,design,desire,desired,desirer,desist,desize,desk,deslime,desma,desman,desmic,desmid,desmine,desmoid,desmoma,desmon,despair,despect,despise,despite,despoil,despond,despot,dess,dessa,dessert,dessil,destain,destine,destiny,destour,destroy,desuete,desugar,desyl,detach,detail,detain,detar,detax,detect,detent,deter,deterge,detest,detin,detinet,detinue,detour,detract,detrain,detrude,detune,detur,deuce,deuced,deul,deuton,dev,deva,devall,devalue,devance,devast,devata,develin,develop,devest,deviant,deviate,device,devil,deviled,deviler,devilet,devilry,devily,devious,devisal,devise,devisee,deviser,devisor,devoice,devoid,devoir,devolve,devote,devoted,devotee,devoter,devour,devout,devow,devvel,dew,dewan,dewanee,dewater,dewax,dewbeam,dewclaw,dewcup,dewdamp,dewdrop,dewer,dewfall,dewily,dewlap,dewless,dewlike,dewool,deworm,dewret,dewtry,dewworm,dewy,dexter,dextrad,dextral,dextran,dextrin,dextro,dey,deyship,dezinc,dha,dhabb,dhai,dhak,dhamnoo,dhan,dhangar,dhanuk,dhanush,dharana,dharani,dharma,dharna,dhaura,dhauri,dhava,dhaw,dheri,dhobi,dhole,dhoni,dhoon,dhoti,dhoul,dhow,dhu,dhunchi,dhurra,dhyal,dhyana,di,diabase,diacid,diacle,diacope,diact,diactin,diadem,diaderm,diaene,diagram,dial,dialect,dialer,dialin,dialing,dialist,dialkyl,diallel,diallyl,dialyze,diamb,diambic,diamide,diamine,diamond,dian,diander,dianite,diapase,diapasm,diaper,diaplex,diapsid,diarch,diarchy,diarial,diarian,diarist,diarize,diary,diastem,diaster,diasyrm,diatom,diaulic,diaulos,diaxial,diaxon,diazide,diazine,diazoic,diazole,diazoma,dib,dibase,dibasic,dibatag,dibber,dibble,dibbler,dibbuk,dibhole,dibrach,dibrom,dibs,dicast,dice,dicebox,dicecup,diceman,dicer,dicetyl,dich,dichas,dichord,dicing,dick,dickens,dicker,dickey,dicky,dicolic,dicolon,dicot,dicotyl,dicta,dictate,dictic,diction,dictum,dicycle,did,didder,diddle,diddler,diddy,didelph,didie,didine,didle,didna,didnt,didromy,didst,didym,didymia,didymus,die,dieb,dieback,diedral,diedric,diehard,dielike,diem,diene,dier,diesel,diesis,diet,dietal,dietary,dieter,diethyl,dietic,dietics,dietine,dietist,diewise,diffame,differ,diffide,difform,diffuse,dig,digamma,digamy,digenic,digeny,digest,digger,digging,dight,dighter,digit,digital,digitus,diglot,diglyph,digmeat,dignify,dignity,digram,digraph,digress,digs,dihalo,diiamb,diiodo,dika,dikage,dike,diker,diketo,dikkop,dilate,dilated,dilater,dilator,dildo,dilemma,dilker,dill,dilli,dillier,dilling,dillue,dilluer,dilly,dilo,dilogy,diluent,dilute,diluted,dilutee,diluter,dilutor,diluvia,dim,dimber,dimble,dime,dimer,dimeran,dimeric,dimeter,dimiss,dimit,dimity,dimly,dimmed,dimmer,dimmest,dimmet,dimmish,dimness,dimoric,dimorph,dimple,dimply,dimps,dimpsy,din,dinar,dinder,dindle,dine,diner,dineric,dinero,dinette,ding,dingar,dingbat,dinge,dingee,dinghee,dinghy,dingily,dingle,dingly,dingo,dingus,dingy,dinic,dinical,dining,dinitro,dink,dinkey,dinkum,dinky,dinmont,dinner,dinnery,dinomic,dinsome,dint,dinus,diobely,diobol,diocese,diode,diodont,dioecy,diol,dionise,dionym,diopter,dioptra,dioptry,diorama,diorite,diose,diosmin,diota,diotic,dioxane,dioxide,dioxime,dioxy,dip,dipetto,diphase,diphead,diplex,diploe,diploic,diploid,diplois,diploma,diplont,diplopy,dipnoan,dipnoid,dipode,dipodic,dipody,dipolar,dipole,diporpa,dipped,dipper,dipping,dipsas,dipsey,dipter,diptote,diptych,dipware,dipygus,dipylon,dipyre,dird,dirdum,dire,direct,direful,direly,dirempt,dirge,dirgler,dirhem,dirk,dirl,dirndl,dirt,dirten,dirtily,dirty,dis,disable,disagio,disally,disarm,disavow,disawa,disazo,disband,disbar,disbark,disbody,disbud,disbury,disc,discage,discal,discard,discase,discept,discern,discerp,discoid,discord,discous,discus,discuss,disdain,disdub,disease,disedge,diseme,disemic,disfame,disfen,disgig,disglut,disgood,disgown,disgulf,disgust,dish,dished,dishelm,disher,dishful,dishome,dishorn,dishpan,dishrag,disject,disjoin,disjune,disk,disleaf,dislike,dislimn,dislink,dislip,disload,dislove,dismain,dismal,disman,dismark,dismask,dismast,dismay,disme,dismiss,disna,disnest,disnew,disobey,disodic,disomic,disomus,disorb,disown,dispark,dispart,dispel,dispend,display,dispone,dispope,disport,dispose,dispost,dispulp,dispute,disrank,disrate,disring,disrobe,disroof,disroot,disrump,disrupt,diss,disseat,dissect,dissent,dissert,dissoul,dissuit,distad,distaff,distain,distal,distale,distant,distend,distent,distich,distill,distome,distort,distune,disturb,disturn,disuse,diswood,disyoke,dit,dita,dital,ditch,ditcher,dite,diter,dither,dithery,dithion,ditolyl,ditone,dittamy,dittany,dittay,dittied,ditto,ditty,diurnal,diurne,div,diva,divan,divata,dive,divel,diver,diverge,divers,diverse,divert,divest,divide,divided,divider,divine,diviner,diving,divinyl,divisor,divorce,divot,divoto,divulge,divulse,divus,divvy,diwata,dixie,dixit,dixy,dizain,dizen,dizoic,dizzard,dizzily,dizzy,djave,djehad,djerib,djersa,do,doab,doable,doarium,doat,doated,doater,doating,doatish,dob,dobbed,dobber,dobbin,dobbing,dobby,dobe,dobla,doblon,dobra,dobrao,dobson,doby,doc,docent,docible,docile,docity,dock,dockage,docken,docker,docket,dockize,dockman,docmac,doctor,doctrix,dod,dodd,doddart,dodded,dodder,doddery,doddie,dodding,doddle,doddy,dodecyl,dodge,dodger,dodgery,dodgily,dodgy,dodkin,dodlet,dodman,dodo,dodoism,dodrans,doe,doebird,doeglic,doer,does,doeskin,doesnt,doest,doff,doffer,dog,dogal,dogate,dogbane,dogbite,dogblow,dogboat,dogbolt,dogbush,dogcart,dogdom,doge,dogedom,dogface,dogfall,dogfish,dogfoot,dogged,dogger,doggery,doggess,doggish,doggo,doggone,doggrel,doggy,doghead,doghole,doghood,dogie,dogless,doglike,dogly,dogma,dogman,dogmata,dogs,dogship,dogskin,dogtail,dogtie,dogtrot,dogvane,dogwood,dogy,doigt,doiled,doily,doina,doing,doings,doit,doited,doitkin,doke,dokhma,dola,dolabra,dolcan,dolcian,dolcino,doldrum,dole,doleful,dolent,doless,doli,dolia,dolina,doline,dolium,doll,dollar,dolldom,dollier,dollish,dollop,dolly,dolman,dolmen,dolor,dolose,dolous,dolphin,dolt,doltish,dom,domain,domal,domba,dome,doment,domer,domett,domic,domical,domine,dominie,domino,dominus,domite,domitic,domn,domnei,domoid,dompt,domy,don,donable,donary,donate,donated,donatee,donator,donax,done,donee,doney,dong,donga,dongon,donjon,donkey,donna,donnert,donnish,donnism,donnot,donor,donship,donsie,dont,donum,doob,doocot,doodab,doodad,doodle,doodler,dooja,dook,dooket,dookit,dool,doolee,dooley,dooli,doolie,dooly,doom,doomage,doomer,doomful,dooms,doon,door,doorba,doorboy,doored,doorman,doorway,dop,dopa,dopatta,dope,doper,dopey,dopper,doppia,dor,dorab,dorad,dorado,doree,dorhawk,doria,dorje,dorlach,dorlot,dorm,dormant,dormer,dormie,dormy,dorn,dorneck,dornic,dornick,dornock,dorp,dorsad,dorsal,dorsale,dorsel,dorser,dorsum,dorter,dorts,dorty,doruck,dory,dos,dosa,dosadh,dosage,dose,doser,dosis,doss,dossal,dossel,dosser,dossier,dossil,dossman,dot,dotage,dotal,dotard,dotardy,dotate,dotchin,dote,doted,doter,doting,dotish,dotkin,dotless,dotlike,dotted,dotter,dottily,dotting,dottle,dottler,dotty,doty,douar,double,doubled,doubler,doublet,doubly,doubt,doubter,douc,douce,doucely,doucet,douche,doucin,doucine,doudle,dough,dought,doughty,doughy,doum,doup,douping,dour,dourine,dourly,douse,douser,dout,douter,doutous,dove,dovecot,dovekey,dovekie,dovelet,dover,dovish,dow,dowable,dowager,dowcet,dowd,dowdily,dowdy,dowed,dowel,dower,doweral,dowery,dowf,dowie,dowily,dowitch,dowl,dowlas,dowless,down,downby,downcry,downcut,downer,downily,downlie,downset,downway,downy,dowp,dowry,dowse,dowser,dowset,doxa,doxy,doze,dozed,dozen,dozener,dozenth,dozer,dozily,dozy,dozzled,drab,drabbet,drabble,drabby,drably,drachm,drachma,dracma,draff,draffy,draft,draftee,drafter,drafty,drag,dragade,dragbar,dragged,dragger,draggle,draggly,draggy,dragman,dragnet,drago,dragon,dragoon,dragsaw,drail,drain,draine,drained,drainer,drake,dram,drama,dramm,dramme,drammed,drammer,drang,drank,drant,drape,draper,drapery,drassid,drastic,drat,drate,dratted,draught,dravya,draw,drawarm,drawbar,drawboy,drawcut,drawee,drawer,drawers,drawing,drawk,drawl,drawler,drawly,drawn,drawnet,drawoff,drawout,drawrod,dray,drayage,drayman,drazel,dread,dreader,dreadly,dream,dreamer,dreamsy,dreamt,dreamy,drear,drearly,dreary,dredge,dredger,dree,dreep,dreepy,dreg,dreggy,dregs,drench,dreng,dress,dressed,dresser,dressy,drest,drew,drewite,drias,drib,dribble,driblet,driddle,dried,drier,driest,drift,drifter,drifty,drill,driller,drillet,dringle,drink,drinker,drinn,drip,dripper,dripple,drippy,drisk,drivage,drive,drivel,driven,driver,driving,drizzle,drizzly,droddum,drogh,drogher,drogue,droit,droll,drolly,drome,dromic,dromond,dromos,drona,dronage,drone,droner,drongo,dronish,drony,drool,droop,drooper,droopt,droopy,drop,droplet,dropman,dropout,dropper,droppy,dropsy,dropt,droshky,drosky,dross,drossel,drosser,drossy,drostdy,droud,drought,drouk,drove,drover,drovy,drow,drown,drowner,drowse,drowsy,drub,drubber,drubbly,drucken,drudge,drudger,druery,drug,drugger,drugget,druggy,drugman,druid,druidic,druidry,druith,drum,drumble,drumlin,drumly,drummer,drummy,drung,drungar,drunk,drunken,drupal,drupe,drupel,druse,drusy,druxy,dry,dryad,dryadic,dryas,drycoal,dryfoot,drying,dryish,dryly,dryness,dryster,dryth,duad,duadic,dual,duali,dualin,dualism,dualist,duality,dualize,dually,duarch,duarchy,dub,dubash,dubb,dubba,dubbah,dubber,dubbing,dubby,dubiety,dubious,dubs,ducal,ducally,ducape,ducat,ducato,ducdame,duces,duchess,duchy,duck,ducker,duckery,duckie,ducking,duckpin,duct,ducted,ductile,duction,ductor,ductule,dud,dudaim,dudder,duddery,duddies,dude,dudeen,dudgeon,dudine,dudish,dudism,dudler,dudley,dudman,due,duel,dueler,dueling,duelist,duello,dueness,duenna,duer,duet,duff,duffel,duffer,duffing,dufoil,dufter,duftery,dug,dugal,dugdug,duggler,dugong,dugout,dugway,duhat,duiker,duim,duit,dujan,duke,dukedom,dukely,dukery,dukhn,dukker,dulbert,dulcet,dulcian,dulcify,dulcose,duledge,duler,dulia,dull,dullard,duller,dullery,dullify,dullish,dullity,dully,dulosis,dulotic,dulse,dult,dultie,duly,dum,duma,dumaist,dumb,dumba,dumbcow,dumbly,dumdum,dummel,dummy,dumose,dump,dumpage,dumper,dumpily,dumping,dumpish,dumple,dumpoke,dumpy,dumsola,dun,dunair,dunal,dunbird,dunce,duncery,dunch,duncify,duncish,dunder,dune,dunfish,dung,dungeon,dunger,dungol,dungon,dungy,dunite,dunk,dunker,dunlin,dunnage,dunne,dunner,dunness,dunnish,dunnite,dunnock,dunny,dunst,dunt,duntle,duny,duo,duodena,duodene,duole,duopod,duopoly,duotone,duotype,dup,dupable,dupe,dupedom,duper,dupery,dupion,dupla,duple,duplet,duplex,duplify,duplone,duppy,dura,durable,durably,durain,dural,duramen,durance,durant,durax,durbar,dure,durene,durenol,duress,durgan,durian,during,durity,durmast,durn,duro,durra,durrie,durrin,durry,durst,durwaun,duryl,dusack,duscle,dush,dusio,dusk,dusken,duskily,duskish,duskly,dusky,dust,dustbin,dustbox,dustee,duster,dustily,dusting,dustman,dustpan,dustuck,dusty,dutch,duteous,dutied,dutiful,dutra,duty,duumvir,duvet,duvetyn,dux,duyker,dvaita,dvandva,dwale,dwalm,dwang,dwarf,dwarfy,dwell,dwelled,dweller,dwelt,dwindle,dwine,dyad,dyadic,dyarchy,dyaster,dyce,dye,dyeable,dyeing,dyer,dyester,dyeware,dyeweed,dyewood,dying,dyingly,dyke,dyker,dynamic,dynamis,dynamo,dynast,dynasty,dyne,dyphone,dyslogy,dysnomy,dyspnea,dystome,dysuria,dysuric,dzeren,e,ea,each,eager,eagerly,eagle,eagless,eaglet,eagre,ean,ear,earache,earbob,earcap,eardrop,eardrum,eared,earful,earhole,earing,earl,earlap,earldom,earless,earlet,earlike,earlish,earlock,early,earmark,earn,earner,earnest,earnful,earning,earpick,earplug,earring,earshot,earsore,eartab,earth,earthed,earthen,earthly,earthy,earwax,earwig,earworm,earwort,ease,easeful,easel,easer,easier,easiest,easily,easing,east,easter,eastern,easting,easy,eat,eatable,eatage,eaten,eater,eatery,eating,eats,eave,eaved,eaver,eaves,ebb,ebbman,eboe,ebon,ebonist,ebonite,ebonize,ebony,ebriate,ebriety,ebrious,ebulus,eburine,ecad,ecanda,ecarte,ecbatic,ecbole,ecbolic,ecdemic,ecderon,ecdysis,ecesic,ecesis,eche,echea,echelon,echidna,echinal,echinid,echinus,echo,echoer,echoic,echoism,echoist,echoize,ecize,ecklein,eclair,eclat,eclegm,eclegma,eclipse,eclogue,ecoid,ecole,ecology,economy,ecotone,ecotype,ecphore,ecru,ecstasy,ectad,ectal,ectally,ectasia,ectasis,ectatic,ectene,ecthyma,ectiris,ectopia,ectopic,ectopy,ectozoa,ectypal,ectype,eczema,edacity,edaphic,edaphon,edder,eddish,eddo,eddy,edea,edeagra,edeitis,edema,edemic,edenite,edental,edestan,edestin,edge,edged,edgeman,edger,edging,edgrew,edgy,edh,edible,edict,edictal,edicule,edifice,edifier,edify,edit,edital,edition,editor,educand,educate,educe,educive,educt,eductor,eegrass,eel,eelboat,eelbob,eelcake,eeler,eelery,eelfare,eelfish,eellike,eelpot,eelpout,eelshop,eelskin,eelware,eelworm,eely,eer,eerie,eerily,effable,efface,effacer,effect,effects,effendi,effete,effigy,efflate,efflux,efform,effort,effulge,effund,effuse,eft,eftest,egad,egality,egence,egeran,egest,egesta,egg,eggcup,egger,eggfish,egghead,egghot,egging,eggler,eggless,egglike,eggnog,eggy,egilops,egipto,egma,ego,egohood,egoism,egoist,egoity,egoize,egoizer,egol,egomism,egotism,egotist,egotize,egress,egret,eh,eheu,ehlite,ehuawa,eident,eider,eidetic,eidolic,eidolon,eight,eighth,eighty,eigne,eimer,einkorn,eisodic,either,eject,ejecta,ejector,ejoo,ekaha,eke,eker,ekerite,eking,ekka,ekphore,ektene,ektenes,el,elaidic,elaidin,elain,elaine,elance,eland,elanet,elapid,elapine,elapoid,elapse,elastic,elastin,elatcha,elate,elated,elater,elation,elative,elator,elb,elbow,elbowed,elbower,elbowy,elcaja,elchee,eld,elder,elderly,eldest,eldin,elding,eldress,elect,electee,electly,elector,electro,elegant,elegiac,elegist,elegit,elegize,elegy,eleidin,element,elemi,elemin,elench,elenchi,elenge,elevate,eleven,elevon,elf,elfhood,elfic,elfin,elfish,elfkin,elfland,elflike,elflock,elfship,elfwife,elfwort,elicit,elide,elision,elisor,elite,elixir,elk,elkhorn,elkslip,elkwood,ell,ellagic,elle,elleck,ellfish,ellipse,ellops,ellwand,elm,elmy,elocute,elod,eloge,elogium,eloign,elope,eloper,elops,els,else,elsehow,elsin,elt,eluate,elude,eluder,elusion,elusive,elusory,elute,elution,elutor,eluvial,eluvium,elvan,elver,elves,elvet,elvish,elysia,elytral,elytrin,elytron,elytrum,em,emanant,emanate,emanium,emarcid,emball,embalm,embank,embar,embargo,embark,embassy,embathe,embay,embed,embelic,ember,embind,embira,emblaze,emblem,emblema,emblic,embody,embog,embole,embolic,embolo,embolum,embolus,emboly,embosom,emboss,embound,embow,embowed,embowel,embower,embox,embrace,embrail,embroil,embrown,embryo,embryon,embuia,embus,embusk,emcee,eme,emeer,emend,emender,emerald,emerge,emerize,emerse,emersed,emery,emesis,emetic,emetine,emgalla,emigree,eminent,emir,emirate,emit,emitter,emma,emmenic,emmer,emmet,emodin,emoloa,emote,emotion,emotive,empall,empanel,empaper,empark,empasm,empathy,emperor,empery,empire,empiric,emplace,emplane,employ,emplume,emporia,empower,empress,emprise,empt,emptier,emptily,emptins,emption,emptor,empty,empyema,emu,emulant,emulate,emulous,emulsin,emulsor,emyd,emydian,en,enable,enabler,enact,enactor,enaena,enage,enalid,enam,enamber,enamdar,enamel,enamor,enapt,enarbor,enarch,enarm,enarme,enate,enatic,enation,enbrave,encage,encake,encamp,encase,encash,encauma,encave,encell,enchain,enchair,enchant,enchase,enchest,encina,encinal,encist,enclasp,enclave,encloak,enclose,encloud,encoach,encode,encoil,encolor,encomia,encomic,encoop,encore,encowl,encraal,encraty,encreel,encrisp,encrown,encrust,encrypt,encup,encurl,encyst,end,endable,endarch,endaze,endear,ended,endemic,ender,endere,enderon,endevil,endew,endgate,ending,endite,endive,endless,endlong,endmost,endogen,endome,endopod,endoral,endore,endorse,endoss,endotys,endow,endower,endozoa,endue,endura,endure,endurer,endways,endwise,endyma,endymal,endysis,enema,enemy,energic,energid,energy,eneuch,eneugh,enface,enfelon,enfeoff,enfever,enfile,enfiled,enflesh,enfoil,enfold,enforce,enfork,enfoul,enframe,enfree,engage,engaged,engager,engaol,engarb,engaud,engaze,engem,engild,engine,engird,engirt,englad,englobe,engloom,englory,englut,englyn,engobe,engold,engore,engorge,engrace,engraff,engraft,engrail,engrain,engram,engrasp,engrave,engreen,engross,enguard,engulf,enhalo,enhance,enhat,enhaunt,enheart,enhedge,enhelm,enherit,enhusk,eniac,enigma,enisle,enjail,enjamb,enjelly,enjewel,enjoin,enjoy,enjoyer,enkraal,enlace,enlard,enlarge,enleaf,enlief,enlife,enlight,enlink,enlist,enliven,enlock,enlodge,enmask,enmass,enmesh,enmist,enmity,enmoss,ennead,ennerve,enniche,ennoble,ennoic,ennomic,ennui,enocyte,enodal,enoil,enol,enolate,enolic,enolize,enomoty,enoplan,enorm,enough,enounce,enow,enplane,enquire,enquiry,enrace,enrage,enraged,enrange,enrank,enrapt,enray,enrib,enrich,enring,enrive,enrobe,enrober,enrol,enroll,enroot,enrough,enruin,enrut,ens,ensaint,ensand,ensate,enscene,ense,enseam,enseat,enseem,enserf,ensete,enshade,enshawl,enshell,ensign,ensile,ensky,enslave,ensmall,ensnare,ensnarl,ensnow,ensoul,enspell,enstamp,enstar,enstate,ensteel,enstool,enstore,ensuant,ensue,ensuer,ensure,ensurer,ensweep,entach,entad,entail,ental,entame,entasia,entasis,entelam,entente,enter,enteral,enterer,enteria,enteric,enteron,entheal,enthral,enthuse,entia,entice,enticer,entify,entire,entiris,entitle,entity,entoil,entomb,entomic,entone,entopic,entotic,entozoa,entrail,entrain,entrant,entrap,entreat,entree,entropy,entrust,entry,entwine,entwist,enure,enurny,envapor,envault,enveil,envelop,envenom,envied,envier,envious,environ,envoy,envy,envying,enwiden,enwind,enwisen,enwoman,enwomb,enwood,enwound,enwrap,enwrite,enzone,enzooty,enzym,enzyme,enzymic,eoan,eolith,eon,eonism,eophyte,eosate,eoside,eosin,eosinic,eozoon,epacme,epacrid,epact,epactal,epagoge,epanody,eparch,eparchy,epaule,epaulet,epaxial,epee,epeeist,epeiric,epeirid,epergne,epha,ephah,ephebe,ephebic,ephebos,ephebus,ephelis,ephetae,ephete,ephetic,ephod,ephor,ephoral,ephoric,ephorus,ephyra,epibole,epiboly,epic,epical,epicarp,epicede,epicele,epicene,epichil,epicism,epicist,epicly,epicure,epicyte,epidemy,epiderm,epidote,epigeal,epigean,epigeic,epigene,epigone,epigram,epigyne,epigyny,epihyal,epikeia,epilate,epilobe,epimer,epimere,epimyth,epinaos,epinine,epiotic,epipial,episode,epistle,epitaph,epitela,epithem,epithet,epitoke,epitome,epiural,epizoa,epizoal,epizoan,epizoic,epizoon,epoch,epocha,epochal,epode,epodic,eponym,eponymy,epopee,epopt,epoptes,epoptic,epos,epsilon,epulary,epulis,epulo,epuloid,epural,epurate,equable,equably,equal,equally,equant,equate,equator,equerry,equid,equine,equinia,equinox,equinus,equip,equiped,equison,equites,equity,equoid,er,era,erade,eral,eranist,erase,erased,eraser,erasion,erasure,erbia,erbium,erd,erdvark,ere,erect,erecter,erectly,erector,erelong,eremic,eremite,erenach,erenow,erepsin,erept,ereptic,erethic,erg,ergal,ergasia,ergates,ergodic,ergoism,ergon,ergot,ergoted,ergotic,ergotin,ergusia,eria,eric,ericad,erical,ericius,ericoid,erika,erikite,erineum,erinite,erinose,eristic,erizo,erlking,ermelin,ermine,ermined,erminee,ermines,erne,erode,eroded,erodent,erogeny,eros,erose,erosely,erosion,erosive,eroteme,erotic,erotica,erotism,err,errable,errancy,errand,errant,errata,erratic,erratum,errhine,erring,errite,error,ers,ersatz,erth,erthen,erthly,eruc,eruca,erucic,erucin,eruct,erudit,erudite,erugate,erupt,eryngo,es,esca,escalan,escalin,escalop,escape,escapee,escaper,escarp,eschar,eschara,escheat,eschew,escoba,escolar,escort,escribe,escrol,escrow,escudo,esculin,esere,eserine,esexual,eshin,esker,esne,esodic,esotery,espadon,esparto,espave,espial,espier,espinal,espino,esplees,espouse,espy,esquire,ess,essang,essay,essayer,essed,essence,essency,essling,essoin,estadal,estadio,estado,estamp,estate,esteem,ester,estevin,estival,estmark,estoc,estoile,estop,estrade,estray,estre,estreat,estrepe,estrin,estriol,estrone,estrous,estrual,estuary,estufa,estuous,estus,eta,etacism,etacist,etalon,etamine,etch,etcher,etching,eternal,etesian,ethal,ethanal,ethane,ethanol,ethel,ethene,ethenic,ethenol,ethenyl,ether,ethered,etheric,etherin,ethic,ethical,ethics,ethid,ethide,ethine,ethiops,ethmoid,ethnal,ethnic,ethnize,ethnos,ethos,ethoxyl,ethrog,ethyl,ethylic,ethylin,ethyne,ethynyl,etiolin,etna,ettle,etua,etude,etui,etym,etymic,etymon,etypic,eu,euaster,eucaine,euchre,euchred,euclase,eucone,euconic,eucrasy,eucrite,euge,eugenic,eugenol,eugeny,eulalia,eulogia,eulogic,eulogy,eumenid,eunicid,eunomy,eunuch,euonym,euonymy,euouae,eupad,eupathy,eupepsy,euphemy,euphon,euphone,euphony,euphory,euphroe,eupione,euploid,eupnea,eureka,euripus,eurite,eurobin,euryon,eusol,eustyle,eutaxic,eutaxy,eutexia,eutony,evacue,evacuee,evade,evader,evalue,evangel,evanish,evase,evasion,evasive,eve,evejar,evelong,even,evener,evening,evenly,evens,event,eveque,ever,evert,evertor,everwho,every,evestar,evetide,eveweed,evict,evictor,evident,evil,evilly,evince,evirate,evisite,evitate,evocate,evoe,evoke,evoker,evolute,evolve,evolver,evovae,evulse,evzone,ewder,ewe,ewer,ewerer,ewery,ewry,ex,exact,exacter,exactly,exactor,exalate,exalt,exalted,exalter,exam,examen,examine,example,exarate,exarch,exarchy,excamb,excave,exceed,excel,except,excerpt,excess,excide,exciple,excise,excisor,excite,excited,exciter,excitor,exclaim,exclave,exclude,excreta,excrete,excurse,excusal,excuse,excuser,excuss,excyst,exdie,exeat,execute,exedent,exedra,exegete,exempt,exequy,exergue,exert,exes,exeunt,exflect,exhale,exhaust,exhibit,exhort,exhume,exhumer,exigent,exile,exiler,exilian,exilic,exility,exist,exister,exit,exite,exition,exitus,exlex,exocarp,exocone,exode,exoderm,exodic,exodist,exodos,exodus,exody,exogamy,exogen,exogeny,exomion,exomis,exon,exoner,exopod,exordia,exormia,exosmic,exostra,exotic,exotism,expand,expanse,expect,expede,expel,expend,expense,expert,expiate,expire,expiree,expirer,expiry,explain,explant,explode,exploit,explore,expone,export,exposal,expose,exposed,exposer,exposit,expound,express,expugn,expulse,expunge,expurge,exradio,exscind,exsect,exsert,exship,exsurge,extant,extend,extense,extent,exter,extern,externe,extima,extinct,extine,extol,extoll,extort,extra,extract,extrait,extreme,extrude,extund,exudate,exude,exult,exultet,exuviae,exuvial,ey,eyah,eyalet,eyas,eye,eyeball,eyebalm,eyebar,eyebeam,eyebolt,eyebree,eyebrow,eyecup,eyed,eyedot,eyedrop,eyeflap,eyeful,eyehole,eyelash,eyeless,eyelet,eyelid,eyelike,eyeline,eyemark,eyen,eyepit,eyer,eyeroot,eyeseed,eyeshot,eyesome,eyesore,eyespot,eyewash,eyewear,eyewink,eyewort,eyey,eying,eyn,eyne,eyot,eyoty,eyra,eyre,eyrie,eyrir,ezba,f,fa,fabella,fabes,fable,fabled,fabler,fabliau,fabling,fabric,fabular,facadal,facade,face,faced,faceman,facer,facet,facete,faceted,facia,facial,faciend,facient,facies,facile,facing,fack,fackins,facks,fact,factful,faction,factish,factive,factor,factory,factrix,factual,factum,facture,facty,facula,facular,faculty,facund,facy,fad,fadable,faddish,faddism,faddist,faddle,faddy,fade,faded,fadedly,faden,fader,fadge,fading,fady,fae,faerie,faery,faff,faffle,faffy,fag,fagald,fage,fager,fagger,faggery,fagging,fagine,fagot,fagoter,fagoty,faham,fahlerz,fahlore,faience,fail,failing,faille,failure,fain,fainly,fains,faint,fainter,faintly,faints,fainty,faipule,fair,fairer,fairily,fairing,fairish,fairly,fairm,fairway,fairy,faith,faitour,fake,faker,fakery,fakir,faky,falbala,falcade,falcate,falcer,falces,falcial,falcon,falcula,faldage,faldfee,fall,fallace,fallacy,fallage,fallen,faller,falling,fallow,fallway,fally,falsary,false,falsely,falsen,falser,falsie,falsify,falsism,faltche,falter,falutin,falx,fam,famble,fame,fameful,familia,family,famine,famish,famous,famulus,fan,fana,fanal,fanam,fanatic,fanback,fancied,fancier,fancify,fancy,fand,fandom,fanega,fanfare,fanfoot,fang,fanged,fangle,fangled,fanglet,fangot,fangy,fanion,fanlike,fanman,fannel,fanner,fannier,fanning,fanon,fant,fantail,fantast,fantasy,fantod,fanweed,fanwise,fanwork,fanwort,faon,far,farad,faraday,faradic,faraway,farce,farcer,farcial,farcied,farcify,farcing,farcist,farcy,farde,fardel,fardh,fardo,fare,farer,farfara,farfel,fargood,farina,faring,farish,farl,farleu,farm,farmage,farmer,farmery,farming,farmost,farmy,farness,faro,farrago,farrand,farrier,farrow,farruca,farse,farseer,farset,farther,fasces,fascet,fascia,fascial,fascine,fascis,fascism,fascist,fash,fasher,fashery,fashion,fass,fast,fasten,faster,fasting,fastish,fastus,fat,fatal,fatally,fatbird,fate,fated,fateful,fathead,father,fathmur,fathom,fatidic,fatigue,fatiha,fatil,fatless,fatling,fatly,fatness,fatsia,fatten,fatter,fattily,fattish,fatty,fatuism,fatuity,fatuoid,fatuous,fatwood,faucal,fauces,faucet,faucial,faucre,faugh,fauld,fault,faulter,faulty,faun,faunal,faunish,faunist,faunule,fause,faust,fautor,fauve,favella,favilla,favism,favissa,favn,favor,favored,favorer,favose,favous,favus,fawn,fawner,fawnery,fawning,fawny,fay,fayles,faze,fazenda,fe,feague,feak,feal,fealty,fear,feared,fearer,fearful,feasor,feast,feasten,feaster,feat,feather,featly,featous,feature,featy,feaze,febrile,fecal,feces,feck,feckful,feckly,fecula,fecund,fed,feddan,federal,fee,feeable,feeble,feebly,feed,feedbin,feedbox,feeder,feeding,feedman,feedway,feedy,feel,feeler,feeless,feeling,feer,feere,feering,feetage,feeze,fegary,fei,feif,feigher,feign,feigned,feigner,feil,feint,feis,feist,feisty,felid,feline,fell,fellage,fellah,fellen,feller,fellic,felling,felloe,fellow,felly,feloid,felon,felonry,felony,fels,felsite,felt,felted,felter,felting,felty,felucca,felwort,female,feme,femic,feminal,feminie,feminin,femora,femoral,femur,fen,fenbank,fence,fencer,fenchyl,fencing,fend,fender,fendy,fenite,fenks,fenland,fenman,fennec,fennel,fennig,fennish,fenny,fensive,fent,fenter,feod,feodal,feodary,feoff,feoffee,feoffor,feower,feral,feralin,ferash,ferdwit,ferfet,feria,ferial,feridgi,ferie,ferine,ferity,ferk,ferling,ferly,fermail,ferme,ferment,fermery,fermila,fern,ferned,fernery,ferny,feroher,ferrado,ferrate,ferrean,ferret,ferrety,ferri,ferric,ferrier,ferrite,ferrous,ferrule,ferrum,ferry,fertile,feru,ferula,ferule,ferulic,fervent,fervid,fervor,fescue,fess,fessely,fest,festal,fester,festine,festive,festoon,festuca,fet,fetal,fetch,fetched,fetcher,fetial,fetid,fetidly,fetish,fetlock,fetlow,fetor,fetter,fettle,fettler,fetus,feu,feuage,feuar,feucht,feud,feudal,feudee,feudist,feued,feuille,fever,feveret,few,fewness,fewsome,fewter,fey,feyness,fez,fezzed,fezzy,fi,fiacre,fiance,fiancee,fiar,fiard,fiasco,fiat,fib,fibber,fibbery,fibdom,fiber,fibered,fibril,fibrin,fibrine,fibroid,fibroin,fibroma,fibrose,fibrous,fibry,fibster,fibula,fibulae,fibular,ficary,fice,ficelle,fiche,fichu,fickle,fickly,fico,ficoid,fictile,fiction,fictive,fid,fidalgo,fidate,fiddle,fiddler,fiddley,fide,fideism,fideist,fidfad,fidge,fidget,fidgety,fiducia,fie,fiefdom,field,fielded,fielder,fieldy,fiend,fiendly,fient,fierce,fiercen,fierily,fiery,fiesta,fife,fifer,fifie,fifish,fifo,fifteen,fifth,fifthly,fifty,fig,figaro,figbird,figent,figged,figgery,figging,figgle,figgy,fight,fighter,figless,figlike,figment,figural,figure,figured,figurer,figury,figworm,figwort,fike,fikie,filace,filacer,filao,filar,filaria,filasse,filate,filator,filbert,filch,filcher,file,filemot,filer,filet,filial,filiate,filibeg,filical,filicic,filicin,filiety,filing,filings,filippo,filite,fill,filled,filler,fillet,filleul,filling,fillip,fillock,filly,film,filmdom,filmet,filmic,filmily,filmish,filmist,filmize,filmy,filo,filose,fils,filter,filth,filthy,fimble,fimbria,fin,finable,finagle,final,finale,finally,finance,finback,finch,finched,find,findal,finder,finding,findjan,fine,fineish,finely,finer,finery,finesse,finetop,finfish,finfoot,fingent,finger,fingery,finial,finical,finick,finific,finify,finikin,fining,finis,finish,finite,finity,finjan,fink,finkel,finland,finless,finlet,finlike,finnac,finned,finner,finnip,finny,fiord,fiorded,fiorin,fiorite,fip,fipenny,fipple,fique,fir,firca,fire,firearm,firebox,fireboy,firebug,fired,firedog,firefly,firelit,fireman,firer,firetop,firing,firk,firker,firkin,firlot,firm,firman,firmer,firmly,firn,firring,firry,first,firstly,firth,fisc,fiscal,fise,fisetin,fish,fishbed,fished,fisher,fishery,fishet,fisheye,fishful,fishgig,fishify,fishily,fishing,fishlet,fishman,fishpot,fishway,fishy,fisnoga,fissate,fissile,fission,fissive,fissure,fissury,fist,fisted,fister,fistful,fistic,fistify,fisting,fistuca,fistula,fistule,fisty,fit,fitch,fitched,fitchee,fitcher,fitchet,fitchew,fitful,fitly,fitment,fitness,fitout,fitroot,fittage,fitted,fitten,fitter,fitters,fittily,fitting,fitty,fitweed,five,fivebar,fiver,fives,fix,fixable,fixage,fixate,fixatif,fixator,fixed,fixedly,fixer,fixing,fixity,fixture,fixure,fizgig,fizz,fizzer,fizzle,fizzy,fjeld,flabby,flabrum,flaccid,flack,flacked,flacker,flacket,flaff,flaffer,flag,flagger,flaggy,flaglet,flagman,flagon,flail,flair,flaith,flak,flakage,flake,flaker,flakily,flaky,flam,flamant,flamb,flame,flamed,flamen,flamer,flamfew,flaming,flamy,flan,flanch,flandan,flane,flange,flanger,flank,flanked,flanker,flanky,flannel,flanque,flap,flapper,flare,flaring,flary,flaser,flash,flasher,flashet,flashly,flashy,flask,flasker,flasket,flasque,flat,flatcap,flatcar,flatdom,flated,flathat,flatlet,flatly,flatman,flatten,flatter,flattie,flattop,flatus,flatway,flaught,flaunt,flaunty,flavedo,flavic,flavid,flavin,flavine,flavo,flavone,flavor,flavory,flavour,flaw,flawed,flawful,flawn,flawy,flax,flaxen,flaxman,flaxy,flay,flayer,flea,fleam,fleay,flebile,fleche,fleck,flecken,flecker,flecky,flector,fled,fledge,fledgy,flee,fleece,fleeced,fleecer,fleech,fleecy,fleer,fleerer,fleet,fleeter,fleetly,flemish,flench,flense,flenser,flerry,flesh,fleshed,fleshen,flesher,fleshly,fleshy,flet,fletch,flether,fleuret,fleury,flew,flewed,flewit,flews,flex,flexed,flexile,flexion,flexor,flexure,fley,flick,flicker,flicky,flidder,flier,fligger,flight,flighty,flimmer,flimp,flimsy,flinch,flinder,fling,flinger,flingy,flint,flinter,flinty,flioma,flip,flipe,flipper,flirt,flirter,flirty,flisk,flisky,flit,flitch,flite,fliting,flitter,flivver,flix,float,floater,floaty,flob,flobby,floc,floccus,flock,flocker,flocky,flocoon,flodge,floe,floey,flog,flogger,flokite,flong,flood,flooded,flooder,floody,floor,floorer,floozy,flop,flopper,floppy,flora,floral,floran,florate,floreal,florent,flores,floret,florid,florin,florist,floroon,florula,flory,flosh,floss,flosser,flossy,flot,flota,flotage,flotant,flotsam,flounce,flour,floury,flouse,flout,flouter,flow,flowage,flower,flowery,flowing,flown,flowoff,flu,fluate,fluavil,flub,flubdub,flucan,flue,flued,flueman,fluency,fluent,fluer,fluey,fluff,fluffer,fluffy,fluible,fluid,fluidal,fluidic,fluidly,fluke,fluked,flukily,fluking,fluky,flume,flummer,flummox,flump,flung,flunk,flunker,flunky,fluor,fluoran,fluoric,fluoryl,flurn,flurr,flurry,flush,flusher,flushy,flusk,flusker,fluster,flute,fluted,fluter,flutina,fluting,flutist,flutter,fluty,fluvial,flux,fluxer,fluxile,fluxion,fly,flyable,flyaway,flyback,flyball,flybane,flybelt,flyblow,flyboat,flyboy,flyer,flyflap,flying,flyleaf,flyless,flyman,flyness,flype,flytail,flytier,flytrap,flyway,flywort,foal,foaly,foam,foambow,foamer,foamily,foaming,foamy,fob,focal,focally,foci,focoids,focsle,focus,focuser,fod,fodda,fodder,foder,fodge,fodgel,fodient,foe,foehn,foeish,foeless,foelike,foeman,foeship,fog,fogbow,fogdog,fogdom,fogey,foggage,fogged,fogger,foggily,foggish,foggy,foghorn,fogle,fogless,fogman,fogo,fogon,fogou,fogram,fogus,fogy,fogydom,fogyish,fogyism,fohat,foible,foil,foiler,foiling,foining,foison,foist,foister,foisty,foiter,fold,foldage,folded,folden,folder,folding,foldure,foldy,fole,folia,foliage,folial,foliar,foliary,foliate,folie,folio,foliole,foliose,foliot,folious,folium,folk,folkmot,folksy,folkway,folky,folles,follis,follow,folly,foment,fomes,fomites,fondak,fondant,fondish,fondle,fondler,fondly,fondu,fondue,fonduk,fonly,fonnish,fono,fons,font,fontal,fonted,fontful,fontlet,foo,food,fooder,foodful,foody,fool,fooldom,foolery,fooless,fooling,foolish,fooner,fooster,foot,footage,footboy,footed,footer,footful,foothot,footing,footle,footler,footman,footpad,foots,footway,footy,foozle,foozler,fop,fopling,foppery,foppish,foppy,fopship,for,fora,forage,forager,foramen,forane,foray,forayer,forb,forbade,forbar,forbear,forbid,forbit,forbled,forblow,forbore,forbow,forby,force,forced,forceps,forcer,forche,forcing,ford,fordays,fording,fordo,fordone,fordy,fore,foreact,forearm,forebay,forecar,foreday,forefin,forefit,forego,foreign,forel,forelay,foreleg,foreman,forepad,forepaw,foreran,forerib,forerun,foresay,foresee,foreset,foresin,forest,foresty,foretop,foreuse,forever,forevow,forfar,forfare,forfars,forfeit,forfend,forge,forged,forger,forgery,forget,forgie,forging,forgive,forgo,forgoer,forgot,forgrow,forhoo,forhooy,forhow,forint,fork,forked,forker,forkful,forkman,forky,forleft,forlet,forlorn,form,formal,formant,format,formate,forme,formed,formee,formel,formene,former,formful,formic,formin,forming,formose,formula,formule,formy,formyl,fornent,fornix,forpet,forpine,forpit,forrad,forrard,forride,forrit,forrue,forsake,forset,forslow,fort,forte,forth,forthgo,forthy,forties,fortify,fortin,fortis,fortlet,fortune,forty,forum,forward,forwean,forwent,fosh,fosie,fossa,fossage,fossane,fosse,fossed,fossick,fossil,fossor,fossula,fossule,fostell,foster,fot,fotch,fother,fotmal,fotui,fou,foud,fouette,fougade,fought,foughty,foujdar,foul,foulage,foulard,fouler,fouling,foulish,foully,foumart,foun,found,founder,foundry,fount,four,fourble,fourche,fourer,fourre,fourth,foussa,foute,fouter,fouth,fovea,foveal,foveate,foveola,foveole,fow,fowk,fowl,fowler,fowlery,fowling,fox,foxbane,foxchop,foxer,foxery,foxfeet,foxfish,foxhole,foxily,foxing,foxish,foxlike,foxship,foxskin,foxtail,foxwood,foxy,foy,foyaite,foyboat,foyer,fozy,fra,frab,frabbit,frabous,fracas,frache,frack,fracted,frae,fraghan,fragile,fraid,fraik,frail,frailly,frailty,fraise,fraiser,frame,framea,framed,framer,framing,frammit,franc,franco,frank,franker,frankly,frantic,franzy,frap,frappe,frasco,frase,frasier,frass,frat,fratch,fratchy,frater,fratery,fratry,fraud,fraught,frawn,fraxin,fray,frayed,fraying,frayn,fraze,frazer,frazil,frazzle,freak,freaky,fream,freath,freck,frecken,frecket,freckle,freckly,free,freed,freedom,freeing,freeish,freely,freeman,freer,freet,freety,freeway,freeze,freezer,freight,freir,freit,freity,fremd,fremdly,frenal,frenate,frenum,frenzy,fresco,fresh,freshen,freshet,freshly,fresnel,fresno,fret,fretful,frett,frette,fretted,fretter,fretty,fretum,friable,friand,friar,friarly,friary,frib,fribble,fribby,fried,friend,frier,frieze,friezer,friezy,frig,frigate,friggle,fright,frighty,frigid,frijol,frike,frill,frilled,friller,frilly,frim,fringe,fringed,fringy,frisca,frisk,frisker,frisket,frisky,frison,frist,frisure,frit,frith,fritt,fritter,frivol,frixion,friz,frize,frizer,frizz,frizzer,frizzle,frizzly,frizzy,fro,frock,froe,frog,frogbit,frogeye,frogged,froggy,frogleg,froglet,frogman,froise,frolic,from,frond,fronded,front,frontad,frontal,fronted,fronter,froom,frore,frory,frosh,frost,frosted,froster,frosty,frot,froth,frother,frothy,frotton,frough,froughy,frounce,frow,froward,frower,frowl,frown,frowner,frowny,frowst,frowsty,frowy,frowze,frowzly,frowzy,froze,frozen,fructed,frugal,fruggan,fruit,fruited,fruiter,fruity,frump,frumple,frumpy,frush,frustum,frutify,fry,fryer,fu,fub,fubby,fubsy,fucate,fuchsin,fuci,fucoid,fucosan,fucose,fucous,fucus,fud,fuddle,fuddler,fuder,fudge,fudger,fudgy,fuel,fueler,fuerte,fuff,fuffy,fugal,fugally,fuggy,fugient,fugle,fugler,fugu,fugue,fuguist,fuidhir,fuji,fulcral,fulcrum,fulfill,fulgent,fulgid,fulgide,fulgor,fulham,fulk,full,fullam,fuller,fullery,fulling,fullish,fullom,fully,fulmar,fulmine,fulsome,fulth,fulvene,fulvid,fulvous,fulwa,fulyie,fulzie,fum,fumado,fumage,fumaric,fumaryl,fumble,fumbler,fume,fumer,fumet,fumette,fumily,fuming,fumose,fumous,fumy,fun,fund,fundal,funded,funder,fundi,fundic,funds,fundus,funeral,funest,fungal,fungate,fungi,fungian,fungic,fungin,fungo,fungoid,fungose,fungous,fungus,fungusy,funicle,funis,funk,funker,funky,funnel,funnily,funny,funori,funt,fur,fural,furan,furazan,furbish,furca,furcal,furcate,furcula,furdel,furfur,furiant,furied,furify,furil,furilic,furiosa,furioso,furious,furison,furl,furler,furless,furlong,furnace,furnage,furner,furnish,furoic,furoid,furoin,furole,furor,furore,furphy,furred,furrier,furrily,furring,furrow,furrowy,furry,further,furtive,fury,furyl,furze,furzed,furzery,furzy,fusain,fusate,fusc,fuscin,fuscous,fuse,fused,fusee,fusht,fusible,fusibly,fusil,fusilly,fusion,fusoid,fuss,fusser,fussify,fussily,fussock,fussy,fust,fustee,fustet,fustian,fustic,fustily,fustin,fustle,fusty,fusuma,fusure,fut,futchel,fute,futhorc,futile,futtock,futural,future,futuric,futwa,fuye,fuze,fuzz,fuzzily,fuzzy,fyke,fylfot,fyrd,g,ga,gab,gabbard,gabber,gabble,gabbler,gabbro,gabby,gabelle,gabgab,gabi,gabion,gable,gablet,gablock,gaby,gad,gadbee,gadbush,gadded,gadder,gaddi,gadding,gaddish,gade,gadfly,gadge,gadger,gadget,gadid,gadling,gadman,gadoid,gadroon,gadsman,gaduin,gadwall,gaen,gaet,gaff,gaffe,gaffer,gaffle,gag,gagate,gage,gagee,gageite,gager,gagger,gaggery,gaggle,gaggler,gagman,gagor,gagroot,gahnite,gaiassa,gaiety,gaily,gain,gainage,gaine,gainer,gainful,gaining,gainly,gains,gainsay,gainset,gainst,gair,gait,gaited,gaiter,gaiting,gaize,gaj,gal,gala,galah,galanas,galanga,galant,galany,galatea,galaxy,galban,gale,galea,galeage,galeate,galee,galeeny,galeid,galena,galenic,galeoid,galera,galerum,galerus,galet,galey,galgal,gali,galilee,galiot,galipot,gall,galla,gallah,gallant,gallate,galled,gallein,galleon,galler,gallery,gallet,galley,gallfly,gallic,galline,galling,gallium,gallnut,gallon,galloon,gallop,gallous,gallows,gally,galoot,galop,galore,galosh,galp,galt,galumph,galuth,galyac,galyak,gam,gamahe,gamasid,gamb,gamba,gambade,gambado,gambang,gambeer,gambet,gambia,gambier,gambist,gambit,gamble,gambler,gamboge,gambol,gambrel,game,gamebag,gameful,gamely,gamene,gametal,gamete,gametic,gamic,gamily,gamin,gaming,gamma,gammer,gammick,gammock,gammon,gammy,gamont,gamori,gamp,gamut,gamy,gan,ganam,ganch,gander,gandul,gandum,gane,ganef,gang,ganga,gangan,gangava,gangdom,gange,ganger,ganging,gangism,ganglia,gangly,gangman,gangrel,gangue,gangway,ganja,ganner,gannet,ganoid,ganoin,ganosis,gansel,gansey,gansy,gant,ganta,gantang,gantlet,ganton,gantry,gantsl,ganza,ganzie,gaol,gaoler,gap,gapa,gape,gaper,gapes,gaping,gapo,gappy,gapy,gar,gara,garad,garage,garance,garava,garawi,garb,garbage,garbel,garbell,garbill,garble,garbler,garboil,garbure,garce,gardant,gardeen,garden,gardeny,gardy,gare,gareh,garetta,garfish,garget,gargety,gargle,gargol,garial,gariba,garish,garland,garle,garlic,garment,garn,garnel,garner,garnet,garnets,garnett,garnetz,garnice,garniec,garnish,garoo,garrafa,garran,garret,garrot,garrote,garrupa,garse,garsil,garston,garten,garter,garth,garum,garvey,garvock,gas,gasbag,gaseity,gaseous,gash,gashes,gashful,gashly,gashy,gasify,gasket,gaskin,gasking,gaskins,gasless,gaslit,gaslock,gasman,gasp,gasper,gasping,gaspy,gasser,gassing,gassy,gast,gaster,gastral,gastric,gastrin,gat,gata,gatch,gate,gateado,gateage,gated,gateman,gater,gateway,gather,gating,gator,gatter,gau,gaub,gauby,gauche,gaud,gaudery,gaudful,gaudily,gaudy,gaufer,gauffer,gauffre,gaufre,gauge,gauger,gauging,gaulin,gault,gaulter,gaum,gaumish,gaumy,gaun,gaunt,gaunted,gauntly,gauntry,gaunty,gaup,gaupus,gaur,gaus,gauss,gauster,gaut,gauze,gauzily,gauzy,gavall,gave,gavel,gaveler,gavial,gavotte,gavyuti,gaw,gawby,gawcie,gawk,gawkily,gawkish,gawky,gawm,gawn,gawney,gawsie,gay,gayal,gayatri,gaybine,gaycat,gayish,gayment,gayness,gaysome,gayyou,gaz,gazabo,gaze,gazebo,gazee,gazel,gazelle,gazer,gazette,gazi,gazing,gazon,gazy,ge,geal,gean,gear,gearbox,geared,gearing,gearman,gearset,gease,geason,geat,gebang,gebanga,gebbie,gebur,geck,gecko,geckoid,ged,gedackt,gedder,gedeckt,gedrite,gee,geebong,geebung,geejee,geek,geelbec,geerah,geest,geet,geezer,gegg,geggee,gegger,geggery,gein,geira,geisha,geison,geitjie,gel,gelable,gelada,gelatin,geld,geldant,gelder,gelding,gelid,gelidly,gelilah,gell,gelly,gelong,gelose,gelosin,gelt,gem,gemauve,gemel,gemeled,gemless,gemlike,gemma,gemmae,gemmate,gemmer,gemmily,gemmoid,gemmula,gemmule,gemmy,gemot,gemsbok,gemul,gemuti,gemwork,gen,gena,genal,genapp,genarch,gender,gene,genear,geneat,geneki,genep,genera,general,generic,genesic,genesis,genet,genetic,geneva,genial,genian,genic,genie,genii,genin,genion,genip,genipa,genipap,genista,genital,genitor,genius,genizah,genoese,genom,genome,genomic,genos,genre,genro,gens,genson,gent,genteel,gentes,gentian,gentile,gentle,gently,gentman,gentry,genty,genu,genua,genual,genuine,genus,genys,geo,geobios,geodal,geode,geodesy,geodete,geodic,geodist,geoduck,geoform,geogeny,geogony,geoid,geoidal,geology,geomaly,geomant,geomyid,geonoma,geopony,georama,georgic,geosid,geoside,geotaxy,geotic,geoty,ger,gerah,geranic,geranyl,gerate,gerated,geratic,geraty,gerb,gerbe,gerbil,gercrow,gerefa,gerenda,gerent,gerenuk,gerim,gerip,germ,germal,german,germane,germen,germin,germina,germing,germon,germule,germy,gernitz,geront,geronto,gers,gersum,gerund,gerusia,gervao,gesith,gesning,gesso,gest,gestant,gestate,geste,gested,gesten,gestic,gestion,gesture,get,geta,getah,getaway,gether,getling,getter,getting,getup,geum,gewgaw,gewgawy,gey,geyan,geyser,gez,ghafir,ghaist,ghalva,gharial,gharnao,gharry,ghastly,ghat,ghatti,ghatwal,ghazi,ghazism,ghebeta,ghee,gheleem,gherkin,ghetti,ghetto,ghizite,ghoom,ghost,ghoster,ghostly,ghosty,ghoul,ghrush,ghurry,giant,giantly,giantry,giardia,giarra,giarre,gib,gibaro,gibbals,gibbed,gibber,gibbet,gibbles,gibbon,gibbose,gibbous,gibbus,gibby,gibe,gibel,giber,gibing,gibleh,giblet,giblets,gibus,gid,giddap,giddea,giddify,giddily,giddy,gidgee,gie,gied,gien,gif,gift,gifted,giftie,gig,gigback,gigeria,gigful,gigger,giggish,giggit,giggle,giggler,giggly,giglet,giglot,gigman,gignate,gigolo,gigot,gigsman,gigster,gigtree,gigunu,gilbert,gild,gilded,gilden,gilder,gilding,gilguy,gilia,gilim,gill,gilled,giller,gillie,gilling,gilly,gilo,gilpy,gilse,gilt,giltcup,gim,gimbal,gimble,gimel,gimlet,gimlety,gimmal,gimmer,gimmick,gimp,gimped,gimper,gimping,gin,ging,ginger,gingery,gingham,gingili,gingiva,gink,ginkgo,ginned,ginner,ginners,ginnery,ginney,ginning,ginnle,ginny,ginseng,ginward,gio,gip,gipon,gipper,gipser,gipsire,giraffe,girasol,girba,gird,girder,girding,girdle,girdler,girl,girleen,girlery,girlie,girling,girlish,girlism,girly,girn,girny,giro,girr,girse,girsh,girsle,girt,girth,gisarme,gish,gisla,gisler,gist,git,gitalin,gith,gitonin,gitoxin,gittern,gittith,give,given,giver,givey,giving,gizz,gizzard,gizzen,gizzern,glace,glaceed,glacial,glacier,glacis,glack,glad,gladden,gladdon,gladdy,glade,gladeye,gladful,gladify,gladii,gladius,gladly,glady,glaga,glaieul,glaik,glaiket,glair,glairy,glaive,glaived,glaked,glaky,glam,glamour,glance,glancer,gland,glandes,glans,glar,glare,glarily,glaring,glarry,glary,glashan,glass,glassen,glasser,glasses,glassie,glassy,glaucin,glaum,glaur,glaury,glaver,glaze,glazed,glazen,glazer,glazier,glazily,glazing,glazy,gleam,gleamy,glean,gleaner,gleary,gleba,glebal,glebe,glebous,glede,gledy,glee,gleed,gleeful,gleek,gleeman,gleet,gleety,gleg,glegly,glen,glenoid,glent,gleyde,glia,gliadin,glial,glib,glibly,glidder,glide,glider,gliding,gliff,glime,glimmer,glimpse,glink,glint,glioma,gliosa,gliosis,glirine,glisk,glisky,glisten,glister,glitter,gloam,gloat,gloater,global,globate,globe,globed,globin,globoid,globose,globous,globule,globy,glochid,glochis,gloea,gloeal,glom,glome,glommox,glomus,glonoin,gloom,gloomth,gloomy,glop,gloppen,glor,glore,glorify,glory,gloss,glossa,glossal,glossed,glosser,glossic,glossy,glost,glottal,glottic,glottid,glottis,glout,glove,glover,glovey,gloving,glow,glower,glowfly,glowing,gloy,gloze,glozing,glub,glucase,glucid,glucide,glucina,glucine,gluck,glucose,glue,glued,gluepot,gluer,gluey,glug,gluish,glum,gluma,glumal,glume,glumly,glummy,glumose,glump,glumpy,glunch,glusid,gluside,glut,glutch,gluteal,gluten,gluteus,glutin,glutoid,glutose,glutter,glutton,glycid,glycide,glycine,glycol,glycose,glycyl,glyoxal,glyoxim,glyoxyl,glyph,glyphic,glyptic,glyster,gnabble,gnar,gnarl,gnarled,gnarly,gnash,gnat,gnathal,gnathic,gnatter,gnatty,gnaw,gnawer,gnawing,gnawn,gneiss,gneissy,gnome,gnomed,gnomic,gnomide,gnomish,gnomist,gnomon,gnosis,gnostic,gnu,go,goa,goad,goaf,goal,goalage,goalee,goalie,goanna,goat,goatee,goateed,goatish,goatly,goaty,goave,gob,goback,goban,gobang,gobbe,gobber,gobbet,gobbin,gobbing,gobble,gobbler,gobby,gobelin,gobi,gobiid,gobioid,goblet,goblin,gobline,gobo,gobony,goburra,goby,gocart,god,goddard,godded,goddess,goddize,gode,godet,godhead,godhood,godkin,godless,godlet,godlike,godlily,godling,godly,godown,godpapa,godsend,godship,godson,godwit,goeduck,goel,goelism,goer,goes,goetia,goetic,goety,goff,goffer,goffle,gog,gogga,goggan,goggle,goggled,goggler,goggly,goglet,gogo,goi,going,goitcho,goiter,goitral,gol,gola,golach,goladar,gold,goldbug,goldcup,golden,golder,goldie,goldin,goldish,goldtit,goldy,golee,golem,golf,golfdom,golfer,goli,goliard,goliath,golland,gollar,golly,goloe,golpe,gomari,gomart,gomavel,gombay,gombeen,gomer,gomeral,gomlah,gomuti,gon,gonad,gonadal,gonadic,gonagra,gonakie,gonal,gonapod,gondang,gondite,gondola,gone,goner,gong,gongman,gonia,goniac,gonial,goniale,gonid,gonidia,gonidic,gonimic,gonion,gonitis,gonium,gonne,gony,gonys,goo,goober,good,gooding,goodish,goodly,goodman,goods,goody,goof,goofer,goofily,goofy,googly,googol,googul,gook,gool,goolah,gools,gooma,goon,goondie,goonie,goose,goosery,goosish,goosy,gopher,gopura,gor,gora,goracco,goral,goran,gorb,gorbal,gorbet,gorble,gorce,gorcock,gorcrow,gore,gorer,gorevan,gorfly,gorge,gorged,gorger,gorget,gorglin,gorhen,goric,gorilla,gorily,goring,gorlin,gorlois,gormaw,gormed,gorra,gorraf,gorry,gorse,gorsedd,gorsy,gory,gos,gosain,goschen,gosh,goshawk,goslet,gosling,gosmore,gospel,gosport,gossan,gossard,gossip,gossipy,gossoon,gossy,got,gotch,gote,gothite,gotra,gotraja,gotten,gouaree,gouge,gouger,goujon,goulash,goumi,goup,gourami,gourd,gourde,gourdy,gourmet,gousty,gout,goutify,goutily,goutish,goutte,gouty,gove,govern,gowan,gowdnie,gowf,gowfer,gowk,gowked,gowkit,gowl,gown,gownlet,gowpen,goy,goyim,goyin,goyle,gozell,gozzard,gra,grab,grabber,grabble,graben,grace,gracer,gracile,grackle,grad,gradal,gradate,graddan,grade,graded,gradely,grader,gradin,gradine,grading,gradual,gradus,graff,graffer,graft,grafted,grafter,graham,grail,grailer,grain,grained,grainer,grainy,graip,graisse,graith,grallic,gram,grama,grame,grammar,gramme,gramp,grampa,grampus,granada,granage,granary,granate,granch,grand,grandam,grandee,grandly,grandma,grandpa,grane,grange,granger,granite,grank,grannom,granny,grano,granose,grant,grantee,granter,grantor,granula,granule,granza,grape,graped,grapery,graph,graphic,graphy,graping,grapnel,grappa,grapple,grapy,grasp,grasper,grass,grassed,grasser,grasset,grassy,grat,grate,grater,grather,gratify,grating,gratis,gratten,graupel,grave,graved,gravel,gravely,graven,graver,gravic,gravid,graving,gravity,gravure,gravy,grawls,gray,grayfly,grayish,graylag,grayly,graze,grazer,grazier,grazing,grease,greaser,greasy,great,greaten,greater,greatly,greave,greaved,greaves,grebe,grece,gree,greed,greedy,green,greener,greeney,greenly,greenth,greenuk,greeny,greet,greeter,gregal,gregale,grege,greggle,grego,greige,grein,greisen,gremial,gremlin,grenade,greund,grew,grey,greyly,gribble,grice,grid,griddle,gride,griece,grieced,grief,grieve,grieved,griever,griff,griffe,griffin,griffon,grift,grifter,grig,grignet,grigri,grike,grill,grille,grilled,griller,grilse,grim,grimace,grime,grimful,grimily,grimly,grimme,grimp,grimy,grin,grinch,grind,grinder,grindle,gringo,grinner,grinny,grip,gripe,griper,griping,gripman,grippal,grippe,gripper,gripple,grippy,gripy,gris,grisard,griskin,grisly,grison,grist,grister,gristle,gristly,gristy,grit,grith,grits,gritten,gritter,grittle,gritty,grivet,grivna,grizzle,grizzly,groan,groaner,groat,groats,grobian,grocer,grocery,groff,grog,groggy,grogram,groin,groined,grommet,groom,groomer,groomy,groop,groose,groot,grooty,groove,groover,groovy,grope,groper,groping,gropple,gros,groser,groset,gross,grossen,grosser,grossly,grosso,grosz,groszy,grot,grotto,grouch,grouchy,grouf,grough,ground,grounds,groundy,group,grouped,grouper,grouse,grouser,grousy,grout,grouter,grouts,grouty,grouze,grove,groved,grovel,grovy,grow,growan,growed,grower,growing,growl,growler,growly,grown,grownup,growse,growth,growthy,grozart,grozet,grr,grub,grubbed,grubber,grubby,grubs,grudge,grudger,grue,gruel,grueler,gruelly,gruff,gruffly,gruffs,gruffy,grufted,grugru,gruine,grum,grumble,grumbly,grume,grumly,grummel,grummet,grumose,grumous,grump,grumph,grumphy,grumpy,grun,grundy,grunion,grunt,grunter,gruntle,grush,grushie,gruss,grutch,grutten,gryde,grylli,gryllid,gryllos,gryllus,grysbok,guaba,guacimo,guacin,guaco,guaiac,guaiol,guaka,guama,guan,guana,guanaco,guanase,guanay,guango,guanine,guanize,guano,guanyl,guao,guapena,guar,guara,guarabu,guarana,guarani,guard,guarded,guarder,guardo,guariba,guarri,guasa,guava,guavina,guayaba,guayabi,guayabo,guayule,guaza,gubbo,gucki,gud,gudame,guddle,gude,gudge,gudgeon,gudget,gudok,gue,guebucu,guemal,guenepe,guenon,guepard,guerdon,guereza,guess,guesser,guest,guesten,guester,gufa,guff,guffaw,guffer,guffin,guffy,gugal,guggle,gugglet,guglet,guglia,guglio,gugu,guhr,guib,guiba,guidage,guide,guider,guidman,guidon,guige,guignol,guijo,guild,guilder,guildic,guildry,guile,guilery,guilt,guilty,guily,guimpe,guinea,guipure,guisard,guise,guiser,guising,guitar,gul,gula,gulae,gulaman,gular,gularis,gulch,gulden,gule,gules,gulf,gulfy,gulgul,gulix,gull,gullery,gullet,gullion,gullish,gully,gulonic,gulose,gulp,gulper,gulpin,gulping,gulpy,gulsach,gum,gumbo,gumboil,gumby,gumdrop,gumihan,gumless,gumlike,gumly,gumma,gummage,gummata,gummed,gummer,gumming,gummite,gummose,gummous,gummy,gump,gumpus,gumshoe,gumweed,gumwood,gun,guna,gunate,gunboat,gundi,gundy,gunebo,gunfire,gunge,gunite,gunj,gunk,gunl,gunless,gunlock,gunman,gunnage,gunne,gunnel,gunner,gunnery,gunnies,gunning,gunnung,gunny,gunong,gunplay,gunrack,gunsel,gunshop,gunshot,gunsman,gunster,gunter,gunwale,gunyah,gunyang,gunyeh,gup,guppy,gur,gurdle,gurge,gurgeon,gurges,gurgle,gurglet,gurgly,gurjun,gurk,gurl,gurly,gurnard,gurnet,gurniad,gurr,gurrah,gurry,gurt,guru,gush,gusher,gushet,gushily,gushing,gushy,gusla,gusle,guss,gusset,gussie,gust,gustful,gustily,gusto,gusty,gut,gutless,gutlike,gutling,gutt,gutta,guttate,gutte,gutter,guttery,gutti,guttide,guttie,guttle,guttler,guttula,guttule,guttus,gutty,gutweed,gutwise,gutwort,guy,guydom,guyer,guz,guze,guzzle,guzzler,gwag,gweduc,gweed,gweeon,gwely,gwine,gwyniad,gyle,gym,gymel,gymnast,gymnic,gymnics,gymnite,gymnure,gympie,gyn,gyne,gynecic,gynic,gynics,gyp,gype,gypper,gyps,gypsine,gypsite,gypsous,gypster,gypsum,gypsy,gypsyfy,gypsyry,gyral,gyrally,gyrant,gyrate,gyrator,gyre,gyrene,gyri,gyric,gyrinid,gyro,gyrocar,gyroma,gyron,gyronny,gyrose,gyrous,gyrus,gyte,gytling,gyve,h,ha,haab,haaf,habble,habeas,habena,habenal,habenar,habile,habille,habit,habitan,habitat,habited,habitue,habitus,habnab,haboob,habu,habutai,hache,hachure,hack,hackbut,hacked,hackee,hacker,hackery,hackin,hacking,hackle,hackler,hacklog,hackly,hackman,hackney,hacksaw,hacky,had,hadbot,hadden,haddie,haddo,haddock,hade,hading,hadj,hadji,hadland,hadrome,haec,haem,haemony,haet,haff,haffet,haffle,hafiz,hafnium,hafnyl,haft,hafter,hag,hagboat,hagborn,hagbush,hagdon,hageen,hagfish,haggada,haggard,hagged,hagger,haggis,haggish,haggle,haggler,haggly,haggy,hagi,hagia,haglet,haglike,haglin,hagride,hagrope,hagseed,hagship,hagweed,hagworm,hah,haik,haikai,haikal,haikwan,hail,hailer,hailse,haily,hain,haine,hair,haircut,hairdo,haire,haired,hairen,hairif,hairlet,hairpin,hairup,hairy,haje,hajib,hajilij,hak,hakam,hakdar,hake,hakeem,hakim,hako,haku,hala,halakah,halakic,halal,halberd,halbert,halch,halcyon,hale,halebi,haler,halerz,half,halfer,halfman,halfway,halibiu,halibut,halide,halidom,halite,halitus,hall,hallage,hallah,hallan,hallel,hallex,halling,hallman,halloo,hallow,hallux,hallway,halma,halo,halogen,haloid,hals,halse,halsen,halt,halter,halting,halurgy,halutz,halvans,halve,halved,halver,halves,halyard,ham,hamal,hamald,hamate,hamated,hamatum,hamble,hame,hameil,hamel,hamfat,hami,hamlah,hamlet,hammada,hammam,hammer,hammock,hammy,hamose,hamous,hamper,hamsa,hamster,hamular,hamule,hamulus,hamus,hamza,han,hanaper,hanbury,hance,hanced,hanch,hand,handbag,handbow,handcar,handed,hander,handful,handgun,handily,handle,handled,handler,handout,handsaw,handsel,handset,handy,hangar,hangby,hangdog,hange,hangee,hanger,hangie,hanging,hangle,hangman,hangout,hangul,hanif,hank,hanker,hankie,hankle,hanky,hanna,hansa,hanse,hansel,hansom,hant,hantle,hao,haole,haoma,haori,hap,hapless,haplite,haploid,haploma,haplont,haply,happen,happier,happify,happily,happing,happy,hapten,haptene,haptere,haptic,haptics,hapu,hapuku,harass,haratch,harbi,harbor,hard,harden,harder,hardily,hardim,hardish,hardly,hardock,hardpan,hardy,hare,harebur,harelip,harem,harfang,haricot,harish,hark,harka,harl,harling,harlock,harlot,harm,harmal,harmala,harman,harmel,harmer,harmful,harmine,harmony,harmost,harn,harness,harnpan,harp,harpago,harper,harpier,harpist,harpoon,harpula,harr,harrier,harrow,harry,harsh,harshen,harshly,hart,hartal,hartin,hartite,harvest,hasan,hash,hashab,hasher,hashish,hashy,hask,hasky,haslet,haslock,hasp,hassar,hassel,hassle,hassock,hasta,hastate,hastati,haste,hasten,haster,hastily,hastish,hastler,hasty,hat,hatable,hatband,hatbox,hatbrim,hatch,hatchel,hatcher,hatchet,hate,hateful,hater,hatful,hath,hathi,hatless,hatlike,hatpin,hatrack,hatrail,hatred,hatress,hatt,hatted,hatter,hattery,hatting,hattock,hatty,hau,hauberk,haugh,haught,haughty,haul,haulage,hauld,hauler,haulier,haulm,haulmy,haunch,haunchy,haunt,haunter,haunty,hause,hausen,hausse,hautboy,hauteur,havage,have,haveage,havel,haven,havener,havenet,havent,haver,haverel,haverer,havers,havier,havoc,haw,hawbuck,hawer,hawk,hawkbit,hawked,hawker,hawkery,hawkie,hawking,hawkish,hawknut,hawky,hawm,hawok,hawse,hawser,hay,haya,hayband,haybird,haybote,haycap,haycart,haycock,hayey,hayfork,haylift,hayloft,haymow,hayrack,hayrake,hayrick,hayseed,haysel,haysuck,haytime,hayward,hayweed,haywire,hayz,hazard,haze,hazel,hazeled,hazelly,hazen,hazer,hazily,hazing,hazle,hazy,hazzan,he,head,headcap,headed,header,headful,headily,heading,headman,headset,headway,heady,heaf,heal,heald,healder,healer,healful,healing,health,healthy,heap,heaper,heaps,heapy,hear,hearer,hearing,hearken,hearsay,hearse,hearst,heart,hearted,hearten,hearth,heartly,hearts,hearty,heat,heater,heatful,heath,heathen,heather,heathy,heating,heaume,heaumer,heave,heaven,heavens,heaver,heavies,heavily,heaving,heavity,heavy,hebamic,hebenon,hebete,hebetic,hech,heck,heckle,heckler,hectare,hecte,hectic,hector,heddle,heddler,hedebo,heder,hederic,hederin,hedge,hedger,hedging,hedgy,hedonic,heed,heeder,heedful,heedily,heedy,heehaw,heel,heelcap,heeled,heeler,heeltap,heer,heeze,heezie,heezy,heft,hefter,heftily,hefty,hegari,hegemon,hegira,hegumen,hei,heiau,heifer,heigh,height,heii,heimin,heinous,heir,heirdom,heiress,heitiki,hekteus,helbeh,helcoid,helder,hele,helenin,heliast,helical,heliced,helices,helicin,helicon,helide,heling,helio,helioid,helium,helix,hell,hellbox,hellcat,helldog,heller,helleri,hellhag,hellier,hellion,hellish,hello,helluo,helly,helm,helmage,helmed,helmet,helodes,heloe,heloma,helonin,helosis,helotry,help,helper,helpful,helping,helply,helve,helvell,helver,helvite,hem,hemad,hemal,hemapod,hemase,hematal,hematic,hematid,hematin,heme,hemen,hemera,hemiamb,hemic,hemin,hemina,hemine,heminee,hemiope,hemipic,heml,hemlock,hemmel,hemmer,hemocry,hemoid,hemol,hemopod,hemp,hempen,hempy,hen,henad,henbane,henbill,henbit,hence,hencoop,hencote,hend,hendly,henfish,henism,henlike,henna,hennery,hennin,hennish,henny,henotic,henpeck,henpen,henry,hent,henter,henware,henwife,henwise,henyard,hep,hepar,heparin,hepatic,hepcat,heppen,hepper,heptace,heptad,heptal,heptane,heptene,heptine,heptite,heptoic,heptose,heptyl,heptyne,her,herald,herb,herbage,herbal,herbane,herbary,herbish,herbist,herblet,herbman,herbose,herbous,herby,herd,herdboy,herder,herdic,herding,here,hereat,hereby,herein,herem,hereof,hereon,heresy,heretic,hereto,herile,heriot,heritor,herl,herling,herma,hermaic,hermit,hern,hernani,hernant,herne,hernia,hernial,hero,heroess,heroic,heroid,heroify,heroin,heroine,heroism,heroize,heron,heroner,heronry,herpes,herring,hers,herse,hersed,herself,hership,hersir,hertz,hessite,hest,hestern,het,hetaera,hetaery,heteric,hetero,hething,hetman,hetter,heuau,heugh,heumite,hevi,hew,hewable,hewel,hewer,hewhall,hewn,hewt,hex,hexa,hexace,hexacid,hexact,hexad,hexadic,hexagon,hexagyn,hexane,hexaped,hexapla,hexapod,hexarch,hexene,hexer,hexerei,hexeris,hexine,hexis,hexitol,hexode,hexogen,hexoic,hexone,hexonic,hexosan,hexose,hexyl,hexylic,hexyne,hey,heyday,hi,hia,hiant,hiatal,hiate,hiation,hiatus,hibbin,hic,hicatee,hiccup,hick,hickey,hickory,hidable,hidage,hidalgo,hidated,hidden,hide,hided,hideous,hider,hidling,hie,hieder,hield,hiemal,hieron,hieros,higdon,higgle,higgler,high,highboy,higher,highest,highish,highly,highman,hight,hightop,highway,higuero,hijack,hike,hiker,hilch,hilding,hill,hiller,hillet,hillman,hillock,hilltop,hilly,hilsa,hilt,hilum,hilus,him,himp,himself,himward,hin,hinau,hinch,hind,hinder,hing,hinge,hinger,hingle,hinney,hinny,hinoid,hinoki,hint,hinter,hiodont,hip,hipbone,hipe,hiper,hiphalt,hipless,hipmold,hipped,hippen,hippian,hippic,hipping,hippish,hipple,hippo,hippoid,hippus,hippy,hipshot,hipwort,hirable,hircine,hire,hired,hireman,hirer,hirmos,hiro,hirple,hirse,hirsel,hirsle,hirsute,his,hish,hisn,hispid,hiss,hisser,hissing,hist,histie,histoid,histon,histone,history,histrio,hit,hitch,hitcher,hitchy,hithe,hither,hitless,hitter,hive,hiver,hives,hizz,ho,hoar,hoard,hoarder,hoarily,hoarish,hoarse,hoarsen,hoary,hoast,hoatzin,hoax,hoaxee,hoaxer,hob,hobber,hobbet,hobbil,hobble,hobbler,hobbly,hobby,hoblike,hobnail,hobnob,hobo,hoboism,hocco,hock,hocker,hocket,hockey,hocky,hocus,hod,hodden,hodder,hoddle,hoddy,hodful,hodman,hoe,hoecake,hoedown,hoeful,hoer,hog,hoga,hogan,hogback,hogbush,hogfish,hogged,hogger,hoggery,hogget,hoggie,hoggin,hoggish,hoggism,hoggy,hogherd,hoghide,hoghood,hoglike,hogling,hogmace,hognose,hognut,hogpen,hogship,hogskin,hogsty,hogward,hogwash,hogweed,hogwort,hogyard,hoi,hoick,hoin,hoise,hoist,hoister,hoit,hoju,hokey,hokum,holard,holcad,hold,holdall,holden,holder,holding,holdout,holdup,hole,holeman,holer,holey,holia,holiday,holily,holing,holism,holl,holla,holler,hollin,hollo,hollock,hollong,hollow,holly,holm,holmia,holmic,holmium,holmos,holour,holster,holt,holy,holyday,homage,homager,home,homelet,homely,homelyn,homeoid,homer,homey,homily,hominal,hominid,hominy,homish,homo,homodox,homogen,homonym,homrai,homy,honda,hondo,hone,honest,honesty,honey,honeyed,hong,honied,honily,honk,honker,honor,honoree,honorer,hontish,hontous,hooch,hood,hoodcap,hooded,hoodful,hoodie,hoodlum,hoodman,hoodoo,hoodshy,hooey,hoof,hoofed,hoofer,hoofish,hooflet,hoofrot,hoofs,hoofy,hook,hookah,hooked,hooker,hookers,hookish,hooklet,hookman,hooktip,hookum,hookup,hooky,hoolock,hooly,hoon,hoop,hooped,hooper,hooping,hoopla,hoople,hoopman,hoopoe,hoose,hoosh,hoot,hootay,hooter,hoove,hooven,hoovey,hop,hopbine,hopbush,hope,hoped,hopeful,hopeite,hoper,hopi,hoplite,hopoff,hopped,hopper,hoppers,hoppet,hoppity,hopple,hoppy,hoptoad,hopvine,hopyard,hora,horal,horary,hordary,horde,hordein,horizon,horme,hormic,hormigo,hormion,hormist,hormone,hormos,horn,horned,horner,hornet,hornety,hornful,hornify,hornily,horning,hornish,hornist,hornito,hornlet,horntip,horny,horrent,horreum,horrid,horrify,horror,horse,horser,horsify,horsily,horsing,horst,horsy,hortite,hory,hosanna,hose,hosed,hosel,hoseman,hosier,hosiery,hospice,host,hostage,hostel,hoster,hostess,hostie,hostile,hosting,hostler,hostly,hostry,hot,hotbed,hotbox,hotch,hotel,hotfoot,hothead,hoti,hotly,hotness,hotspur,hotter,hottery,hottish,houbara,hough,hougher,hounce,hound,hounder,houndy,hour,hourful,houri,hourly,housage,housal,house,housel,houser,housing,housty,housy,houtou,houvari,hove,hovel,hoveler,hoven,hover,hoverer,hoverly,how,howadji,howbeit,howdah,howder,howdie,howdy,howe,howel,however,howff,howish,howk,howkit,howl,howler,howlet,howling,howlite,howso,hox,hoy,hoyden,hoyle,hoyman,huaca,huaco,huarizo,hub,hubb,hubba,hubber,hubble,hubbly,hubbub,hubby,hubshi,huchen,hucho,huck,huckle,hud,huddle,huddler,huddock,huddup,hue,hued,hueful,hueless,huer,huff,huffier,huffily,huffish,huffle,huffler,huffy,hug,huge,hugely,hugeous,hugger,hugging,huggle,hugsome,huh,huia,huipil,huitain,huke,hula,huldee,hulk,hulkage,hulking,hulky,hull,huller,hullock,hulloo,hulsite,hulster,hulu,hulver,hum,human,humane,humanly,humate,humble,humbler,humblie,humbly,humbo,humbug,humbuzz,humdrum,humect,humeral,humeri,humerus,humet,humetty,humhum,humic,humid,humidly,humidor,humific,humify,humin,humite,humlie,hummel,hummer,hummie,humming,hummock,humor,humoral,humous,hump,humped,humph,humpty,humpy,humus,hunch,hunchet,hunchy,hundi,hundred,hung,hunger,hungry,hunh,hunk,hunker,hunkers,hunkies,hunks,hunky,hunt,hunting,hup,hura,hurdies,hurdis,hurdle,hurdler,hurds,hure,hureek,hurgila,hurkle,hurl,hurled,hurler,hurley,hurling,hurlock,hurly,huron,hurr,hurrah,hurried,hurrier,hurrock,hurroo,hurry,hurst,hurt,hurted,hurter,hurtful,hurting,hurtle,hurty,husband,huse,hush,hushaby,husheen,hushel,husher,hushful,hushing,hushion,husho,husk,husked,husker,huskily,husking,husky,huso,huspil,huss,hussar,hussy,husting,hustle,hustler,hut,hutch,hutcher,hutchet,huthold,hutia,hutlet,hutment,huvelyk,huzoor,huzz,huzza,huzzard,hyaena,hyaline,hyalite,hyaloid,hybosis,hybrid,hydatid,hydnoid,hydrant,hydrate,hydrazo,hydria,hydric,hydride,hydro,hydroa,hydroid,hydrol,hydrome,hydrone,hydrops,hydrous,hydroxy,hydrula,hyena,hyenic,hyenine,hyenoid,hyetal,hygeist,hygiene,hygric,hygrine,hygroma,hying,hyke,hyle,hyleg,hylic,hylism,hylist,hyloid,hymen,hymenal,hymenic,hymn,hymnal,hymnary,hymner,hymnic,hymnist,hymnode,hymnody,hynde,hyne,hyoid,hyoidal,hyoidan,hyoides,hyp,hypate,hypaton,hyper,hypha,hyphal,hyphema,hyphen,hypho,hypnody,hypnoid,hypnone,hypo,hypogee,hypoid,hyponym,hypopus,hyporit,hyppish,hypural,hyraces,hyracid,hyrax,hyson,hyssop,i,iamb,iambi,iambic,iambist,iambize,iambus,iao,iatric,iba,iberite,ibex,ibices,ibid,ibidine,ibis,ibolium,ibota,icaco,ice,iceberg,iceboat,icebone,icebox,icecap,iced,icefall,icefish,iceland,iceleaf,iceless,icelike,iceman,iceroot,icework,ich,ichnite,icho,ichor,ichthus,ichu,icica,icicle,icicled,icily,iciness,icing,icon,iconic,iconism,icosian,icotype,icteric,icterus,ictic,ictuate,ictus,icy,id,idalia,idant,iddat,ide,idea,ideaed,ideaful,ideal,ideally,ideate,ideist,identic,ides,idgah,idiasm,idic,idiocy,idiom,idiot,idiotcy,idiotic,idiotry,idite,iditol,idle,idleful,idleman,idler,idleset,idlety,idlish,idly,idol,idola,idolify,idolism,idolist,idolize,idolous,idolum,idoneal,idorgan,idose,idryl,idyl,idyler,idylism,idylist,idylize,idyllic,ie,if,ife,iffy,igloo,ignatia,ignavia,igneous,ignify,ignite,igniter,ignitor,ignoble,ignobly,ignore,ignorer,ignote,iguana,iguanid,ihi,ihleite,ihram,iiwi,ijma,ijolite,ikat,ikey,ikona,ikra,ileac,ileitis,ileon,ilesite,ileum,ileus,ilex,ilia,iliac,iliacus,iliahi,ilial,iliau,ilicic,ilicin,ilima,ilium,ilk,ilka,ilkane,ill,illapse,illeck,illegal,illeism,illeist,illess,illfare,illicit,illish,illium,illness,illocal,illogic,illoyal,illth,illude,illuder,illume,illumer,illupi,illure,illusor,illy,ilot,ilvaite,image,imager,imagery,imagine,imagism,imagist,imago,imam,imamah,imamate,imamic,imaret,imban,imband,imbarge,imbark,imbarn,imbased,imbat,imbauba,imbe,imbed,imber,imbibe,imbiber,imbondo,imbosom,imbower,imbrex,imbrue,imbrute,imbue,imburse,imi,imide,imidic,imine,imino,imitant,imitate,immane,immask,immense,immerd,immerge,immerit,immerse,immew,immi,immit,immix,immoral,immound,immund,immune,immure,immute,imonium,imp,impack,impact,impages,impaint,impair,impala,impale,impaler,impall,impalm,impalsy,impane,impanel,impar,impark,imparl,impart,impasse,impaste,impasto,impave,impavid,impawn,impeach,impearl,impede,impeder,impel,impen,impend,impent,imperia,imperil,impest,impetre,impetus,imphee,impi,impiety,impinge,impious,impish,implant,implate,implead,implete,implex,implial,impling,implode,implore,implume,imply,impofo,impone,impoor,import,imposal,impose,imposer,impost,impot,impound,impreg,impregn,impresa,imprese,impress,imprest,imprime,imprint,improof,improve,impship,impubic,impugn,impulse,impure,impute,imputer,impy,imshi,imsonic,imu,in,inachid,inadept,inagile,inaja,inane,inanely,inanga,inanity,inapt,inaptly,inarch,inarm,inaugur,inaxon,inbe,inbeing,inbent,inbirth,inblow,inblown,inboard,inbond,inborn,inbound,inbread,inbreak,inbred,inbreed,inbring,inbuilt,inburnt,inburst,inby,incarn,incase,incast,incense,incept,incest,inch,inched,inchpin,incide,incisal,incise,incisor,incite,inciter,incivic,incline,inclip,inclose,include,inclusa,incluse,incog,income,incomer,inconnu,incrash,increep,increst,incross,incrust,incubi,incubus,incudal,incudes,incult,incur,incurse,incurve,incus,incuse,incut,indaba,indan,indane,indart,indazin,indazol,inde,indebt,indeed,indeedy,indene,indent,index,indexed,indexer,indic,indican,indices,indicia,indict,indign,indigo,indite,inditer,indium,indogen,indole,indoles,indolyl,indoor,indoors,indorse,indoxyl,indraft,indrawn,indri,induce,induced,inducer,induct,indue,indulge,indult,indulto,induna,indwell,indy,indyl,indylic,inearth,inept,ineptly,inequal,inerm,inert,inertia,inertly,inesite,ineunt,inexact,inexist,inface,infall,infame,infamy,infancy,infand,infang,infant,infanta,infante,infarct,infare,infaust,infect,infeed,infeft,infelt,infer,infern,inferno,infest,infidel,infield,infill,infilm,infirm,infit,infix,inflame,inflate,inflect,inflex,inflict,inflood,inflow,influx,infold,inform,infra,infract,infula,infuse,infuser,ing,ingate,ingenit,ingenue,ingest,ingesta,ingiver,ingle,inglobe,ingoing,ingot,ingraft,ingrain,ingrate,ingress,ingross,ingrow,ingrown,inguen,ingulf,inhabit,inhale,inhaler,inhaul,inhaust,inhere,inherit,inhiate,inhibit,inhuman,inhume,inhumer,inial,iniome,inion,initial,initis,initive,inject,injelly,injunct,injure,injured,injurer,injury,ink,inkbush,inken,inker,inket,inkfish,inkhorn,inkish,inkle,inkless,inklike,inkling,inknot,inkosi,inkpot,inkroot,inks,inkshed,inkweed,inkwell,inkwood,inky,inlaid,inlaik,inlake,inland,inlaut,inlaw,inlawry,inlay,inlayer,inleak,inlet,inlier,inlook,inly,inlying,inmate,inmeats,inmost,inn,innate,inneity,inner,innerly,innerve,inness,innest,innet,inning,innless,innyard,inocyte,inogen,inoglia,inolith,inoma,inone,inopine,inorb,inosic,inosin,inosite,inower,inphase,inport,inpour,inpush,input,inquest,inquiet,inquire,inquiry,inring,inro,inroad,inroll,inrub,inrun,inrush,insack,insane,insculp,insea,inseam,insect,insee,inseer,insense,insert,inset,inshave,inshell,inship,inshoe,inshoot,inshore,inside,insider,insight,insigne,insipid,insist,insnare,insofar,insole,insolid,insooth,insorb,insoul,inspan,inspeak,inspect,inspire,inspoke,install,instant,instar,instate,instead,insteam,insteep,instep,instill,insula,insular,insulin,insulse,insult,insunk,insure,insured,insurer,insurge,inswamp,inswell,inswept,inswing,intact,intake,intaker,integer,inteind,intend,intense,intent,inter,interim,intern,intext,inthrow,intil,intima,intimal,intine,into,intoed,intone,intoner,intort,intown,intrada,intrait,intrant,intreat,intrine,introit,intrude,intruse,intrust,intube,intue,intuent,intuit,inturn,intwist,inula,inulase,inulin,inuloid,inunct,inure,inured,inurn,inutile,invade,invader,invalid,inveigh,inveil,invein,invent,inverse,invert,invest,invigor,invised,invital,invite,invitee,inviter,invivid,invoice,invoke,invoker,involve,inwale,inwall,inward,inwards,inweave,inweed,inwick,inwind,inwit,inwith,inwood,inwork,inworn,inwound,inwoven,inwrap,inwrit,inyoite,inyoke,io,iodate,iodic,iodide,iodine,iodism,iodite,iodize,iodizer,iodo,iodol,iodoso,iodous,iodoxy,iolite,ion,ionic,ionium,ionize,ionizer,ionogen,ionone,iota,iotize,ipecac,ipid,ipil,ipomea,ipseand,ipseity,iracund,irade,irate,irately,ire,ireful,ireless,irene,irenic,irenics,irian,irid,iridal,iridate,irides,iridial,iridian,iridic,iridin,iridine,iridite,iridium,iridize,iris,irised,irisin,iritic,iritis,irk,irksome,irok,iroko,iron,irone,ironer,ironice,ironish,ironism,ironist,ironize,ironly,ironman,irony,irrisor,irrupt,is,isagoge,isagon,isamine,isatate,isatic,isatide,isatin,isazoxy,isba,ischiac,ischial,ischium,ischury,iserine,iserite,isidium,isidoid,island,islandy,islay,isle,islet,isleted,islot,ism,ismal,ismatic,ismdom,ismy,iso,isoamyl,isobar,isobare,isobase,isobath,isochor,isocola,isocrat,isodont,isoflor,isogamy,isogen,isogeny,isogon,isogram,isohel,isohyet,isolate,isology,isomer,isomere,isomery,isoneph,isonomy,isonym,isonymy,isopag,isopod,isopoly,isoptic,isopyre,isotac,isotely,isotome,isotony,isotope,isotopy,isotron,isotype,isoxime,issei,issite,issuant,issue,issuer,issuing,ist,isthmi,isthmic,isthmus,istle,istoke,isuret,isuroid,it,itacism,itacist,italics,italite,itch,itching,itchy,itcze,item,iteming,itemize,itemy,iter,iterant,iterate,ither,itmo,itoubou,its,itself,iturite,itzebu,iva,ivied,ivin,ivoried,ivorine,ivorist,ivory,ivy,ivylike,ivyweed,ivywood,ivywort,iwa,iwaiwa,iwis,ixodian,ixodic,ixodid,iyo,izar,izard,izle,izote,iztle,izzard,j,jab,jabbed,jabber,jabbing,jabble,jabers,jabia,jabiru,jabot,jabul,jacal,jacamar,jacami,jacamin,jacana,jacare,jacate,jacchus,jacent,jacinth,jack,jackal,jackass,jackbox,jackboy,jackdaw,jackeen,jacker,jacket,jackety,jackleg,jackman,jacko,jackrod,jacksaw,jacktan,jacobus,jacoby,jaconet,jactant,jacu,jacuaru,jadder,jade,jaded,jadedly,jadeite,jadery,jadish,jady,jaeger,jag,jagat,jager,jagged,jagger,jaggery,jaggy,jagir,jagla,jagless,jagong,jagrata,jagua,jaguar,jail,jailage,jaildom,jailer,jailish,jajman,jake,jakes,jako,jalap,jalapa,jalapin,jalkar,jalopy,jalouse,jam,jama,jaman,jamb,jambeau,jambo,jambone,jambool,jambosa,jamdani,jami,jamlike,jammer,jammy,jampan,jampani,jamwood,janapa,janapan,jane,jangada,jangkar,jangle,jangler,jangly,janitor,jank,janker,jann,jannock,jantu,janua,jaob,jap,japan,jape,japer,japery,japing,japish,jaquima,jar,jara,jaragua,jarbird,jarble,jarbot,jarfly,jarful,jarg,jargon,jarkman,jarl,jarldom,jarless,jarnut,jarool,jarra,jarrah,jarring,jarry,jarvey,jasey,jaseyed,jasmine,jasmone,jasper,jaspery,jaspis,jaspoid,jass,jassid,jassoid,jatha,jati,jato,jaudie,jauk,jaun,jaunce,jaunder,jaunt,jauntie,jaunty,jaup,javali,javelin,javer,jaw,jawab,jawbone,jawed,jawfall,jawfish,jawfoot,jawless,jawy,jay,jayhawk,jaypie,jaywalk,jazz,jazzer,jazzily,jazzy,jealous,jean,jeans,jecoral,jecorin,jed,jedcock,jedding,jeddock,jeel,jeep,jeer,jeerer,jeering,jeery,jeff,jehu,jehup,jejunal,jejune,jejunum,jelab,jelick,jell,jellica,jellico,jellied,jellify,jellily,jelloid,jelly,jemadar,jemmily,jemmy,jenkin,jenna,jennet,jennier,jenny,jeofail,jeopard,jerboa,jereed,jerez,jerib,jerk,jerker,jerkily,jerkin,jerkish,jerky,jerl,jerm,jerque,jerquer,jerry,jersey,jert,jervia,jervina,jervine,jess,jessamy,jessant,jessed,jessur,jest,jestee,jester,jestful,jesting,jet,jetbead,jete,jetsam,jettage,jetted,jetter,jettied,jetton,jetty,jetware,jewbird,jewbush,jewel,jeweler,jewelry,jewely,jewfish,jezail,jeziah,jharal,jheel,jhool,jhow,jib,jibbah,jibber,jibby,jibe,jibhead,jibi,jibman,jiboa,jibstay,jicama,jicara,jiff,jiffle,jiffy,jig,jigger,jiggers,jigget,jiggety,jiggish,jiggle,jiggly,jiggy,jiglike,jigman,jihad,jikungu,jillet,jilt,jiltee,jilter,jiltish,jimbang,jimjam,jimmy,jimp,jimply,jina,jing,jingal,jingle,jingled,jingler,jinglet,jingly,jingo,jinja,jinjili,jink,jinker,jinket,jinkle,jinks,jinn,jinni,jinny,jinriki,jinx,jipper,jiqui,jirble,jirga,jiti,jitneur,jitney,jitro,jitter,jitters,jittery,jiva,jive,jixie,jo,job,jobade,jobarbe,jobber,jobbery,jobbet,jobbing,jobbish,jobble,jobless,jobman,jobo,joch,jock,jocker,jockey,jocko,jocoque,jocose,jocote,jocu,jocular,jocum,jocuma,jocund,jodel,jodelr,joe,joebush,joewood,joey,jog,jogger,joggle,joggler,joggly,johnin,join,joinant,joinder,joiner,joinery,joining,joint,jointed,jointer,jointly,jointy,joist,jojoba,joke,jokelet,joker,jokish,jokist,jokul,joky,joll,jollier,jollify,jollily,jollity,jollop,jolly,jolt,jolter,jolting,jolty,jonque,jonquil,joola,joom,jordan,joree,jorum,joseite,josh,josher,joshi,josie,joskin,joss,josser,jostle,jostler,jot,jota,jotisi,jotter,jotting,jotty,joubarb,joug,jough,jouk,joule,joulean,jounce,journal,journey,jours,joust,jouster,jovial,jow,jowar,jowari,jowel,jower,jowery,jowl,jowler,jowlish,jowlop,jowly,jowpy,jowser,jowter,joy,joyance,joyancy,joyant,joyful,joyhop,joyleaf,joyless,joylet,joyous,joysome,joyweed,juba,jubate,jubbah,jubbe,jube,jubilee,jubilus,juck,juckies,jud,judcock,judex,judge,judger,judices,judo,jufti,jug,jugal,jugale,jugate,jugated,juger,jugerum,jugful,jugger,juggins,juggle,juggler,juglone,jugular,jugulum,jugum,juice,juicily,juicy,jujitsu,juju,jujube,jujuism,jujuist,juke,jukebox,julep,julid,julidan,julio,juloid,julole,julolin,jumart,jumba,jumble,jumbler,jumbly,jumbo,jumbuck,jumby,jumelle,jument,jumfru,jumma,jump,jumper,jumpy,juncite,juncous,june,jungle,jungled,jungli,jungly,juniata,junior,juniper,junk,junker,junket,junking,junkman,junt,junta,junto,jupati,jupe,jupon,jural,jurally,jurant,jurara,jurat,jurator,jure,jurel,juridic,juring,jurist,juror,jury,juryman,jussel,jussion,jussive,jussory,just,justen,justice,justify,justly,justo,jut,jute,jutka,jutting,jutty,juvenal,juvia,juvite,jyngine,jynx,k,ka,kabaya,kabel,kaberu,kabiet,kabuki,kachin,kadaya,kadein,kados,kaffir,kafir,kafirin,kafiz,kafta,kago,kagu,kaha,kahar,kahau,kahili,kahu,kahuna,kai,kaid,kaik,kaikara,kail,kainga,kainite,kainsi,kainyn,kairine,kaiser,kaitaka,kaiwi,kajawah,kaka,kakapo,kakar,kaki,kakkak,kakke,kala,kalasie,kale,kalema,kalends,kali,kalian,kalium,kallah,kallege,kalo,kalon,kalong,kalpis,kamahi,kamala,kamansi,kamao,kamas,kamassi,kambal,kamboh,kame,kamerad,kamias,kamichi,kamik,kampong,kan,kana,kanae,kanagi,kanap,kanara,kanari,kanat,kanchil,kande,kandol,kaneh,kang,kanga,kangani,kankie,kannume,kanoon,kans,kantele,kanten,kaolin,kapa,kapai,kapeika,kapok,kapp,kappa,kappe,kapur,kaput,karagan,karaka,karakul,karamu,karaoke,karate,karaya,karbi,karch,kareao,kareeta,karela,karite,karma,karmic,karo,kaross,karou,karree,karri,karroo,karsha,karst,karstic,kartel,kartos,karwar,karyon,kasa,kasbah,kasbeke,kasher,kashga,kashi,kashima,kasida,kasm,kassu,kastura,kat,katar,katcina,kath,katha,kathal,katipo,katmon,katogle,katsup,katuka,katun,katurai,katydid,kauri,kava,kavaic,kavass,kawaka,kawika,kay,kayak,kayaker,kayles,kayo,kazi,kazoo,kea,keach,keacorn,keawe,keb,kebab,kebbie,kebbuck,kechel,keck,keckle,kecksy,kecky,ked,keddah,kedge,kedger,kedlock,keech,keek,keeker,keel,keelage,keeled,keeler,keelfat,keelie,keeling,keelman,keelson,keen,keena,keened,keener,keenly,keep,keeper,keeping,keest,keet,keeve,kef,keffel,kefir,kefiric,keg,kegler,kehaya,keita,keitloa,kekuna,kelchin,keld,kele,kelebe,keleh,kelek,kelep,kelk,kell,kella,kellion,kelly,keloid,kelp,kelper,kelpie,kelpy,kelt,kelter,kelty,kelvin,kemb,kemp,kempite,kemple,kempt,kempy,ken,kenaf,kenareh,kench,kend,kendir,kendyr,kenlore,kenmark,kennel,kenner,kenning,kenno,keno,kenosis,kenotic,kenspac,kent,kenyte,kep,kepi,kept,kerana,kerasin,kerat,keratin,keratto,kerchoo,kerchug,kerel,kerf,kerflap,kerflop,kermes,kermis,kern,kernel,kerner,kernish,kernite,kernos,kerogen,kerrie,kerril,kerrite,kerry,kersey,kerslam,kerugma,kerwham,kerygma,kestrel,ket,keta,ketal,ketch,ketchup,keten,ketene,ketipic,keto,ketogen,ketol,ketole,ketone,ketonic,ketose,ketosis,kette,ketting,kettle,kettler,ketty,ketuba,ketupa,ketyl,keup,kevalin,kevel,kewpie,kex,kexy,key,keyage,keyed,keyhole,keyless,keylet,keylock,keynote,keyway,khaddar,khadi,khahoon,khaiki,khair,khaja,khajur,khaki,khakied,khalifa,khalsa,khamsin,khan,khanate,khanda,khanjar,khanjee,khankah,khanum,khar,kharaj,kharua,khass,khat,khatib,khatri,khediva,khedive,khepesh,khet,khilat,khir,khirka,khoja,khoka,khot,khu,khubber,khula,khutbah,khvat,kiack,kiaki,kialee,kiang,kiaugh,kibber,kibble,kibbler,kibe,kibei,kibitka,kibitz,kiblah,kibosh,kiby,kick,kickee,kicker,kicking,kickish,kickoff,kickout,kickup,kidder,kiddier,kiddish,kiddush,kiddy,kidhood,kidlet,kidling,kidnap,kidney,kidskin,kidsman,kiekie,kiel,kier,kieye,kikar,kike,kiki,kiku,kikuel,kikumon,kil,kiladja,kilah,kilan,kildee,kileh,kilerg,kiley,kilhig,kiliare,kilim,kill,killas,killcu,killeen,killer,killick,killing,killy,kiln,kilneye,kilnman,kilnrib,kilo,kilobar,kiloton,kilovar,kilp,kilt,kilter,kiltie,kilting,kim,kimbang,kimnel,kimono,kin,kina,kinah,kinase,kinbote,kinch,kinchin,kincob,kind,kindle,kindler,kindly,kindred,kinepox,kinesic,kinesis,kinetic,king,kingcob,kingcup,kingdom,kinglet,kingly,kingpin,kingrow,kink,kinkhab,kinkily,kinkle,kinkled,kinkly,kinky,kinless,kino,kinship,kinsman,kintar,kioea,kiosk,kiotome,kip,kipage,kipe,kippeen,kipper,kippy,kipsey,kipskin,kiri,kirimon,kirk,kirker,kirkify,kirking,kirkman,kirmew,kirn,kirombo,kirsch,kirtle,kirtled,kirve,kirver,kischen,kish,kishen,kishon,kishy,kismet,kisra,kiss,kissage,kissar,kisser,kissing,kissy,kist,kistful,kiswa,kit,kitab,kitabis,kitar,kitcat,kitchen,kite,kith,kithe,kitish,kitling,kittel,kitten,kitter,kittle,kittles,kittly,kittock,kittul,kitty,kiva,kiver,kivu,kiwi,kiyas,kiyi,klafter,klam,klavern,klaxon,klepht,kleptic,klicket,klip,klipbok,klipdas,klippe,klippen,klister,klom,klop,klops,klosh,kmet,knab,knabble,knack,knacker,knacky,knag,knagged,knaggy,knap,knape,knappan,knapper,knar,knark,knarred,knarry,knave,knavery,knavess,knavish,knawel,knead,kneader,knee,kneecap,kneed,kneel,kneeler,kneelet,kneepad,kneepan,knell,knelt,knet,knew,knez,knezi,kniaz,kniazi,knick,knicker,knife,knifer,knight,knit,knitch,knitted,knitter,knittle,knived,knivey,knob,knobbed,knobber,knobble,knobbly,knobby,knock,knocker,knockup,knoll,knoller,knolly,knop,knopite,knopped,knopper,knoppy,knosp,knosped,knot,knotted,knotter,knotty,knout,know,knowe,knower,knowing,known,knub,knubbly,knubby,knublet,knuckle,knuckly,knur,knurl,knurled,knurly,knut,knutty,knyaz,knyazi,ko,koa,koae,koala,koali,kob,koban,kobi,kobird,kobold,kobong,kobu,koda,kodak,kodaker,kodakry,kodro,koel,koff,koft,koftgar,kohemp,kohl,kohua,koi,koil,koila,koilon,koine,koinon,kojang,kokako,kokam,kokan,kokil,kokio,koklas,koklass,koko,kokoon,kokowai,kokra,koku,kokum,kokumin,kola,kolach,kolea,kolhoz,kolkhos,kolkhoz,kollast,koller,kolo,kolobus,kolsun,komatik,kombu,kommos,kompeni,kon,kona,konak,kongoni,kongu,konini,konjak,kooka,kookery,kookri,koolah,koombar,koomkie,kootcha,kop,kopeck,koph,kopi,koppa,koppen,koppite,kor,kora,koradji,korait,korakan,korari,kore,korec,koreci,korero,kori,korin,korona,korova,korrel,koruna,korzec,kos,kosher,kosin,kosong,koswite,kotal,koto,kotuku,kotwal,kotyle,kotylos,kou,koulan,kouza,kovil,kowhai,kowtow,koyan,kozo,kra,kraal,kraft,krait,kraken,kral,krama,kran,kras,krasis,krausen,kraut,kreis,krelos,kremlin,krems,kreng,krieker,krimmer,krina,krocket,krome,krona,krone,kronen,kroner,kronor,kronur,kroon,krosa,krypsis,kryptic,kryptol,krypton,kuan,kuba,kubba,kuchen,kudize,kudos,kudu,kudzu,kuei,kuge,kugel,kuichua,kukri,kuku,kukui,kukupa,kula,kulack,kulah,kulaite,kulak,kulang,kulimit,kulm,kulmet,kumbi,kumhar,kumiss,kummel,kumquat,kumrah,kunai,kung,kunk,kunkur,kunzite,kuphar,kupper,kurbash,kurgan,kuruma,kurung,kurus,kurvey,kusa,kusam,kusha,kuskite,kuskos,kuskus,kusti,kusum,kutcha,kuttab,kuttar,kuttaur,kuvasz,kvass,kvint,kvinter,kwamme,kwan,kwarta,kwazoku,kyack,kyah,kyar,kyat,kyaung,kyl,kyle,kylite,kylix,kyrine,kyte,l,la,laager,laang,lab,labara,labarum,labba,labber,labefy,label,labeler,labella,labia,labial,labiate,labile,labiose,labis,labium,lablab,labor,labored,laborer,labour,labra,labral,labret,labroid,labrose,labrum,labrys,lac,lacca,laccaic,laccase,laccol,lace,laced,laceman,lacepod,lacer,lacery,lacet,lache,laches,lachsa,lacily,lacing,lacinia,lacis,lack,lacker,lackey,lackwit,lacmoid,lacmus,laconic,lacquer,lacrym,lactam,lactant,lactary,lactase,lactate,lacteal,lactean,lactic,lactid,lactide,lactify,lactim,lacto,lactoid,lactol,lactone,lactose,lactyl,lacuna,lacunae,lacunal,lacunar,lacune,lacwork,lacy,lad,ladakin,ladanum,ladder,laddery,laddess,laddie,laddish,laddock,lade,lademan,laden,lader,ladhood,ladies,ladify,lading,ladkin,ladle,ladler,ladrone,lady,ladybug,ladydom,ladyfly,ladyfy,ladyish,ladyism,ladykin,ladyly,laet,laeti,laetic,lag,lagan,lagarto,lagen,lagena,lagend,lager,lagetto,laggar,laggard,lagged,laggen,lagger,laggin,lagging,laglast,lagna,lagoon,lagwort,lai,laic,laical,laich,laicism,laicity,laicize,laid,laigh,lain,laine,laiose,lair,lairage,laird,lairdie,lairdly,lairman,lairy,laity,lak,lakatoi,lake,lakelet,laker,lakie,laking,lakish,lakism,lakist,laky,lalang,lall,lalling,lalo,lam,lama,lamaic,lamany,lamb,lamba,lambale,lambda,lambeau,lambent,lamber,lambert,lambie,lambish,lambkin,lambly,lamboys,lamby,lame,lamedh,lamel,lamella,lamely,lament,lameter,lametta,lamia,lamiger,lamiid,lamin,lamina,laminae,laminar,lamish,lamiter,lammas,lammer,lammock,lammy,lamnid,lamnoid,lamp,lampad,lampas,lamper,lampern,lampers,lampfly,lampful,lamping,lampion,lampist,lamplet,lamplit,lampman,lampoon,lamprey,lan,lanas,lanate,lanated,lanaz,lance,lanced,lancely,lancer,lances,lancet,lancha,land,landau,landed,lander,landing,landman,landmil,lane,lanete,laneway,laney,langaha,langca,langi,langite,langle,langoon,langsat,langued,languet,languid,languor,langur,laniary,laniate,lanific,lanioid,lanista,lank,lanket,lankily,lankish,lankly,lanky,lanner,lanolin,lanose,lansat,lanseh,lanson,lant,lantaca,lantern,lantum,lanugo,lanum,lanx,lanyard,lap,lapacho,lapcock,lapel,lapeler,lapful,lapillo,lapon,lappage,lapped,lapper,lappet,lapping,lapse,lapsed,lapser,lapsi,lapsing,lapwing,lapwork,laquear,laqueus,lar,larceny,larch,larchen,lard,larder,lardite,lardon,lardy,large,largely,largen,largess,largish,largo,lari,lariat,larick,larid,larigo,larigot,lariid,larin,larine,larixin,lark,larker,larking,larkish,larky,larmier,larnax,laroid,larrup,larry,larva,larvae,larval,larvate,larve,larvule,larynx,las,lasa,lascar,laser,lash,lasher,lask,lasket,lasque,lass,lasset,lassie,lasso,lassock,lassoer,last,lastage,laster,lasting,lastly,lastre,lasty,lat,lata,latah,latch,latcher,latchet,late,latebra,lated,lateen,lately,laten,latence,latency,latent,later,latera,laterad,lateral,latest,latex,lath,lathe,lathee,lathen,lather,lathery,lathing,lathy,latices,latigo,lation,latish,latitat,latite,latomy,latrant,latria,latrine,latro,latrobe,latron,latten,latter,lattice,latus,lauan,laud,lauder,laudist,laugh,laughee,laugher,laughy,lauia,laun,launce,launch,laund,launder,laundry,laur,laura,laurate,laurel,lauric,laurin,laurite,laurone,lauryl,lava,lavable,lavabo,lavacre,lavage,lavanga,lavant,lavaret,lavatic,lave,laveer,laver,lavic,lavish,lavolta,law,lawbook,lawful,lawing,lawish,lawk,lawless,lawlike,lawman,lawn,lawned,lawner,lawnlet,lawny,lawsuit,lawter,lawyer,lawyery,lawzy,lax,laxate,laxism,laxist,laxity,laxly,laxness,lay,layaway,layback,layboy,layer,layered,layery,layette,laying,layland,layman,layne,layoff,layout,layover,layship,laystow,lazar,lazaret,lazarly,laze,lazily,lazule,lazuli,lazy,lazyish,lea,leach,leacher,leachy,lead,leadage,leaded,leaden,leader,leadin,leading,leadman,leadoff,leadout,leadway,leady,leaf,leafage,leafboy,leafcup,leafdom,leafed,leafen,leafer,leafery,leafit,leaflet,leafy,league,leaguer,leak,leakage,leaker,leaky,leal,lealand,leally,lealty,leam,leamer,lean,leaner,leaning,leanish,leanly,leant,leap,leaper,leaping,leapt,lear,learn,learned,learner,learnt,lease,leaser,leash,leasing,leasow,least,leat,leath,leather,leatman,leave,leaved,leaven,leaver,leaves,leaving,leavy,leawill,leban,lebbek,lecama,lech,lecher,lechery,lechwe,leck,lecker,lectern,lection,lector,lectual,lecture,lecyth,led,lede,leden,ledge,ledged,ledger,ledging,ledgy,ledol,lee,leech,leecher,leeches,leed,leefang,leek,leekish,leeky,leep,leepit,leer,leerily,leerish,leery,lees,leet,leetman,leewan,leeward,leeway,leewill,left,leftish,leftism,leftist,leg,legacy,legal,legally,legate,legatee,legato,legator,legend,legenda,leger,leges,legged,legger,legging,leggy,leghorn,legible,legibly,legific,legion,legist,legit,legitim,leglen,legless,leglet,leglike,legman,legoa,legpull,legrope,legua,leguan,legume,legumen,legumin,lehr,lehrman,lehua,lei,leister,leisure,lek,lekach,lekane,lekha,leman,lemel,lemma,lemmata,lemming,lemnad,lemon,lemony,lempira,lemur,lemures,lemurid,lenad,lenard,lench,lend,lendee,lender,lene,length,lengthy,lenient,lenify,lenis,lenitic,lenity,lennow,leno,lens,lensed,lent,lenth,lentigo,lentil,lentisc,lentisk,lento,lentoid,lentor,lentous,lenvoi,lenvoy,leonine,leonite,leopard,leotard,lepa,leper,lepered,leporid,lepra,lepric,leproid,leproma,leprose,leprosy,leprous,leptid,leptite,leptome,lepton,leptus,lerot,lerp,lerret,lesche,lesion,lesiy,less,lessee,lessen,lesser,lessive,lessn,lesson,lessor,lest,lestrad,let,letch,letchy,letdown,lete,lethal,letoff,letten,letter,lettrin,lettuce,letup,leu,leuch,leucine,leucism,leucite,leuco,leucoid,leucoma,leucon,leucous,leucyl,leud,leuk,leuma,lev,levance,levant,levator,levee,level,leveler,levelly,lever,leverer,leveret,levers,levier,levin,levir,levity,levo,levulic,levulin,levy,levyist,lew,lewd,lewdly,lewis,lewth,lexia,lexical,lexicon,ley,leyland,leysing,li,liable,liaison,liana,liang,liar,liard,libant,libate,libber,libbet,libbra,libel,libelee,libeler,liber,liberal,liberty,libido,libken,libra,libral,library,librate,licca,license,lich,licham,lichen,licheny,lichi,licit,licitly,lick,licker,licking,licorn,licorne,lictor,lid,lidded,lidder,lidgate,lidless,lie,lied,lief,liege,liegely,lieger,lien,lienal,lienee,lienic,lienor,lier,lierne,lierre,liesh,lieu,lieue,lieve,life,lifeday,lifeful,lifelet,lifer,lifey,lifo,lift,lifter,lifting,liftman,ligable,ligas,ligate,ligator,ligger,light,lighten,lighter,lightly,ligne,lignify,lignin,lignite,lignone,lignose,lignum,ligula,ligular,ligule,ligulin,ligure,liin,lija,likable,like,likely,liken,liker,likin,liking,liknon,lilac,lilacin,lilacky,lile,lilied,lill,lilt,lily,lilyfy,lim,limacel,limacon,liman,limb,limbal,limbat,limbate,limbeck,limbed,limber,limbers,limbic,limbie,limbo,limbous,limbus,limby,lime,limeade,limeman,limen,limer,limes,limetta,limey,liminal,liming,limit,limital,limited,limiter,limma,limmer,limmock,limmu,limn,limner,limnery,limniad,limnite,limoid,limonin,limose,limous,limp,limper,limpet,limpid,limpily,limpin,limping,limpish,limpkin,limply,limpsy,limpy,limsy,limu,limulid,limy,lin,lina,linable,linaga,linage,linaloa,linalol,linch,linchet,linctus,lindane,linden,linder,lindo,line,linea,lineage,lineal,linear,lineate,linecut,lined,linelet,lineman,linen,liner,ling,linga,linge,lingel,linger,lingo,lingtow,lingua,lingual,linguet,lingula,lingy,linha,linhay,linie,linin,lining,linitis,liniya,linja,linje,link,linkage,linkboy,linked,linker,linking,linkman,links,linky,linn,linnet,lino,linolic,linolin,linon,linous,linoxin,linoxyn,linpin,linseed,linsey,lint,lintel,linten,linter,lintern,lintie,linty,linwood,liny,lion,lioncel,lionel,lioness,lionet,lionism,lionize,lionly,lip,lipa,liparid,lipase,lipemia,lipide,lipin,lipless,liplet,liplike,lipoid,lipoma,lipopod,liposis,lipped,lippen,lipper,lipping,lippy,lipuria,lipwork,liquate,liquefy,liqueur,liquid,liquidy,liquor,lira,lirate,lire,lirella,lis,lisere,lish,lisk,lisle,lisp,lisper,lispund,liss,lissom,lissome,list,listed,listel,listen,lister,listing,listred,lit,litany,litas,litch,litchi,lite,liter,literal,lith,lithe,lithely,lithi,lithia,lithic,lithify,lithite,lithium,litho,lithoid,lithous,lithy,litmus,litotes,litra,litster,litten,litter,littery,little,lituite,liturgy,litus,lituus,litz,livable,live,lived,livedo,lively,liven,liver,livered,livery,livid,lividly,livier,living,livor,livre,liwan,lixive,lizard,llama,llano,llautu,llyn,lo,loa,loach,load,loadage,loaded,loaden,loader,loading,loaf,loafer,loafing,loaflet,loam,loamily,loaming,loamy,loan,loaner,loanin,loath,loathe,loather,loathly,loave,lob,lobal,lobar,lobate,lobated,lobber,lobbish,lobby,lobbyer,lobcock,lobe,lobed,lobelet,lobelin,lobfig,lobing,lobiped,lobo,lobola,lobose,lobster,lobtail,lobular,lobule,lobworm,loca,locable,local,locale,locally,locanda,locate,locator,loch,lochage,lochan,lochia,lochial,lochus,lochy,loci,lock,lockage,lockbox,locked,locker,locket,lockful,locking,lockjaw,locklet,lockman,lockout,lockpin,lockram,lockup,locky,loco,locoism,locular,locule,loculus,locum,locus,locust,locusta,locutor,lod,lode,lodge,lodged,lodger,lodging,loess,loessal,loessic,lof,loft,lofter,loftily,lofting,loftman,lofty,log,loganin,logbook,logcock,loge,logeion,logeum,loggat,logged,logger,loggia,loggin,logging,loggish,loghead,logia,logic,logical,logie,login,logion,logium,loglet,loglike,logman,logoi,logos,logroll,logway,logwise,logwood,logwork,logy,lohan,lohoch,loimic,loin,loined,loir,loiter,loka,lokao,lokaose,loke,loket,lokiec,loll,loller,lollop,lollopy,lolly,loma,lombard,lomboy,loment,lomita,lommock,lone,lonely,long,longa,longan,longbow,longe,longear,longer,longfin,longful,longing,longish,longjaw,longly,longs,longue,longway,lontar,loo,looby,lood,loof,loofah,loofie,look,looker,looking,lookout,lookum,loom,loomer,loomery,looming,loon,loonery,looney,loony,loop,looper,loopful,looping,loopist,looplet,loopy,loose,loosely,loosen,looser,loosing,loosish,loot,looten,looter,lootie,lop,lope,loper,lophiid,lophine,loppard,lopper,loppet,lopping,loppy,lopseed,loquat,loquent,lora,loral,loran,lorate,lorcha,lord,lording,lordkin,lordlet,lordly,lordy,lore,loreal,lored,lori,loric,lorica,lorilet,lorimer,loriot,loris,lormery,lorn,loro,lorry,lors,lorum,lory,losable,lose,losel,loser,losh,losing,loss,lost,lot,lota,lotase,lote,lotic,lotion,lotment,lotrite,lots,lotter,lottery,lotto,lotus,lotusin,louch,loud,louden,loudish,loudly,louey,lough,louk,loukoum,loulu,lounder,lounge,lounger,loungy,loup,loupe,lour,lourdy,louse,lousily,louster,lousy,lout,louter,louther,loutish,louty,louvar,louver,lovable,lovably,lovage,love,loveful,lovely,loveman,lover,lovered,loverly,loving,low,lowa,lowan,lowbell,lowborn,lowboy,lowbred,lowdah,lowder,loweite,lower,lowerer,lowery,lowish,lowland,lowlily,lowly,lowmen,lowmost,lown,lowness,lownly,lowth,lowwood,lowy,lox,loxia,loxic,loxotic,loy,loyal,loyally,loyalty,lozenge,lozengy,lubber,lube,lubra,lubric,lubrify,lucanid,lucarne,lucban,luce,lucence,lucency,lucent,lucern,lucerne,lucet,lucible,lucid,lucida,lucidly,lucifee,lucific,lucigen,lucivee,luck,lucken,luckful,luckie,luckily,lucky,lucre,lucrify,lucule,lucumia,lucy,ludden,ludibry,ludo,lue,lues,luetic,lufbery,luff,lug,luge,luger,luggage,luggar,lugged,lugger,luggie,lugmark,lugsail,lugsome,lugworm,luhinga,luigino,luke,lukely,lulab,lull,lullaby,luller,lulu,lum,lumbago,lumbang,lumbar,lumber,lumen,luminal,lumine,lummox,lummy,lump,lumper,lumpet,lumpily,lumping,lumpish,lumpkin,lumpman,lumpy,luna,lunacy,lunar,lunare,lunary,lunate,lunatic,lunatum,lunch,luncher,lune,lunes,lunette,lung,lunge,lunged,lunger,lungful,lungi,lungie,lungis,lungy,lunn,lunoid,lunt,lunula,lunular,lunule,lunulet,lupe,lupeol,lupeose,lupine,lupinin,lupis,lupoid,lupous,lupulic,lupulin,lupulus,lupus,lura,lural,lurch,lurcher,lurdan,lure,lureful,lurer,lurg,lurid,luridly,lurk,lurker,lurky,lurrier,lurry,lush,lusher,lushly,lushy,lusk,lusky,lusory,lust,luster,lustful,lustily,lustra,lustral,lustrum,lusty,lut,lutany,lute,luteal,lutecia,lutein,lutelet,luteo,luteoma,luteous,luter,luteway,lutfisk,luthern,luthier,luting,lutist,lutose,lutrin,lutrine,lux,luxate,luxe,luxury,luxus,ly,lyam,lyard,lyceal,lyceum,lycid,lycopin,lycopod,lycosid,lyctid,lyddite,lydite,lye,lyery,lygaeid,lying,lyingly,lymph,lymphad,lymphy,lyncean,lynch,lyncher,lyncine,lynx,lyra,lyrate,lyrated,lyraway,lyre,lyreman,lyric,lyrical,lyrism,lyrist,lys,lysate,lyse,lysin,lysine,lysis,lysogen,lyssa,lyssic,lytic,lytta,lyxose,m,ma,maam,mabi,mabolo,mac,macabre,macaco,macadam,macan,macana,macao,macaque,macaw,macco,mace,maceman,macer,machan,machar,machete,machi,machila,machin,machine,machree,macies,mack,mackins,mackle,macle,macled,maco,macrame,macro,macron,macuca,macula,macular,macule,macuta,mad,madam,madame,madcap,madden,madder,madding,maddish,maddle,made,madefy,madhuca,madid,madling,madly,madman,madnep,madness,mado,madoqua,madrier,madrona,madship,maduro,madweed,madwort,mae,maenad,maestri,maestro,maffia,maffick,maffle,mafflin,mafic,mafoo,mafura,mag,magadis,magani,magas,mage,magenta,magged,maggle,maggot,maggoty,magi,magic,magical,magiric,magma,magnate,magnes,magnet,magneta,magneto,magnify,magnum,magot,magpie,magpied,magsman,maguari,maguey,maha,mahaleb,mahalla,mahant,mahar,maharao,mahatma,mahmal,mahmudi,mahoe,maholi,mahone,mahout,mahseer,mahua,mahuang,maid,maidan,maiden,maidish,maidism,maidkin,maidy,maiefic,maigre,maiid,mail,mailbag,mailbox,mailed,mailer,mailie,mailman,maim,maimed,maimer,maimon,main,mainly,mainour,mainpin,mains,maint,maintop,maioid,maire,maize,maizer,majagua,majesty,majo,majoon,major,makable,make,makedom,maker,makhzan,maki,making,makluk,mako,makuk,mal,mala,malacia,malacon,malady,malagma,malaise,malakin,malambo,malanga,malapi,malar,malaria,malarin,malate,malati,malax,malduck,male,malease,maleate,maleic,malella,maleo,malfed,mali,malic,malice,malicho,malign,malik,maline,malines,malism,malison,malist,malkin,mall,mallard,malleal,mallear,mallee,mallein,mallet,malleus,mallow,mallum,mallus,malm,malmsey,malmy,malo,malodor,malonic,malonyl,malouah,malpais,malt,maltase,malter,maltha,malting,maltman,maltose,malty,mamba,mambo,mamma,mammal,mammary,mammate,mammee,mammer,mammock,mammon,mammoth,mammula,mammy,mamo,man,mana,manacle,manage,managee,manager,manaism,manakin,manal,manas,manatee,manavel,manbird,manbot,manche,manchet,mancono,mancus,mand,mandala,mandant,mandate,mandil,mandola,mandom,mandora,mandore,mandra,mandrel,mandrin,mandua,mandyas,mane,maned,manege,manei,manent,manes,maness,maney,manful,mang,manga,mangal,mange,mangeao,mangel,manger,mangi,mangily,mangle,mangler,mango,mangona,mangue,mangy,manhead,manhole,manhood,mani,mania,maniac,manic,manid,manify,manikin,manila,manilla,manille,manioc,maniple,manism,manist,manito,maniu,manjak,mank,mankin,mankind,manless,manlet,manlike,manlily,manling,manly,manna,mannan,manner,manners,manness,mannide,mannie,mannify,manning,mannish,mannite,mannose,manny,mano,manoc,manomin,manor,manque,manred,manrent,manroot,manrope,mansard,manse,manship,mansion,manso,mant,manta,mantal,manteau,mantel,manter,mantes,mantic,mantid,mantis,mantle,mantled,mantlet,manto,mantoid,mantra,mantrap,mantua,manual,manuao,manuka,manul,manuma,manumea,manumit,manure,manurer,manus,manward,manway,manweed,manwise,many,manzana,manzil,mao,maomao,map,mapach,mapau,mapland,maple,mapo,mapper,mappist,mappy,mapwise,maqui,maquis,mar,marabou,maraca,maracan,marae,maral,marang,marara,mararie,marasca,maraud,marble,marbled,marbler,marbles,marbly,marc,marcel,march,marcher,marcid,marco,marconi,marcor,mardy,mare,maremma,marengo,marfire,margay,marge,margent,margin,margosa,marhala,maria,marid,marimba,marina,marine,mariner,mariola,maris,marish,marital,mark,marka,marked,marker,market,markhor,marking,markka,markman,markup,marl,marled,marler,marli,marlin,marline,marlite,marlock,marlpit,marly,marm,marmit,marmite,marmose,marmot,maro,marok,maroon,marplot,marque,marquee,marquis,marrano,marree,marrer,married,marrier,marron,marrot,marrow,marrowy,marry,marryer,marsh,marshal,marshy,marsoon,mart,martel,marten,martext,martial,martin,martite,martlet,martyr,martyry,maru,marvel,marver,mary,marybud,mas,masa,mascara,mascled,mascot,masculy,masdeu,mash,masha,mashal,masher,mashie,mashing,mashman,mashru,mashy,masjid,mask,masked,masker,maskoid,maslin,mason,masoned,masoner,masonic,masonry,masooka,masoola,masque,masquer,mass,massa,massage,masse,massel,masser,masseur,massier,massif,massily,massive,massoy,massula,massy,mast,mastaba,mastage,mastax,masted,master,mastery,mastful,mastic,mastiff,masting,mastman,mastoid,masty,masu,mat,mataco,matador,matai,matalan,matanza,matapan,matapi,matara,matax,match,matcher,matchy,mate,mately,mater,matey,math,mathes,matico,matin,matinal,matinee,mating,matins,matipo,matka,matless,matlow,matra,matral,matrass,matreed,matric,matris,matrix,matron,matross,matsu,matsuri,matta,mattaro,matte,matted,matter,mattery,matti,matting,mattock,mattoid,mattoir,mature,maturer,matweed,maty,matzo,matzoon,matzos,matzoth,mau,maud,maudle,maudlin,mauger,maugh,maul,mauler,mauley,mauling,maumet,maun,maund,maunder,maundy,maunge,mauther,mauve,mauvine,maux,mavis,maw,mawk,mawkish,mawky,mawp,maxilla,maxim,maxima,maximal,maximed,maximum,maximus,maxixe,maxwell,may,maya,maybe,maybush,maycock,mayday,mayfish,mayhap,mayhem,maynt,mayor,mayoral,maypop,maysin,mayten,mayweed,maza,mazame,mazard,maze,mazed,mazedly,mazeful,mazer,mazic,mazily,mazuca,mazuma,mazurka,mazut,mazy,mazzard,mbalolo,mbori,me,meable,mead,meader,meadow,meadowy,meager,meagre,meak,meal,mealer,mealies,mealily,mealman,mealy,mean,meander,meaned,meaner,meaning,meanish,meanly,meant,mease,measle,measled,measles,measly,measure,meat,meatal,meated,meatily,meatman,meatus,meaty,mecate,mecon,meconic,meconin,medal,medaled,medalet,meddle,meddler,media,mediacy,mediad,medial,median,mediant,mediate,medic,medical,medico,mediety,medimn,medimno,medino,medio,medium,medius,medlar,medley,medrick,medulla,medusal,medusan,meebos,meece,meed,meek,meeken,meekly,meered,meerkat,meese,meet,meeten,meeter,meeting,meetly,megabar,megaerg,megafog,megapod,megaron,megaton,megerg,megilp,megmho,megohm,megrim,mehalla,mehari,mehtar,meile,mein,meinie,meio,meiobar,meiosis,meiotic,meith,mel,mela,melada,melagra,melam,melamed,melange,melanic,melanin,melano,melasma,melch,meld,melder,meldrop,mele,melee,melena,melene,melenic,melic,melilot,meline,melisma,melitis,mell,mellate,mellay,meller,mellit,mellite,mellon,mellow,mellowy,melodia,melodic,melody,meloe,meloid,melon,melonry,melos,melosa,melt,meltage,melted,melter,melters,melting,melton,mem,member,membral,memento,meminna,memo,memoir,memoria,memory,men,menace,menacer,menacme,menage,menald,mend,mendee,mender,mending,mendole,mends,menfolk,meng,menhir,menial,meninx,menkind,mennom,mensa,mensal,mense,menses,mensk,mensual,mental,mentary,menthol,menthyl,mention,mentor,mentum,menu,meny,menyie,menzie,merbaby,mercal,mercer,mercery,merch,merchet,mercy,mere,merel,merely,merfold,merfolk,merge,merger,mergh,meriah,merice,meril,merism,merist,merit,merited,meriter,merk,merkhet,merkin,merl,merle,merlin,merlon,mermaid,merman,mero,merop,meropia,meros,merrily,merrow,merry,merse,mesa,mesad,mesail,mesal,mesally,mesange,mesarch,mescal,mese,mesem,mesenna,mesh,meshed,meshy,mesiad,mesial,mesian,mesic,mesilla,mesion,mesityl,mesne,meso,mesobar,mesode,mesodic,mesole,meson,mesonic,mesopic,mespil,mess,message,messan,messe,messer,messet,messily,messin,messing,messman,messor,messrs,messtin,messy,mestee,mester,mestiza,mestizo,mestome,met,meta,metad,metage,metal,metaler,metamer,metanym,metate,metayer,mete,metel,meteor,meter,methane,methene,mether,methid,methide,methine,method,methyl,metic,metier,metis,metochy,metonym,metope,metopic,metopon,metra,metreta,metrete,metria,metric,metrics,metrify,metrist,mettar,mettle,mettled,metusia,metze,meuse,meute,mew,meward,mewer,mewl,mewler,mezcal,mezuzah,mezzo,mho,mi,miamia,mian,miaow,miaower,mias,miasm,miasma,miasmal,miasmic,miaul,miauler,mib,mica,micate,mice,micelle,miche,micher,miching,micht,mick,mickle,mico,micrify,micro,microbe,microhm,micron,miction,mid,midday,midden,middle,middler,middy,mide,midge,midget,midgety,midgy,midiron,midland,midleg,midmain,midmorn,midmost,midnoon,midpit,midrash,midrib,midriff,mids,midship,midst,midtap,midvein,midward,midway,midweek,midwife,midwise,midyear,mien,miff,miffy,mig,might,mightnt,mighty,miglio,mignon,migrant,migrate,mihrab,mijl,mikado,mike,mikie,mil,mila,milady,milch,milcher,milchy,mild,milden,milder,mildew,mildewy,mildish,mildly,mile,mileage,miler,mileway,milfoil,milha,miliary,milieu,militia,milium,milk,milken,milker,milkily,milking,milkman,milksop,milky,mill,milla,millage,milldam,mille,milled,miller,millet,millful,milliad,millile,milline,milling,million,millman,milner,milo,milord,milpa,milreis,milsey,milsie,milt,milter,milty,milvine,mim,mima,mimbar,mimble,mime,mimeo,mimer,mimesis,mimetic,mimic,mimical,mimicry,mimine,mimly,mimmest,mimmock,mimmood,mimmoud,mimosis,mimp,mimsey,min,mina,minable,minar,minaret,minaway,mince,mincer,mincing,mind,minded,minder,mindful,minding,mine,miner,mineral,minery,mines,minette,ming,minge,mingle,mingler,mingy,minhag,minhah,miniate,minibus,minicam,minify,minikin,minim,minima,minimal,minimum,minimus,mining,minion,minish,minium,miniver,minivet,mink,minkery,minkish,minnie,minning,minnow,minny,mino,minoize,minor,minot,minster,mint,mintage,minter,mintman,minty,minuend,minuet,minus,minute,minuter,minutia,minx,minxish,miny,minyan,miqra,mir,mirach,miracle,mirador,mirage,miragy,mirate,mirbane,mird,mirdaha,mire,mirid,mirific,mirish,mirk,miro,mirror,mirrory,mirth,miry,mirza,misact,misadd,misaim,misally,misbias,misbill,misbind,misbode,misborn,misbusy,miscall,miscast,mischio,miscoin,miscook,miscrop,miscue,miscut,misdate,misdaub,misdeal,misdeed,misdeem,misdiet,misdo,misdoer,misdraw,mise,misease,misedit,miser,miserly,misery,misfare,misfile,misfire,misfit,misfond,misform,misgive,misgo,misgrow,mishap,mishmee,misjoin,miskeep,misken,miskill,misknow,misky,mislay,mislead,mislear,misled,mislest,mislike,mislive,mismade,mismake,mismate,mismove,misname,misobey,mispage,mispart,mispay,mispick,misplay,misput,misrate,misread,misrule,miss,missal,missay,misseem,missel,misset,missile,missing,mission,missis,missish,missive,misstay,misstep,missy,mist,mistake,mistbow,misted,mistell,mistend,mister,misterm,mistful,mistic,mistide,mistify,mistily,mistime,mistle,mistone,mistook,mistral,mistry,misturn,misty,misura,misuse,misuser,miswed,miswish,misword,misyoke,mite,miter,mitered,miterer,mitis,mitome,mitosis,mitotic,mitra,mitral,mitrate,mitre,mitrer,mitt,mitten,mitty,mity,miurus,mix,mixable,mixed,mixedly,mixen,mixer,mixhill,mixible,mixite,mixtion,mixture,mixy,mizmaze,mizzen,mizzle,mizzler,mizzly,mizzy,mneme,mnemic,mnesic,mnestic,mnioid,mo,moan,moanful,moaning,moat,mob,mobable,mobber,mobbish,mobbism,mobbist,mobby,mobcap,mobed,mobile,moble,moblike,mobship,mobsman,mobster,mocha,mochras,mock,mockado,mocker,mockery,mockful,mocmain,mocuck,modal,modally,mode,model,modeler,modena,modern,modest,modesty,modicum,modify,modish,modist,modiste,modius,modular,module,modulo,modulus,moellon,mofette,moff,mog,mogador,mogdad,moggan,moggy,mogo,moguey,moha,mohabat,mohair,mohar,mohel,moho,mohr,mohur,moider,moidore,moieter,moiety,moil,moiler,moiles,moiley,moiling,moineau,moio,moire,moise,moist,moisten,moistly,moisty,moit,moity,mojarra,mojo,moke,moki,moko,moksha,mokum,moky,mola,molal,molar,molary,molassy,molave,mold,molder,moldery,molding,moldy,mole,moleism,moler,molest,molimen,moline,molka,molland,molle,mollie,mollify,mollusk,molly,molman,moloid,moloker,molompi,molosse,molpe,molt,molten,molter,moly,mombin,momble,mome,moment,momenta,momism,momme,mommet,mommy,momo,mon,mona,monad,monadic,monaene,monal,monarch,monas,monase,monaxon,mone,monel,monepic,moner,moneral,moneran,moneric,moneron,monesia,money,moneyed,moneyer,mong,monger,mongery,mongler,mongrel,mongst,monial,moniker,monism,monist,monitor,monk,monkdom,monkery,monkess,monkey,monkish,monkism,monkly,monny,mono,monoazo,monocle,monocot,monodic,monody,monoid,monomer,mononch,monont,mononym,monose,monotic,monsoon,monster,montage,montana,montane,montant,monte,montem,month,monthly,monthon,montjoy,monton,monture,moo,mooch,moocha,moocher,mood,mooder,moodily,moodish,moodle,moody,mooing,mool,moolet,mools,moolum,moon,moonack,mooned,mooner,moonery,mooneye,moonily,mooning,moonish,moonite,moonja,moonjah,moonlet,moonlit,moonman,moonset,moonway,moony,moop,moor,moorage,mooring,moorish,moorman,moorn,moorpan,moors,moorup,moory,moosa,moose,moosey,moost,moot,mooter,mooth,mooting,mootman,mop,mopane,mope,moper,moph,mophead,moping,mopish,mopla,mopper,moppet,moppy,mopsy,mopus,mor,mora,moraine,moral,morale,morally,morals,morass,morassy,morat,morate,moray,morbid,morbify,mordant,mordent,mordore,more,moreen,moreish,morel,morella,morello,mores,morfrey,morg,morga,morgan,morgay,morgen,morglay,morgue,moric,moriche,morin,morinel,morion,morkin,morlop,mormaor,mormo,mormon,mormyr,mormyre,morn,morne,morned,morning,moro,moroc,morocco,moron,moroncy,morong,moronic,moronry,morose,morosis,morph,morphea,morphew,morphia,morphic,morphon,morris,morrow,morsal,morse,morsel,morsing,morsure,mort,mortal,mortar,mortary,morth,mortier,mortify,mortise,morula,morular,morule,morvin,morwong,mosaic,mosaist,mosette,mosey,mosker,mosque,moss,mossed,mosser,mossery,mossful,mossy,most,moste,mostly,mot,mote,moted,motel,moter,motet,motey,moth,mothed,mother,mothery,mothy,motif,motific,motile,motion,motive,motley,motmot,motor,motored,motoric,motory,mott,motte,mottle,mottled,mottler,motto,mottoed,motyka,mou,mouche,moud,moudie,moudy,mouflon,mouille,moujik,moul,mould,moulded,moule,moulin,mouls,moulter,mouly,mound,moundy,mount,mounted,mounter,moup,mourn,mourner,mouse,mouser,mousery,mousey,mousily,mousing,mousle,mousmee,mousse,moustoc,mousy,mout,moutan,mouth,mouthed,mouther,mouthy,mouton,mouzah,movable,movably,movant,move,mover,movie,moving,mow,mowable,mowana,mowburn,mowch,mowcht,mower,mowha,mowie,mowing,mowland,mown,mowra,mowrah,mowse,mowt,mowth,moxa,moy,moyen,moyenne,moyite,moyle,moyo,mozing,mpret,mu,muang,mubarat,mucago,mucaro,mucedin,much,muchly,mucic,mucid,mucific,mucigen,mucin,muck,mucker,mucket,muckite,muckle,muckman,muckna,mucksy,mucky,mucluc,mucoid,muconic,mucopus,mucor,mucosa,mucosal,mucose,mucous,mucro,mucus,mucusin,mud,mudar,mudbank,mudcap,mudd,mudde,mudden,muddify,muddily,mudding,muddish,muddle,muddler,muddy,mudee,mudfish,mudflow,mudhead,mudhole,mudir,mudiria,mudland,mudlark,mudless,mudra,mudsill,mudweed,mudwort,muermo,muezzin,muff,muffed,muffet,muffin,muffish,muffle,muffled,muffler,mufflin,muffy,mufti,mufty,mug,muga,mugful,mugg,mugger,mugget,muggily,muggins,muggish,muggles,muggy,mugient,mugweed,mugwort,mugwump,muid,muir,muist,mukluk,muktar,mukti,mulatta,mulatto,mulch,mulcher,mulct,mulder,mule,muleman,muleta,muletta,muley,mulga,mulier,mulish,mulism,mulita,mulk,mull,mulla,mullah,mullar,mullein,muller,mullet,mullets,mulley,mullid,mullion,mullite,mullock,mulloid,mulmul,mulse,mulsify,mult,multum,multure,mum,mumble,mumbler,mummer,mummery,mummick,mummied,mummify,mumming,mummy,mumness,mump,mumper,mumpish,mumps,mun,munch,muncher,munchet,mund,mundane,mundic,mundify,mundil,mundle,mung,munga,munge,mungey,mungo,mungofa,munguba,mungy,munific,munity,munj,munjeet,munnion,munshi,munt,muntin,muntjac,mura,murage,mural,muraled,murally,murchy,murder,murdrum,mure,murex,murexan,murga,murgavi,murgeon,muriate,muricid,murid,murine,murinus,muriti,murium,murk,murkily,murkish,murkly,murky,murlin,murly,murmur,murphy,murra,murrain,murre,murrey,murrina,murshid,muruxi,murva,murza,musal,musang,musar,muscade,muscat,muscid,muscle,muscled,muscly,muscoid,muscone,muscose,muscot,muscovy,muscule,muse,mused,museful,museist,muser,musery,musette,museum,mush,musha,mushaa,mushed,musher,mushily,mushla,mushru,mushy,music,musical,musico,musie,musily,musimon,musing,musk,muskat,muskeg,musket,muskie,muskish,muskrat,musky,muslin,musnud,musquaw,musrol,muss,mussal,mussel,mussily,mussuk,mussy,must,mustang,mustard,mustee,muster,mustify,mustily,mustnt,musty,muta,mutable,mutably,mutage,mutant,mutase,mutate,mutch,mute,mutedly,mutely,muth,mutic,mutiny,mutism,mutist,mutive,mutsje,mutt,mutter,mutton,muttony,mutual,mutuary,mutule,mutuum,mux,muyusa,muzhik,muzz,muzzily,muzzle,muzzler,muzzy,my,myal,myalgia,myalgic,myalism,myall,myarian,myatony,mycele,mycelia,mycoid,mycose,mycosin,mycosis,mycotic,mydine,myelic,myelin,myeloic,myeloid,myeloma,myelon,mygale,mygalid,myiasis,myiosis,myitis,mykiss,mymarid,myna,myocele,myocyte,myogen,myogram,myoid,myology,myoma,myomere,myoneme,myope,myophan,myopia,myopic,myops,myopy,myosin,myosis,myosote,myotic,myotome,myotomy,myotony,myowun,myoxine,myrcene,myrcia,myriad,myriare,myrica,myricin,myricyl,myringa,myron,myronic,myrosin,myrrh,myrrhed,myrrhic,myrrhol,myrrhy,myrtal,myrtle,myrtol,mysel,myself,mysell,mysid,mysoid,mysost,myst,mystax,mystery,mystes,mystic,mystify,myth,mythify,mythism,mythist,mythize,mythos,mythus,mytilid,myxa,myxemia,myxo,myxoid,myxoma,myxopod,myzont,n,na,naa,naam,nab,nabak,nabber,nabk,nabla,nable,nabob,nabobry,nabs,nacarat,nace,nacelle,nach,nachani,nacket,nacre,nacred,nacrine,nacrite,nacrous,nacry,nadder,nadir,nadiral,nae,naebody,naegate,nael,naether,nag,naga,nagaika,nagana,nagara,nagger,naggin,nagging,naggish,naggle,naggly,naggy,naght,nagmaal,nagman,nagnag,nagnail,nagor,nagsman,nagster,nagual,naiad,naiant,naid,naif,naifly,naig,naigie,naik,nail,nailbin,nailer,nailery,nailing,nailrod,naily,nain,nainsel,naio,naipkin,nairy,nais,naish,naither,naive,naively,naivete,naivety,nak,nake,naked,nakedly,naker,nakhod,nakhoda,nako,nakong,nakoo,nallah,nam,namable,namaqua,namaz,namda,name,namely,namer,naming,nammad,nan,nana,nancy,nandi,nandine,nandow,nandu,nane,nanes,nanga,nanism,nankeen,nankin,nanny,nanoid,nanpie,nant,nantle,naology,naos,nap,napa,napal,napalm,nape,napead,naperer,napery,naphtha,naphtho,naphtol,napkin,napless,napoo,nappe,napped,napper,napping,nappy,napron,napu,nar,narcism,narcist,narcoma,narcose,narcous,nard,nardine,nardoo,nares,nargil,narial,naric,narica,narine,nark,narky,narr,narra,narras,narrate,narrow,narrowy,narthex,narwhal,nary,nasab,nasal,nasalis,nasally,nasard,nascent,nasch,nash,nashgab,nashgob,nasi,nasial,nasion,nasitis,nasrol,nast,nastic,nastika,nastily,nasty,nasus,nasute,nasutus,nat,nataka,natal,natals,natant,natator,natch,nates,nathe,nather,nation,native,natr,natrium,natron,natter,nattily,nattle,natty,natuary,natural,nature,naucrar,nauger,naught,naughty,naumk,naunt,nauntle,nausea,naut,nautch,nauther,nautic,nautics,naval,navally,navar,navarch,nave,navel,naveled,navet,navette,navew,navite,navvy,navy,naw,nawab,nawt,nay,nayaur,naysay,nayward,nayword,naze,nazim,nazir,ne,nea,neal,neanic,neap,neaped,nearby,nearest,nearish,nearly,neat,neaten,neath,neatify,neatly,neb,neback,nebbed,nebbuck,nebbuk,nebby,nebel,nebris,nebula,nebulae,nebular,nebule,neck,neckar,necked,necker,neckful,necking,necklet,necktie,necrose,nectar,nectary,nedder,neddy,nee,neebor,neebour,need,needer,needful,needham,needily,needing,needle,needled,needler,needles,needly,needs,needy,neeger,neeld,neele,neem,neep,neepour,neer,neese,neet,neetup,neeze,nef,nefast,neffy,neftgil,negate,negator,neger,neglect,negrine,negro,negus,nei,neif,neigh,neigher,neiper,neist,neither,nekton,nelson,nema,nematic,nemeses,nemesic,nemoral,nenta,neo,neocyte,neogamy,neolith,neology,neon,neonate,neorama,neossin,neoteny,neotype,neoza,nep,neper,nephele,nephesh,nephew,nephria,nephric,nephron,nephros,nepman,nepotal,nepote,nepotic,nereite,nerine,neritic,nerval,nervate,nerve,nerver,nervid,nervily,nervine,nerving,nervish,nervism,nervose,nervous,nervule,nervure,nervy,nese,nesh,neshly,nesiote,ness,nest,nestage,nester,nestful,nestle,nestler,nesty,net,netball,netbush,netcha,nete,neter,netful,neth,nether,neti,netleaf,netlike,netman,netop,netsman,netsuke,netted,netter,netting,nettle,nettler,nettly,netty,netwise,network,neuma,neume,neumic,neurad,neural,neurale,neuric,neurin,neurine,neurism,neurite,neuroid,neuroma,neuron,neurone,neurula,neuter,neutral,neutron,neve,nevel,never,nevo,nevoid,nevoy,nevus,new,newcal,newcome,newel,newelty,newing,newings,newish,newly,newness,news,newsboy,newsful,newsman,newsy,newt,newtake,newton,nexal,next,nextly,nexum,nexus,neyanda,ngai,ngaio,ngapi,ni,niacin,niata,nib,nibbana,nibbed,nibber,nibble,nibbler,nibby,niblick,niblike,nibong,nibs,nibsome,nice,niceish,nicely,nicety,niche,nicher,nick,nickel,nicker,nickey,nicking,nickle,nicky,nicolo,nicotia,nicotic,nictate,nid,nidal,nidana,niddick,niddle,nide,nidge,nidget,nidgety,nidi,nidify,niding,nidor,nidulus,nidus,niece,nielled,niello,niepa,nieve,nieveta,nife,niffer,nific,nifle,nifling,nifty,nig,niggard,nigger,niggery,niggle,niggler,niggly,nigh,nighly,night,nighted,nightie,nightly,nights,nignay,nignye,nigori,nigre,nigrify,nigrine,nigrous,nigua,nikau,nil,nilgai,nim,nimb,nimbed,nimbi,nimble,nimbly,nimbose,nimbus,nimiety,niminy,nimious,nimmer,nimshi,nincom,nine,ninepin,nineted,ninety,ninny,ninon,ninth,ninthly,nintu,ninut,niobate,niobic,niobite,niobium,niobous,niog,niota,nip,nipa,nipper,nippers,nippily,nipping,nipple,nippy,nipter,nirles,nirvana,nisei,nishiki,nisnas,nispero,nisse,nisus,nit,nitch,nitency,niter,nitered,nither,nithing,nitid,nito,niton,nitrate,nitric,nitride,nitrify,nitrile,nitrite,nitro,nitrous,nitryl,nitter,nitty,nitwit,nival,niveous,nix,nixie,niyoga,nizam,nizamut,nizy,njave,no,noa,nob,nobber,nobbily,nobble,nobbler,nobbut,nobby,noble,nobley,nobly,nobody,nobs,nocake,nocent,nock,nocket,nocktat,noctuid,noctule,nocturn,nocuity,nocuous,nod,nodal,nodated,nodder,nodding,noddle,noddy,node,noded,nodi,nodiak,nodical,nodose,nodous,nodular,nodule,noduled,nodulus,nodus,noel,noetic,noetics,nog,nogada,nogal,noggen,noggin,nogging,noghead,nohow,noil,noilage,noiler,noily,noint,noir,noise,noisily,noisome,noisy,nokta,noll,nolle,nolo,noma,nomad,nomadic,nomancy,nomarch,nombril,nome,nomial,nomic,nomina,nominal,nominee,nominy,nomism,nomisma,nomos,non,nonacid,nonact,nonage,nonagon,nonaid,nonair,nonane,nonary,nonbase,nonce,noncock,noncom,noncome,noncon,nonda,nondo,none,nonego,nonene,nonent,nonepic,nones,nonet,nonevil,nonfact,nonfarm,nonfat,nonfood,nonform,nonfrat,nongas,nongod,nongold,nongray,nongrey,nonhero,nonic,nonion,nonius,nonjury,nonlife,nonly,nonnant,nonnat,nonoic,nonoily,nonomad,nonpaid,nonpar,nonpeak,nonplus,nonpoet,nonport,nonrun,nonsale,nonsane,nonself,nonsine,nonskid,nonslip,nonstop,nonsuit,nontan,nontax,nonterm,nonuple,nonuse,nonuser,nonwar,nonya,nonyl,nonylic,nonzero,noodle,nook,nooked,nookery,nooking,nooklet,nooky,noology,noon,noonday,nooning,noonlit,noop,noose,nooser,nopal,nopalry,nope,nor,norard,norate,noreast,norelin,norgine,nori,noria,norie,norimon,norite,norland,norm,norma,normal,norsel,north,norther,norward,norwest,nose,nosean,nosed,nosegay,noser,nosey,nosine,nosing,nosism,nostic,nostril,nostrum,nosy,not,notable,notably,notaeal,notaeum,notal,notan,notary,notate,notator,notch,notched,notchel,notcher,notchy,note,noted,notedly,notekin,notelet,noter,nother,nothing,nothous,notice,noticer,notify,notion,notitia,notour,notself,notum,nougat,nought,noun,nounal,nounize,noup,nourice,nourish,nous,nouther,nova,novalia,novate,novator,novcic,novel,novelet,novella,novelly,novelry,novelty,novem,novena,novene,novice,novity,now,nowaday,noway,noways,nowed,nowel,nowhat,nowhen,nowhere,nowhit,nowise,nowness,nowt,nowy,noxa,noxal,noxally,noxious,noy,noyade,noyau,nozzle,nozzler,nth,nu,nuance,nub,nubbin,nubble,nubbly,nubby,nubia,nubile,nucal,nucha,nuchal,nucin,nucleal,nuclear,nuclei,nuclein,nucleon,nucleus,nuclide,nucule,nuculid,nudate,nuddle,nude,nudely,nudge,nudger,nudiped,nudish,nudism,nudist,nudity,nugator,nuggar,nugget,nuggety,nugify,nuke,nul,null,nullah,nullify,nullism,nullity,nullo,numb,number,numbing,numble,numbles,numbly,numda,numdah,numen,numeral,numero,nummary,nummi,nummus,numud,nun,nunatak,nunbird,nunch,nuncio,nuncle,nundine,nunhood,nunky,nunlet,nunlike,nunnari,nunnery,nunni,nunnify,nunnish,nunship,nuptial,nuque,nuraghe,nurhag,nurly,nurse,nurser,nursery,nursing,nursle,nursy,nurture,nusfiah,nut,nutant,nutate,nutcake,nutgall,nuthook,nutlet,nutlike,nutmeg,nutpick,nutria,nutrice,nutrify,nutseed,nutted,nutter,nuttery,nuttily,nutting,nuttish,nutty,nuzzer,nuzzle,nyanza,nye,nylast,nylon,nymil,nymph,nympha,nymphae,nymphal,nymphet,nymphic,nymphid,nymphly,nyxis,o,oadal,oaf,oafdom,oafish,oak,oaken,oaklet,oaklike,oakling,oakum,oakweb,oakwood,oaky,oam,oar,oarage,oarcock,oared,oarfish,oarhole,oarial,oaric,oaritic,oaritis,oarium,oarless,oarlike,oarlock,oarlop,oarman,oarsman,oarweed,oary,oasal,oasean,oases,oasis,oasitic,oast,oat,oatbin,oatcake,oatear,oaten,oatfowl,oath,oathay,oathed,oathful,oathlet,oatland,oatlike,oatmeal,oatseed,oaty,oban,obclude,obe,obeah,obeche,obeism,obelia,obeliac,obelial,obelion,obelisk,obelism,obelize,obelus,obese,obesely,obesity,obex,obey,obeyer,obi,obispo,obit,obitual,object,objure,oblate,obley,oblige,obliged,obligee,obliger,obligor,oblique,oblong,obloquy,oboe,oboist,obol,obolary,obole,obolet,obolus,oboval,obovate,obovoid,obscene,obscure,obsede,obsequy,observe,obsess,obtain,obtect,obtest,obtrude,obtund,obtuse,obverse,obvert,obviate,obvious,obvolve,ocarina,occamy,occiput,occlude,occluse,occult,occupy,occur,ocean,oceaned,oceanet,oceanic,ocellar,ocelli,ocellus,oceloid,ocelot,och,ochava,ochavo,ocher,ochery,ochone,ochrea,ochro,ochroid,ochrous,ocht,ock,oclock,ocote,ocque,ocracy,ocrea,ocreate,octad,octadic,octagon,octan,octane,octant,octapla,octarch,octary,octaval,octave,octavic,octavo,octene,octet,octic,octine,octoad,octoate,octofid,octoic,octoid,octonal,octoon,octoped,octopi,octopod,octopus,octose,octoyl,octroi,octroy,octuor,octuple,octuply,octyl,octyne,ocuby,ocular,oculary,oculate,oculist,oculus,od,oda,odacoid,odal,odalisk,odaller,odalman,odd,oddish,oddity,oddlegs,oddly,oddman,oddment,oddness,odds,oddsman,ode,odel,odelet,odeon,odeum,odic,odinite,odious,odist,odium,odology,odontic,odoom,odor,odorant,odorate,odored,odorful,odorize,odorous,odso,odum,odyl,odylic,odylism,odylist,odylize,oe,oecist,oecus,oenin,oenolin,oenomel,oer,oersted,oes,oestrid,oestrin,oestrum,oestrus,of,off,offal,offbeat,offcast,offcome,offcut,offend,offense,offer,offeree,offerer,offeror,offhand,office,officer,offing,offish,offlet,offlook,offscum,offset,offtake,offtype,offward,oflete,oft,often,oftens,ofter,oftest,oftly,oftness,ofttime,ogaire,ogam,ogamic,ogdoad,ogdoas,ogee,ogeed,ogham,oghamic,ogival,ogive,ogived,ogle,ogler,ogmic,ogre,ogreish,ogreism,ogress,ogrish,ogrism,ogtiern,ogum,oh,ohelo,ohia,ohm,ohmage,ohmic,oho,ohoy,oidioid,oii,oil,oilbird,oilcan,oilcoat,oilcup,oildom,oiled,oiler,oilery,oilfish,oilhole,oilily,oilless,oillet,oillike,oilman,oilseed,oilskin,oilway,oily,oilyish,oime,oinomel,oint,oisin,oitava,oka,okapi,okee,okenite,oket,oki,okia,okonite,okra,okrug,olam,olamic,old,olden,older,oldish,oldland,oldness,oldster,oldwife,oleana,olease,oleate,olefin,olefine,oleic,olein,olena,olenid,olent,oleo,oleose,oleous,olfact,olfacty,oliban,olid,oligist,olio,olitory,oliva,olivary,olive,olived,olivet,olivil,olivile,olivine,olla,ollamh,ollapod,ollock,olm,ologist,ology,olomao,olona,oloroso,olpe,oltonde,oltunna,olycook,olykoek,om,omagra,omalgia,omao,omasum,omber,omega,omegoid,omelet,omen,omened,omental,omentum,omer,omicron,omina,ominous,omit,omitis,omitter,omlah,omneity,omniana,omnibus,omnific,omnify,omnist,omnium,on,ona,onager,onagra,onanism,onanist,onca,once,oncetta,oncia,oncin,oncome,oncosis,oncost,ondatra,ondine,ondy,one,onefold,onegite,onehow,oneiric,oneism,onement,oneness,oner,onerary,onerous,onery,oneself,onetime,oneyer,onfall,onflow,ongaro,ongoing,onicolo,onion,onionet,oniony,onium,onkos,onlay,onlepy,onliest,onlook,only,onmarch,onrush,ons,onset,onshore,onside,onsight,onstand,onstead,onsweep,ontal,onto,onus,onward,onwards,onycha,onychia,onychin,onym,onymal,onymity,onymize,onymous,onymy,onyx,onyxis,onza,ooblast,oocyst,oocyte,oodles,ooecial,ooecium,oofbird,ooftish,oofy,oogamy,oogeny,ooglea,oogone,oograph,ooid,ooidal,oolak,oolemma,oolite,oolitic,oolly,oologic,oology,oolong,oomancy,oometer,oometry,oons,oont,oopak,oophore,oophyte,ooplasm,ooplast,oopod,oopodal,oorali,oord,ooscope,ooscopy,oosperm,oospore,ootheca,ootid,ootype,ooze,oozily,oozooid,oozy,opacate,opacify,opacite,opacity,opacous,opah,opal,opaled,opaline,opalish,opalize,opaloid,opaque,ope,opelet,open,opener,opening,openly,opera,operae,operand,operant,operate,opercle,operose,ophic,ophioid,ophite,ophitic,ophryon,opianic,opianyl,opiate,opiatic,opiism,opinant,opine,opiner,opinion,opium,opossum,oppidan,oppose,opposed,opposer,opposit,oppress,oppugn,opsonic,opsonin,opsy,opt,optable,optably,optant,optate,optic,optical,opticon,optics,optimal,optime,optimum,option,optive,opulent,opulus,opus,oquassa,or,ora,orach,oracle,orad,orage,oral,oraler,oralism,oralist,orality,oralize,orally,oralogy,orang,orange,oranger,orangey,orant,orarian,orarion,orarium,orary,orate,oration,orator,oratory,oratrix,orb,orbed,orbic,orbical,orbicle,orbific,orbit,orbital,orbitar,orbite,orbless,orblet,orby,orc,orcanet,orcein,orchard,orchat,orchel,orchic,orchid,orchil,orcin,orcinol,ordain,ordeal,order,ordered,orderer,orderly,ordinal,ordinar,ordinee,ordines,ordu,ordure,ore,oread,orectic,orellin,oreman,orenda,oreweed,orewood,orexis,orf,orfgild,organ,organal,organdy,organer,organic,organon,organry,organum,orgasm,orgeat,orgia,orgiac,orgiacs,orgiasm,orgiast,orgic,orgue,orgy,orgyia,oribi,oriel,oriency,orient,orifice,oriform,origan,origin,orignal,orihon,orillon,oriole,orison,oristic,orle,orlean,orlet,orlo,orlop,ormer,ormolu,orna,ornate,ornery,ornis,ornoite,oroanal,orogen,orogeny,oroide,orology,oronoco,orotund,orphan,orpheon,orpheum,orphrey,orpine,orrery,orrhoid,orris,orsel,orselle,ort,ortalid,ortet,orthal,orthian,orthic,orthid,orthite,ortho,orthose,orthron,ortiga,ortive,ortolan,ortygan,ory,oryssid,os,osamin,osamine,osazone,oscella,oscheal,oscin,oscine,oscnode,oscular,oscule,osculum,ose,osela,oshac,oside,osier,osiered,osiery,osmate,osmatic,osmesis,osmetic,osmic,osmin,osmina,osmious,osmium,osmose,osmosis,osmotic,osmous,osmund,osone,osophy,osprey,ossal,osse,ossein,osselet,osseous,ossicle,ossific,ossify,ossuary,osteal,ostein,ostemia,ostent,osteoid,osteoma,ostial,ostiary,ostiate,ostiole,ostitis,ostium,ostmark,ostosis,ostrich,otalgia,otalgic,otalgy,otarian,otarine,otary,otate,other,othmany,otiant,otiatry,otic,otidine,otidium,otiose,otitic,otitis,otkon,otocyst,otolite,otolith,otology,otosis,ototomy,ottar,otter,otterer,otto,oturia,ouabain,ouabaio,ouabe,ouakari,ouch,ouenite,ouf,ough,ought,oughtnt,oukia,oulap,ounce,ounds,ouphe,ouphish,our,ourie,ouroub,ours,ourself,oust,ouster,out,outact,outage,outarde,outask,outawe,outback,outbake,outban,outbar,outbark,outbawl,outbeam,outbear,outbeg,outbent,outbid,outblot,outblow,outbond,outbook,outborn,outbow,outbowl,outbox,outbrag,outbray,outbred,outbud,outbulk,outburn,outbuy,outbuzz,outby,outcant,outcase,outcast,outcity,outcome,outcrop,outcrow,outcry,outcull,outcure,outcut,outdare,outdate,outdo,outdoer,outdoor,outdraw,outdure,outeat,outecho,outed,outedge,outen,outer,outerly,outeye,outeyed,outface,outfall,outfame,outfast,outfawn,outfeat,outfish,outfit,outflow,outflue,outflux,outfly,outfold,outfool,outfoot,outform,outfort,outgain,outgame,outgang,outgas,outgate,outgaze,outgive,outglad,outglow,outgnaw,outgo,outgoer,outgone,outgrin,outgrow,outgun,outgush,outhaul,outhear,outheel,outher,outhire,outhiss,outhit,outhold,outhowl,outhue,outhunt,outhurl,outhut,outhymn,outing,outish,outjazz,outjest,outjet,outjinx,outjump,outjut,outkick,outkill,outking,outkiss,outknee,outlaid,outland,outlash,outlast,outlaw,outlay,outlean,outleap,outler,outlet,outlie,outlier,outlimb,outlimn,outline,outlip,outlive,outlook,outlord,outlove,outlung,outly,outman,outmate,outmode,outmost,outmove,outname,outness,outnook,outoven,outpace,outpage,outpart,outpass,outpath,outpay,outpeal,outpeep,outpeer,outpick,outpipe,outpity,outplan,outplay,outplod,outplot,outpoll,outpomp,outpop,outport,outpost,outpour,outpray,outpry,outpull,outpurl,outpush,output,outrace,outrage,outrail,outrank,outrant,outrap,outrate,outrave,outray,outre,outread,outrede,outrick,outride,outrig,outring,outroar,outroll,outroot,outrove,outrow,outrun,outrush,outsail,outsay,outsea,outseam,outsee,outseek,outsell,outsert,outset,outshot,outshow,outshut,outside,outsift,outsigh,outsin,outsing,outsit,outsize,outskip,outsoar,outsole,outspan,outspin,outspit,outspue,outstay,outstep,outsuck,outsulk,outsum,outswim,outtalk,outtask,outtear,outtell,outtire,outtoil,outtop,outtrot,outturn,outvie,outvier,outvote,outwait,outwake,outwale,outwalk,outwall,outwar,outward,outwash,outwave,outwear,outweed,outweep,outwell,outwent,outwick,outwile,outwill,outwind,outwing,outwish,outwit,outwith,outwoe,outwood,outword,outwore,outwork,outworn,outyard,outyell,outyelp,outzany,ouzel,ova,oval,ovalish,ovalize,ovally,ovaloid,ovant,ovarial,ovarian,ovarin,ovarium,ovary,ovate,ovated,ovately,ovation,oven,ovenful,ovenly,ovenman,over,overact,overage,overall,overapt,overarm,overawe,overawn,overbet,overbid,overbig,overbit,overbow,overbuy,overby,overcap,overcow,overcoy,overcry,overcup,overcut,overdo,overdry,overdue,overdye,overeat,overegg,overeye,overfag,overfar,overfat,overfed,overfee,overfew,overfit,overfix,overfly,overget,overgo,overgod,overgun,overhit,overhot,overink,overjob,overjoy,overlap,overlax,overlay,overleg,overlie,overlip,overlow,overly,overman,overmix,overnet,overnew,overpay,overpet,overply,overpot,overrim,overrun,oversad,oversea,oversee,overset,oversew,oversot,oversow,overt,overtax,overtip,overtly,overtoe,overtop,overuse,overway,overweb,overwet,overwin,ovest,ovey,ovicell,ovicide,ovicyst,oviduct,oviform,ovigerm,ovile,ovine,ovinia,ovipara,ovisac,ovism,ovist,ovistic,ovocyte,ovoid,ovoidal,ovolo,ovology,ovular,ovulary,ovulate,ovule,ovulist,ovum,ow,owd,owe,owelty,ower,owerby,owght,owing,owk,owl,owldom,owler,owlery,owlet,owlhead,owling,owlish,owlism,owllike,owly,own,owner,ownhood,ownness,ownself,owrehip,owrelay,owse,owsen,owser,owtchah,ox,oxacid,oxalan,oxalate,oxalic,oxalite,oxalyl,oxamate,oxamic,oxamid,oxamide,oxan,oxanate,oxane,oxanic,oxazine,oxazole,oxbane,oxberry,oxbird,oxbiter,oxblood,oxbow,oxboy,oxbrake,oxcart,oxcheek,oxea,oxeate,oxen,oxeote,oxer,oxetone,oxeye,oxfly,oxgang,oxgoad,oxhead,oxheal,oxheart,oxhide,oxhoft,oxhorn,oxhouse,oxhuvud,oxidant,oxidase,oxidate,oxide,oxidic,oxidize,oximate,oxime,oxland,oxlike,oxlip,oxman,oxonic,oxonium,oxozone,oxphony,oxreim,oxshoe,oxskin,oxtail,oxter,oxwort,oxy,oxyacid,oxygas,oxygen,oxyl,oxymel,oxyntic,oxyopia,oxysalt,oxytone,oyapock,oyer,oyster,ozena,ozonate,ozone,ozoned,ozonic,ozonide,ozonify,ozonize,ozonous,ozophen,ozotype,p,pa,paal,paar,paauw,pabble,pablo,pabouch,pabular,pabulum,pac,paca,pacable,pacate,pacay,pacaya,pace,paced,pacer,pachak,pachisi,pacific,pacify,pack,package,packer,packery,packet,packly,packman,packway,paco,pact,paction,pad,padder,padding,paddle,paddled,paddler,paddock,paddy,padella,padfoot,padge,padle,padlike,padlock,padnag,padre,padtree,paean,paegel,paegle,paenula,paeon,paeonic,paga,pagan,paganic,paganly,paganry,page,pageant,pagedom,pageful,pager,pagina,paginal,pagoda,pagrus,pagurid,pagus,pah,paha,pahi,pahlavi,pahmi,paho,pahutan,paigle,paik,pail,pailful,pailou,pain,pained,painful,paining,paint,painted,painter,painty,paip,pair,paired,pairer,pais,paisa,paiwari,pajama,pajock,pakchoi,pakeha,paktong,pal,palace,palaced,paladin,palaite,palama,palame,palanka,palar,palas,palatal,palate,palated,palatic,palaver,palay,palazzi,palch,pale,palea,paleate,paled,palely,paleola,paler,palet,paletot,palette,paletz,palfrey,palgat,pali,palikar,palila,palinal,paling,palisfy,palish,palkee,pall,palla,pallae,pallah,pallall,palled,pallet,palli,pallial,pallid,pallion,pallium,pallone,pallor,pally,palm,palma,palmad,palmar,palmary,palmate,palmed,palmer,palmery,palmful,palmist,palmite,palmito,palmo,palmula,palmus,palmy,palmyra,palolo,palp,palpal,palpate,palped,palpi,palpon,palpus,palsied,palster,palsy,palt,palter,paltry,paludal,paludic,palule,palulus,palus,paly,pam,pament,pamment,pampas,pampean,pamper,pampero,pampre,pan,panace,panacea,panache,panada,panade,panama,panaris,panary,panax,pancake,pand,panda,pandal,pandan,pandect,pandemy,pander,pandita,pandle,pandora,pandour,pandrop,pandura,pandy,pane,paned,paneity,panel,panela,paneler,panfil,panfish,panful,pang,pangamy,pangane,pangen,pangene,pangful,pangi,panhead,panic,panical,panicky,panicle,panisc,panisca,panisic,pank,pankin,panman,panmixy,panmug,pannade,pannage,pannam,panne,pannel,panner,pannery,pannier,panning,pannose,pannum,pannus,panocha,panoche,panoply,panoram,panse,panside,pansied,pansy,pant,pantas,panter,panther,pantie,panties,pantile,panting,pantle,pantler,panto,pantod,panton,pantoon,pantoum,pantry,pants,pantun,panty,panung,panurgy,panyar,paolo,paon,pap,papa,papable,papabot,papacy,papain,papal,papally,papalty,papane,papaw,papaya,papboat,pape,paper,papered,paperer,papern,papery,papess,papey,papilla,papion,papish,papism,papist,papize,papless,papmeat,papoose,pappi,pappose,pappox,pappus,pappy,papreg,paprica,paprika,papula,papular,papule,papyr,papyral,papyri,papyrin,papyrus,paquet,par,para,parable,paracme,parade,parader,parado,parados,paradox,parafle,parage,paragon,parah,paraiba,parale,param,paramo,parang,parao,parapet,paraph,parapod,pararek,parasol,paraspy,parate,paraxon,parbake,parboil,parcel,parch,parcher,parchy,parcook,pard,pardao,parded,pardesi,pardine,pardner,pardo,pardon,pare,parel,parella,paren,parent,parer,paresis,paretic,parfait,pargana,parge,parget,pargo,pari,pariah,parial,parian,paries,parify,parilla,parine,paring,parish,parisis,parison,parity,park,parka,parkee,parker,parkin,parking,parkish,parkway,parky,parlay,parle,parley,parling,parlish,parlor,parlous,parly,parma,parmak,parnas,parnel,paroch,parode,parodic,parodos,parody,paroecy,parol,parole,parolee,paroli,paronym,parotic,parotid,parotis,parous,parpal,parquet,parr,parrel,parrier,parrock,parrot,parroty,parry,parse,parsec,parser,parsley,parsnip,parson,parsony,part,partake,partan,parted,parter,partial,partile,partite,partlet,partly,partner,parto,partook,parture,party,parulis,parure,paruria,parvenu,parvis,parvule,pasan,pasang,paschal,pascual,pash,pasha,pashm,pasi,pasmo,pasquil,pasquin,pass,passade,passado,passage,passant,passe,passee,passen,passer,passewa,passing,passion,passir,passive,passkey,passman,passo,passout,passus,passway,past,paste,pasted,pastel,paster,pastern,pasteur,pastil,pastile,pastime,pasting,pastor,pastose,pastry,pasture,pasty,pasul,pat,pata,pataca,patacao,pataco,patagon,pataka,patamar,patao,patapat,pataque,patas,patball,patch,patcher,patchy,pate,patefy,patel,patella,paten,patency,patener,patent,pater,patera,patesi,path,pathed,pathema,pathic,pathlet,pathos,pathway,pathy,patible,patient,patina,patine,patined,patio,patly,patness,pato,patois,patola,patonce,patria,patrial,patrice,patrico,patrin,patriot,patrist,patrix,patrol,patron,patroon,patta,patte,pattee,patten,patter,pattern,pattu,patty,patu,patwari,paty,pau,paucify,paucity,paughty,paukpan,paular,paulie,paulin,paunch,paunchy,paup,pauper,pausal,pause,pauser,paussid,paut,pauxi,pavage,pavan,pavane,pave,paver,pavid,pavier,paving,pavior,paviour,pavis,paviser,pavisor,pavy,paw,pawdite,pawer,pawing,pawk,pawkery,pawkily,pawkrie,pawky,pawl,pawn,pawnage,pawnee,pawner,pawnie,pawnor,pawpaw,pax,paxilla,paxiuba,paxwax,pay,payable,payably,payday,payed,payee,payeny,payer,paying,payment,paynim,payoff,payong,payor,payroll,pea,peace,peach,peachen,peacher,peachy,peacoat,peacock,peacod,peafowl,peag,peage,peahen,peai,peaiism,peak,peaked,peaker,peakily,peaking,peakish,peaky,peal,pealike,pean,peanut,pear,pearl,pearled,pearler,pearlet,pearlin,pearly,peart,pearten,peartly,peasant,peasen,peason,peasy,peat,peatery,peatman,peaty,peavey,peavy,peba,pebble,pebbled,pebbly,pebrine,pecan,peccant,peccary,peccavi,pech,pecht,pecite,peck,pecked,pecker,pecket,peckful,peckish,peckle,peckled,peckly,pecky,pectase,pectate,pecten,pectic,pectin,pectize,pectora,pectose,pectous,pectus,ped,peda,pedage,pedagog,pedal,pedaler,pedant,pedary,pedate,pedated,pedder,peddle,peddler,pedee,pedes,pedesis,pedicab,pedicel,pedicle,pedion,pedlar,pedlary,pedocal,pedrail,pedrero,pedro,pedule,pedum,pee,peed,peek,peel,peele,peeled,peeler,peeling,peelman,peen,peenge,peeoy,peep,peeper,peepeye,peepy,peer,peerage,peerdom,peeress,peerie,peerly,peery,peesash,peeve,peeved,peever,peevish,peewee,peg,pega,pegall,pegasid,pegbox,pegged,pegger,pegging,peggle,peggy,pegless,peglet,peglike,pegman,pegwood,peho,peine,peisage,peise,peiser,peixere,pekan,pekin,pekoe,peladic,pelage,pelagic,pelamyd,pelanos,pelean,pelecan,pelf,pelican,pelick,pelike,peliom,pelioma,pelisse,pelite,pelitic,pell,pellage,pellar,pellard,pellas,pellate,peller,pellet,pellety,pellile,pellock,pelmet,pelon,peloria,peloric,pelorus,pelota,peloton,pelt,pelta,peltast,peltate,pelter,pelting,peltry,pelu,peludo,pelves,pelvic,pelvis,pembina,pemican,pen,penal,penally,penalty,penance,penang,penates,penbard,pence,pencel,pencil,pend,penda,pendant,pendent,pending,pendle,pendom,pendule,penfold,penful,pengo,penguin,penhead,penial,penide,penile,penis,penk,penlike,penman,penna,pennae,pennage,pennant,pennate,penner,pennet,penni,pennia,pennied,pennill,penning,pennon,penny,penrack,penship,pensile,pension,pensive,penster,pensum,pensy,pent,penta,pentace,pentad,pentail,pentane,pentene,pentine,pentit,pentite,pentode,pentoic,pentol,pentose,pentrit,pentyl,pentyne,penuchi,penult,penury,peon,peonage,peonism,peony,people,peopler,peoplet,peotomy,pep,pepful,pepino,peplos,peplum,peplus,pepo,pepper,peppery,peppily,peppin,peppy,pepsin,pepsis,peptic,peptide,peptize,peptone,per,peracid,peract,perbend,percale,percent,percept,perch,percha,percher,percid,percoct,percoid,percur,percuss,perdu,perdure,pereion,pereira,peres,perfect,perfidy,perform,perfume,perfumy,perfuse,pergola,perhaps,peri,periapt,peridot,perigee,perigon,peril,perine,period,periost,perique,perish,perit,perite,periwig,perjink,perjure,perjury,perk,perkily,perkin,perking,perkish,perky,perle,perlid,perlite,perloir,perm,permit,permute,pern,pernine,pernor,pernyi,peroba,peropod,peropus,peroral,perosis,perotic,peroxy,peroxyl,perpend,perpera,perplex,perrier,perron,perry,persalt,perse,persico,persis,persist,person,persona,pert,pertain,perten,pertish,pertly,perturb,pertuse,perty,peruke,perula,perule,perusal,peruse,peruser,pervade,pervert,pes,pesa,pesade,pesage,peseta,peshkar,peshwa,peskily,pesky,peso,pess,pessary,pest,peste,pester,pestful,pestify,pestle,pet,petal,petaled,petalon,petaly,petard,petary,petasos,petasus,petcock,pete,peteca,peteman,peter,petful,petiole,petit,petite,petitor,petkin,petling,peto,petrary,petre,petrean,petrel,petrie,petrify,petrol,petrosa,petrous,petted,petter,pettily,pettish,pettle,petty,petune,petwood,petzite,peuhl,pew,pewage,pewdom,pewee,pewful,pewing,pewit,pewless,pewmate,pewter,pewtery,pewy,peyote,peyotl,peyton,peytrel,pfennig,pfui,pfund,phacoid,phaeism,phaeton,phage,phalanx,phalera,phallic,phallin,phallus,phanic,phano,phantom,phare,pharmic,pharos,pharynx,phase,phaseal,phasemy,phases,phasic,phasis,phasm,phasma,phasmid,pheal,phellem,phemic,phenate,phene,phenene,phenic,phenin,phenol,phenyl,pheon,phew,phi,phial,phiale,philter,philtra,phit,phiz,phizes,phizog,phlegm,phlegma,phlegmy,phloem,phloxin,pho,phobiac,phobic,phobism,phobist,phoby,phoca,phocal,phocid,phocine,phocoid,phoebe,phoenix,phoh,pholad,pholcid,pholido,phon,phonal,phonate,phone,phoneme,phonic,phonics,phonism,phono,phony,phoo,phoresy,phoria,phorid,phorone,phos,phose,phosis,phospho,phossy,phot,photal,photic,photics,photism,photo,photoma,photon,phragma,phrasal,phrase,phraser,phrasy,phrator,phratry,phrenic,phrynid,phrynin,phthor,phu,phugoid,phulwa,phut,phycite,phyla,phyle,phylic,phyllin,phylon,phylum,phyma,phymata,physic,physics,phytase,phytic,phytin,phytoid,phytol,phytoma,phytome,phyton,phytyl,pi,pia,piaba,piacaba,piacle,piaffe,piaffer,pial,pialyn,pian,pianic,pianino,pianism,pianist,piannet,piano,pianola,piaster,piastre,piation,piazine,piazza,pibcorn,pibroch,pic,pica,picador,pical,picamar,picara,picarel,picaro,picary,piccolo,pice,picene,piceous,pichi,picine,pick,pickage,pickax,picked,pickee,pickeer,picker,pickery,picket,pickle,pickler,pickman,pickmaw,pickup,picky,picnic,pico,picoid,picot,picotah,picotee,picra,picrate,picric,picrite,picrol,picryl,pict,picture,pictury,picuda,picudo,picul,piculet,pidan,piddle,piddler,piddock,pidgin,pie,piebald,piece,piecen,piecer,piecing,pied,piedly,pieless,pielet,pielum,piemag,pieman,pien,piend,piepan,pier,pierage,pierce,pierced,piercel,piercer,pierid,pierine,pierrot,pieshop,piet,pietas,pietic,pietism,pietist,pietose,piety,piewife,piewipe,piezo,piff,piffle,piffler,pifine,pig,pigdan,pigdom,pigeon,pigface,pigfish,pigfoot,pigful,piggery,piggin,pigging,piggish,piggle,piggy,pighead,pigherd,pightle,pigless,piglet,pigling,pigly,pigman,pigment,pignon,pignus,pignut,pigpen,pigroot,pigskin,pigsney,pigsty,pigtail,pigwash,pigweed,pigyard,piitis,pik,pika,pike,piked,pikel,pikelet,pikeman,piker,pikey,piki,piking,pikle,piky,pilage,pilapil,pilar,pilary,pilau,pilaued,pilch,pilcher,pilcorn,pilcrow,pile,pileata,pileate,piled,pileous,piler,piles,pileus,pilfer,pilger,pilgrim,pili,pilifer,piligan,pilikai,pilin,piline,piling,pilkins,pill,pillage,pillar,pillary,pillas,pillbox,pilled,pillet,pilleus,pillion,pillory,pillow,pillowy,pilm,pilmy,pilon,pilori,pilose,pilosis,pilot,pilotee,pilotry,pilous,pilpul,piltock,pilula,pilular,pilule,pilum,pilus,pily,pimaric,pimelic,pimento,pimlico,pimola,pimp,pimpery,pimping,pimpish,pimple,pimpled,pimplo,pimploe,pimply,pin,pina,pinaces,pinacle,pinacol,pinang,pinax,pinball,pinbone,pinbush,pincase,pincer,pincers,pinch,pinche,pinched,pinchem,pincher,pind,pinda,pinder,pindy,pine,pineal,pined,pinene,piner,pinery,pinesap,pinetum,piney,pinfall,pinfish,pinfold,ping,pingle,pingler,pingue,pinguid,pinguin,pinhead,pinhold,pinhole,pinhook,pinic,pining,pinion,pinite,pinitol,pinjane,pinjra,pink,pinked,pinkeen,pinken,pinker,pinkeye,pinkie,pinkify,pinkily,pinking,pinkish,pinkly,pinky,pinless,pinlock,pinna,pinnace,pinnae,pinnal,pinnate,pinned,pinnel,pinner,pinnet,pinning,pinnock,pinnula,pinnule,pinny,pino,pinole,pinolia,pinolin,pinon,pinonic,pinrail,pinsons,pint,pinta,pintado,pintail,pintano,pinte,pintle,pinto,pintura,pinulus,pinweed,pinwing,pinwork,pinworm,piny,pinyl,pinyon,pioneer,pioted,piotine,piotty,pioury,pious,piously,pip,pipa,pipage,pipal,pipe,pipeage,piped,pipeful,pipeman,piper,piperic,piperly,piperno,pipery,pipet,pipette,pipi,piping,pipiri,pipit,pipkin,pipless,pipped,pipper,pippin,pippy,piprine,piproid,pipy,piquant,pique,piquet,piquia,piqure,pir,piracy,piragua,piranha,pirate,piraty,pirl,pirn,pirner,pirnie,pirny,pirogue,pirol,pirr,pirrmaw,pisaca,pisang,pisay,piscary,piscian,piscina,piscine,pisco,pise,pish,pishaug,pishu,pisk,pisky,pismire,piso,piss,pissant,pist,pistic,pistil,pistle,pistol,pistole,piston,pistrix,pit,pita,pitanga,pitapat,pitarah,pitau,pitaya,pitch,pitcher,pitchi,pitchy,piteous,pitfall,pith,pithful,pithily,pithole,pithos,pithy,pitier,pitiful,pitless,pitlike,pitman,pitmark,pitmirk,pitpan,pitpit,pitside,pitted,pitter,pittine,pitting,pittite,pittoid,pituite,pituri,pitwood,pitwork,pity,pitying,piuri,pivalic,pivot,pivotal,pivoter,pix,pixie,pixy,pize,pizza,pizzle,placard,placate,place,placebo,placer,placet,placid,plack,placket,placode,placoid,placula,plaga,plagal,plagate,plage,plagium,plagose,plague,plagued,plaguer,plaguy,plaice,plaid,plaided,plaidie,plaidy,plain,plainer,plainly,plaint,plait,plaited,plaiter,plak,plakat,plan,planaea,planar,planate,planch,plandok,plane,planer,planet,planeta,planful,plang,plangor,planish,planity,plank,planker,planky,planner,plant,planta,plantad,plantal,plantar,planter,planula,planury,planxty,plap,plaque,plash,plasher,plashet,plashy,plasm,plasma,plasmic,plasome,plass,plasson,plaster,plastic,plastid,plastin,plat,platan,platane,platano,platch,plate,platea,plateau,plated,platen,plater,platery,platic,platina,plating,platode,platoid,platoon,platted,platten,platter,platty,platy,plaud,plaudit,play,playa,playbox,playboy,playday,player,playful,playlet,playman,playock,playpen,plaza,plea,pleach,plead,pleader,please,pleaser,pleat,pleater,pleb,plebe,plebify,plebs,pleck,plectre,pled,pledge,pledgee,pledger,pledget,pledgor,pleion,plenary,plenipo,plenish,plenism,plenist,plenty,plenum,pleny,pleon,pleonal,pleonic,pleopod,pleroma,plerome,plessor,pleura,pleural,pleuric,pleuron,pleurum,plew,plex,plexal,plexor,plexure,plexus,pliable,pliably,pliancy,pliant,plica,plical,plicate,plied,plier,plies,pliers,plight,plim,plinth,pliskie,plisky,ploat,ploce,plock,plod,plodder,plodge,plomb,plook,plop,plosion,plosive,plot,plote,plotful,plotted,plotter,plotty,plough,plouk,plouked,plouky,plounce,plout,plouter,plover,plovery,plow,plowboy,plower,plowing,plowman,ploy,pluck,plucked,plucker,plucky,plud,pluff,pluffer,pluffy,plug,plugged,plugger,pluggy,plugman,plum,pluma,plumach,plumade,plumage,plumate,plumb,plumber,plumbet,plumbic,plumbog,plumbum,plumcot,plume,plumed,plumer,plumery,plumet,plumier,plumify,plumist,plumlet,plummer,plummet,plummy,plumose,plumous,plump,plumpen,plumper,plumply,plumps,plumpy,plumula,plumule,plumy,plunder,plunge,plunger,plunk,plup,plural,pluries,plurify,plus,plush,plushed,plushy,pluteal,plutean,pluteus,pluvial,pluvian,pluvine,ply,plyer,plying,plywood,pneuma,po,poach,poacher,poachy,poalike,pob,pobby,pobs,pochade,pochard,pochay,poche,pock,pocket,pockety,pockily,pocky,poco,pocosin,pod,podagra,podal,podalic,podatus,podded,podder,poddish,poddle,poddy,podeon,podesta,podex,podge,podger,podgily,podgy,podial,podical,podices,podite,poditic,poditti,podium,podler,podley,podlike,podogyn,podsol,poduran,podurid,podware,podzol,poe,poem,poemet,poemlet,poesie,poesis,poesy,poet,poetdom,poetess,poetic,poetics,poetito,poetize,poetly,poetry,pogge,poggy,pogonip,pogrom,pogy,poh,poha,pohna,poi,poietic,poignet,poil,poilu,poind,poinder,point,pointed,pointel,pointer,pointy,poise,poised,poiser,poison,poitrel,pokable,poke,poked,pokeful,pokeout,poker,pokey,pokily,poking,pokomoo,pokunt,poky,pol,polacca,polack,polacre,polar,polaric,polarly,polaxis,poldavy,polder,pole,polearm,poleax,poleaxe,polecat,poleman,polemic,polenta,poler,poley,poliad,police,policed,policy,poligar,polio,polis,polish,polite,politic,polity,polk,polka,poll,pollack,polladz,pollage,pollam,pollan,pollard,polled,pollen,pollent,poller,pollex,polling,pollock,polloi,pollute,pollux,polo,poloist,polony,polos,polska,polt,poltina,poly,polyact,polyad,polygam,polygon,polygyn,polymer,polyose,polyp,polyped,polypi,polypod,polypus,pom,pomace,pomade,pomane,pomate,pomato,pomatum,pombe,pombo,pome,pomelo,pomey,pomfret,pomme,pommee,pommel,pommet,pommey,pommy,pomonal,pomonic,pomp,pompa,pompal,pompano,pompey,pomphus,pompier,pompion,pompist,pompon,pompous,pomster,pon,ponce,ponceau,poncho,pond,pondage,ponder,pondful,pondlet,pondman,pondok,pondus,pondy,pone,ponent,ponerid,poney,pong,ponga,pongee,poniard,ponica,ponier,ponja,pont,pontage,pontal,pontee,pontes,pontic,pontiff,pontify,pontil,pontile,pontin,pontine,pontist,ponto,ponton,pontoon,pony,ponzite,pooa,pooch,pooder,poodle,poof,poogye,pooh,pook,pooka,pookaun,pookoo,pool,pooler,pooli,pooly,poon,poonac,poonga,poop,pooped,poor,poorish,poorly,poot,pop,popadam,popal,popcorn,popdock,pope,popedom,popeism,popeler,popely,popery,popess,popeye,popeyed,popgun,popify,popinac,popish,popjoy,poplar,poplin,popover,poppa,poppean,poppel,popper,poppet,poppied,poppin,popple,popply,poppy,popshop,popular,populin,popweed,poral,porcate,porch,porched,porcine,pore,pored,porer,porge,porger,porgy,poring,porism,porite,pork,porker,porkery,porket,porkish,porkman,porkpie,porky,porogam,poroma,poros,porose,porosis,porotic,porous,porr,porrect,porret,porrigo,porry,port,porta,portage,portail,portal,portass,ported,portend,portent,porter,portia,portico,portify,portio,portion,portlet,portly,portman,porto,portray,portway,porty,porule,porus,pory,posca,pose,poser,poseur,posey,posh,posing,posit,positor,positum,posnet,posole,poss,posse,possess,posset,possum,post,postage,postal,postbag,postbox,postboy,posted,posteen,poster,postern,postfix,postic,postil,posting,postman,posture,postwar,posy,pot,potable,potamic,potash,potass,potassa,potate,potato,potator,potbank,potboil,potboy,potch,potcher,potdar,pote,poteen,potence,potency,potent,poter,poteye,potful,potgirl,potgun,pothead,potheen,pother,potherb,pothery,pothole,pothook,pothunt,potifer,potion,potleg,potlid,potlike,potluck,potman,potong,potoo,potoroo,potpie,potrack,pott,pottage,pottagy,pottah,potted,potter,pottery,potting,pottle,pottled,potto,potty,potware,potwork,potwort,pouce,poucer,poucey,pouch,pouched,pouchy,pouf,poulard,poulp,poulpe,poult,poulter,poultry,pounamu,pounce,pounced,pouncer,pouncet,pound,poundal,pounder,pour,pourer,pourie,pouring,pouser,pout,pouter,poutful,pouting,pouty,poverty,pow,powder,powdery,powdike,powdry,power,powered,powitch,pownie,powwow,pox,poxy,poy,poyou,praam,prabble,prabhu,practic,prad,praecox,praetor,prairie,praise,praiser,prajna,praline,pram,prana,prance,prancer,prancy,prank,pranked,pranker,prankle,pranky,prase,prasine,prasoid,prastha,prat,pratal,prate,prater,pratey,prating,prattle,prattly,prau,pravity,prawn,prawner,prawny,praxis,pray,praya,prayer,prayful,praying,preach,preachy,preacid,preact,preaged,preally,preanal,prearm,preaver,prebake,prebend,prebid,prebill,preboil,preborn,preburn,precant,precary,precast,precava,precede,precent,precept,preces,precess,precipe,precis,precise,precite,precoil,precook,precool,precopy,precox,precure,precut,precyst,predamn,predark,predata,predate,predawn,preday,predefy,predeny,predial,predict,prediet,predine,predoom,predraw,predry,predusk,preen,preener,preeze,prefab,preface,prefect,prefer,prefine,prefix,prefool,preform,pregain,pregust,prehaps,preheal,preheat,prehend,preidea,preknit,preknow,prelacy,prelate,prelect,prelim,preloan,preloss,prelude,premake,premate,premial,premier,premise,premiss,premium,premix,premold,premove,prename,prender,prendre,preomit,preopen,preoral,prep,prepare,prepave,prepay,prepink,preplan,preplot,prepose,prepuce,prepupa,prerent,prerich,prerupt,presage,presay,preseal,presee,presell,present,preses,preset,preship,preshow,preside,presift,presign,prespur,press,pressel,presser,pressor,prest,prester,presto,presume,pretan,pretell,pretend,pretest,pretext,pretire,pretone,pretry,pretty,pretzel,prevail,prevene,prevent,preverb,preveto,previde,preview,previse,prevoid,prevote,prevue,prewar,prewarn,prewash,prewhip,prewire,prewrap,prexy,prey,preyer,preyful,prezone,price,priced,pricer,prich,prick,pricked,pricker,pricket,prickle,prickly,pricks,pricky,pride,pridian,priding,pridy,pried,prier,priest,prig,prigdom,prigger,prigman,prill,prim,prima,primacy,primage,primal,primar,primary,primate,prime,primely,primer,primero,primine,priming,primly,primost,primp,primsie,primula,primus,primy,prince,princox,prine,pringle,prink,prinker,prinkle,prinky,print,printed,printer,prion,prionid,prior,prioral,priorly,priory,prisage,prisal,priscan,prism,prismal,prismed,prismy,prison,priss,prissy,pritch,prithee,prius,privacy,privant,private,privet,privily,privity,privy,prize,prizer,prizery,pro,proa,proal,proarmy,prob,probabl,probal,probang,probant,probate,probe,probeer,prober,probity,problem,procarp,proceed,process,proctal,proctor,procure,prod,prodder,proddle,prodigy,produce,product,proem,proetid,prof,profane,profert,profess,proffer,profile,profit,profuse,prog,progeny,progger,progne,program,project,proke,proker,prolan,prolate,proleg,prolify,proline,prolix,prolong,prolyl,promic,promise,promote,prompt,pronaos,pronate,pronavy,prone,pronely,proneur,prong,pronged,pronger,pronic,pronoun,pronpl,pronto,pronuba,proo,proof,proofer,proofy,prop,propago,propale,propane,propend,propene,proper,prophet,propine,proplex,propone,propons,propose,propoxy,propper,props,propupa,propyl,propyne,prorata,prorate,prore,prorean,prorsad,prorsal,prosaic,prosar,prose,prosect,proser,prosify,prosily,prosing,prosish,prosist,proso,prosode,prosody,prosoma,prosper,pross,prossy,prosy,protax,prote,protea,protead,protean,protect,protege,proteic,protein,protend,protest,protext,prothyl,protide,protist,protium,proto,protoma,protome,proton,protone,protore,protyl,protyle,protype,proudly,provand,provant,prove,provect,proved,proven,prover,proverb,provide,provine,proving,proviso,provoke,provost,prow,prowar,prowed,prowess,prowl,prowler,proxeny,proximo,proxy,proxysm,prozone,prude,prudely,prudent,prudery,prudish,prudist,prudity,pruh,prunase,prune,prunell,pruner,pruning,prunt,prunted,prurigo,prussic,prut,prutah,pry,pryer,prying,pryler,pryse,prytany,psalis,psalm,psalmic,psalmy,psaloid,psalter,psaltes,pschent,pseudo,psha,pshaw,psi,psiloi,psoadic,psoas,psoatic,psocid,psocine,psoitis,psora,psoric,psoroid,psorous,pst,psych,psychal,psyche,psychic,psychid,psychon,psykter,psylla,psyllid,ptarmic,ptereal,pteric,pterion,pteroid,pteroma,pteryla,ptinid,ptinoid,ptisan,ptomain,ptosis,ptotic,ptyalin,ptyxis,pu,pua,puan,pub,pubal,pubble,puberal,puberty,pubes,pubian,pubic,pubis,public,publish,puccoon,puce,pucelle,puchero,puck,pucka,pucker,puckery,puckish,puckle,puckrel,pud,puddee,pudder,pudding,puddle,puddled,puddler,puddly,puddock,puddy,pudency,pudenda,pudent,pudge,pudgily,pudgy,pudiano,pudic,pudical,pudsey,pudsy,pudu,pueblo,puerer,puerile,puerman,puff,puffed,puffer,puffery,puffily,puffin,puffing,pufflet,puffwig,puffy,pug,pugged,pugger,puggi,pugging,puggish,puggle,puggree,puggy,pugh,pugil,pugman,pugmill,puisne,puist,puistie,puja,puka,pukatea,puke,pukeko,puker,pukish,pukras,puku,puky,pul,pulahan,pulasan,pule,pulegol,puler,puli,pulicat,pulicid,puling,pulish,pulk,pulka,pull,pulldoo,pullen,puller,pullery,pullet,pulley,pulli,pullus,pulp,pulpal,pulper,pulpify,pulpily,pulpit,pulpous,pulpy,pulque,pulsant,pulsate,pulse,pulsion,pulsive,pulton,pulu,pulvic,pulvil,pulvino,pulwar,puly,puma,pumice,pumiced,pumicer,pummel,pummice,pump,pumpage,pumper,pumpkin,pumple,pumpman,pun,puna,punaise,punalua,punatoo,punch,puncher,punchy,punct,punctal,punctum,pundit,pundita,pundum,puneca,pung,punga,pungar,pungent,punger,pungey,pungi,pungle,pungled,punicin,punily,punish,punjum,punk,punkah,punkie,punky,punless,punlet,punnage,punner,punnet,punnic,punster,punt,punta,puntal,puntel,punter,punti,puntil,puntist,punto,puntout,punty,puny,punyish,punyism,pup,pupa,pupal,pupate,pupelo,pupil,pupilar,pupiled,pupoid,puppet,puppify,puppily,puppy,pupulo,pupunha,pur,purana,puranic,puraque,purdah,purdy,pure,pured,puree,purely,purer,purfle,purfled,purfler,purfly,purga,purge,purger,purgery,purging,purify,purine,puriri,purism,purist,purity,purl,purler,purlieu,purlin,purlman,purloin,purpart,purple,purply,purport,purpose,purpura,purpure,purr,purre,purree,purreic,purrel,purrer,purring,purrone,purry,purse,pursed,purser,pursily,purslet,pursley,pursual,pursue,pursuer,pursuit,pursy,purusha,purvey,purview,purvoe,pus,push,pusher,pushful,pushing,pushpin,puss,pusscat,pussley,pussy,pustule,put,putage,putamen,putback,putchen,putcher,puteal,putelee,puther,puthery,putid,putidly,putlog,putois,putrefy,putrid,putt,puttee,putter,puttier,puttock,putty,puture,puxy,puzzle,puzzled,puzzler,pya,pyal,pyche,pycnia,pycnial,pycnid,pycnite,pycnium,pyelic,pyemia,pyemic,pygal,pygarg,pygidid,pygmoid,pygmy,pygofer,pygopod,pyic,pyin,pyjama,pyke,pyknic,pyla,pylar,pylic,pylon,pyloric,pylorus,pyocele,pyocyst,pyocyte,pyoid,pyosis,pyr,pyral,pyralid,pyralis,pyramid,pyran,pyranyl,pyre,pyrena,pyrene,pyrenic,pyrenin,pyretic,pyrex,pyrexia,pyrexic,pyrgom,pyridic,pyridyl,pyrite,pyrites,pyritic,pyro,pyrogen,pyroid,pyrone,pyrope,pyropen,pyropus,pyrosis,pyrotic,pyrrhic,pyrrol,pyrrole,pyrroyl,pyrryl,pyruvic,pyruvil,pyruvyl,python,pyuria,pyvuril,pyx,pyxides,pyxie,pyxis,q,qasida,qere,qeri,qintar,qoph,qua,quab,quabird,quachil,quack,quackle,quacky,quad,quadded,quaddle,quadra,quadral,quadrat,quadric,quadrum,quaedam,quaff,quaffer,quag,quagga,quaggle,quaggy,quahog,quail,quaily,quaint,quake,quaker,quaking,quaky,quale,qualify,quality,qualm,qualmy,quan,quandy,quannet,quant,quanta,quantic,quantum,quar,quare,quark,quarl,quarle,quarred,quarrel,quarry,quart,quartan,quarter,quartet,quartic,quarto,quartz,quartzy,quash,quashey,quashy,quasi,quasky,quassin,quat,quata,quatch,quatern,quaters,quatral,quatre,quatrin,quattie,quatuor,quauk,quave,quaver,quavery,quaw,quawk,quay,quayage,quayful,quayman,qubba,queach,queachy,queak,queal,quean,queasom,queasy,quedful,queechy,queen,queenly,queer,queerer,queerly,queery,queest,queet,queeve,quegh,quei,quelch,quell,queller,quemado,queme,quemely,quench,quercic,quercin,querent,querier,querist,querken,querl,quern,quernal,query,quest,quester,questor,quet,quetch,quetzal,queue,quey,quiapo,quib,quibble,quiblet,quica,quick,quicken,quickie,quickly,quid,quidder,quiddit,quiddle,quiesce,quiet,quieten,quieter,quietly,quietus,quiff,quila,quiles,quilkin,quill,quillai,quilled,quiller,quillet,quilly,quilt,quilted,quilter,quin,quina,quinary,quinate,quince,quinch,quinia,quinic,quinin,quinina,quinine,quinism,quinite,quinize,quink,quinnat,quinnet,quinoa,quinoid,quinol,quinone,quinova,quinoyl,quinse,quinsy,quint,quintad,quintal,quintan,quinte,quintet,quintic,quintin,quinto,quinton,quintus,quinyl,quinze,quip,quipful,quipo,quipper,quippy,quipu,quira,quire,quirk,quirky,quirl,quirt,quis,quisby,quiscos,quisle,quit,quitch,quite,quits,quitted,quitter,quittor,quiver,quivery,quiz,quizzee,quizzer,quizzy,quo,quod,quoin,quoined,quoit,quoiter,quoits,quondam,quoniam,quop,quorum,quot,quota,quote,quotee,quoter,quoth,quotha,quotity,quotum,r,ra,raad,raash,rab,raband,rabanna,rabat,rabatte,rabbet,rabbi,rabbin,rabbit,rabbity,rabble,rabbler,rabboni,rabic,rabid,rabidly,rabies,rabific,rabinet,rabitic,raccoon,raccroc,race,raceme,racemed,racemic,racer,raceway,rach,rache,rachial,rachis,racial,racily,racing,racism,racist,rack,rackan,racker,racket,rackett,rackety,rackful,racking,rackle,rackway,racloir,racon,racoon,racy,rad,rada,radar,raddle,radial,radiale,radian,radiant,radiate,radical,radicel,radices,radicle,radii,radio,radiode,radish,radium,radius,radix,radman,radome,radon,radula,raff,raffe,raffee,raffery,raffia,raffing,raffish,raffle,raffler,raft,raftage,rafter,raftman,rafty,rag,raga,rage,rageful,rageous,rager,ragfish,ragged,raggedy,raggee,ragger,raggery,raggety,raggil,raggily,ragging,raggle,raggled,raggy,raging,raglan,raglet,raglin,ragman,ragout,ragshag,ragtag,ragtime,ragule,raguly,ragweed,ragwort,rah,rahdar,raia,raid,raider,rail,railage,railer,railing,railly,railman,railway,raiment,rain,rainbow,rainer,rainful,rainily,rainy,raioid,rais,raise,raised,raiser,raisin,raising,raisiny,raj,raja,rajah,rakan,rake,rakeage,rakeful,raker,rakery,rakh,raki,rakily,raking,rakish,rakit,raku,rallier,ralline,rally,ralph,ram,ramada,ramage,ramal,ramanas,ramass,ramate,rambeh,ramble,rambler,rambong,rame,rameal,ramed,ramekin,rament,rameous,ramet,ramex,ramhead,ramhood,rami,ramie,ramify,ramlike,ramline,rammack,rammel,rammer,rammish,rammy,ramose,ramous,ramp,rampage,rampant,rampart,ramped,ramper,rampick,rampike,ramping,rampion,rampire,rampler,ramplor,ramrace,ramrod,ramsch,ramson,ramstam,ramtil,ramular,ramule,ramulus,ramus,ran,rana,ranal,rance,rancel,rancer,ranch,ranche,rancher,rancho,rancid,rancor,rand,randan,randem,rander,randing,randir,randle,random,randy,rane,rang,range,ranged,ranger,rangey,ranging,rangle,rangler,rangy,rani,ranid,ranine,rank,ranked,ranker,rankish,rankle,rankly,rann,rannel,ranny,ransack,ransel,ransom,rant,rantan,ranter,ranting,rantock,ranty,ranula,ranular,rap,rape,rapeful,raper,raphany,raphe,raphide,raphis,rapic,rapid,rapidly,rapier,rapillo,rapine,rapiner,raping,rapinic,rapist,raploch,rappage,rappe,rappel,rapper,rapping,rappist,rapport,rapt,raptly,raptor,raptril,rapture,raptury,raptus,rare,rarebit,rarefy,rarely,rarish,rarity,ras,rasa,rasant,rascal,rasceta,rase,rasen,raser,rasgado,rash,rasher,rashful,rashing,rashly,rasion,rasp,rasped,rasper,rasping,raspish,raspite,raspy,rasse,rassle,raster,rastik,rastle,rasure,rat,rata,ratable,ratably,ratafee,ratafia,ratal,ratbite,ratch,ratchel,ratcher,ratchet,rate,rated,ratel,rater,ratfish,rath,rathe,rathed,rathely,rather,rathest,rathite,rathole,ratify,ratine,rating,ratio,ration,ratite,ratlike,ratline,ratoon,rattage,rattail,rattan,ratteen,ratten,ratter,rattery,ratti,rattish,rattle,rattled,rattler,rattles,rattly,ratton,rattrap,ratty,ratwa,ratwood,raucid,raucity,raucous,raught,rauk,raukle,rauli,raun,raunge,raupo,rauque,ravage,ravager,rave,ravel,raveler,ravelin,ravelly,raven,ravener,ravenry,ravens,raver,ravin,ravine,ravined,raviney,raving,ravioli,ravish,ravison,raw,rawhead,rawhide,rawish,rawness,rax,ray,raya,rayage,rayed,rayful,rayless,raylet,rayon,raze,razee,razer,razoo,razor,razz,razzia,razzly,re,rea,reaal,reabuse,reach,reacher,reachy,react,reactor,read,readapt,readd,reader,readily,reading,readmit,readopt,readorn,ready,reagent,reagin,reagree,reak,real,realarm,reales,realest,realgar,realign,realism,realist,reality,realive,realize,reallot,reallow,really,realm,realter,realtor,realty,ream,reamage,reamass,reamend,reamer,reamuse,reamy,reannex,reannoy,reanvil,reap,reaper,reapply,rear,rearer,reargue,rearise,rearm,rearray,reask,reason,reassay,reasty,reasy,reatus,reaudit,reavail,reave,reaver,reavoid,reavow,reawait,reawake,reaward,reaware,reb,rebab,reback,rebag,rebait,rebake,rebale,reban,rebar,rebase,rebasis,rebate,rebater,rebathe,rebato,rebawl,rebear,rebeat,rebec,rebeck,rebed,rebeg,rebeget,rebegin,rebel,rebelly,rebend,rebeset,rebia,rebias,rebid,rebill,rebind,rebirth,rebite,reblade,reblame,reblast,reblend,rebless,reblock,rebloom,reblot,reblow,reblue,rebluff,reboant,reboard,reboast,rebob,reboil,reboise,rebold,rebolt,rebone,rebook,rebop,rebore,reborn,rebound,rebox,rebrace,rebraid,rebrand,rebreed,rebrew,rebribe,rebrick,rebring,rebrown,rebrush,rebud,rebuff,rebuild,rebuilt,rebuke,rebuker,rebulk,rebunch,rebuoy,reburn,reburst,rebury,rebus,rebush,rebusy,rebut,rebute,rebuy,recable,recage,recalk,recall,recant,recap,recarry,recart,recarve,recase,recash,recast,recatch,recce,recco,reccy,recede,receder,receipt,receive,recency,recense,recent,recept,recess,rechafe,rechain,rechal,rechant,rechaos,rechar,rechase,rechaw,recheat,recheck,recheer,rechew,rechip,rechuck,rechurn,recipe,recital,recite,reciter,reck,reckla,reckon,reclaim,reclama,reclang,reclasp,reclass,reclean,reclear,reclimb,recline,reclose,recluse,recoach,recoal,recoast,recoat,recock,recoct,recode,recoil,recoin,recoke,recolor,recomb,recon,recook,recool,recopy,record,recork,recount,recoup,recover,recramp,recrank,recrate,recrew,recroon,recrop,recross,recrowd,recrown,recruit,recrush,rect,recta,rectal,recti,rectify,rection,recto,rector,rectory,rectrix,rectum,rectus,recur,recure,recurl,recurse,recurve,recuse,recut,recycle,red,redact,redan,redare,redarn,redart,redate,redaub,redawn,redback,redbait,redbill,redbird,redbone,redbuck,redbud,redcap,redcoat,redd,redden,redder,redding,reddish,reddock,reddy,rede,redeal,redebit,redeck,redeed,redeem,redefer,redefy,redeify,redelay,redeny,redeye,redfin,redfish,redfoot,redhead,redhoop,redia,redient,redig,redip,redive,redleg,redlegs,redly,redness,redo,redock,redoom,redoubt,redound,redowa,redox,redpoll,redraft,redrag,redrape,redraw,redream,redress,redrill,redrive,redroot,redry,redsear,redskin,redtab,redtail,redtop,redub,reduce,reduced,reducer,reduct,redue,redux,redward,redware,redweed,redwing,redwood,redye,ree,reechy,reed,reeded,reeden,reeder,reedily,reeding,reedish,reedman,reedy,reef,reefer,reefing,reefy,reek,reeker,reeky,reel,reeled,reeler,reem,reeming,reemish,reen,reenge,reeper,reese,reeshle,reesk,reesle,reest,reester,reestle,reesty,reet,reetam,reetle,reeve,ref,reface,refall,refan,refavor,refect,refeed,refeel,refeign,refel,refence,refer,referee,refetch,refight,refill,refilm,refind,refine,refined,refiner,refire,refit,refix,reflag,reflame,reflash,reflate,reflect,reflee,reflex,refling,refloat,reflog,reflood,refloor,reflow,reflush,reflux,refly,refocus,refold,refont,refool,refoot,reforce,reford,reforge,reform,refound,refract,refrain,reframe,refresh,refront,reft,refuel,refuge,refugee,refulge,refund,refurl,refusal,refuse,refuser,refutal,refute,refuter,reg,regain,regal,regale,regaler,regalia,regally,regard,regatta,regauge,regency,regent,reges,reget,regia,regift,regild,regill,regime,regimen,regin,reginal,region,regive,reglair,reglaze,regle,reglet,regloss,reglove,reglow,reglue,regma,regnal,regnant,regorge,regrade,regraft,regrant,regrasp,regrass,regrate,regrede,regreen,regreet,regress,regret,regrind,regrip,regroup,regrow,reguard,reguide,regula,regular,reguli,regulus,regur,regurge,regush,reh,rehair,rehale,rehang,reharm,rehash,rehaul,rehead,reheal,reheap,rehear,reheat,rehedge,reheel,rehoe,rehoist,rehonor,rehood,rehook,rehoop,rehouse,rehung,reif,reify,reign,reim,reimage,reimpel,reimply,rein,reina,reincur,reindue,reinfer,reins,reinter,reis,reissue,reit,reitbok,reiter,reiver,rejail,reject,rejerk,rejoice,rejoin,rejolt,rejudge,rekick,rekill,reking,rekiss,reknit,reknow,rel,relabel,relace,relade,reladen,relais,relamp,reland,relap,relapse,relast,relata,relatch,relate,related,relater,relator,relatum,relax,relaxed,relaxer,relay,relbun,relead,releap,relearn,release,relend,relent,relet,relevel,relevy,reliant,relic,relick,relict,relief,relier,relieve,relievo,relift,relight,relime,relimit,reline,reliner,relink,relish,relishy,relist,relive,reload,reloan,relock,relodge,relook,relose,relost,relot,relove,relower,reluct,relume,rely,remade,remail,remain,remains,remake,remaker,reman,remand,remanet,remap,remarch,remark,remarry,remask,remass,remast,rematch,remble,remeant,remede,remedy,remeet,remelt,remend,remerge,remetal,remex,remica,remicle,remiges,remill,remimic,remind,remint,remiped,remise,remiss,remit,remix,remnant,remock,remodel,remold,remop,remora,remord,remorse,remote,remould,remount,removal,remove,removed,remover,renable,renably,renail,renal,rename,rend,render,reneg,renege,reneger,renegue,renerve,renes,renet,renew,renewal,renewer,renin,renish,renk,renky,renne,rennet,rennin,renown,rent,rentage,rental,rented,rentee,renter,renvoi,renvoy,reoccur,reoffer,reoil,reomit,reopen,reorder,reown,rep,repace,repack,repage,repaint,repair,repale,repand,repanel,repaper,repark,repass,repast,repaste,repatch,repave,repawn,repay,repayal,repeal,repeat,repeg,repel,repen,repent,repew,rephase,repic,repick,repiece,repile,repin,repine,repiner,repipe,repique,repitch,repkie,replace,replait,replan,replane,replant,replate,replay,replead,repleat,replete,replevy,replica,replier,replod,replot,replow,replum,replume,reply,repoint,repoll,repolon,repone,repope,report,reposal,repose,reposed,reposer,reposit,repost,repot,repound,repour,repp,repped,repray,repress,reprice,reprime,reprint,reprise,reproof,reprove,reprune,reps,reptant,reptile,repuff,repugn,repulse,repump,repurge,repute,reputed,requeen,request,requiem,requin,require,requit,requite,requiz,requote,rerack,rerail,reraise,rerake,rerank,rerate,reread,reredos,reree,rereel,rereeve,rereign,rerent,rerig,rering,rerise,rerival,rerivet,rerob,rerobe,reroll,reroof,reroot,rerope,reroute,rerow,rerub,rerun,resaca,resack,resail,resale,resalt,resaw,resawer,resay,rescan,rescind,rescore,rescrub,rescue,rescuer,reseal,reseam,reseat,resect,reseda,resee,reseed,reseek,reseise,reseize,reself,resell,resend,resene,resent,reserve,reset,resever,resew,resex,resh,reshake,reshape,reshare,reshave,reshear,reshift,reshine,reship,reshoe,reshoot,reshun,reshunt,reshut,reside,resider,residua,residue,resift,resigh,resign,resile,resin,resina,resiner,resing,resinic,resink,resinol,resiny,resist,resize,resizer,reskin,reslash,reslate,reslay,reslide,reslot,resmell,resmelt,resmile,resnap,resnub,resoak,resoap,resoil,resole,resolve,resorb,resort,resound,resow,resp,respace,respade,respan,respeak,respect,respell,respin,respire,respite,resplit,respoke,respond,respot,respray,respue,ressala,ressaut,rest,restack,restaff,restain,restake,restamp,restant,restart,restate,restaur,resteal,resteel,resteep,restem,restep,rester,restes,restful,restiad,restiff,resting,restir,restis,restive,restock,restore,restow,restrap,restrip,restudy,restuff,resty,restyle,resuck,resue,resuing,resuit,result,resume,resumer,resun,resup,resurge,reswage,resward,reswarm,reswear,resweat,resweep,reswell,reswill,reswim,ret,retable,retack,retag,retail,retain,retake,retaker,retalk,retama,retame,retan,retape,retard,retare,retaste,retax,retch,reteach,retell,retem,retempt,retene,retent,retest,rethank,rethaw,rethe,rethink,rethrow,retia,retial,retiary,reticle,retie,retier,retile,retill,retime,retin,retina,retinal,retinol,retinue,retip,retiral,retire,retired,retirer,retoast,retold,retomb,retook,retool,retooth,retort,retoss,retotal,retouch,retour,retrace,retrack,retract,retrad,retrade,retrain,retral,retramp,retread,retreat,retree,retrial,retrim,retrip,retrot,retrude,retrue,retrust,retry,retted,retter,rettery,retting,rettory,retube,retuck,retune,returf,return,retuse,retwine,retwist,retying,retype,retzian,reune,reunify,reunion,reunite,reurge,reuse,reutter,rev,revalue,revamp,revary,reve,reveal,reveil,revel,reveler,revelly,revelry,revend,revenge,revent,revenue,rever,reverb,revere,revered,reverer,reverie,revers,reverse,reversi,reverso,revert,revery,revest,revet,revete,revie,review,revile,reviler,revisal,revise,revisee,reviser,revisit,revisor,revival,revive,reviver,revivor,revoice,revoke,revoker,revolt,revolve,revomit,revote,revue,revuist,rewade,rewager,rewake,rewaken,rewall,reward,rewarm,rewarn,rewash,rewater,rewave,rewax,rewayle,rewear,reweave,rewed,reweigh,reweld,rewend,rewet,rewhelp,rewhirl,rewiden,rewin,rewind,rewire,rewish,rewood,reword,rework,rewound,rewove,rewoven,rewrap,rewrite,rex,rexen,reyield,reyoke,reyouth,rhabdom,rhabdos,rhabdus,rhagite,rhagon,rhagose,rhamn,rhamnal,rhason,rhatany,rhe,rhea,rhebok,rheeboc,rheebok,rheen,rheic,rhein,rheinic,rhema,rheme,rhenium,rheotan,rhesian,rhesus,rhetor,rheum,rheumed,rheumic,rheumy,rhexis,rhinal,rhine,rhinion,rhino,rhizine,rhizoid,rhizoma,rhizome,rhizote,rho,rhodic,rhoding,rhodite,rhodium,rhomb,rhombic,rhombos,rhombus,rhubarb,rhumb,rhumba,rhyme,rhymer,rhymery,rhymic,rhymist,rhymy,rhyptic,rhythm,rhyton,ria,rial,riancy,riant,riantly,riata,rib,ribald,riband,ribat,ribband,ribbed,ribber,ribbet,ribbing,ribble,ribbon,ribbony,ribby,ribe,ribless,riblet,riblike,ribonic,ribose,ribskin,ribwork,ribwort,rice,ricer,ricey,rich,richdom,richen,riches,richly,richt,ricin,ricine,ricinic,ricinus,rick,ricker,rickets,rickety,rickey,rickle,ricksha,ricrac,rictal,rictus,rid,ridable,ridably,riddam,riddel,ridden,ridder,ridding,riddle,riddler,ride,rideau,riden,rident,rider,ridered,ridge,ridged,ridgel,ridger,ridgil,ridging,ridgy,riding,ridotto,rie,riem,riempie,rier,rife,rifely,riff,riffle,riffler,rifle,rifler,riflery,rifling,rift,rifter,rifty,rig,rigbane,riggald,rigger,rigging,riggish,riggite,riggot,right,righten,righter,rightle,rightly,righto,righty,rigid,rigidly,rigling,rignum,rigol,rigor,rigsby,rikisha,rikk,riksha,rikshaw,rilawa,rile,riley,rill,rillet,rillett,rillock,rilly,rim,rima,rimal,rimate,rimbase,rime,rimer,rimfire,rimland,rimless,rimmed,rimmer,rimose,rimous,rimpi,rimple,rimrock,rimu,rimula,rimy,rinceau,rinch,rincon,rind,rinded,rindle,rindy,rine,ring,ringe,ringed,ringent,ringer,ringeye,ringing,ringite,ringle,ringlet,ringman,ringtaw,ringy,rink,rinka,rinker,rinkite,rinner,rinse,rinser,rinsing,rio,riot,rioter,rioting,riotist,riotous,riotry,rip,ripa,ripal,ripcord,ripe,ripely,ripen,ripener,riper,ripgut,ripieno,ripier,ripost,riposte,ripper,rippet,rippier,ripping,rippit,ripple,rippler,ripplet,ripply,rippon,riprap,ripsack,ripsaw,ripup,risala,risberm,rise,risen,riser,rishi,risible,risibly,rising,risk,risker,riskful,riskily,riskish,risky,risp,risper,risque,risquee,rissel,risser,rissle,rissoid,rist,ristori,rit,rita,rite,ritling,ritual,ritzy,riva,rivage,rival,rivalry,rive,rivel,rivell,riven,river,rivered,riverly,rivery,rivet,riveter,riving,rivose,rivulet,rix,rixy,riyal,rizzar,rizzle,rizzom,roach,road,roadbed,roaded,roader,roading,roadite,roadman,roadway,roam,roamage,roamer,roaming,roan,roanoke,roar,roarer,roaring,roast,roaster,rob,robalo,roband,robber,robbery,robbin,robbing,robe,rober,roberd,robin,robinet,robing,robinin,roble,robomb,robot,robotry,robur,robust,roc,rocher,rochet,rock,rockaby,rocker,rockery,rocket,rockety,rocking,rockish,rocklay,rocklet,rockman,rocky,rococo,rocta,rod,rodd,roddin,rodding,rode,rodent,rodeo,rodge,rodham,roding,rodless,rodlet,rodlike,rodman,rodney,rodsman,rodster,rodwood,roe,roebuck,roed,roelike,roer,roey,rog,rogan,roger,roggle,rogue,roguery,roguing,roguish,rohan,rohob,rohun,rohuna,roi,roid,roil,roily,roister,roit,roka,roke,rokeage,rokee,rokelay,roker,rokey,roky,role,roleo,roll,rolled,roller,rolley,rollick,rolling,rollix,rollmop,rollock,rollway,roloway,romaika,romaine,romal,romance,romancy,romanza,romaunt,rombos,romeite,romero,rommack,romp,romper,romping,rompish,rompu,rompy,roncet,ronco,rond,ronde,rondeau,rondel,rondino,rondle,rondo,rondure,rone,rongeur,ronquil,rontgen,ronyon,rood,roodle,roof,roofage,roofer,roofing,rooflet,roofman,roofy,rooibok,rooinek,rook,rooker,rookery,rookie,rookish,rooklet,rooky,rool,room,roomage,roomed,roomer,roomful,roomie,roomily,roomlet,roomth,roomthy,roomy,roon,roosa,roost,roosted,rooster,root,rootage,rootcap,rooted,rooter,rootery,rootle,rootlet,rooty,roove,ropable,rope,ropeman,roper,ropery,ropes,ropeway,ropily,roping,ropish,ropp,ropy,roque,roquer,roquet,roquist,roral,roric,rorqual,rorty,rory,rosal,rosario,rosary,rosated,roscid,rose,roseal,roseate,rosebay,rosebud,rosed,roseine,rosel,roselet,rosella,roselle,roseola,roseous,rosery,roset,rosetan,rosette,rosetty,rosetum,rosety,rosied,rosier,rosilla,rosillo,rosily,rosin,rosiny,rosland,rosoli,rosolic,rosolio,ross,rosser,rossite,rostel,roster,rostra,rostral,rostrum,rosular,rosy,rot,rota,rotal,rotaman,rotan,rotang,rotary,rotate,rotated,rotator,rotch,rote,rotella,roter,rotge,rotgut,rother,rotifer,roto,rotor,rottan,rotten,rotter,rotting,rottle,rottock,rottolo,rotula,rotulad,rotular,rotulet,rotulus,rotund,rotunda,rotundo,roub,roucou,roud,roue,rouelle,rouge,rougeau,rougeot,rough,roughen,rougher,roughet,roughie,roughly,roughy,rougy,rouille,rouky,roulade,rouleau,roun,rounce,rouncy,round,rounded,roundel,rounder,roundly,roundup,roundy,roup,rouper,roupet,roupily,roupit,roupy,rouse,rouser,rousing,roust,rouster,rout,route,router,routh,routhie,routhy,routine,routing,routous,rove,rover,rovet,rovetto,roving,row,rowable,rowan,rowboat,rowdily,rowdy,rowed,rowel,rowen,rower,rowet,rowing,rowlet,rowlock,rowport,rowty,rowy,rox,roxy,royal,royale,royalet,royally,royalty,royet,royt,rozum,ruach,ruana,rub,rubasse,rubato,rubbed,rubber,rubbers,rubbery,rubbing,rubbish,rubble,rubbler,rubbly,rubdown,rubelet,rubella,rubelle,rubeola,rubiate,rubican,rubidic,rubied,rubific,rubify,rubine,rubious,ruble,rublis,rubor,rubric,rubrica,rubrify,ruby,ruche,ruching,ruck,rucker,ruckle,rucksey,ruckus,rucky,ruction,rud,rudas,rudd,rudder,ruddied,ruddily,ruddle,ruddock,ruddy,rude,rudely,ruderal,rudesby,rudge,rudish,rudity,rue,rueful,ruelike,ruelle,ruen,ruer,ruesome,ruewort,ruff,ruffed,ruffer,ruffian,ruffin,ruffle,ruffled,ruffler,ruffly,rufous,rufter,rufus,rug,ruga,rugate,rugged,rugging,ruggle,ruggy,ruglike,rugosa,rugose,rugous,ruin,ruinate,ruined,ruiner,ruing,ruinous,rukh,rulable,rule,ruledom,ruler,ruling,rull,ruller,rullion,rum,rumal,rumble,rumbler,rumbly,rumbo,rumen,ruminal,rumkin,rumless,rumly,rummage,rummagy,rummer,rummily,rummish,rummy,rumness,rumney,rumor,rumorer,rump,rumpad,rumpade,rumple,rumply,rumpus,rumshop,run,runaway,runback,runby,runch,rundale,rundle,rundlet,rune,runed,runer,runfish,rung,runic,runite,runkle,runkly,runless,runlet,runman,runnel,runner,runnet,running,runny,runoff,runout,runover,runrig,runt,runted,runtee,runtish,runty,runway,rupa,rupee,rupia,rupiah,rupial,rupie,rupitic,ruptile,ruption,ruptive,rupture,rural,rurally,rurban,ruru,ruse,rush,rushed,rushen,rusher,rushing,rushlit,rushy,rusine,rusk,ruskin,rusky,rusma,rusot,ruspone,russel,russet,russety,russia,russud,rust,rustful,rustic,rustily,rustle,rustler,rustly,rustre,rustred,rusty,ruswut,rut,rutate,rutch,ruth,ruther,ruthful,rutic,rutile,rutin,ruttee,rutter,ruttish,rutty,rutyl,ruvid,rux,ryal,ryania,rybat,ryder,rye,ryen,ryme,rynd,rynt,ryot,ryotwar,rype,rypeck,s,sa,saa,sab,sabalo,sabanut,sabbat,sabbath,sabe,sabeca,sabella,saber,sabered,sabicu,sabina,sabine,sabino,sable,sably,sabora,sabot,saboted,sabra,sabulum,saburra,sabutan,sabzi,sac,sacaton,sacatra,saccade,saccate,saccos,saccule,saccus,sachem,sachet,sack,sackage,sackbag,sackbut,sacked,sacken,sacker,sackful,sacking,sackman,saclike,saco,sacope,sacque,sacra,sacrad,sacral,sacred,sacring,sacrist,sacro,sacrum,sad,sadden,saddik,saddish,saddle,saddled,saddler,sade,sadh,sadhe,sadhu,sadic,sadiron,sadism,sadist,sadly,sadness,sado,sadr,saecula,saeter,saeume,safari,safe,safely,safen,safener,safety,saffian,safflor,safflow,saffron,safrole,saft,sag,saga,sagaie,sagaman,sagathy,sage,sagely,sagene,sagger,sagging,saggon,saggy,saging,sagitta,sagless,sago,sagoin,saguaro,sagum,saguran,sagwire,sagy,sah,sahh,sahib,sahme,sahukar,sai,saic,said,saiga,sail,sailage,sailed,sailer,sailing,sailor,saily,saim,saimiri,saimy,sain,saint,sainted,saintly,saip,sair,sairly,sairve,sairy,saithe,saj,sajou,sake,sakeber,sakeen,saker,sakeret,saki,sakieh,sakulya,sal,salaam,salable,salably,salacot,salad,salago,salal,salamo,salar,salary,salat,salay,sale,salele,salema,salep,salfern,salic,salicin,salicyl,salient,salify,saligot,salina,saline,salite,salited,saliva,salival,salix,salle,sallee,sallet,sallier,salloo,sallow,sallowy,sally,salma,salmiac,salmine,salmis,salmon,salol,salomon,salon,saloon,saloop,salp,salpa,salpian,salpinx,salpoid,salse,salsify,salt,salta,saltant,saltary,saltate,saltcat,salted,saltee,salten,salter,saltern,saltery,saltfat,saltier,saltine,salting,saltish,saltly,saltman,saltpan,saltus,salty,saluki,salung,salute,saluter,salvage,salve,salver,salviol,salvo,salvor,salvy,sam,samadh,samadhi,samaj,saman,samara,samaria,samarra,samba,sambal,sambar,sambo,sambuk,sambuke,same,samekh,samel,samely,samen,samh,samhita,samiel,samiri,samisen,samite,samkara,samlet,sammel,sammer,sammier,sammy,samovar,samp,sampan,sampi,sample,sampler,samsara,samshu,samson,samurai,san,sanable,sanai,sancho,sanct,sancta,sanctum,sand,sandak,sandal,sandan,sandbag,sandbin,sandbox,sandboy,sandbur,sanded,sander,sanders,sandhi,sanding,sandix,sandman,sandust,sandy,sane,sanely,sang,sanga,sangar,sangei,sanger,sangha,sangley,sangrel,sangsue,sanicle,sanies,sanify,sanious,sanity,sanjak,sank,sankha,sannup,sans,sansei,sansi,sant,santal,santene,santimi,santims,santir,santon,sao,sap,sapa,sapajou,sapan,sapbush,sapek,sapful,saphead,saphena,saphie,sapid,sapient,sapin,sapinda,saple,sapless,sapling,sapo,saponin,sapor,sapota,sapote,sappare,sapper,sapphic,sapping,sapples,sappy,saprine,sapsago,sapsuck,sapwood,sapwort,sar,saraad,saraf,sarangi,sarcasm,sarcast,sarcine,sarcle,sarcler,sarcode,sarcoid,sarcoma,sarcous,sard,sardel,sardine,sardius,sare,sargo,sargus,sari,sarif,sarigue,sarinda,sarip,sark,sarkar,sarkful,sarkine,sarking,sarkit,sarlak,sarlyk,sarment,sarna,sarod,saron,sarong,saronic,saros,sarpler,sarpo,sarra,sarraf,sarsa,sarsen,sart,sartage,sartain,sartor,sarus,sarwan,sasa,sasan,sasani,sash,sashay,sashery,sashing,sasin,sasine,sassaby,sassy,sat,satable,satan,satang,satanic,satara,satchel,sate,sateen,satiate,satient,satiety,satin,satine,satined,satiny,satire,satiric,satisfy,satlijk,satrap,satrapy,satron,sattle,sattva,satura,satyr,satyric,sauce,saucer,saucily,saucy,sauf,sauger,saugh,saughen,sauld,saulie,sault,saulter,saum,saumon,saumont,sauna,saunter,sauqui,saur,saurel,saurian,saury,sausage,saut,saute,sauteur,sauty,sauve,savable,savacu,savage,savanna,savant,savarin,save,saved,saveloy,saver,savin,saving,savior,savola,savor,savored,savorer,savory,savour,savoy,savoyed,savssat,savvy,saw,sawah,sawali,sawarra,sawback,sawbill,sawbuck,sawbwa,sawder,sawdust,sawed,sawer,sawfish,sawfly,sawing,sawish,sawlike,sawman,sawmill,sawmon,sawmont,sawn,sawney,sawt,sawway,sawwort,sawyer,sax,saxhorn,saxten,saxtie,saxtuba,say,saya,sayable,sayer,sayette,sayid,saying,sazen,sblood,scab,scabbed,scabble,scabby,scabid,scabies,scabish,scabrid,scad,scaddle,scads,scaff,scaffer,scaffie,scaffle,scaglia,scala,scalage,scalar,scalare,scald,scalded,scalder,scaldic,scaldy,scale,scaled,scalena,scalene,scaler,scales,scaling,scall,scalled,scallom,scallop,scalma,scaloni,scalp,scalpel,scalper,scalt,scaly,scam,scamble,scamell,scamler,scamles,scamp,scamper,scan,scandal,scandia,scandic,scanmag,scanner,scant,scantle,scantly,scanty,scap,scape,scapel,scapha,scapoid,scapose,scapple,scapula,scapus,scar,scarab,scarce,scarcen,scare,scarer,scarf,scarfed,scarfer,scarfy,scarid,scarify,scarily,scarlet,scarman,scarn,scaroid,scarp,scarred,scarrer,scarry,scart,scarth,scarus,scarved,scary,scase,scasely,scat,scatch,scathe,scatter,scatty,scatula,scaul,scaum,scaup,scauper,scaur,scaurie,scaut,scavage,scavel,scaw,scawd,scawl,scazon,sceat,scena,scenary,scend,scene,scenery,scenic,scenist,scenite,scent,scented,scenter,scepsis,scepter,sceptic,sceptry,scerne,schanz,schappe,scharf,schelly,schema,scheme,schemer,schemy,schene,schepel,schepen,scherm,scherzi,scherzo,schesis,schism,schisma,schist,schloop,schmelz,scho,schola,scholae,scholar,scholia,schone,school,schoon,schorl,schorly,schout,schtoff,schuh,schuhe,schuit,schule,schuss,schute,schwa,schwarz,sciapod,sciarid,sciatic,scibile,science,scient,scincid,scind,sciniph,scintle,scion,scious,scirrhi,scissel,scissor,sciurid,sclaff,sclate,sclater,sclaw,scler,sclera,scleral,sclere,scliff,sclim,sclimb,scoad,scob,scobby,scobs,scoff,scoffer,scog,scoggan,scogger,scoggin,scoke,scolb,scold,scolder,scolex,scolia,scoliid,scolion,scolite,scollop,scolog,sconce,sconcer,scone,scoon,scoop,scooped,scooper,scoot,scooter,scopa,scopate,scope,scopet,scopic,scopine,scopola,scops,scopula,scorch,score,scored,scorer,scoria,scoriac,scoriae,scorify,scoring,scorn,scorned,scorner,scorny,scorper,scorse,scot,scotale,scotch,scote,scoter,scotia,scotino,scotoma,scotomy,scouch,scouk,scoup,scour,scoured,scourer,scourge,scoury,scouse,scout,scouter,scouth,scove,scovel,scovy,scow,scowder,scowl,scowler,scowman,scrab,scrabe,scrae,scrag,scraggy,scraily,scram,scran,scranch,scrank,scranky,scranny,scrap,scrape,scraped,scraper,scrapie,scrappy,scrapy,scrat,scratch,scrath,scrauch,scraw,scrawk,scrawl,scrawly,scrawm,scrawny,scray,scraze,screak,screaky,scream,screamy,scree,screech,screed,screek,screel,screen,screeny,screet,screeve,screich,screigh,screve,screver,screw,screwed,screwer,screwy,scribal,scribe,scriber,scride,scrieve,scrike,scrim,scrime,scrimer,scrimp,scrimpy,scrin,scrinch,scrine,scringe,scrip,scripee,script,scritch,scrive,scriven,scriver,scrob,scrobe,scrobis,scrod,scroff,scrog,scroggy,scrolar,scroll,scrolly,scroo,scrooch,scrooge,scroop,scrota,scrotal,scrotum,scrouge,scrout,scrow,scroyle,scrub,scrubby,scruf,scruff,scruffy,scruft,scrum,scrump,scrunch,scrunge,scrunt,scruple,scrush,scruto,scruze,scry,scryer,scud,scudder,scuddle,scuddy,scudi,scudler,scudo,scuff,scuffed,scuffer,scuffle,scuffly,scuffy,scuft,scufter,scug,sculch,scull,sculler,scullog,sculp,sculper,sculpin,sculpt,sculsh,scum,scumber,scumble,scummed,scummer,scummy,scun,scunder,scunner,scup,scupful,scupper,scuppet,scur,scurdy,scurf,scurfer,scurfy,scurry,scurvy,scuse,scut,scuta,scutage,scutal,scutate,scutch,scute,scutel,scutter,scuttle,scutty,scutula,scutum,scybala,scye,scypha,scyphae,scyphi,scyphoi,scyphus,scyt,scytale,scythe,sdeath,se,sea,seadog,seafare,seafolk,seafowl,seagirt,seagoer,seah,seak,seal,sealant,sealch,sealed,sealer,sealery,sealess,sealet,sealike,sealine,sealing,seam,seaman,seamark,seamed,seamer,seaming,seamlet,seamost,seamrog,seamy,seance,seaport,sear,searce,searcer,search,seared,searer,searing,seary,seasick,seaside,season,seat,seatang,seated,seater,seathe,seating,seatron,seave,seavy,seawant,seaward,seaware,seaway,seaweed,seawife,seaworn,seax,sebacic,sebait,sebate,sebific,sebilla,sebkha,sebum,sebundy,sec,secable,secalin,secancy,secant,secede,seceder,secern,secesh,sech,seck,seclude,secluse,secohm,second,seconde,secos,secpar,secque,secre,secrecy,secret,secreta,secrete,secreto,sect,sectary,sectile,section,sectism,sectist,sective,sector,secular,secund,secure,securer,sedan,sedate,sedent,sedge,sedged,sedging,sedgy,sedile,sedilia,seduce,seducee,seducer,seduct,sedum,see,seeable,seech,seed,seedage,seedbed,seedbox,seeded,seeder,seedful,seedily,seedkin,seedlet,seedlip,seedman,seedy,seege,seeing,seek,seeker,seeking,seel,seelful,seely,seem,seemer,seeming,seemly,seen,seenie,seep,seepage,seeped,seepy,seer,seeress,seerpaw,seesaw,seesee,seethe,seg,seggar,seggard,segged,seggrom,segment,sego,segol,seiche,seidel,seine,seiner,seise,seism,seismal,seismic,seit,seity,seize,seizer,seizin,seizing,seizor,seizure,sejant,sejoin,sejunct,sekos,selah,selamin,seldom,seldor,sele,select,selenic,self,selfdom,selfful,selfish,selfism,selfist,selfly,selion,sell,sella,sellar,sellate,seller,sellie,selling,sellout,selly,selsyn,selt,selva,selvage,semarum,sematic,semball,semble,seme,semeed,semeia,semeion,semen,semence,semese,semi,semiape,semiarc,semibay,semic,semicup,semidry,semiegg,semifib,semifit,semify,semigod,semihot,seminal,seminar,semiorb,semiped,semipro,semiraw,semis,semita,semitae,semital,semiurn,semmet,semmit,semola,semsem,sen,senaite,senam,senary,senate,senator,sence,sencion,send,sendal,sendee,sender,sending,senega,senegin,senesce,senile,senior,senna,sennet,sennit,sennite,sensa,sensal,sensate,sense,sensed,sensify,sensile,sension,sensism,sensist,sensive,sensize,senso,sensor,sensory,sensual,sensum,sensyne,sent,sentry,sepad,sepal,sepaled,sephen,sepia,sepian,sepiary,sepic,sepioid,sepion,sepiost,sepium,sepone,sepoy,seppuku,seps,sepsine,sepsis,sept,septa,septal,septan,septane,septate,septave,septet,septic,septier,septile,septime,septoic,septole,septum,septuor,sequa,sequel,sequela,sequent,sequest,sequin,ser,sera,serab,seragli,serai,serail,seral,serang,serape,seraph,serau,seraw,sercial,serdab,sere,sereh,serene,serf,serfage,serfdom,serfish,serfism,serge,serger,serging,serial,seriary,seriate,sericea,sericin,seriema,series,serif,serific,serin,serine,seringa,serio,serious,serment,sermo,sermon,sero,serolin,seron,seroon,seroot,seropus,serosa,serous,serow,serpent,serphid,serpigo,serpula,serra,serrage,serran,serrana,serrano,serrate,serried,serry,sert,serta,sertule,sertum,serum,serumal,serut,servage,serval,servant,serve,server,servery,servet,service,servile,serving,servist,servo,sesame,sesma,sesqui,sess,sessile,session,sestet,sesti,sestiad,sestina,sestine,sestole,sestuor,set,seta,setae,setal,setback,setbolt,setdown,setfast,seth,sethead,setier,setline,setness,setoff,seton,setose,setous,setout,setover,setsman,sett,settee,setter,setting,settle,settled,settler,settlor,setula,setule,setup,setwall,setwise,setwork,seugh,seven,sevener,seventh,seventy,sever,several,severe,severer,severy,sew,sewable,sewage,sewan,sewed,sewen,sewer,sewered,sewery,sewing,sewless,sewn,sex,sexed,sexern,sexfid,sexfoil,sexhood,sexifid,sexiped,sexless,sexlike,sexly,sext,sextain,sextan,sextans,sextant,sextar,sextary,sextern,sextet,sextic,sextile,sexto,sextole,sexton,sextry,sextula,sexual,sexuale,sexuous,sexy,sey,sfoot,sh,sha,shab,shabash,shabbed,shabble,shabby,shachle,shachly,shack,shackle,shackly,shacky,shad,shade,shaded,shader,shadily,shadine,shading,shadkan,shadoof,shadow,shadowy,shady,shaffle,shaft,shafted,shafter,shafty,shag,shagbag,shagged,shaggy,shaglet,shagrag,shah,shahdom,shahi,shahin,shaikh,shaitan,shake,shaken,shaker,shakers,shakha,shakily,shaking,shako,shakti,shaku,shaky,shale,shall,shallal,shallon,shallop,shallot,shallow,shallu,shalom,shalt,shalwar,shaly,sham,shama,shamal,shamalo,shaman,shamba,shamble,shame,shamed,shamer,shamir,shammed,shammer,shammy,shampoo,shan,shandry,shandy,shangan,shank,shanked,shanker,shanna,shanny,shansa,shant,shanty,shap,shape,shaped,shapely,shapen,shaper,shaping,shaps,shapy,shard,sharded,shardy,share,sharer,shargar,shark,sharky,sharn,sharny,sharp,sharpen,sharper,sharpie,sharply,sharps,sharpy,sharrag,sharry,shaster,shastra,shastri,shat,shatan,shatter,shaugh,shaul,shaup,shauri,shauwe,shave,shaved,shavee,shaven,shaver,shavery,shaving,shaw,shawl,shawled,shawm,shawny,shawy,shay,she,shea,sheaf,sheafy,sheal,shear,sheard,shearer,shears,sheat,sheath,sheathe,sheathy,sheave,sheaved,shebang,shebeen,shed,shedded,shedder,sheder,shedman,shee,sheely,sheen,sheenly,sheeny,sheep,sheepy,sheer,sheered,sheerly,sheet,sheeted,sheeter,sheety,sheik,sheikly,shekel,shela,sheld,shelder,shelf,shelfy,shell,shellac,shelled,sheller,shellum,shelly,shelta,shelter,shelty,shelve,shelver,shelvy,shend,sheng,sheolic,sheppey,sher,sherbet,sheriat,sherif,sherifa,sheriff,sherifi,sherify,sherry,sheth,sheugh,sheva,shevel,shevri,shewa,shewel,sheyle,shi,shibah,shibar,shice,shicer,shicker,shide,shied,shiel,shield,shier,shies,shiest,shift,shifter,shifty,shigram,shih,shikar,shikara,shikari,shikimi,shikken,shiko,shikra,shilf,shilfa,shill,shilla,shillet,shilloo,shilpit,shim,shimal,shimmer,shimmy,shimose,shimper,shin,shindig,shindle,shindy,shine,shiner,shingle,shingly,shinily,shining,shinner,shinny,shinty,shiny,shinza,ship,shipboy,shipful,shiplap,shiplet,shipman,shipped,shipper,shippo,shippon,shippy,shipway,shire,shirk,shirker,shirky,shirl,shirpit,shirr,shirt,shirty,shish,shisham,shisn,shita,shither,shittah,shittim,shiv,shive,shiver,shivery,shivey,shivoo,shivy,sho,shoad,shoader,shoal,shoaler,shoaly,shoat,shock,shocker,shod,shodden,shoddy,shode,shoder,shoe,shoeboy,shoeing,shoeman,shoer,shoful,shog,shogaol,shoggie,shoggle,shoggly,shogi,shogun,shohet,shoji,shola,shole,shone,shoneen,shoo,shood,shoofa,shoofly,shooi,shook,shool,shooler,shoop,shoor,shoot,shootee,shooter,shop,shopboy,shopful,shophar,shoplet,shopman,shoppe,shopper,shoppy,shoq,shor,shoran,shore,shored,shorer,shoring,shorn,short,shorten,shorter,shortly,shorts,shot,shote,shotgun,shotman,shott,shotted,shotten,shotter,shotty,shou,should,shout,shouter,shoval,shove,shovel,shover,show,showdom,shower,showery,showily,showing,showish,showman,shown,showup,showy,shoya,shrab,shradh,shraf,shrag,shram,shrank,shrap,shrave,shravey,shred,shreddy,shree,shreeve,shrend,shrew,shrewd,shrewdy,shrewly,shriek,shrieky,shrift,shrike,shrill,shrilly,shrimp,shrimpi,shrimpy,shrinal,shrine,shrink,shrinky,shrip,shrite,shrive,shrivel,shriven,shriver,shroff,shrog,shroud,shroudy,shrove,shrover,shrub,shrubby,shruff,shrug,shrunk,shrups,shuba,shuck,shucker,shucks,shudder,shuff,shuffle,shug,shul,shuler,shumac,shun,shune,shunner,shunt,shunter,shure,shurf,shush,shusher,shut,shutoff,shutout,shutten,shutter,shuttle,shy,shyer,shyish,shyly,shyness,shyster,si,siak,sial,sialic,sialid,sialoid,siamang,sib,sibbed,sibbens,sibber,sibby,sibilus,sibling,sibness,sibrede,sibship,sibyl,sibylic,sibylla,sic,sicca,siccant,siccate,siccity,sice,sick,sickbed,sicken,sicker,sickish,sickle,sickled,sickler,sickly,sicsac,sicula,sicular,sidder,siddur,side,sideage,sidearm,sidecar,sided,sider,sideral,siderin,sides,sideway,sidhe,sidi,siding,sidle,sidler,sidling,sidth,sidy,sie,siege,sieger,sienna,sier,siering,sierra,sierran,siesta,sieve,siever,sievy,sifac,sifaka,sife,siffle,sifflet,sifflot,sift,siftage,sifted,sifter,sifting,sig,sigger,sigh,sigher,sighful,sighing,sight,sighted,sighten,sighter,sightly,sighty,sigil,sigla,siglos,sigma,sigmate,sigmoid,sign,signal,signary,signate,signee,signer,signet,signify,signior,signist,signman,signory,signum,sika,sikar,sikatch,sike,sikerly,siket,sikhara,sikhra,sil,silage,silane,sile,silen,silence,silency,sileni,silenic,silent,silenus,silesia,silex,silica,silicam,silicic,silicle,silico,silicon,silicyl,siliqua,silique,silk,silked,silken,silker,silkie,silkily,silkman,silky,sill,sillar,siller,sillily,sillock,sillon,silly,silo,siloist,silphid,silt,siltage,silting,silty,silurid,silva,silvan,silver,silvern,silvery,silvics,silyl,sima,simal,simar,simball,simbil,simblin,simblot,sime,simiad,simial,simian,similar,simile,similor,simioid,simious,simity,simkin,simlin,simling,simmer,simmon,simnel,simony,simool,simoom,simoon,simous,simp,simpai,simper,simple,simpler,simplex,simply,simsim,simson,simular,simuler,sin,sina,sinaite,sinal,sinamay,sinapic,sinapis,sinawa,since,sincere,sind,sinder,sindle,sindoc,sindon,sindry,sine,sinew,sinewed,sinewy,sinful,sing,singe,singed,singer,singey,singh,singing,single,singled,singler,singles,singlet,singly,singult,sinh,sink,sinkage,sinker,sinking,sinky,sinless,sinlike,sinnen,sinner,sinnet,sinopia,sinople,sinsion,sinsyne,sinter,sintoc,sinuate,sinuose,sinuous,sinus,sinusal,sinward,siol,sion,sip,sipage,sipe,siper,siphoid,siphon,sipid,siping,sipling,sipper,sippet,sippio,sir,sircar,sirdar,sire,siren,sirene,sirenic,sireny,siress,sirgang,sirian,siricid,sirih,siris,sirkeer,sirki,sirky,sirloin,siroc,sirocco,sirpea,sirple,sirpoon,sirrah,sirree,sirship,sirup,siruped,siruper,sirupy,sis,sisal,sise,sisel,sish,sisham,sisi,siskin,siss,sissify,sissoo,sissy,sist,sister,sistern,sistle,sistrum,sit,sitao,sitar,sitch,site,sitfast,sith,sithe,sithens,sitient,sitio,sittee,sitten,sitter,sittine,sitting,situal,situate,situla,situlae,situs,siva,siver,sivvens,siwash,six,sixain,sixer,sixfoil,sixfold,sixsome,sixte,sixteen,sixth,sixthet,sixthly,sixty,sizable,sizably,sizal,sizar,size,sized,sizeman,sizer,sizes,sizing,sizy,sizygia,sizz,sizzard,sizzing,sizzle,sjambok,skaddle,skaff,skaffie,skag,skair,skal,skance,skart,skasely,skat,skate,skater,skatiku,skating,skatist,skatole,skaw,skean,skedge,skee,skeed,skeeg,skeel,skeely,skeen,skeer,skeered,skeery,skeet,skeeter,skeezix,skeg,skegger,skeif,skeigh,skeily,skein,skeiner,skeipp,skel,skelder,skelf,skelic,skell,skellat,skeller,skellum,skelly,skelp,skelper,skelpin,skelter,skemmel,skemp,sken,skene,skeo,skeough,skep,skepful,skeptic,sker,skere,skerret,skerry,sketch,sketchy,skete,skevish,skew,skewed,skewer,skewl,skewly,skewy,skey,ski,skiapod,skibby,skice,skid,skidded,skidder,skiddoo,skiddy,skidpan,skidway,skied,skieppe,skier,skies,skiff,skift,skiing,skijore,skil,skilder,skill,skilled,skillet,skilly,skilpot,skilts,skim,skime,skimmed,skimmer,skimp,skimpy,skin,skinch,skinful,skink,skinker,skinkle,skinned,skinner,skinny,skip,skipman,skippel,skipper,skippet,skipple,skippy,skirl,skirp,skirr,skirreh,skirret,skirt,skirted,skirter,skirty,skit,skite,skiter,skither,skitter,skittle,skitty,skiv,skive,skiver,skiving,sklate,sklater,sklent,skoal,skoo,skookum,skoptsy,skout,skraigh,skrike,skrupul,skua,skulk,skulker,skull,skulled,skully,skulp,skun,skunk,skunky,skuse,sky,skybal,skyey,skyful,skyish,skylark,skyless,skylike,skylook,skyman,skyphoi,skyphos,skyre,skysail,skyugle,skyward,skyway,sla,slab,slabbed,slabber,slabby,slabman,slack,slacked,slacken,slacker,slackly,slad,sladang,slade,slae,slag,slagger,slaggy,slagman,slain,slainte,slait,slake,slaker,slaking,slaky,slam,slamp,slander,slane,slang,slangy,slank,slant,slantly,slap,slape,slapper,slare,slart,slarth,slash,slashed,slasher,slashy,slat,slatch,slate,slater,slath,slather,slatify,slating,slatish,slatted,slatter,slaty,slaum,slave,slaved,slaver,slavery,slavey,slaving,slavish,slaw,slay,slayer,slaying,sleathy,sleave,sleaved,sleazy,sleck,sled,sledded,sledder,sledful,sledge,sledger,slee,sleech,sleechy,sleek,sleeken,sleeker,sleekit,sleekly,sleeky,sleep,sleeper,sleepry,sleepy,sleer,sleet,sleety,sleeve,sleeved,sleever,sleigh,sleight,slender,slent,slepez,slept,slete,sleuth,slew,slewed,slewer,slewing,sley,sleyer,slice,sliced,slicer,slich,slicht,slicing,slick,slicken,slicker,slickly,slid,slidage,slidden,slidder,slide,slided,slider,sliding,slifter,slight,slighty,slim,slime,slimer,slimily,slimish,slimly,slimpsy,slimsy,slimy,sline,sling,slinge,slinger,slink,slinker,slinky,slip,slipe,slipman,slipped,slipper,slippy,slipway,slirt,slish,slit,slitch,slite,slither,slithy,slitted,slitter,slitty,slive,sliver,slivery,sliving,sloan,slob,slobber,slobby,slock,slocken,slod,slodder,slodge,slodger,sloe,slog,slogan,slogger,sloka,sloke,slon,slone,slonk,sloo,sloom,sloomy,sloop,sloosh,slop,slope,sloped,slopely,sloper,sloping,slopped,sloppy,slops,slopy,slorp,slosh,slosher,sloshy,slot,slote,sloted,sloth,slotted,slotter,slouch,slouchy,slough,sloughy,slour,sloush,sloven,slow,slowish,slowly,slowrie,slows,sloyd,slub,slubber,slubby,slud,sludder,sludge,sludged,sludger,sludgy,slue,sluer,slug,slugged,slugger,sluggy,sluice,sluicer,sluicy,sluig,sluit,slum,slumber,slumdom,slumgum,slummer,slummy,slump,slumpy,slung,slunge,slunk,slunken,slur,slurbow,slurp,slurry,slush,slusher,slushy,slut,slutch,slutchy,sluther,slutter,slutty,sly,slyish,slyly,slyness,slype,sma,smack,smackee,smacker,smaik,small,smallen,smaller,smalls,smally,smalm,smalt,smalter,smalts,smaragd,smarm,smarmy,smart,smarten,smartly,smarty,smash,smasher,smashup,smatter,smaze,smear,smeared,smearer,smeary,smectic,smectis,smeddum,smee,smeech,smeek,smeeky,smeer,smeeth,smegma,smell,smelled,smeller,smelly,smelt,smelter,smeth,smethe,smeuse,smew,smich,smicker,smicket,smiddie,smiddum,smidge,smidgen,smilax,smile,smiler,smilet,smiling,smily,smirch,smirchy,smiris,smirk,smirker,smirkle,smirkly,smirky,smirtle,smit,smitch,smite,smiter,smith,smitham,smither,smithy,smiting,smitten,smock,smocker,smog,smoke,smoked,smoker,smokery,smokily,smoking,smokish,smoky,smolder,smolt,smooch,smoochy,smoodge,smook,smoot,smooth,smopple,smore,smote,smother,smotter,smouch,smous,smouse,smouser,smout,smriti,smudge,smudged,smudger,smudgy,smug,smuggle,smugism,smugly,smuisty,smur,smurr,smurry,smuse,smush,smut,smutch,smutchy,smutted,smutter,smutty,smyth,smytrie,snab,snabbie,snabble,snack,snackle,snaff,snaffle,snafu,snag,snagged,snagger,snaggy,snagrel,snail,snails,snaily,snaith,snake,snaker,snakery,snakily,snaking,snakish,snaky,snap,snapbag,snape,snaper,snapped,snapper,snapps,snappy,snaps,snapy,snare,snarer,snark,snarl,snarler,snarly,snary,snaste,snatch,snatchy,snath,snathe,snavel,snavvle,snaw,snead,sneak,sneaker,sneaky,sneap,sneath,sneathe,sneb,sneck,snecker,snecket,sned,snee,sneer,sneerer,sneery,sneesh,sneest,sneesty,sneeze,sneezer,sneezy,snell,snelly,snerp,snew,snib,snibble,snibel,snicher,snick,snicker,snicket,snickey,snickle,sniddle,snide,sniff,sniffer,sniffle,sniffly,sniffy,snift,snifter,snifty,snig,snigger,sniggle,snip,snipe,sniper,sniping,snipish,snipper,snippet,snippy,snipy,snirl,snirt,snirtle,snitch,snite,snithe,snithy,snittle,snivel,snively,snivy,snob,snobber,snobby,snobdom,snocher,snock,snocker,snod,snodly,snoek,snog,snoga,snoke,snood,snooded,snook,snooker,snoop,snooper,snoopy,snoose,snoot,snooty,snoove,snooze,snoozer,snoozle,snoozy,snop,snore,snorer,snoring,snork,snorkel,snorker,snort,snorter,snortle,snorty,snot,snotter,snotty,snouch,snout,snouted,snouter,snouty,snow,snowcap,snowie,snowily,snowish,snowk,snowl,snowy,snozzle,snub,snubbed,snubbee,snubber,snubby,snuck,snudge,snuff,snuffer,snuffle,snuffly,snuffy,snug,snugger,snuggle,snugify,snugly,snum,snup,snupper,snur,snurl,snurly,snurp,snurt,snuzzle,sny,snying,so,soak,soakage,soaked,soaken,soaker,soaking,soakman,soaky,soally,soam,soap,soapbox,soaper,soapery,soapily,soapsud,soapy,soar,soarer,soaring,soary,sob,sobber,sobbing,sobby,sobeit,sober,soberer,soberly,sobful,soboles,soc,socage,socager,soccer,soce,socht,social,society,socii,socius,sock,socker,socket,sockeye,socky,socle,socman,soco,sod,soda,sodaic,sodded,sodden,sodding,soddite,soddy,sodic,sodio,sodium,sodless,sodoku,sodomic,sodomy,sodwork,sody,soe,soekoe,soever,sofa,sofane,sofar,soffit,soft,softa,soften,softish,softly,softner,softy,sog,soger,soget,soggily,sogging,soggy,soh,soho,soil,soilage,soiled,soiling,soilure,soily,soiree,soja,sojourn,sok,soka,soke,sokeman,soken,sol,sola,solace,solacer,solan,solanal,solanum,solar,solate,solatia,solay,sold,soldado,soldan,solder,soldi,soldier,soldo,sole,solea,soleas,soleil,solely,solemn,solen,solent,soler,soles,soleus,soleyn,soli,solicit,solid,solidi,solidly,solidum,solidus,solio,soliped,solist,sollar,solo,solod,solodi,soloist,solon,soloth,soluble,solubly,solum,solute,solvate,solve,solvend,solvent,solver,soma,somal,somata,somatic,somber,sombre,some,someday,somehow,someone,somers,someway,somewhy,somital,somite,somitic,somma,somnial,somnify,somnus,sompay,sompne,sompner,son,sonable,sonance,sonancy,sonant,sonar,sonata,sond,sondeli,soneri,song,songful,songish,songle,songlet,songman,songy,sonhood,sonic,soniou,sonk,sonless,sonlike,sonly,sonnet,sonny,sonoric,sons,sonship,sonsy,sontag,soodle,soodly,sook,sooky,sool,sooloos,soon,sooner,soonish,soonly,soorawn,soord,soorkee,soot,sooter,sooth,soothe,soother,sootily,sooty,sop,sope,soph,sophia,sophic,sophism,sophy,sopite,sopor,sopper,sopping,soppy,soprani,soprano,sora,sorage,soral,sorb,sorbate,sorbent,sorbic,sorbile,sorbin,sorbite,sorbose,sorbus,sorcer,sorcery,sorchin,sorda,sordes,sordid,sordine,sordino,sordor,sore,soredia,soree,sorehon,sorely,sorema,sorgho,sorghum,sorgo,sori,soricid,sorite,sorites,sorn,sornare,sornari,sorner,sorning,soroban,sororal,sorose,sorosis,sorra,sorrel,sorrily,sorroa,sorrow,sorrowy,sorry,sort,sortal,sorted,sorter,sortie,sortly,sorty,sorus,sorva,sory,sosh,soshed,soso,sosoish,soss,sossle,sot,sotie,sotnia,sotnik,sotol,sots,sottage,sotted,sotter,sottish,sou,souari,soubise,soucar,souchet,souchy,soud,souffle,sough,sougher,sought,soul,soulack,souled,soulful,soulish,souly,soum,sound,sounder,soundly,soup,soupcon,souper,souple,soupy,sour,source,soured,souren,sourer,souring,sourish,sourly,sourock,soursop,sourtop,soury,souse,souser,souslik,soutane,souter,south,souther,sov,soviet,sovite,sovkhoz,sovran,sow,sowable,sowan,sowans,sowar,sowarry,sowback,sowbane,sowel,sowens,sower,sowfoot,sowing,sowins,sowl,sowle,sowlike,sowlth,sown,sowse,sowt,sowte,soy,soya,soybean,sozin,sozolic,sozzle,sozzly,spa,space,spaced,spacer,spacing,spack,spacy,spad,spade,spaded,spader,spadger,spading,spadix,spadone,spae,spaedom,spaeman,spaer,spahi,spaid,spaik,spairge,spak,spald,spalder,spale,spall,spaller,spalt,span,spancel,spandle,spandy,spane,spanemy,spang,spangle,spangly,spaniel,spaning,spank,spanker,spanky,spann,spannel,spanner,spanule,spar,sparada,sparch,spare,sparely,sparer,sparge,sparger,sparid,sparing,spark,sparked,sparker,sparkle,sparkly,sparks,sparky,sparm,sparoid,sparred,sparrer,sparrow,sparry,sparse,spart,sparth,spartle,sparver,spary,spasm,spasmed,spasmic,spastic,spat,spate,spatha,spathal,spathe,spathed,spathic,spatial,spatted,spatter,spattle,spatula,spatule,spave,spaver,spavie,spavied,spaviet,spavin,spawn,spawner,spawny,spay,spayad,spayard,spaying,speak,speaker,speal,spean,spear,spearer,speary,spec,spece,special,specie,species,specify,speck,specked,speckle,speckly,specks,specky,specs,specter,spectra,spectry,specula,specus,sped,speech,speed,speeder,speedy,speel,speen,speer,speiss,spelder,spelk,spell,speller,spelt,spelter,speltz,spelunk,spence,spencer,spend,spender,spense,spent,speos,sperate,sperity,sperket,sperm,sperma,spermic,spermy,sperone,spet,spetch,spew,spewer,spewing,spewy,spex,sphacel,sphecid,spheges,sphegid,sphene,sphenic,spheral,sphere,spheric,sphery,sphinx,spica,spical,spicant,spicate,spice,spiced,spicer,spicery,spicily,spicing,spick,spicket,spickle,spicose,spicous,spicula,spicule,spicy,spider,spidery,spidger,spied,spiegel,spiel,spieler,spier,spiff,spiffed,spiffy,spig,spignet,spigot,spike,spiked,spiker,spikily,spiking,spiky,spile,spiler,spiling,spilite,spill,spiller,spillet,spilly,spiloma,spilt,spilth,spilus,spin,spina,spinach,spinae,spinage,spinal,spinate,spinder,spindle,spindly,spine,spined,spinel,spinet,spingel,spink,spinner,spinney,spinoid,spinose,spinous,spinule,spiny,spionid,spiral,spirale,spiran,spirant,spirate,spire,spirea,spired,spireme,spiring,spirit,spirity,spirket,spiro,spiroid,spirous,spirt,spiry,spise,spit,spital,spitbox,spite,spitful,spitish,spitted,spitten,spitter,spittle,spitz,spiv,spivery,splash,splashy,splat,splatch,splay,splayed,splayer,spleen,spleeny,spleet,splenic,splet,splice,splicer,spline,splint,splinty,split,splodge,splodgy,splore,splosh,splotch,splunge,splurge,splurgy,splurt,spoach,spode,spodium,spoffle,spoffy,spogel,spoil,spoiled,spoiler,spoilt,spoke,spoken,spoky,spole,spolia,spolium,spondee,spondyl,spong,sponge,sponged,sponger,spongin,spongy,sponsal,sponson,sponsor,spoof,spoofer,spook,spooky,spool,spooler,spoom,spoon,spooner,spoony,spoor,spoorer,spoot,spor,sporal,spore,spored,sporid,sporoid,sporont,sporous,sporran,sport,sporter,sportly,sports,sporty,sporule,sposh,sposhy,spot,spotted,spotter,spottle,spotty,spousal,spouse,spousy,spout,spouter,spouty,sprack,sprad,sprag,spraich,sprain,spraint,sprang,sprank,sprat,spratty,sprawl,sprawly,spray,sprayer,sprayey,spread,spready,spreath,spree,spreeuw,spreng,sprent,spret,sprew,sprewl,spried,sprier,spriest,sprig,spriggy,spring,springe,springy,sprink,sprint,sprit,sprite,spritty,sproat,sprod,sprogue,sproil,sprong,sprose,sprout,sprowsy,spruce,sprue,spruer,sprug,spruit,sprung,sprunny,sprunt,spry,spryly,spud,spudder,spuddle,spuddy,spuffle,spug,spuke,spume,spumone,spumose,spumous,spumy,spun,spung,spunk,spunkie,spunky,spunny,spur,spurge,spuriae,spurl,spurlet,spurn,spurner,spurred,spurrer,spurry,spurt,spurter,spurtle,spurway,sput,sputa,sputter,sputum,spy,spyboat,spydom,spyer,spyhole,spyism,spyship,squab,squabby,squacco,squad,squaddy,squail,squalid,squall,squally,squalm,squalor,squam,squama,squamae,squame,square,squared,squarer,squark,squary,squash,squashy,squat,squatly,squatty,squaw,squawk,squawky,squdge,squdgy,squeak,squeaky,squeal,squeald,squeam,squeamy,squeege,squeeze,squeezy,squelch,squench,squib,squid,squidge,squidgy,squiffy,squilla,squin,squinch,squinny,squinsy,squint,squinty,squire,squiret,squirk,squirm,squirmy,squirr,squirt,squirty,squish,squishy,squit,squitch,squoze,squush,squushy,sraddha,sramana,sri,sruti,ssu,st,staab,stab,stabber,stabile,stable,stabler,stably,staboy,stacher,stachys,stack,stacker,stacte,stadda,staddle,stade,stadia,stadic,stadion,stadium,staff,staffed,staffer,stag,stage,staged,stager,stagery,stagese,stagger,staggie,staggy,stagily,staging,stagnum,stagy,staia,staid,staidly,stain,stainer,staio,stair,staired,stairy,staith,staiver,stake,staker,stale,stalely,staling,stalk,stalked,stalker,stalko,stalky,stall,stallar,staller,stam,stambha,stamen,stamin,stamina,stammel,stammer,stamnos,stamp,stampee,stamper,stample,stance,stanch,stand,standee,standel,stander,stane,stang,stanine,stanjen,stank,stankie,stannel,stanner,stannic,stanno,stannum,stannyl,stanza,stanze,stap,stapes,staple,stapled,stapler,star,starch,starchy,stardom,stare,staree,starer,starets,starful,staring,stark,starken,starkly,starky,starlet,starlit,starn,starnel,starnie,starost,starred,starry,start,starter,startle,startly,startor,starty,starve,starved,starver,starvy,stary,stases,stash,stashie,stasis,statal,statant,state,stated,stately,stater,static,statics,station,statism,statist,stative,stator,statue,statued,stature,status,statute,stauk,staumer,staun,staunch,staup,stauter,stave,staver,stavers,staving,staw,stawn,staxis,stay,stayed,stayer,staynil,stays,stchi,stead,steady,steak,steal,stealed,stealer,stealth,stealy,steam,steamer,steamy,stean,stearic,stearin,stearyl,steatin,stech,steddle,steed,steek,steel,steeler,steely,steen,steenth,steep,steepen,steeper,steeple,steeply,steepy,steer,steerer,steeve,steever,steg,steid,steigh,stein,stekan,stela,stelae,stelai,stelar,stele,stell,stella,stellar,stem,stema,stemlet,stemma,stemmed,stemmer,stemmy,stemple,stemson,sten,stenar,stench,stenchy,stencil,stend,steng,stengah,stenion,steno,stenog,stent,stenter,stenton,step,steppe,stepped,stepper,stepson,stept,stepway,stere,stereo,steri,steric,sterics,steride,sterile,sterin,sterk,sterlet,stern,sterna,sternad,sternal,sterned,sternly,sternum,stero,steroid,sterol,stert,stertor,sterve,stet,stetch,stevel,steven,stevia,stew,steward,stewed,stewpan,stewpot,stewy,stey,sthenia,sthenic,stib,stibial,stibic,stibine,stibium,stich,stichic,stichid,stick,sticked,sticker,stickit,stickle,stickly,sticks,stickum,sticky,stid,stiddy,stife,stiff,stiffen,stiffly,stifle,stifler,stigma,stigmai,stigmal,stigme,stile,stilet,still,stiller,stilly,stilt,stilted,stilter,stilty,stim,stime,stimuli,stimy,stine,sting,stinge,stinger,stingo,stingy,stink,stinker,stint,stinted,stinter,stinty,stion,stionic,stipe,stiped,stipel,stipend,stipes,stippen,stipple,stipply,stipula,stipule,stir,stirk,stirp,stirps,stirra,stirrer,stirrup,stitch,stite,stith,stithy,stive,stiver,stivy,stoa,stoach,stoat,stoater,stob,stocah,stock,stocker,stocks,stocky,stod,stodge,stodger,stodgy,stoep,stof,stoff,stog,stoga,stogie,stogy,stoic,stoical,stoke,stoker,stola,stolae,stole,stoled,stolen,stolid,stolist,stollen,stolon,stoma,stomach,stomata,stomate,stomium,stomp,stomper,stond,stone,stoned,stonen,stoner,stong,stonied,stonify,stonily,stoning,stonish,stonker,stony,stood,stooded,stooden,stoof,stooge,stook,stooker,stookie,stool,stoon,stoond,stoop,stooper,stoory,stoot,stop,stopa,stope,stoper,stopgap,stoping,stopped,stopper,stoppit,stopple,storage,storax,store,storeen,storer,storge,storied,storier,storify,stork,storken,storm,stormer,stormy,story,stosh,stoss,stot,stotter,stoun,stound,stoup,stour,stoury,stoush,stout,stouten,stouth,stoutly,stouty,stove,stoven,stover,stow,stowage,stowce,stower,stowing,stra,strack,stract,strad,strade,stradl,stradld,strae,strafe,strafer,strag,straik,strain,straint,strait,strake,straked,straky,stram,stramp,strand,strang,strange,strany,strap,strass,strata,stratal,strath,strati,stratic,stratum,stratus,strave,straw,strawen,strawer,strawy,stray,strayer,stre,streak,streaky,stream,streamy,streck,stree,streek,streel,streen,streep,street,streets,streite,streke,stremma,streng,strent,strenth,strepen,strepor,stress,stret,stretch,strette,stretti,stretto,strew,strewer,strewn,strey,streyne,stria,striae,strial,striate,strich,striche,strick,strict,strid,stride,strider,stridor,strife,strig,striga,strigae,strigal,stright,strigil,strike,striker,strind,string,stringy,striola,strip,stripe,striped,striper,stript,stripy,strit,strive,strived,striven,striver,strix,stroam,strobic,strode,stroil,stroke,stroker,stroky,strold,stroll,strolld,strom,stroma,stromal,stromb,strome,strone,strong,strook,stroot,strop,strophe,stroth,stroud,stroup,strove,strow,strowd,strown,stroy,stroyer,strub,struck,strudel,strue,strum,struma,strumae,strung,strunt,strut,struth,struv,strych,stub,stubb,stubbed,stubber,stubble,stubbly,stubboy,stubby,stuber,stuboy,stucco,stuck,stud,studder,studdie,studdle,stude,student,studia,studied,studier,studio,studium,study,stue,stuff,stuffed,stuffer,stuffy,stug,stuggy,stuiver,stull,stuller,stulm,stum,stumble,stumbly,stumer,stummer,stummy,stump,stumper,stumpy,stun,stung,stunk,stunner,stunsle,stunt,stunted,stunter,stunty,stupa,stupe,stupefy,stupend,stupent,stupex,stupid,stupor,stupose,stupp,stuprum,sturdy,sturine,sturk,sturt,sturtan,sturtin,stuss,stut,stutter,sty,styan,styca,styful,stylar,stylate,style,styler,stylet,styline,styling,stylish,stylist,stylite,stylize,stylo,styloid,stylops,stylus,stymie,stypsis,styptic,styrax,styrene,styrol,styrone,styryl,stythe,styward,suable,suably,suade,suaharo,suant,suantly,suasion,suasive,suasory,suave,suavely,suavify,suavity,sub,subacid,subact,subage,subah,subaid,subanal,subarch,subarea,subatom,subaud,subband,subbank,subbase,subbass,subbeau,subbias,subbing,subcase,subcash,subcast,subcell,subcity,subclan,subcool,subdate,subdean,subdeb,subdial,subdie,subdual,subduce,subduct,subdue,subdued,subduer,subecho,subedit,suber,suberic,suberin,subface,subfeu,subfief,subfix,subform,subfusc,subfusk,subgape,subgens,subget,subgit,subgod,subgrin,subgyre,subhall,subhead,subherd,subhero,subicle,subidar,subidea,subitem,subjack,subject,subjee,subjoin,subking,sublate,sublet,sublid,sublime,sublong,sublot,submaid,submain,subman,submind,submiss,submit,subnect,subness,subnex,subnote,subnude,suboral,suborn,suboval,subpart,subpass,subpial,subpimp,subplat,subplot,subplow,subpool,subport,subrace,subrent,subroot,subrule,subsale,subsalt,subsea,subsect,subsept,subset,subside,subsidy,subsill,subsist,subsoil,subsult,subsume,subtack,subtend,subtext,subtile,subtill,subtle,subtly,subtone,subtype,subunit,suburb,subvein,subvene,subvert,subvola,subway,subwink,subzone,succade,succeed,succent,success,succi,succin,succise,succor,succory,succous,succub,succuba,succube,succula,succumb,succuss,such,suck,suckage,sucken,sucker,sucking,suckle,suckler,suclat,sucrate,sucre,sucrose,suction,sucuri,sucuriu,sud,sudamen,sudary,sudate,sudd,sudden,sudder,suddle,suddy,sudoral,sudoric,suds,sudsman,sudsy,sue,suede,suer,suet,suety,suff,suffect,suffer,suffete,suffice,suffix,sufflue,suffuse,sugamo,sugan,sugar,sugared,sugarer,sugary,sugent,suggest,sugh,sugi,suguaro,suhuaro,suicide,suid,suidian,suiform,suimate,suine,suing,suingly,suint,suist,suit,suite,suiting,suitor,suity,suji,sulcal,sulcar,sulcate,sulcus,suld,sulea,sulfa,sulfato,sulfion,sulfury,sulk,sulka,sulker,sulkily,sulky,sull,sulla,sullage,sullen,sullow,sully,sulpha,sulpho,sulphur,sultam,sultan,sultana,sultane,sultone,sultry,sulung,sum,sumac,sumatra,sumbul,sumless,summage,summand,summar,summary,summate,summed,summer,summery,summist,summit,summity,summon,summons,summula,summut,sumner,sump,sumpage,sumper,sumph,sumphy,sumpit,sumple,sumpman,sumpter,sun,sunbeam,sunbird,sunbow,sunburn,suncup,sundae,sundang,sundari,sundek,sunder,sundew,sundial,sundik,sundog,sundown,sundra,sundri,sundry,sune,sunfall,sunfast,sunfish,sung,sungha,sunglo,sunglow,sunk,sunken,sunket,sunlamp,sunland,sunless,sunlet,sunlike,sunlit,sunn,sunnily,sunnud,sunny,sunray,sunrise,sunroom,sunset,sunsmit,sunspot,sunt,sunup,sunward,sunway,sunways,sunweed,sunwise,sunyie,sup,supa,supari,supawn,supe,super,superb,supine,supper,supping,supple,supply,support,suppose,suppost,supreme,sur,sura,surah,surahi,sural,suranal,surat,surbase,surbate,surbed,surcoat,surcrue,surculi,surd,surdent,surdity,sure,surely,sures,surette,surety,surf,surface,surfacy,surfeit,surfer,surfle,surfman,surfuse,surfy,surge,surgent,surgeon,surgery,surging,surgy,suriga,surlily,surly,surma,surmark,surmise,surname,surnap,surnay,surpass,surplus,surra,surrey,surtax,surtout,survey,survive,suscept,susi,suslik,suspect,suspend,suspire,sustain,susu,susurr,suther,sutile,sutler,sutlery,sutor,sutra,suttee,sutten,suttin,suttle,sutural,suture,suum,suwarro,suwe,suz,svelte,swa,swab,swabber,swabble,swack,swacken,swad,swaddle,swaddy,swag,swage,swager,swagger,swaggie,swaggy,swagman,swain,swaird,swale,swaler,swaling,swallet,swallo,swallow,swam,swami,swamp,swamper,swampy,swan,swang,swangy,swank,swanker,swanky,swanner,swanny,swap,swape,swapper,swaraj,swarbie,sward,swardy,sware,swarf,swarfer,swarm,swarmer,swarmy,swarry,swart,swarth,swarthy,swartly,swarty,swarve,swash,swasher,swashy,swat,swatch,swath,swathe,swather,swathy,swatter,swattle,swaver,sway,swayed,swayer,swayful,swaying,sweal,swear,swearer,sweat,sweated,sweater,sweath,sweaty,swedge,sweeny,sweep,sweeper,sweepy,sweer,sweered,sweet,sweeten,sweetie,sweetly,sweety,swego,swell,swelled,sweller,swelly,swelp,swelt,swelter,swelth,sweltry,swelty,swep,swept,swerd,swerve,swerver,swick,swidge,swift,swiften,swifter,swifty,swig,swigger,swiggle,swile,swill,swiller,swim,swimmer,swimmy,swimy,swindle,swine,swinely,swinery,swiney,swing,swinge,swinger,swingle,swingy,swinish,swink,swinney,swipe,swiper,swipes,swiple,swipper,swipy,swird,swire,swirl,swirly,swish,swisher,swishy,swiss,switch,switchy,swith,swithe,swithen,swither,swivel,swivet,swiz,swizzle,swob,swollen,swom,swonken,swoon,swooned,swoony,swoop,swooper,swoosh,sword,swore,sworn,swosh,swot,swotter,swounds,swow,swum,swung,swungen,swure,syagush,sybotic,syce,sycee,sycock,sycoma,syconid,syconus,sycosis,sye,syenite,sylid,syllab,syllabe,syllabi,sylloge,sylph,sylphic,sylphid,sylphy,sylva,sylvae,sylvage,sylvan,sylvate,sylvic,sylvine,sylvite,symbion,symbiot,symbol,sympode,symptom,synacme,synacmy,synange,synapse,synapte,synaxar,synaxis,sync,syncarp,synch,synchro,syncope,syndic,syndoc,syne,synema,synergy,synesis,syngamy,synod,synodal,synoecy,synonym,synopsy,synovia,syntan,syntax,synthol,syntomy,syntone,syntony,syntype,synusia,sypher,syre,syringa,syringe,syrinx,syrma,syrphid,syrt,syrtic,syrup,syruped,syruper,syrupy,syssel,system,systole,systyle,syzygy,t,ta,taa,taar,tab,tabacin,tabacum,tabanid,tabard,tabaret,tabaxir,tabber,tabby,tabefy,tabella,taberna,tabes,tabet,tabetic,tabic,tabid,tabidly,tabific,tabinet,tabla,table,tableau,tabled,tabler,tables,tablet,tabling,tabloid,tabog,taboo,taboot,tabor,taborer,taboret,taborin,tabour,tabret,tabu,tabula,tabular,tabule,tabut,taccada,tach,tache,tachiol,tacit,tacitly,tack,tacker,tacket,tackety,tackey,tacking,tackle,tackled,tackler,tacky,tacnode,tacso,tact,tactful,tactic,tactics,tactile,taction,tactite,tactive,tactor,tactual,tactus,tad,tade,tadpole,tae,tael,taen,taenia,taenial,taenian,taenite,taennin,taffeta,taffety,taffle,taffy,tafia,taft,tafwiz,tag,tagetol,tagged,tagger,taggle,taggy,taglet,taglike,taglock,tagrag,tagsore,tagtail,tagua,taguan,tagwerk,taha,taheen,tahil,tahin,tahr,tahsil,tahua,tai,taiaha,taich,taiga,taigle,taihoa,tail,tailage,tailed,tailer,tailet,tailge,tailing,taille,taillie,tailor,tailory,tailpin,taily,tailzee,tailzie,taimen,tain,taint,taintor,taipan,taipo,tairge,tairger,tairn,taisch,taise,taissle,tait,taiver,taivers,taivert,taj,takable,takar,take,takeful,taken,taker,takin,taking,takings,takosis,takt,taky,takyr,tal,tala,talabon,talahib,talaje,talak,talao,talar,talari,talaria,talaric,talayot,talbot,talc,talcer,talcky,talcoid,talcose,talcous,talcum,tald,tale,taled,taleful,talent,taler,tales,tali,taliage,taliera,talion,talipat,taliped,talipes,talipot,talis,talisay,talite,talitol,talk,talker,talkful,talkie,talking,talky,tall,tallage,tallboy,taller,tallero,talles,tallet,talliar,tallier,tallis,tallish,tallit,tallith,talloel,tallote,tallow,tallowy,tally,tallyho,talma,talon,taloned,talonic,talonid,talose,talpid,talpify,talpine,talpoid,talthib,taluk,taluka,talus,taluto,talwar,talwood,tam,tamable,tamably,tamale,tamandu,tamanu,tamara,tamarao,tamarin,tamas,tamasha,tambac,tamber,tambo,tamboo,tambor,tambour,tame,tamein,tamely,tamer,tamis,tamise,tamlung,tammie,tammock,tammy,tamp,tampala,tampan,tampang,tamper,tampin,tamping,tampion,tampon,tampoon,tan,tana,tanach,tanager,tanaist,tanak,tanan,tanbark,tanbur,tancel,tandan,tandem,tandle,tandour,tane,tang,tanga,tanged,tangelo,tangent,tanger,tangham,tanghan,tanghin,tangi,tangie,tangka,tanglad,tangle,tangler,tangly,tango,tangram,tangs,tangue,tangum,tangun,tangy,tanh,tanha,tania,tanica,tanier,tanist,tanjib,tanjong,tank,tanka,tankage,tankah,tankard,tanked,tanker,tankert,tankful,tankle,tankman,tanling,tannage,tannaic,tannaim,tannase,tannate,tanned,tanner,tannery,tannic,tannide,tannin,tanning,tannoid,tannyl,tanoa,tanquam,tanquen,tanrec,tansy,tantara,tanti,tantivy,tantle,tantra,tantric,tantrik,tantrum,tantum,tanwood,tanyard,tanzeb,tanzib,tanzy,tao,taotai,taoyin,tap,tapa,tapalo,tapas,tapasvi,tape,tapeman,tapen,taper,tapered,taperer,taperly,tapet,tapetal,tapete,tapeti,tapetum,taphole,tapia,tapioca,tapir,tapis,tapism,tapist,taplash,taplet,tapmost,tapnet,tapoa,tapoun,tappa,tappall,tappaul,tappen,tapper,tappet,tapping,tappoon,taproom,taproot,taps,tapster,tapu,tapul,taqua,tar,tara,taraf,tarage,tarairi,tarand,taraph,tarapin,tarata,taratah,tarau,tarbet,tarboy,tarbush,tardily,tardive,tardle,tardy,tare,tarea,tarefa,tarente,tarfa,targe,targer,target,tarhood,tari,tarie,tariff,tarin,tariric,tarish,tarkhan,tarlike,tarmac,tarman,tarn,tarnal,tarnish,taro,taroc,tarocco,tarok,tarot,tarp,tarpan,tarpon,tarpot,tarpum,tarr,tarrack,tarras,tarrass,tarred,tarrer,tarri,tarrie,tarrier,tarrify,tarrily,tarrish,tarrock,tarrow,tarry,tars,tarsal,tarsale,tarse,tarsi,tarsia,tarsier,tarsome,tarsus,tart,tartago,tartan,tartana,tartane,tartar,tarten,tartish,tartle,tartlet,tartly,tartro,tartryl,tarve,tarweed,tarwood,taryard,tasajo,tascal,tasco,tash,tashie,tashlik,tashrif,task,taskage,tasker,taskit,taslet,tass,tassago,tassah,tassal,tassard,tasse,tassel,tassely,tasser,tasset,tassie,tassoo,taste,tasted,tasten,taster,tastily,tasting,tasty,tasu,tat,tataupa,tatbeb,tatchy,tate,tater,tath,tatie,tatinek,tatler,tatou,tatouay,tatsman,tatta,tatter,tattery,tatther,tattied,tatting,tattle,tattler,tattoo,tattva,tatty,tatu,tau,taught,taula,taum,taun,taunt,taunter,taupe,taupo,taupou,taur,taurean,taurian,tauric,taurine,taurite,tauryl,taut,tautaug,tauted,tauten,tautit,tautly,tautog,tav,tave,tavell,taver,tavern,tavers,tavert,tavola,taw,tawa,tawdry,tawer,tawery,tawie,tawite,tawkee,tawkin,tawn,tawney,tawnily,tawnle,tawny,tawpi,tawpie,taws,tawse,tawtie,tax,taxable,taxably,taxator,taxed,taxeme,taxemic,taxer,taxi,taxibus,taxicab,taximan,taxine,taxing,taxis,taxite,taxitic,taxless,taxman,taxon,taxor,taxpaid,taxwax,taxy,tay,tayer,tayir,tayra,taysaam,tazia,tch,tchai,tcharik,tchast,tche,tchick,tchu,tck,te,tea,teabox,teaboy,teacake,teacart,teach,teache,teacher,teachy,teacup,tead,teadish,teaer,teaey,teagle,teaish,teaism,teak,teal,tealery,tealess,team,teaman,teameo,teamer,teaming,teamman,tean,teanal,teap,teapot,teapoy,tear,tearage,tearcat,tearer,tearful,tearing,tearlet,tearoom,tearpit,teart,teary,tease,teasel,teaser,teashop,teasing,teasler,teasy,teat,teated,teathe,teather,teatime,teatman,teaty,teave,teaware,teaze,teazer,tebbet,tec,teca,tecali,tech,techily,technic,techous,techy,teck,tecomin,tecon,tectal,tectum,tecum,tecuma,ted,tedder,tedge,tedious,tedium,tee,teedle,teel,teem,teemer,teemful,teeming,teems,teen,teenage,teenet,teens,teensy,teenty,teeny,teer,teerer,teest,teet,teetan,teeter,teeth,teethe,teethy,teeting,teety,teevee,teff,teg,tegmen,tegmina,tegua,tegula,tegular,tegumen,tehseel,tehsil,teicher,teil,teind,teinder,teioid,tejon,teju,tekiah,tekke,tekken,tektite,tekya,telamon,telang,telar,telary,tele,teledu,telega,teleost,teleran,telergy,telesia,telesis,teleuto,televox,telfer,telford,teli,telial,telic,telical,telium,tell,tellach,tellee,teller,telling,tellt,telome,telomic,telpath,telpher,telson,telt,telurgy,telyn,temacha,teman,tembe,temblor,temenos,temiak,temin,temp,temper,tempera,tempery,tempest,tempi,templar,temple,templed,templet,tempo,tempora,tempre,tempt,tempter,temse,temser,ten,tenable,tenably,tenace,tenai,tenancy,tenant,tench,tend,tendant,tendent,tender,tending,tendon,tendour,tendril,tendron,tenebra,tenent,teneral,tenet,tenfold,teng,tengere,tengu,tenible,tenio,tenline,tenne,tenner,tennis,tennisy,tenon,tenoner,tenor,tenpin,tenrec,tense,tensely,tensify,tensile,tension,tensity,tensive,tenson,tensor,tent,tentage,tented,tenter,tentful,tenth,tenthly,tentigo,tention,tentlet,tenture,tenty,tenuate,tenues,tenuis,tenuity,tenuous,tenure,teopan,tepache,tepal,tepee,tepefy,tepid,tepidly,tepor,tequila,tera,terap,teras,terbia,terbic,terbium,tercel,tercer,tercet,tercia,tercine,tercio,terebic,terebra,teredo,terek,terete,tereu,terfez,tergal,tergant,tergite,tergum,term,terma,termage,termen,termer,termin,termine,termini,termino,termite,termly,termon,termor,tern,terna,ternal,ternar,ternary,ternate,terne,ternery,ternion,ternize,ternlet,terp,terpane,terpene,terpin,terpine,terrace,terrage,terrain,terral,terrane,terrar,terrene,terret,terrier,terrify,terrine,terron,terror,terry,terse,tersely,tersion,tertia,tertial,tertian,tertius,terton,tervee,terzina,terzo,tesack,teskere,tessara,tessel,tessera,test,testa,testacy,testar,testata,testate,teste,tested,testee,tester,testes,testify,testily,testing,testis,teston,testone,testoon,testor,testril,testudo,testy,tetanic,tetanus,tetany,tetard,tetch,tetchy,tete,tetel,teth,tether,tethery,tetra,tetract,tetrad,tetrane,tetrazo,tetric,tetrode,tetrole,tetrose,tetryl,tetter,tettery,tettix,teucrin,teufit,teuk,teviss,tew,tewel,tewer,tewit,tewly,tewsome,text,textile,textlet,textman,textual,texture,tez,tezkere,th,tha,thack,thacker,thakur,thalami,thaler,thalli,thallic,thallus,thameng,than,thana,thanage,thanan,thane,thank,thankee,thanker,thanks,thapes,thapsia,thar,tharf,tharm,that,thatch,thatchy,thatn,thats,thaught,thave,thaw,thawer,thawn,thawy,the,theah,theasum,theat,theater,theatry,theave,theb,theca,thecae,thecal,thecate,thecia,thecium,thecla,theclan,thecoid,thee,theek,theeker,theelin,theelol,theer,theet,theezan,theft,thegn,thegnly,theine,their,theirn,theirs,theism,theist,thelium,them,thema,themata,theme,themer,themis,themsel,then,thenal,thenar,thence,theody,theorbo,theorem,theoria,theoric,theorum,theory,theow,therapy,there,thereas,thereat,thereby,therein,thereof,thereon,theres,therese,thereto,thereup,theriac,therial,therm,thermae,thermal,thermic,thermit,thermo,thermos,theroid,these,theses,thesial,thesis,theta,thetch,thetic,thetics,thetin,thetine,theurgy,thew,thewed,thewy,they,theyll,theyre,thiamin,thiasi,thiasoi,thiasos,thiasus,thick,thicken,thicket,thickly,thief,thienyl,thieve,thiever,thig,thigger,thigh,thighed,thight,thilk,thill,thiller,thilly,thimber,thimble,thin,thine,thing,thingal,thingly,thingum,thingy,think,thinker,thinly,thinner,thio,thiol,thiolic,thionic,thionyl,thir,third,thirdly,thirl,thirst,thirsty,thirt,thirty,this,thishow,thisn,thissen,thistle,thistly,thither,thiuram,thivel,thixle,tho,thob,thocht,thof,thoft,thoke,thokish,thole,tholi,tholoi,tholos,tholus,thon,thonder,thone,thong,thonged,thongy,thoo,thooid,thoom,thoral,thorax,thore,thoria,thoric,thorina,thorite,thorium,thorn,thorned,thornen,thorny,thoro,thoron,thorp,thort,thorter,those,thou,though,thought,thouse,thow,thowel,thowt,thrack,thraep,thrail,thrain,thrall,thram,thrang,thrap,thrash,thrast,thrave,thraver,thraw,thrawn,thread,thready,threap,threat,three,threne,threnos,threose,thresh,threw,thrice,thrift,thrifty,thrill,thrilly,thrimp,thring,thrip,thripel,thrips,thrive,thriven,thriver,thro,throat,throaty,throb,throck,throddy,throe,thronal,throne,throng,throu,throuch,through,throve,throw,thrower,thrown,thrum,thrummy,thrush,thrushy,thrust,thrutch,thruv,thrymsa,thud,thug,thugdom,thuggee,thujene,thujin,thujone,thujyl,thulia,thulir,thulite,thulium,thulr,thuluth,thumb,thumbed,thumber,thumble,thumby,thump,thumper,thunder,thung,thunge,thuoc,thurify,thurl,thurm,thurmus,thurse,thurt,thus,thusly,thutter,thwack,thwaite,thwart,thwite,thy,thyine,thymate,thyme,thymele,thymene,thymic,thymine,thymol,thymoma,thymus,thymy,thymyl,thynnid,thyroid,thyrse,thyrsus,thysel,thyself,thysen,ti,tiang,tiao,tiar,tiara,tib,tibby,tibet,tibey,tibia,tibiad,tibiae,tibial,tibiale,tiburon,tic,tical,ticca,tice,ticer,tick,ticked,ticken,ticker,ticket,tickey,tickie,ticking,tickle,tickled,tickler,tickly,tickney,ticky,ticul,tid,tidal,tidally,tidbit,tiddle,tiddler,tiddley,tiddy,tide,tided,tideful,tidely,tideway,tidily,tiding,tidings,tidley,tidy,tidyism,tie,tieback,tied,tien,tiepin,tier,tierce,tierced,tiered,tierer,tietick,tiewig,tiff,tiffany,tiffie,tiffin,tiffish,tiffle,tiffy,tift,tifter,tig,tige,tigella,tigelle,tiger,tigerly,tigery,tigger,tight,tighten,tightly,tights,tiglic,tignum,tigress,tigrine,tigroid,tigtag,tikka,tikker,tiklin,tikor,tikur,til,tilaite,tilaka,tilbury,tilde,tile,tiled,tiler,tilery,tilikum,tiling,till,tillage,tiller,tilley,tillite,tillot,tilly,tilmus,tilpah,tilt,tilter,tilth,tilting,tiltup,tilty,tilyer,timable,timar,timarau,timawa,timbal,timbale,timbang,timbe,timber,timbern,timbery,timbo,timbre,timbrel,time,timed,timeful,timely,timeous,timer,times,timid,timidly,timing,timish,timist,timon,timor,timothy,timpani,timpano,tin,tinamou,tincal,tinchel,tinclad,tinct,tind,tindal,tindalo,tinder,tindery,tine,tinea,tineal,tinean,tined,tineid,tineine,tineman,tineoid,tinety,tinful,ting,tinge,tinged,tinger,tingi,tingid,tingle,tingler,tingly,tinguy,tinhorn,tinily,tining,tink,tinker,tinkle,tinkler,tinkly,tinlet,tinlike,tinman,tinned,tinner,tinnery,tinnet,tinnily,tinning,tinnock,tinny,tinosa,tinsel,tinsman,tint,tinta,tintage,tinted,tinter,tintie,tinting,tintist,tinty,tintype,tinwald,tinware,tinwork,tiny,tip,tipburn,tipcart,tipcat,tipe,tipful,tiphead,tipiti,tiple,tipless,tiplet,tipman,tipmost,tiponi,tipped,tippee,tipper,tippet,tipping,tipple,tippler,tipply,tippy,tipsify,tipsily,tipster,tipsy,tiptail,tiptilt,tiptoe,tiptop,tipulid,tipup,tirade,tiralee,tire,tired,tiredly,tiredom,tireman,tirer,tiriba,tiring,tirl,tirma,tirr,tirret,tirrlie,tirve,tirwit,tisane,tisar,tissual,tissue,tissued,tissuey,tiswin,tit,titania,titanic,titano,titanyl,titar,titbit,tite,titer,titfish,tithal,tithe,tither,tithing,titi,titian,titien,titlark,title,titled,titler,titlike,titling,titlist,titmal,titman,titoki,titrate,titre,titter,tittery,tittie,tittle,tittler,tittup,tittupy,titty,titular,titule,titulus,tiver,tivoli,tivy,tiza,tizeur,tizzy,tji,tjosite,tlaco,tmema,tmesis,to,toa,toad,toadeat,toader,toadery,toadess,toadier,toadish,toadlet,toady,toast,toastee,toaster,toasty,toat,toatoa,tobacco,tobe,tobine,tobira,toby,tobyman,toccata,tocher,tock,toco,tocome,tocsin,tocusso,tod,today,todder,toddick,toddite,toddle,toddler,toddy,tode,tody,toe,toecap,toed,toeless,toelike,toenail,toetoe,toff,toffee,toffing,toffish,toffy,toft,tofter,toftman,tofu,tog,toga,togaed,togata,togate,togated,toggel,toggery,toggle,toggler,togless,togs,togt,togue,toher,toheroa,toho,tohunga,toi,toil,toiled,toiler,toilet,toilful,toiling,toise,toit,toitish,toity,tokay,toke,token,tokened,toko,tokopat,tol,tolan,tolane,told,toldo,tole,tolite,toll,tollage,toller,tollery,tolling,tollman,tolly,tolsey,tolt,tolter,tolu,toluate,toluene,toluic,toluide,toluido,toluol,toluyl,tolyl,toman,tomato,tomb,tombac,tombal,tombe,tombic,tomblet,tombola,tombolo,tomboy,tomcat,tomcod,tome,tomeful,tomelet,toment,tomfool,tomial,tomin,tomish,tomium,tomjohn,tomkin,tommy,tomnoup,tomorn,tomosis,tompon,tomtate,tomtit,ton,tonal,tonally,tonant,tondino,tone,toned,toneme,toner,tonetic,tong,tonga,tonger,tongman,tongs,tongue,tongued,tonguer,tonguey,tonic,tonify,tonight,tonish,tonite,tonjon,tonk,tonkin,tonlet,tonnage,tonneau,tonner,tonnish,tonous,tonsil,tonsor,tonsure,tontine,tonus,tony,too,toodle,took,tooken,tool,toolbox,tooler,tooling,toolman,toom,toomly,toon,toop,toorie,toorock,tooroo,toosh,toot,tooter,tooth,toothed,toother,toothy,tootle,tootler,tootsy,toozle,toozoo,top,toparch,topass,topaz,topazy,topcap,topcast,topcoat,tope,topee,topeng,topepo,toper,topfull,toph,tophus,topi,topia,topiary,topic,topical,topknot,topless,toplike,topline,topman,topmast,topmost,topo,toponym,topped,topper,topping,topple,toppler,topply,toppy,toprail,toprope,tops,topsail,topside,topsl,topsman,topsoil,toptail,topwise,toque,tor,tora,torah,toral,toran,torc,torcel,torch,torcher,torchon,tore,tored,torero,torfel,torgoch,toric,torii,torma,tormen,torment,tormina,torn,tornade,tornado,tornal,tornese,torney,tornote,tornus,toro,toroid,torose,torous,torpedo,torpent,torpid,torpify,torpor,torque,torqued,torques,torrefy,torrent,torrid,torsade,torse,torsel,torsile,torsion,torsive,torsk,torso,tort,torta,torteau,tortile,tortive,tortula,torture,toru,torula,torulin,torulus,torus,torve,torvid,torvity,torvous,tory,tosh,tosher,toshery,toshly,toshy,tosily,toss,tosser,tossily,tossing,tosspot,tossup,tossy,tost,toston,tosy,tot,total,totally,totara,totchka,tote,totem,totemic,totemy,toter,tother,totient,toto,totora,totquot,totter,tottery,totting,tottle,totty,totuava,totum,toty,totyman,tou,toucan,touch,touched,toucher,touchy,toug,tough,toughen,toughly,tought,tould,toumnah,toup,toupee,toupeed,toupet,tour,touraco,tourer,touring,tourism,tourist,tourize,tourn,tournay,tournee,tourney,tourte,tousche,touse,touser,tousle,tously,tousy,tout,touter,tovar,tow,towable,towage,towai,towan,toward,towards,towboat,towcock,towd,towel,towelry,tower,towered,towery,towght,towhead,towhee,towing,towkay,towlike,towline,towmast,town,towned,townee,towner,townet,townful,townify,townish,townist,townlet,townly,townman,towny,towpath,towrope,towser,towy,tox,toxa,toxamin,toxcatl,toxemia,toxemic,toxic,toxical,toxicum,toxifer,toxin,toxity,toxoid,toxon,toxone,toxosis,toxotae,toy,toydom,toyer,toyful,toying,toyish,toyland,toyless,toylike,toyman,toyon,toyshop,toysome,toytown,toywort,toze,tozee,tozer,tra,trabal,trabant,trabea,trabeae,trabuch,trace,tracer,tracery,trachea,trachle,tracing,track,tracked,tracker,tract,tractor,tradal,trade,trader,trading,tradite,traduce,trady,traffic,trag,tragal,tragedy,tragi,tragic,tragus,trah,traheen,traik,trail,trailer,traily,train,trained,trainee,trainer,trainy,traipse,trait,traitor,traject,trajet,tralira,tram,trama,tramal,tramcar,trame,tramful,tramman,trammel,trammer,trammon,tramp,tramper,trample,trampot,tramway,trance,tranced,traneen,trank,tranka,tranker,trankum,tranky,transit,transom,trant,tranter,trap,trapes,trapeze,trapped,trapper,trappy,traps,trash,traship,trashy,trass,trasy,trauma,travail,travale,trave,travel,travis,travois,travoy,trawl,trawler,tray,trayful,treacle,treacly,tread,treader,treadle,treason,treat,treatee,treater,treator,treaty,treble,trebly,treddle,tree,treed,treeful,treeify,treelet,treeman,treen,treetop,treey,tref,trefle,trefoil,tregerg,tregohm,trehala,trek,trekker,trellis,tremble,trembly,tremie,tremolo,tremor,trenail,trench,trend,trendle,trental,trepan,trepang,trepid,tress,tressed,tresson,tressy,trest,trestle,tret,trevet,trews,trey,tri,triable,triace,triacid,triact,triad,triadic,triaene,triage,trial,triamid,triarch,triarii,triatic,triaxon,triazin,triazo,tribade,tribady,tribal,tribase,tribble,tribe,triblet,tribrac,tribual,tribuna,tribune,tribute,trica,tricae,tricar,trice,triceps,trichi,trichia,trichy,trick,tricker,trickle,trickly,tricksy,tricky,triclad,tricorn,tricot,trident,triduan,triduum,tried,triedly,triene,triens,trier,trifa,trifid,trifle,trifler,triflet,trifoil,trifold,trifoly,triform,trig,trigamy,trigger,triglid,triglot,trigly,trigon,trigone,trigram,trigyn,trikaya,trike,triker,triketo,trikir,trilabe,trilby,trilit,trilite,trilith,trill,trillet,trilli,trillo,trilobe,trilogy,trim,trimer,trimly,trimmer,trin,trinal,trinary,trindle,trine,trinely,tringle,trinity,trink,trinket,trinkle,trinode,trinol,trintle,trio,triobol,triode,triodia,triole,triolet,trionym,trior,triose,trip,tripal,tripara,tripart,tripe,tripel,tripery,triple,triplet,triplex,triplum,triply,tripod,tripody,tripoli,tripos,tripper,trippet,tripple,tripsis,tripy,trireme,trisalt,trisazo,trisect,triseme,trishna,trismic,trismus,trisome,trisomy,trist,trisul,trisula,tritaph,trite,tritely,tritish,tritium,tritolo,triton,tritone,tritor,trityl,triumph,triunal,triune,triurid,trivant,trivet,trivia,trivial,trivium,trivvet,trizoic,trizone,troat,troca,trocar,trochal,troche,trochee,trochi,trochid,trochus,trock,troco,trod,trodden,trode,troft,trog,trogger,troggin,trogon,trogs,trogue,troika,troke,troker,troll,troller,trolley,trollol,trollop,trolly,tromba,trombe,trommel,tromp,trompe,trompil,tromple,tron,trona,tronage,tronc,trone,troner,troolie,troop,trooper,troot,tropal,tropary,tropate,trope,tropeic,troper,trophal,trophi,trophic,trophy,tropic,tropine,tropism,tropist,tropoyl,tropyl,trot,troth,trotlet,trotol,trotter,trottie,trotty,trotyl,trouble,troubly,trough,troughy,trounce,troupe,trouper,trouse,trouser,trout,trouter,trouty,trove,trover,trow,trowel,trowing,trowman,trowth,troy,truancy,truant,trub,trubu,truce,trucial,truck,trucker,truckle,trucks,truddo,trudge,trudgen,trudger,true,truer,truff,truffle,trug,truish,truism,trull,truller,trullo,truly,trummel,trump,trumper,trumpet,trumph,trumpie,trun,truncal,trunch,trundle,trunk,trunked,trunnel,trush,trusion,truss,trussed,trusser,trust,trustee,trusten,truster,trustle,trusty,truth,truthy,truvat,try,trygon,trying,tryma,tryout,tryp,trypa,trypan,trypsin,tryptic,trysail,tryst,tryster,tryt,tsadik,tsamba,tsantsa,tsar,tsardom,tsarina,tsatlee,tsere,tsetse,tsia,tsine,tst,tsuba,tsubo,tsun,tsunami,tsungtu,tu,tua,tuan,tuarn,tuart,tuatara,tuatera,tuath,tub,tuba,tubae,tubage,tubal,tubar,tubate,tubba,tubbal,tubbeck,tubber,tubbie,tubbing,tubbish,tubboe,tubby,tube,tubeful,tubelet,tubeman,tuber,tuberin,tubfish,tubful,tubicen,tubifer,tubig,tubik,tubing,tublet,tublike,tubman,tubular,tubule,tubulet,tubuli,tubulus,tuchit,tuchun,tuck,tucker,tucket,tucking,tuckner,tucktoo,tucky,tucum,tucuma,tucuman,tudel,tue,tueiron,tufa,tufan,tuff,tuffet,tuffing,tuft,tufted,tufter,tuftily,tufting,tuftlet,tufty,tug,tugboat,tugger,tuggery,tugging,tughra,tugless,tuglike,tugman,tugrik,tugui,tui,tuik,tuille,tuilyie,tuism,tuition,tuitive,tuke,tukra,tula,tulare,tulasi,tulchan,tulchin,tule,tuliac,tulip,tulipy,tulisan,tulle,tulsi,tulwar,tum,tumasha,tumbak,tumble,tumbled,tumbler,tumbly,tumbrel,tume,tumefy,tumid,tumidly,tummals,tummel,tummer,tummock,tummy,tumor,tumored,tump,tumtum,tumular,tumuli,tumult,tumulus,tun,tuna,tunable,tunably,tunca,tund,tunder,tundish,tundra,tundun,tune,tuned,tuneful,tuner,tunful,tung,tungate,tungo,tunhoof,tunic,tunicin,tunicle,tuning,tunish,tunist,tunk,tunket,tunlike,tunmoot,tunna,tunnel,tunner,tunnery,tunnor,tunny,tuno,tunu,tuny,tup,tupara,tupek,tupelo,tupik,tupman,tupuna,tuque,tur,turacin,turb,turban,turbary,turbeh,turbid,turbine,turbit,turbith,turbo,turbot,turco,turd,turdine,turdoid,tureen,turf,turfage,turfdom,turfed,turfen,turfing,turfite,turfman,turfy,turgent,turgid,turgite,turgoid,turgor,turgy,turio,turion,turjite,turk,turken,turkey,turkis,turkle,turm,turma,turment,turmit,turmoil,turn,turncap,turndun,turned,turnel,turner,turnery,turney,turning,turnip,turnipy,turnix,turnkey,turnoff,turnout,turnpin,turnrow,turns,turnup,turp,turpeth,turpid,turps,turr,turret,turse,tursio,turtle,turtler,turtlet,turtosa,tururi,turus,turwar,tusche,tush,tushed,tusher,tushery,tusk,tuskar,tusked,tusker,tuskish,tusky,tussah,tussal,tusser,tussis,tussive,tussle,tussock,tussore,tussur,tut,tutania,tutball,tute,tutee,tutela,tutelar,tutenag,tuth,tutin,tutly,tutman,tutor,tutorer,tutorly,tutory,tutoyer,tutress,tutrice,tutrix,tuts,tutsan,tutster,tutti,tutty,tutu,tutulus,tutwork,tuwi,tux,tuxedo,tuyere,tuza,tuzzle,twa,twaddle,twaddly,twaddy,twae,twagger,twain,twaite,twal,twale,twalt,twang,twanger,twangle,twangy,twank,twanker,twankle,twanky,twant,twarly,twas,twasome,twat,twattle,tway,twazzy,tweag,tweak,tweaker,tweaky,twee,tweed,tweeded,tweedle,tweedy,tweeg,tweel,tween,tweeny,tweesh,tweesht,tweest,tweet,tweeter,tweeze,tweezer,tweil,twelfth,twelve,twenty,twere,twerp,twibil,twice,twicer,twicet,twick,twiddle,twiddly,twifoil,twifold,twig,twigful,twigged,twiggen,twigger,twiggy,twiglet,twilit,twill,twilled,twiller,twilly,twilt,twin,twindle,twine,twiner,twinge,twingle,twinism,twink,twinkle,twinkly,twinly,twinned,twinner,twinter,twiny,twire,twirk,twirl,twirler,twirly,twiscar,twisel,twist,twisted,twister,twistle,twisty,twit,twitch,twitchy,twite,twitten,twitter,twitty,twixt,twizzle,two,twofold,twoling,twoness,twosome,tychism,tychite,tycoon,tyddyn,tydie,tye,tyee,tyg,tying,tyke,tyken,tykhana,tyking,tylarus,tylion,tyloma,tylopod,tylose,tylosis,tylote,tylotic,tylotus,tylus,tymp,tympan,tympana,tympani,tympany,tynd,typal,type,typer,typeset,typhia,typhic,typhlon,typhoid,typhoon,typhose,typhous,typhus,typic,typica,typical,typicon,typicum,typify,typist,typo,typobar,typonym,typp,typy,tyranny,tyrant,tyre,tyro,tyroma,tyrone,tyronic,tyrosyl,tyste,tyt,tzolkin,tzontle,u,uang,uayeb,uberant,uberous,uberty,ubi,ubiety,ubiquit,ubussu,uckia,udal,udaler,udaller,udalman,udasi,udder,uddered,udell,udo,ug,ugh,uglify,uglily,ugly,ugsome,uhlan,uhllo,uhtsong,uily,uinal,uintjie,uitspan,uji,ukase,uke,ukiyoye,ukulele,ula,ulcer,ulcered,ulcery,ule,ulema,uletic,ulex,ulexine,ulexite,ulitis,ull,ulla,ullage,ullaged,uller,ulling,ulluco,ulmic,ulmin,ulminic,ulmo,ulmous,ulna,ulnad,ulnae,ulnar,ulnare,ulnaria,uloid,uloncus,ulster,ultima,ultimo,ultimum,ultra,ulu,ulua,uluhi,ululant,ululate,ululu,um,umbel,umbeled,umbella,umber,umbilic,umble,umbo,umbonal,umbone,umbones,umbonic,umbra,umbrae,umbrage,umbral,umbrel,umbril,umbrine,umbrose,umbrous,ume,umiak,umiri,umlaut,ump,umph,umpire,umpirer,umpteen,umpty,umu,un,unable,unably,unact,unacted,unacute,unadapt,unadd,unadded,unadopt,unadorn,unadult,unafire,unaflow,unaged,unagile,unaging,unaided,unaimed,unaired,unakin,unakite,unal,unalarm,unalert,unalike,unalist,unalive,unallow,unalone,unaloud,unamend,unamiss,unamo,unample,unamply,unangry,unannex,unapart,unapt,unaptly,unarch,unark,unarm,unarmed,unarray,unarted,unary,unasked,unau,unavian,unawake,unaware,unaway,unawed,unawful,unawned,unaxled,unbag,unbain,unbait,unbaked,unbale,unbank,unbar,unbarb,unbare,unbark,unbase,unbased,unbaste,unbated,unbay,unbe,unbear,unbeard,unbeast,unbed,unbefit,unbeget,unbegot,unbegun,unbeing,unbell,unbelt,unbench,unbend,unbent,unberth,unbeset,unbesot,unbet,unbias,unbid,unbind,unbit,unbitt,unblade,unbled,unblent,unbless,unblest,unblind,unbliss,unblock,unbloom,unblown,unblued,unblush,unboat,unbody,unbog,unboggy,unbokel,unbold,unbolt,unbone,unboned,unbonny,unboot,unbored,unborn,unborne,unbosom,unbound,unbow,unbowed,unbowel,unbox,unboxed,unboy,unbrace,unbraid,unbran,unbrand,unbrave,unbraze,unbred,unbrent,unbrick,unbrief,unbroad,unbroke,unbrown,unbrute,unbud,unbuild,unbuilt,unbulky,unbung,unburly,unburn,unburnt,unburst,unbury,unbush,unbusk,unbusy,unbuxom,unca,uncage,uncaged,uncake,uncalk,uncall,uncalm,uncaned,uncanny,uncap,uncart,uncase,uncased,uncask,uncast,uncaste,uncate,uncave,unceded,unchain,unchair,uncharm,unchary,uncheat,uncheck,unchid,unchild,unchurn,unci,uncia,uncial,uncinal,uncinch,uncinct,uncini,uncinus,uncite,uncited,uncity,uncivic,uncivil,unclad,unclamp,unclasp,unclay,uncle,unclead,unclean,unclear,uncleft,unclew,unclick,unclify,unclimb,uncling,unclip,uncloak,unclog,unclose,uncloud,unclout,unclub,unco,uncoach,uncoat,uncock,uncoded,uncoif,uncoil,uncoin,uncoked,uncolt,uncoly,uncome,uncomfy,uncomic,uncoop,uncope,uncord,uncore,uncored,uncork,uncost,uncouch,uncous,uncouth,uncover,uncowed,uncowl,uncoy,uncram,uncramp,uncream,uncrest,uncrib,uncried,uncrime,uncrisp,uncrook,uncropt,uncross,uncrown,uncrude,uncruel,unction,uncubic,uncular,uncurb,uncurd,uncured,uncurl,uncurse,uncurst,uncus,uncut,uncuth,undaily,undam,undamn,undared,undark,undate,undated,undaub,undazed,unde,undead,undeaf,undealt,undean,undear,undeck,undecyl,undeep,undeft,undeify,undelve,unden,under,underdo,underer,undergo,underly,undern,undevil,undewed,undewy,undid,undies,undig,undight,undiked,undim,undine,undined,undirk,undo,undock,undoer,undog,undoing,undomed,undon,undone,undoped,undose,undosed,undowny,undrab,undrag,undrape,undraw,undrawn,undress,undried,undrunk,undry,undub,unducal,undue,undug,unduke,undular,undull,unduly,unduped,undust,unduty,undwelt,undy,undye,undyed,undying,uneager,unearly,unearth,unease,uneasy,uneaten,uneath,unebbed,unedge,unedged,unelect,unempt,unempty,unended,unepic,unequal,unerect,unethic,uneven,unevil,unexact,uneye,uneyed,unface,unfaced,unfact,unfaded,unfain,unfaint,unfair,unfaith,unfaked,unfalse,unfamed,unfancy,unfar,unfast,unfeary,unfed,unfeed,unfele,unfelon,unfelt,unfence,unfeted,unfeued,unfew,unfiber,unfiend,unfiery,unfight,unfile,unfiled,unfill,unfilm,unfine,unfined,unfired,unfirm,unfit,unfitly,unfitty,unfix,unfixed,unflag,unflaky,unflank,unflat,unflead,unflesh,unflock,unfloor,unflown,unfluid,unflush,unfoggy,unfold,unfond,unfool,unfork,unform,unfoul,unfound,unfoxy,unfrail,unframe,unfrank,unfree,unfreed,unfret,unfried,unfrill,unfrizz,unfrock,unfrost,unfroze,unfull,unfully,unfumed,unfunny,unfur,unfurl,unfused,unfussy,ungag,ungaged,ungain,ungaite,ungaro,ungaudy,ungear,ungelt,unget,ungiant,ungiddy,ungild,ungill,ungilt,ungird,ungirt,ungirth,ungive,ungiven,ungka,unglad,unglaze,unglee,unglobe,ungloom,unglory,ungloss,unglove,unglue,unglued,ungnaw,ungnawn,ungod,ungodly,ungold,ungone,ungood,ungored,ungorge,ungot,ungouty,ungown,ungrace,ungraft,ungrain,ungrand,ungrasp,ungrave,ungreat,ungreen,ungrip,ungripe,ungross,ungrow,ungrown,ungruff,ungual,unguard,ungueal,unguent,ungues,unguis,ungula,ungulae,ungular,unguled,ungull,ungulp,ungum,unguyed,ungyve,ungyved,unhabit,unhad,unhaft,unhair,unhairy,unhand,unhandy,unhang,unhap,unhappy,unhard,unhardy,unharsh,unhasp,unhaste,unhasty,unhat,unhate,unhated,unhaunt,unhave,unhayed,unhazed,unhead,unheady,unheal,unheard,unheart,unheavy,unhedge,unheed,unheedy,unheld,unhele,unheler,unhelm,unherd,unhero,unhewed,unhewn,unhex,unhid,unhide,unhigh,unhinge,unhired,unhit,unhitch,unhive,unhoard,unhoary,unhoed,unhoist,unhold,unholy,unhome,unhoned,unhood,unhook,unhoop,unhoped,unhorny,unhorse,unhose,unhosed,unhot,unhouse,unhull,unhuman,unhumid,unhung,unhurt,unhusk,uniat,uniate,uniaxal,unible,unice,uniced,unicell,unicism,unicist,unicity,unicorn,unicum,unideal,unidle,unidly,unie,uniface,unific,unified,unifier,uniflow,uniform,unify,unilobe,unimped,uninked,uninn,unio,unioid,union,unioned,unionic,unionid,unioval,unipara,uniped,unipod,unique,unireme,unisoil,unison,unit,unitage,unital,unitary,unite,united,uniter,uniting,unition,unitism,unitive,unitize,unitude,unity,univied,unjaded,unjam,unjewel,unjoin,unjoint,unjolly,unjoyed,unjudge,unjuicy,unjust,unkamed,unked,unkempt,unken,unkept,unket,unkey,unkeyed,unkid,unkill,unkin,unkind,unking,unkink,unkirk,unkiss,unkist,unknave,unknew,unknit,unknot,unknow,unknown,unlace,unlaced,unlade,unladen,unlaid,unlame,unlamed,unland,unlap,unlarge,unlash,unlatch,unlath,unlaugh,unlaved,unlaw,unlawed,unlawly,unlay,unlead,unleaf,unleaky,unleal,unlean,unlearn,unleash,unleave,unled,unleft,unlegal,unlent,unless,unlet,unlevel,unlid,unlie,unlight,unlike,unliked,unliken,unlimb,unlime,unlimed,unlimp,unline,unlined,unlink,unlist,unlisty,unlit,unlive,unload,unloath,unlobed,unlocal,unlock,unlodge,unlofty,unlogic,unlook,unloop,unloose,unlord,unlost,unlousy,unlove,unloved,unlowly,unloyal,unlucid,unluck,unlucky,unlunar,unlured,unlust,unlusty,unlute,unluted,unlying,unmad,unmade,unmagic,unmaid,unmail,unmake,unmaker,unman,unmaned,unmanly,unmarch,unmarry,unmask,unmast,unmate,unmated,unmaze,unmeant,unmeek,unmeet,unmerge,unmerry,unmesh,unmet,unmeted,unmew,unmewed,unmind,unmined,unmired,unmiry,unmist,unmiter,unmix,unmixed,unmodel,unmoist,unmold,unmoldy,unmoor,unmoral,unmount,unmoved,unmowed,unmown,unmuddy,unmuted,unnail,unnaked,unname,unnamed,unneat,unneedy,unnegro,unnerve,unnest,unneth,unnethe,unnew,unnewly,unnice,unnigh,unnoble,unnobly,unnose,unnosed,unnoted,unnovel,unoared,unobese,unode,unoften,unogled,unoil,unoiled,unoily,unold,unoped,unopen,unorbed,unorder,unorn,unornly,unovert,unowed,unowing,unown,unowned,unpaced,unpack,unpagan,unpaged,unpaid,unpaint,unpale,unpaled,unpanel,unpapal,unpaper,unparch,unpared,unpark,unparty,unpass,unpaste,unpave,unpaved,unpawed,unpawn,unpeace,unpeel,unpeg,unpen,unpenal,unpent,unperch,unpetal,unpick,unpiece,unpiety,unpile,unpiled,unpin,unpious,unpiped,unplace,unplaid,unplain,unplait,unplan,unplank,unplant,unplat,unpleat,unplied,unplow,unplug,unplumb,unplume,unplump,unpoise,unpoled,unpope,unposed,unpot,unpower,unpray,unprim,unprime,unprint,unprop,unproud,unpure,unpurse,unput,unqueen,unquick,unquiet,unquit,unquote,unraced,unrack,unrainy,unrake,unraked,unram,unrank,unraped,unrare,unrash,unrated,unravel,unray,unrayed,unrazed,unread,unready,unreal,unreave,unrebel,unred,unreel,unreeve,unregal,unrein,unrent,unrest,unresty,unrhyme,unrich,unricht,unrid,unride,unrife,unrig,unright,unrigid,unrind,unring,unrip,unripe,unriped,unrisen,unrisky,unrived,unriven,unrivet,unroast,unrobe,unrobed,unroll,unroof,unroomy,unroost,unroot,unrope,unroped,unrosed,unroted,unrough,unround,unrove,unroved,unrow,unrowed,unroyal,unrule,unruled,unruly,unrun,unrung,unrural,unrust,unruth,unsack,unsad,unsafe,unsage,unsaid,unsaint,unsalt,unsane,unsappy,unsash,unsated,unsatin,unsaved,unsawed,unsawn,unsay,unscale,unscaly,unscarb,unscent,unscrew,unseal,unseam,unseat,unsee,unseen,unself,unsense,unsent,unset,unsew,unsewed,unsewn,unsex,unsexed,unshade,unshady,unshape,unsharp,unshawl,unsheaf,unshed,unsheet,unshell,unship,unshod,unshoe,unshoed,unshop,unshore,unshorn,unshort,unshot,unshown,unshowy,unshrew,unshut,unshy,unshyly,unsick,unsided,unsiege,unsight,unsilly,unsin,unsinew,unsing,unsized,unskin,unslack,unslain,unslate,unslave,unsleek,unslept,unsling,unslip,unslit,unslot,unslow,unslung,unsly,unsmart,unsmoky,unsmote,unsnaky,unsnap,unsnare,unsnarl,unsneck,unsnib,unsnow,unsober,unsoft,unsoggy,unsoil,unsolar,unsold,unsole,unsoled,unsolid,unsome,unson,unsonsy,unsooty,unsore,unsorry,unsort,unsoul,unsound,unsour,unsowed,unsown,unspan,unspar,unspeak,unsped,unspeed,unspell,unspelt,unspent,unspicy,unspied,unspike,unspin,unspit,unsplit,unspoil,unspot,unspun,unstack,unstagy,unstaid,unstain,unstar,unstate,unsteck,unsteel,unsteep,unstep,unstern,unstick,unstill,unsting,unstock,unstoic,unstone,unstony,unstop,unstore,unstout,unstow,unstrap,unstrip,unstuck,unstuff,unstung,unsty,unsued,unsuit,unsulky,unsun,unsung,unsunk,unsunny,unsure,unswear,unsweat,unsweet,unswell,unswept,unswing,unsworn,unswung,untack,untaint,untaken,untall,untame,untamed,untap,untaped,untar,untaste,untasty,untaut,untawed,untax,untaxed,unteach,unteam,unteem,untell,untense,untent,untenty,untewed,unthank,unthaw,unthick,unthink,unthorn,unthrid,unthrob,untidal,untidy,untie,untied,untight,until,untile,untiled,untill,untilt,untimed,untin,untinct,untine,untipt,untire,untired,unto,untold,untomb,untone,untoned,untooth,untop,untorn,untouch,untough,untown,untrace,untrain,untread,untreed,untress,untried,untrig,untrill,untrim,untripe,untrite,untrod,untruck,untrue,untruly,untruss,untrust,untruth,untuck,untumid,untune,untuned,unturf,unturn,untwine,untwirl,untwist,untying,untz,unugly,unultra,unupset,unurban,unurged,unurn,unurned,unuse,unused,unusual,unvain,unvalid,unvalue,unveil,unvenom,unvest,unvexed,unvicar,unvisor,unvital,unvivid,unvocal,unvoice,unvote,unvoted,unvowed,unwaded,unwaged,unwaked,unwall,unwan,unware,unwarm,unwarn,unwarp,unwary,unwater,unwaved,unwax,unwaxed,unwayed,unweal,unweary,unweave,unweb,unwed,unwedge,unweel,unweft,unweld,unwell,unwept,unwet,unwheel,unwhig,unwhip,unwhite,unwield,unwifed,unwig,unwild,unwill,unwily,unwind,unwindy,unwiped,unwire,unwired,unwise,unwish,unwist,unwitch,unwitty,unwive,unwived,unwoful,unwoman,unwomb,unwon,unwooed,unwoof,unwooly,unwordy,unwork,unworld,unwormy,unworn,unworth,unwound,unwoven,unwrap,unwrit,unwrite,unwrung,unyoke,unyoked,unyoung,unze,unzen,unzone,unzoned,up,upaisle,upalley,upalong,uparch,uparise,uparm,uparna,upas,upattic,upbank,upbar,upbay,upbear,upbeat,upbelch,upbelt,upbend,upbid,upbind,upblast,upblaze,upblow,upboil,upbolt,upboost,upborne,upbotch,upbound,upbrace,upbraid,upbray,upbreak,upbred,upbreed,upbrim,upbring,upbrook,upbrow,upbuild,upbuoy,upburn,upburst,upbuy,upcall,upcanal,upcarry,upcast,upcatch,upchoke,upchuck,upcity,upclimb,upclose,upcoast,upcock,upcoil,upcome,upcover,upcrane,upcrawl,upcreek,upcreep,upcrop,upcrowd,upcry,upcurl,upcurve,upcut,updart,update,updeck,updelve,updive,updo,updome,updraft,updrag,updraw,updrink,updry,upeat,upend,upeygan,upfeed,upfield,upfill,upflame,upflare,upflash,upflee,upfling,upfloat,upflood,upflow,upflung,upfly,upfold,upframe,upfurl,upgale,upgang,upgape,upgaze,upget,upgird,upgirt,upgive,upglean,upglide,upgo,upgorge,upgrade,upgrave,upgrow,upgully,upgush,uphand,uphang,uphasp,upheal,upheap,upheave,upheld,uphelm,uphelya,upher,uphill,uphoard,uphoist,uphold,uphung,uphurl,upjerk,upjet,upkeep,upknell,upknit,upla,uplaid,uplake,upland,uplane,uplay,uplead,upleap,upleg,uplick,uplift,uplight,uplimb,upline,uplock,uplong,uplook,uploom,uploop,uplying,upmast,upmix,upmost,upmount,upmove,upness,upo,upon,uppard,uppent,upper,upperch,upperer,uppers,uppile,upping,uppish,uppity,upplow,uppluck,uppoint,uppoise,uppop,uppour,uppowoc,upprick,upprop,uppuff,uppull,uppush,upraise,upreach,uprear,uprein,uprend,uprest,uprid,upridge,upright,uprip,uprisal,uprise,uprisen,upriser,uprist,uprive,upriver,uproad,uproar,uproom,uproot,uprose,uprouse,uproute,uprun,uprush,upscale,upscrew,upseal,upseek,upseize,upsend,upset,upsey,upshaft,upshear,upshoot,upshore,upshot,upshove,upshut,upside,upsides,upsilon,upsit,upslant,upslip,upslope,upsmite,upsoak,upsoar,upsolve,upspeak,upspear,upspeed,upspew,upspin,upspire,upspout,upspurt,upstaff,upstage,upstair,upstamp,upstand,upstare,upstart,upstate,upstay,upsteal,upsteam,upstem,upstep,upstick,upstir,upsuck,upsun,upsup,upsurge,upswarm,upsway,upsweep,upswell,upswing,uptable,uptake,uptaker,uptear,uptend,upthrow,uptide,uptie,uptill,uptilt,uptorn,uptoss,uptower,uptown,uptrace,uptrack,uptrail,uptrain,uptree,uptrend,uptrill,uptrunk,uptruss,uptube,uptuck,upturn,uptwist,upupoid,upvomit,upwaft,upwall,upward,upwards,upwarp,upwax,upway,upways,upwell,upwent,upwheel,upwhelm,upwhir,upwhirl,upwind,upwith,upwork,upwound,upwrap,upwring,upyard,upyoke,ur,ura,urachal,urachus,uracil,uraemic,uraeus,ural,urali,uraline,uralite,uralium,uramido,uramil,uramino,uran,uranate,uranic,uraniid,uranin,uranine,uranion,uranism,uranist,uranite,uranium,uranous,uranyl,urao,urare,urari,urase,urate,uratic,uratoma,urazine,urazole,urban,urbane,urbian,urbic,urbify,urceole,urceoli,urceus,urchin,urd,urde,urdee,ure,urea,ureal,urease,uredema,uredine,uredo,ureic,ureid,ureide,ureido,uremia,uremic,urent,uresis,uretal,ureter,urethan,urethra,uretic,urf,urge,urgence,urgency,urgent,urger,urging,urheen,urial,uric,urinal,urinant,urinary,urinate,urine,urinose,urinous,urite,urlar,urled,urling,urluch,urman,urn,urna,urnae,urnal,urnful,urning,urnism,urnlike,urocele,urocyst,urodele,urogram,urohyal,urolith,urology,uromere,uronic,uropod,urosis,urosome,urostea,urotoxy,uroxin,ursal,ursine,ursoid,ursolic,urson,ursone,ursuk,urtica,urtite,urubu,urucu,urucuri,uruisg,urunday,urus,urushi,urushic,urva,us,usable,usage,usager,usance,usar,usara,usaron,usation,use,used,usedly,usednt,usee,useful,usehold,useless,usent,user,ush,ushabti,usher,usherer,usings,usitate,usnea,usneoid,usnic,usninic,usque,usself,ussels,ust,uster,ustion,usual,usually,usuary,usucapt,usure,usurer,usuress,usurp,usurper,usurpor,usury,usward,uswards,ut,uta,utahite,utai,utas,utch,utchy,utees,utensil,uteri,uterine,uterus,utick,utile,utility,utilize,utinam,utmost,utopia,utopian,utopism,utopist,utricle,utricul,utrubi,utrum,utsuk,utter,utterer,utterly,utu,utum,uva,uval,uvalha,uvanite,uvate,uvea,uveal,uveitic,uveitis,uveous,uvic,uvid,uviol,uvitic,uvito,uvrou,uvula,uvulae,uvular,uvver,uxorial,uzan,uzara,uzarin,uzaron,v,vaagmer,vaalite,vacancy,vacant,vacate,vacatur,vaccary,vaccina,vaccine,vache,vacoa,vacona,vacoua,vacouf,vacual,vacuate,vacuefy,vacuist,vacuity,vacuole,vacuome,vacuous,vacuum,vacuuma,vade,vadium,vadose,vady,vag,vagal,vagary,vagas,vage,vagile,vagina,vaginal,vagitus,vagrant,vagrate,vagrom,vague,vaguely,vaguish,vaguity,vagus,vahine,vail,vain,vainful,vainly,vair,vairagi,vaire,vairy,vaivode,vajra,vakass,vakia,vakil,valance,vale,valence,valency,valent,valeral,valeric,valerin,valeryl,valet,valeta,valetry,valeur,valgoid,valgus,valhall,vali,valiant,valid,validly,valine,valise,vall,vallar,vallary,vallate,valley,vallis,vallum,valonia,valor,valse,valsoid,valuate,value,valued,valuer,valuta,valva,valval,valvate,valve,valved,valvula,valvule,valyl,vamfont,vamoose,vamp,vamped,vamper,vampire,van,vanadic,vanadyl,vane,vaned,vanfoss,vang,vangee,vangeli,vanglo,vanilla,vanille,vanish,vanity,vanman,vanmost,vanner,vannet,vansire,vantage,vanward,vapid,vapidly,vapor,vapored,vaporer,vapory,vara,varahan,varan,varanid,vardy,vare,varec,vareuse,vari,variant,variate,varical,varices,varied,varier,variety,variola,variole,various,varisse,varix,varlet,varment,varna,varnish,varsha,varsity,varus,varve,varved,vary,vas,vasa,vasal,vase,vaseful,vaselet,vassal,vast,vastate,vastily,vastity,vastly,vasty,vasu,vat,vatful,vatic,vatman,vatter,vau,vaudy,vault,vaulted,vaulter,vaulty,vaunt,vaunted,vaunter,vaunty,vauxite,vavasor,vaward,veal,vealer,vealy,vection,vectis,vector,vecture,vedana,vedette,vedika,vedro,veduis,vee,veen,veep,veer,veery,vegetal,vegete,vehicle,vei,veigle,veil,veiled,veiler,veiling,veily,vein,veinage,veinal,veined,veiner,veinery,veining,veinlet,veinous,veinule,veiny,vejoces,vela,velal,velamen,velar,velaric,velary,velate,velated,veldman,veldt,velic,veliger,vell,vellala,velleda,vellon,vellum,vellumy,velo,velours,velte,velum,velumen,velure,velvet,velvety,venada,venal,venally,venatic,venator,vencola,vend,vendace,vendee,vender,vending,vendor,vendue,veneer,venene,veneral,venerer,venery,venesia,venger,venial,venie,venin,venison,vennel,venner,venom,venomed,venomer,venomly,venomy,venosal,venose,venous,vent,ventage,ventail,venter,ventil,ventose,ventrad,ventral,ventric,venture,venue,venula,venular,venule,venust,vera,veranda,verb,verbal,verbate,verbena,verbene,verbid,verbify,verbile,verbose,verbous,verby,verchok,verd,verdant,verdea,verdet,verdict,verdin,verdoy,verdun,verdure,verek,verge,vergent,verger,vergery,vergi,verglas,veri,veridic,verify,verily,verine,verism,verist,verite,verity,vermeil,vermian,vermin,verminy,vermis,vermix,vernal,vernant,vernier,vernile,vernin,vernine,verre,verrel,verruca,verruga,versal,versant,versate,verse,versed,verser,verset,versify,versine,version,verso,versor,verst,versta,versual,versus,vert,vertex,vertigo,veruled,vervain,verve,vervel,vervet,very,vesania,vesanic,vesbite,vesicae,vesical,vesicle,veskit,vespal,vesper,vespers,vespery,vespid,vespine,vespoid,vessel,vest,vestal,vestee,vester,vestige,vesting,vestlet,vestral,vestry,vesture,vet,veta,vetanda,vetch,vetchy,veteran,vetiver,veto,vetoer,vetoism,vetoist,vetust,vetusty,veuve,vex,vexable,vexed,vexedly,vexer,vexful,vexil,vext,via,viable,viaduct,viagram,viajaca,vial,vialful,viand,viander,viatic,viatica,viator,vibex,vibgyor,vibix,vibrant,vibrate,vibrato,vibrion,vicar,vicarly,vice,viceroy,vicety,vicilin,vicinal,vicine,vicious,vicoite,victim,victor,victory,victrix,victual,vicuna,viddui,video,vidette,vidonia,vidry,viduage,vidual,viduate,viduine,viduity,viduous,vidya,vie,vielle,vier,viertel,view,viewer,viewly,viewy,vifda,viga,vigia,vigil,vignin,vigonia,vigor,vihara,vihuela,vijao,viking,vila,vilayet,vile,vilely,vilify,vility,vill,villa,village,villain,villar,villate,ville,villein,villoid,villose,villous,villus,vim,vimana,vimen,vimful,viminal,vina,vinage,vinal,vinasse,vinata,vincent,vindex,vine,vinea,vineal,vined,vinegar,vineity,vinelet,viner,vinery,vinic,vinny,vino,vinose,vinous,vint,vinta,vintage,vintem,vintner,vintry,viny,vinyl,vinylic,viol,viola,violal,violate,violent,violer,violet,violety,violin,violina,violine,violist,violon,violone,viper,viperan,viperid,vipery,viqueen,viragin,virago,viral,vire,virelay,viremia,viremic,virent,vireo,virga,virgal,virgate,virgin,virgula,virgule,virial,virid,virific,virify,virile,virl,virole,viroled,viron,virose,virosis,virous,virtu,virtual,virtue,virtued,viruela,virus,vis,visa,visage,visaged,visarga,viscera,viscid,viscin,viscose,viscous,viscus,vise,viseman,visible,visibly,visie,visile,vision,visit,visita,visite,visitee,visiter,visitor,visive,visne,vison,visor,vista,vistaed,vistal,visto,visual,vita,vital,vitalic,vitally,vitals,vitamer,vitamin,vitasti,vitiate,vitium,vitrage,vitrail,vitrain,vitraux,vitreal,vitrean,vitreum,vitric,vitrics,vitrify,vitrine,vitriol,vitrite,vitrous,vitta,vittate,vitular,viuva,viva,vivary,vivax,vive,vively,vivency,viver,vivers,vives,vivid,vividly,vivific,vivify,vixen,vixenly,vizard,vizier,vlei,voar,vocable,vocably,vocal,vocalic,vocally,vocate,vocular,vocule,vodka,voe,voet,voeten,vog,voglite,vogue,voguey,voguish,voice,voiced,voicer,voicing,void,voided,voidee,voider,voiding,voidly,voile,voivode,vol,volable,volage,volant,volar,volata,volatic,volcan,volcano,vole,volency,volent,volery,volet,volley,volost,volt,voltage,voltaic,voltize,voluble,volubly,volume,volumed,volupt,volupty,voluta,volute,voluted,volutin,volva,volvate,volvent,vomer,vomica,vomit,vomiter,vomito,vomitus,voodoo,vorago,vorant,vorhand,vorpal,vortex,vota,votable,votal,votally,votary,vote,voteen,voter,voting,votive,votress,vouch,vouchee,voucher,vouge,vow,vowed,vowel,vowely,vower,vowess,vowless,voyage,voyager,voyance,voyeur,vraic,vrbaite,vriddhi,vrother,vug,vuggy,vulgar,vulgare,vulgate,vulgus,vuln,vulnose,vulpic,vulpine,vulture,vulturn,vulva,vulval,vulvar,vulvate,vum,vying,vyingly,w,wa,waag,waapa,waar,wab,wabber,wabble,wabbly,wabby,wabe,wabeno,wabster,wacago,wace,wachna,wack,wacke,wacken,wacker,wacky,wad,waddent,wadder,wadding,waddler,waddly,waddy,wade,wader,wadi,wading,wadlike,wadmal,wadmeal,wadna,wadset,wae,waeg,waer,waesome,waesuck,wafer,waferer,wafery,waff,waffle,waffly,waft,waftage,wafter,wafture,wafty,wag,wagaun,wage,waged,wagedom,wager,wagerer,wages,waggel,wagger,waggery,waggie,waggish,waggle,waggly,waggy,waglike,wagling,wagon,wagoner,wagonry,wagsome,wagtail,wagwag,wagwit,wah,wahahe,wahine,wahoo,waiata,waif,waik,waikly,wail,wailer,wailful,waily,wain,wainage,wainer,wainful,wainman,waipiro,wairch,waird,wairepo,wairsh,waise,waist,waisted,waister,wait,waiter,waiting,waive,waiver,waivery,waivod,waiwode,wajang,waka,wakan,wake,wakeel,wakeful,waken,wakener,waker,wakes,wakf,wakif,wakiki,waking,wakiup,wakken,wakon,wakonda,waky,walahee,wale,waled,waler,wali,waling,walk,walker,walking,walkist,walkout,walkway,wall,wallaba,wallaby,wallah,walled,waller,wallet,walleye,wallful,walling,wallise,wallman,walloon,wallop,wallow,wally,walnut,walrus,walsh,walt,walter,walth,waltz,waltzer,wamara,wambais,wamble,wambly,wame,wamefou,wamel,wamp,wampee,wample,wampum,wampus,wamus,wan,wand,wander,wandery,wandle,wandoo,wandy,wane,waned,wang,wanga,wangala,wangan,wanghee,wangle,wangler,wanhope,wanhorn,wanigan,waning,wankle,wankly,wanle,wanly,wanner,wanness,wannish,wanny,wanrufe,want,wantage,wanter,wantful,wanting,wanton,wantwit,wanty,wany,wap,wapacut,wapatoo,wapiti,wapp,wapper,wapping,war,warabi,waratah,warble,warbled,warbler,warblet,warbly,warch,ward,wardage,warday,warded,warden,warder,warding,wardite,wardman,ware,warehou,wareman,warf,warfare,warful,warily,warish,warison,wark,warl,warless,warlike,warlock,warluck,warly,warm,warman,warmed,warmer,warmful,warming,warmish,warmly,warmth,warmus,warn,warnel,warner,warning,warnish,warnoth,warnt,warp,warpage,warped,warper,warping,warple,warran,warrand,warrant,warree,warren,warrer,warrin,warrior,warrok,warsaw,warse,warsel,warship,warsle,warsler,warst,wart,warted,wartern,warth,wartime,wartlet,warty,warve,warwolf,warworn,wary,was,wasabi,wase,wasel,wash,washday,washed,washen,washer,washery,washin,washing,washman,washoff,washout,washpot,washrag,washtub,washway,washy,wasnt,wasp,waspen,waspily,waspish,waspy,wassail,wassie,wast,wastage,waste,wasted,wastel,waster,wasting,wastrel,wasty,wat,watap,watch,watched,watcher,water,watered,waterer,waterie,watery,wath,watt,wattage,wattape,wattle,wattled,wattman,wauble,wauch,wauchle,waucht,wauf,waugh,waughy,wauken,waukit,waul,waumle,wauner,wauns,waup,waur,wauve,wavable,wavably,wave,waved,wavelet,waver,waverer,wavery,waveson,wavey,wavicle,wavily,waving,wavy,waw,wawa,wawah,wax,waxbill,waxbird,waxbush,waxen,waxer,waxily,waxing,waxlike,waxman,waxweed,waxwing,waxwork,waxy,way,wayaka,wayang,wayback,waybill,waybird,waybook,waybung,wayfare,waygang,waygate,waygone,waying,waylaid,waylay,wayless,wayman,waymark,waymate,waypost,ways,wayside,wayward,waywode,wayworn,waywort,we,weak,weaken,weakish,weakly,weaky,weal,weald,wealth,wealthy,weam,wean,weanel,weaner,weanyer,weapon,wear,wearer,wearied,wearier,wearily,wearing,wearish,weary,weasand,weasel,weaser,weason,weather,weave,weaved,weaver,weaving,weazen,weazeny,web,webbed,webber,webbing,webby,weber,webeye,webfoot,webless,weblike,webster,webwork,webworm,wecht,wed,wedana,wedbed,wedded,wedder,wedding,wede,wedge,wedged,wedger,wedging,wedgy,wedlock,wedset,wee,weeble,weed,weeda,weedage,weeded,weeder,weedery,weedful,weedish,weedow,weedy,week,weekday,weekend,weekly,weekwam,weel,weemen,ween,weeness,weening,weenong,weeny,weep,weeper,weepful,weeping,weeps,weepy,weesh,weeshy,weet,weever,weevil,weevily,weewow,weeze,weft,weftage,wefted,wefty,weigh,weighed,weigher,weighin,weight,weighty,weir,weird,weirdly,weiring,weism,wejack,weka,wekau,wekeen,weki,welcome,weld,welder,welding,weldor,welfare,welk,welkin,well,wellat,welling,wellish,wellman,welly,wels,welsh,welsher,welsium,welt,welted,welter,welting,wem,wemless,wen,wench,wencher,wend,wende,wene,wennish,wenny,went,wenzel,wept,wer,were,werefox,werent,werf,wergil,weri,wert,wervel,wese,weskit,west,weste,wester,western,westing,westy,wet,weta,wetback,wetbird,wetched,wetchet,wether,wetly,wetness,wetted,wetter,wetting,wettish,weve,wevet,wey,wha,whabby,whack,whacker,whacky,whale,whaler,whalery,whaling,whalish,whally,whalm,whalp,whaly,wham,whamble,whame,whammle,whamp,whampee,whample,whan,whand,whang,whangam,whangee,whank,whap,whappet,whapuka,whapuku,whar,whare,whareer,wharf,wharl,wharp,wharry,whart,wharve,whase,whasle,what,whata,whatkin,whatna,whatnot,whats,whatso,whatten,whau,whauk,whaup,whaur,whauve,wheal,whealy,wheam,wheat,wheaten,wheaty,whedder,whee,wheedle,wheel,wheeled,wheeler,wheely,wheem,wheen,wheenge,wheep,wheeple,wheer,wheesht,wheetle,wheeze,wheezer,wheezle,wheezy,wheft,whein,whekau,wheki,whelk,whelked,whelker,whelky,whelm,whelp,whelve,whemmel,when,whenas,whence,wheneer,whenso,where,whereas,whereat,whereby,whereer,wherein,whereof,whereon,whereso,whereto,whereup,wherret,wherrit,wherry,whet,whether,whetile,whetter,whew,whewer,whewl,whewt,whey,wheyey,wheyish,whiba,which,whick,whicken,whicker,whid,whidah,whidder,whiff,whiffer,whiffet,whiffle,whiffy,whift,whig,while,whileen,whilere,whiles,whilie,whilk,whill,whilly,whilock,whilom,whils,whilst,whilter,whim,whimble,whimmy,whimper,whimsey,whimsic,whin,whincow,whindle,whine,whiner,whing,whinge,whinger,whinnel,whinner,whinny,whiny,whip,whipcat,whipman,whippa,whipped,whipper,whippet,whippy,whipsaw,whipt,whir,whirken,whirl,whirled,whirler,whirley,whirly,whirret,whirrey,whirroo,whirry,whirtle,whish,whisk,whisker,whiskey,whisky,whisp,whisper,whissle,whist,whister,whistle,whistly,whit,white,whited,whitely,whiten,whites,whither,whiting,whitish,whitlow,whits,whittaw,whitten,whitter,whittle,whity,whiz,whizgig,whizzer,whizzle,who,whoa,whoever,whole,wholly,whom,whomble,whomso,whone,whoo,whoof,whoop,whoopee,whooper,whoops,whoosh,whop,whopper,whorage,whore,whorish,whorl,whorled,whorly,whort,whortle,whose,whosen,whud,whuff,whuffle,whulk,whulter,whummle,whun,whup,whush,whuskie,whussle,whute,whuther,whutter,whuz,why,whyever,whyfor,whyness,whyo,wi,wice,wicht,wichtje,wick,wicked,wicken,wicker,wicket,wicking,wickiup,wickup,wicky,wicopy,wid,widbin,widder,widdle,widdy,wide,widegab,widely,widen,widener,widgeon,widish,widow,widowed,widower,widowly,widowy,width,widu,wield,wielder,wieldy,wiener,wienie,wife,wifedom,wifeism,wifekin,wifelet,wifely,wifie,wifish,wifock,wig,wigan,wigdom,wigful,wigged,wiggen,wigger,wiggery,wigging,wiggish,wiggism,wiggle,wiggler,wiggly,wiggy,wight,wightly,wigless,wiglet,wiglike,wigtail,wigwag,wigwam,wiikite,wild,wildcat,wilded,wilder,wilding,wildish,wildly,wile,wileful,wilga,wilgers,wilily,wilk,wilkin,will,willawa,willed,willer,willet,willey,willful,willie,willier,willies,willing,willock,willow,willowy,willy,willyer,wilsome,wilt,wilter,wily,wim,wimble,wimbrel,wime,wimick,wimple,win,wince,wincer,wincey,winch,wincher,wincing,wind,windage,windbag,winddog,winded,winder,windigo,windily,winding,windle,windles,windlin,windock,windore,window,windowy,windrow,windup,windway,windy,wine,wined,winemay,winepot,winer,winery,winesop,winevat,winful,wing,wingcut,winged,winger,wingle,winglet,wingman,wingy,winish,wink,winkel,winker,winking,winkle,winklet,winly,winna,winnard,winnel,winner,winning,winnle,winnow,winrace,winrow,winsome,wint,winter,wintle,wintry,winy,winze,wipe,wiper,wippen,wips,wir,wirable,wirble,wird,wire,wirebar,wired,wireman,wirer,wireway,wirily,wiring,wirl,wirling,wirr,wirra,wirrah,wiry,wis,wisdom,wise,wisely,wiseman,wisen,wisent,wiser,wish,wisha,wished,wisher,wishful,wishing,wishly,wishmay,wisht,wisket,wisp,wispish,wispy,wiss,wisse,wissel,wist,wiste,wistful,wistit,wistiti,wit,witan,witch,witched,witchen,witchet,witchy,wite,witess,witful,with,withal,withe,withen,wither,withers,withery,within,without,withy,witjar,witless,witlet,witling,witloof,witness,witney,witship,wittal,witted,witter,wittily,witting,wittol,witty,witwall,wive,wiver,wivern,wiz,wizard,wizen,wizened,wizier,wizzen,wloka,wo,woad,woader,woadman,woady,woak,woald,woan,wob,wobble,wobbler,wobbly,wobster,wod,woddie,wode,wodge,wodgy,woe,woeful,woesome,woevine,woeworn,woffler,woft,wog,wogiet,woibe,wokas,woke,wokowi,wold,woldy,wolf,wolfdom,wolfen,wolfer,wolfish,wolfkin,wolfram,wollop,wolter,wolve,wolver,woman,womanly,womb,wombat,wombed,womble,womby,womera,won,wonder,wone,wonegan,wong,wonga,wongen,wongshy,wongsky,woning,wonky,wonna,wonned,wonner,wonning,wonnot,wont,wonted,wonting,woo,wooable,wood,woodbin,woodcut,wooded,wooden,woodeny,woodine,wooding,woodish,woodlet,woodly,woodman,woodrow,woodsy,woodwax,woody,wooer,woof,woofed,woofell,woofer,woofy,woohoo,wooing,wool,woold,woolder,wooled,woolen,wooler,woolert,woolly,woolman,woolsey,woom,woomer,woon,woons,woorali,woorari,woosh,wootz,woozle,woozy,wop,woppish,wops,worble,word,wordage,worded,worder,wordily,wording,wordish,wordle,wordman,wordy,wore,work,workbag,workbox,workday,worked,worker,working,workman,workout,workpan,works,worky,world,worlded,worldly,worldy,worm,wormed,wormer,wormil,worming,wormy,worn,wornil,worral,worried,worrier,worrit,worry,worse,worsen,worser,worset,worship,worst,worsted,wort,worth,worthy,wosbird,wot,wote,wots,wottest,wotteth,woubit,wouch,wouf,wough,would,wouldnt,wouldst,wound,wounded,wounder,wounds,woundy,wourali,wourari,wournil,wove,woven,wow,wowser,wowsery,wowt,woy,wrack,wracker,wraggle,wraith,wraithe,wraithy,wraitly,wramp,wran,wrang,wrangle,wranny,wrap,wrapped,wrapper,wrasse,wrastle,wrath,wrathy,wraw,wrawl,wrawler,wraxle,wreak,wreat,wreath,wreathe,wreathy,wreck,wrecker,wrecky,wren,wrench,wrenlet,wrest,wrester,wrestle,wretch,wricht,wrick,wride,wried,wrier,wriest,wrig,wriggle,wriggly,wright,wring,wringer,wrinkle,wrinkly,wrist,wristed,wrister,writ,write,writee,writer,writh,writhe,writhed,writhen,writher,writhy,writing,written,writter,wrive,wro,wrocht,wroke,wroken,wrong,wronged,wronger,wrongly,wrossle,wrote,wroth,wrothly,wrothy,wrought,wrox,wrung,wry,wrybill,wryly,wryneck,wryness,wrytail,wud,wuddie,wudge,wudu,wugg,wulk,wull,wullcat,wulliwa,wumble,wumman,wummel,wun,wungee,wunna,wunner,wunsome,wup,wur,wurley,wurmal,wurrus,wurset,wurzel,wush,wusp,wuss,wusser,wust,wut,wuther,wuzu,wuzzer,wuzzle,wuzzy,wy,wyde,wye,wyke,wyle,wymote,wyn,wynd,wyne,wynn,wype,wyson,wyss,wyve,wyver,x,xanthic,xanthin,xanthyl,xarque,xebec,xenia,xenial,xenian,xenium,xenon,xenyl,xerafin,xerarch,xerasia,xeric,xeriff,xerogel,xeroma,xeronic,xerosis,xerotes,xerotic,xi,xiphias,xiphiid,xiphoid,xoana,xoanon,xurel,xyla,xylan,xylate,xylem,xylene,xylenol,xylenyl,xyletic,xylic,xylidic,xylinid,xylite,xylitol,xylogen,xyloid,xylol,xyloma,xylon,xylonic,xylose,xyloyl,xylyl,xylylic,xyphoid,xyrid,xyst,xyster,xysti,xystos,xystum,xystus,y,ya,yaba,yabber,yabbi,yabble,yabby,yabu,yacal,yacca,yachan,yacht,yachter,yachty,yad,yade,yaff,yaffle,yagger,yagi,yagua,yaguaza,yah,yahan,yahoo,yair,yaird,yaje,yajeine,yak,yakalo,yakamik,yakin,yakka,yakman,yalb,yale,yali,yalla,yallaer,yallow,yam,yamamai,yamanai,yamen,yamilke,yammer,yamp,yampa,yamph,yamshik,yan,yander,yang,yangtao,yank,yanking,yanky,yaoort,yaourti,yap,yapa,yaply,yapness,yapok,yapp,yapped,yapper,yapping,yappish,yappy,yapster,yar,yarak,yaray,yarb,yard,yardage,yardang,yardarm,yarder,yardful,yarding,yardman,yare,yareta,yark,yarke,yarl,yarly,yarm,yarn,yarnen,yarner,yarpha,yarr,yarran,yarrow,yarth,yarthen,yarwhip,yas,yashiro,yashmak,yat,yate,yati,yatter,yaud,yauld,yaupon,yautia,yava,yaw,yawl,yawler,yawn,yawner,yawney,yawnful,yawnily,yawning,yawnups,yawny,yawp,yawper,yawroot,yaws,yawweed,yawy,yaxche,yaya,ycie,yday,ye,yea,yeah,yealing,yean,year,yeara,yeard,yearday,yearful,yearly,yearn,yearock,yearth,yeast,yeasty,yeat,yeather,yed,yede,yee,yeel,yees,yegg,yeggman,yeguita,yeld,yeldrin,yelk,yell,yeller,yelling,yelloch,yellow,yellows,yellowy,yelm,yelmer,yelp,yelper,yelt,yen,yender,yeni,yenite,yeo,yeoman,yep,yer,yerb,yerba,yercum,yerd,yere,yerga,yerk,yern,yerth,yes,yese,yeso,yesso,yest,yester,yestern,yesty,yet,yeta,yetapa,yeth,yether,yetlin,yeuk,yeuky,yeven,yew,yex,yez,yezzy,ygapo,yield,yielden,yielder,yieldy,yigh,yill,yilt,yin,yince,yinst,yip,yird,yirk,yirm,yirn,yirr,yirth,yis,yite,ym,yn,ynambu,yo,yobi,yocco,yochel,yock,yockel,yodel,yodeler,yodh,yoe,yoga,yogh,yoghurt,yogi,yogin,yogism,yogist,yogoite,yohimbe,yohimbi,yoi,yoick,yoicks,yojan,yojana,yok,yoke,yokeage,yokel,yokelry,yoker,yoking,yoky,yolden,yolk,yolked,yolky,yom,yomer,yon,yond,yonder,yonner,yonside,yont,yook,yoop,yor,yore,york,yorker,yot,yote,you,youd,youden,youdith,youff,youl,young,younger,youngly,youngun,younker,youp,your,yourn,yours,yoursel,youse,youth,youthen,youthy,youve,youward,youze,yoven,yow,yowie,yowl,yowler,yowley,yowt,yox,yoy,yperite,yr,yttria,yttric,yttrium,yuan,yuca,yucca,yuck,yuckel,yucker,yuckle,yucky,yuft,yugada,yuh,yukkel,yulan,yule,yummy,yungan,yurt,yurta,yus,yusdrum,yutu,yuzlik,yuzluk,z,za,zabeta,zabra,zabti,zabtie,zac,zacate,zacaton,zachun,zad,zadruga,zaffar,zaffer,zafree,zag,zagged,zain,zak,zakkeu,zaman,zamang,zamarra,zamarro,zambo,zamorin,zamouse,zander,zanella,zant,zante,zany,zanyish,zanyism,zanze,zapas,zaphara,zapota,zaptiah,zaptieh,zapupe,zaqqum,zar,zareba,zarf,zarnich,zarp,zat,zati,zattare,zax,zayat,zayin,zeal,zealful,zealot,zealous,zebra,zebraic,zebrass,zebrine,zebroid,zebrula,zebrule,zebu,zebub,zeburro,zechin,zed,zedoary,zee,zeed,zehner,zein,zeism,zeist,zel,zelator,zemeism,zemi,zemmi,zemni,zemstvo,zenana,zendik,zenick,zenith,zenu,zeolite,zephyr,zephyry,zequin,zer,zerda,zero,zeroize,zest,zestful,zesty,zeta,zetetic,zeugma,ziamet,ziara,ziarat,zibet,zibetum,ziega,zieger,ziffs,zig,ziganka,zigzag,zihar,zikurat,zillah,zimarra,zimb,zimbi,zimme,zimmi,zimmis,zimocca,zinc,zincate,zincic,zincide,zincify,zincing,zincite,zincize,zincke,zincky,zinco,zincous,zincum,zing,zingel,zink,zinsang,zip,ziphian,zipper,zipping,zippy,zira,zirai,zircite,zircon,zither,zizz,zloty,zo,zoa,zoacum,zoaria,zoarial,zoarium,zobo,zocco,zoccolo,zodiac,zoea,zoeal,zoeform,zoetic,zogan,zogo,zoic,zoid,zoisite,zoism,zoist,zoistic,zokor,zoll,zolle,zombi,zombie,zonal,zonally,zonar,zonary,zonate,zonated,zone,zoned,zonelet,zonic,zoning,zonite,zonitid,zonoid,zonular,zonule,zonulet,zonure,zonurid,zoo,zoocarp,zoocyst,zooecia,zoogamy,zoogene,zoogeny,zoogony,zooid,zooidal,zooks,zoolite,zoolith,zoology,zoom,zoon,zoonal,zoonic,zoonist,zoonite,zoonomy,zoons,zoonule,zoopery,zoopsia,zoosis,zootaxy,zooter,zootic,zootomy,zootype,zoozoo,zorgite,zoril,zorilla,zorillo,zorro,zoster,zounds,zowie,zudda,zuisin,zumatic,zunyite,zuza,zwitter,zyga,zygal,zygion,zygite,zygoma,zygon,zygose,zygosis,zygote,zygotic,zygous,zymase,zyme,zymic,zymin,zymite,zymogen,zymoid,zymome,zymomin,zymosis,zymotic,zymurgy,zythem,zythum" diff --git a/dpaycligraphenebase/ecdsasig.py b/dpaycligraphenebase/ecdsasig.py new file mode 100755 index 0000000..151dcc9 --- /dev/null +++ b/dpaycligraphenebase/ecdsasig.py @@ -0,0 +1,303 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, str +from builtins import chr +from builtins import range +import sys +import time +import ecdsa +import hashlib +from binascii import hexlify, unhexlify +import struct +import logging +from .account import PrivateKey, PublicKey +from .py23 import py23_bytes, bytes_types +log = logging.getLogger(__name__) + +SECP256K1_MODULE = None +SECP256K1_AVAILABLE = False +CRYPTOGRAPHY_AVAILABLE = False +GMPY2_MODULE = False +if not SECP256K1_MODULE: + try: + import secp256k1 + SECP256K1_MODULE = "secp256k1" + SECP256K1_AVAILABLE = True + except ImportError: + try: + import cryptography + SECP256K1_MODULE = "cryptography" + CRYPTOGRAPHY_AVAILABLE = True + except ImportError: + SECP256K1_MODULE = "ecdsa" + + try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.asymmetric.utils \ + import decode_dss_signature, encode_dss_signature + from cryptography.exceptions import InvalidSignature + CRYPTOGRAPHY_AVAILABLE = True + except ImportError: + CRYPTOGRAPHY_AVAILABLE = False + log.debug("Cryptography not available") + +log.debug("Using SECP256K1 module: %s" % SECP256K1_MODULE) + + +def _is_canonical(sig): + sig = bytearray(sig) + return (not (int(sig[0]) & 0x80) and + not (sig[0] == 0 and not (int(sig[1]) & 0x80)) and + not (int(sig[32]) & 0x80) and + not (sig[32] == 0 and not (int(sig[33]) & 0x80))) + + +def compressedPubkey(pk): + if SECP256K1_MODULE == "cryptography" and not isinstance(pk, ecdsa.keys.VerifyingKey): + order = ecdsa.SECP256k1.order + x = pk.public_numbers().x + y = pk.public_numbers().y + else: + order = pk.curve.generator.order() + p = pk.pubkey.point + x = p.x() + y = p.y() + x_str = ecdsa.util.number_to_string(x, order) + return py23_bytes(chr(2 + (y & 1)), 'ascii') + x_str + + +def recover_public_key(digest, signature, i, message=None): + """ Recover the public key from the the signature + """ + + # See http: //www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6 primarily + curve = ecdsa.SECP256k1.curve + G = ecdsa.SECP256k1.generator + order = ecdsa.SECP256k1.order + yp = (i % 2) + r, s = ecdsa.util.sigdecode_string(signature, order) + # 1.1 + x = r + (i // 2) * order + # 1.3. This actually calculates for either effectively 02||X or 03||X depending on 'k' instead of always for 02||X as specified. + # This substitutes for the lack of reversing R later on. -R actually is defined to be just flipping the y-coordinate in the elliptic curve. + alpha = ((x * x * x) + (curve.a() * x) + curve.b()) % curve.p() + beta = ecdsa.numbertheory.square_root_mod_prime(alpha, curve.p()) + y = beta if (beta - yp) % 2 == 0 else curve.p() - beta + # 1.4 Constructor of Point is supposed to check if nR is at infinity. + R = ecdsa.ellipticcurve.Point(curve, x, y, order) + # 1.5 Compute e + e = ecdsa.util.string_to_number(digest) + # 1.6 Compute Q = r^-1(sR - eG) + Q = ecdsa.numbertheory.inverse_mod(r, order) * (s * R + (-e % order) * G) + + if SECP256K1_MODULE == "cryptography" and message is not None: + if not isinstance(message, bytes_types): + message = py23_bytes(message, "utf-8") + sigder = encode_dss_signature(r, s) + public_key = ec.EllipticCurvePublicNumbers(Q._Point__x, Q._Point__y, ec.SECP256K1()).public_key(default_backend()) + public_key.verify(sigder, message, ec.ECDSA(hashes.SHA256())) + return public_key + else: + # Not strictly necessary, but let's verify the message for paranoia's sake. + if not ecdsa.VerifyingKey.from_public_point(Q, curve=ecdsa.SECP256k1).verify_digest(signature, digest, sigdecode=ecdsa.util.sigdecode_string): + return None + return ecdsa.VerifyingKey.from_public_point(Q, curve=ecdsa.SECP256k1) + + +def recoverPubkeyParameter(message, digest, signature, pubkey): + """ Use to derive a number that allows to easily recover the + public key from the signature + """ + if not isinstance(message, bytes_types): + message = py23_bytes(message, "utf-8") + for i in range(0, 4): + if SECP256K1_MODULE == "secp256k1": + sig = pubkey.ecdsa_recoverable_deserialize(signature, i) + p = secp256k1.PublicKey(pubkey.ecdsa_recover(message, sig)) + if p.serialize() == pubkey.serialize(): + return i + elif SECP256K1_MODULE == "cryptography" and not isinstance(pubkey, PublicKey): + p = recover_public_key(digest, signature, i, message) + p_comp = hexlify(compressedPubkey(p)) + pubkey_comp = hexlify(compressedPubkey(pubkey)) + if (p_comp == pubkey_comp): + return i + else: + p = recover_public_key(digest, signature, i) + p_comp = hexlify(compressedPubkey(p)) + p_string = hexlify(p.to_string()) + if isinstance(pubkey, PublicKey): + pubkey_string = py23_bytes(repr(pubkey), 'latin') + else: + pubkey_string = hexlify(pubkey.to_string()) + if (p_string == pubkey_string or + p_comp == pubkey_string): + return i + return None + + +def sign_message(message, wif, hashfn=hashlib.sha256): + """ Sign a digest with a wif key + + :param str wif: Private key in + """ + + if not isinstance(message, bytes_types): + message = py23_bytes(message, "utf-8") + + digest = hashfn(message).digest() + priv_key = PrivateKey(wif) + if SECP256K1_MODULE == "secp256k1": + p = py23_bytes(priv_key) + ndata = secp256k1.ffi.new("const int *ndata") + ndata[0] = 0 + while True: + ndata[0] += 1 + privkey = secp256k1.PrivateKey(p, raw=True) + sig = secp256k1.ffi.new('secp256k1_ecdsa_recoverable_signature *') + signed = secp256k1.lib.secp256k1_ecdsa_sign_recoverable( + privkey.ctx, + sig, + digest, + privkey.private_key, + secp256k1.ffi.NULL, + ndata + ) + if not signed == 1: + raise AssertionError() + signature, i = privkey.ecdsa_recoverable_serialize(sig) + if _is_canonical(signature): + i += 4 # compressed + i += 27 # compact + break + elif SECP256K1_MODULE == "cryptography": + cnt = 0 + private_key = ec.derive_private_key(int(repr(priv_key), 16), ec.SECP256K1(), default_backend()) + public_key = private_key.public_key() + while True: + cnt += 1 + if not cnt % 20: + log.info("Still searching for a canonical signature. Tried %d times already!" % cnt) + order = ecdsa.SECP256k1.order + sigder = private_key.sign(message, ec.ECDSA(hashes.SHA256())) + r, s = decode_dss_signature(sigder) + signature = ecdsa.util.sigencode_string(r, s, order) + # Make sure signature is canonical! + # + sigder = bytearray(sigder) + lenR = sigder[3] + lenS = sigder[5 + lenR] + if lenR is 32 and lenS is 32: + # Derive the recovery parameter + # + i = recoverPubkeyParameter( + message, digest, signature, public_key) + i += 4 # compressed + i += 27 # compact + break + else: + cnt = 0 + p = py23_bytes(priv_key) + sk = ecdsa.SigningKey.from_string(p, curve=ecdsa.SECP256k1) + while 1: + cnt += 1 + if not cnt % 20: + log.info("Still searching for a canonical signature. Tried %d times already!" % cnt) + + # Deterministic k + # + k = ecdsa.rfc6979.generate_k( + sk.curve.generator.order(), + sk.privkey.secret_multiplier, + hashlib.sha256, + hashlib.sha256( + digest + + struct.pack("d", time.time()) # use the local time to randomize the signature + ).digest()) + + # Sign message + # + sigder = sk.sign_digest( + digest, + sigencode=ecdsa.util.sigencode_der, + k=k) + + # Reformating of signature + # + r, s = ecdsa.util.sigdecode_der(sigder, sk.curve.generator.order()) + signature = ecdsa.util.sigencode_string(r, s, sk.curve.generator.order()) + + # Make sure signature is canonical! + # + sigder = bytearray(sigder) + lenR = sigder[3] + lenS = sigder[5 + lenR] + if lenR is 32 and lenS is 32: + # Derive the recovery parameter + # + i = recoverPubkeyParameter( + message, digest, signature, sk.get_verifying_key()) + i += 4 # compressed + i += 27 # compact + break + + # pack signature + # + sigstr = struct.pack(" 10 and op["type"][-9:] == "operation": + name = op["type"][:-10] + else: + name = op["type"] + self.opId = self.operations().get(name, None) + if self.opId is None: + raise ValueError("Unknown operation") + self.name = name[0].upper() + name[1:] # klassname + try: + klass = self._getklass(self.name) + except Exception: + raise NotImplementedError("Unimplemented Operation %s" % self.name) + self.op = klass(op["value"]) + self.appbase = True + else: + self.op = op + self.name = type(self.op).__name__.lower() # also store name + self.opId = self.operations()[self.name] + + def operations(self): + return operations + + def getOperationNameForId(self, i): + """ Convert an operation id into the corresponding string + """ + for key in self.operations(): + if int(self.operations()[key]) is int(i): + return key + return "Unknown Operation ID %d" % i + + def _getklass(self, name): + module = __import__("graphenebase.operations", fromlist=["operations"]) + class_ = getattr(module, name) + return class_ + + def __bytes__(self): + return py23_bytes(Id(self.opId)) + py23_bytes(self.op) + + def __str__(self): + return json.dumps([self.opId, self.op.toJson()]) + + +@python_2_unicode_compatible +class GrapheneObject(object): + """ Core abstraction class + + This class is used for any JSON reflected object in Graphene. + + * ``instance.__json__()``: encodes data into json format + * ``bytes(instance)``: encodes data into wire format + * ``str(instances)``: dumps json object as string + + """ + def __init__(self, data=None): + self.data = data + + def __bytes__(self): + if self.data is None: + return py23_bytes() + b = b"" + for name, value in list(self.data.items()): + if isinstance(value, string_types): + b += py23_bytes(value, 'utf-8') + else: + b += py23_bytes(value) + return b + + def __json__(self): + if self.data is None: + return {} + d = {} # JSON output is *not* ordered + for name, value in list(self.data.items()): + if isinstance(value, Optional) and value.isempty(): + continue + + if isinstance(value, String): + d.update({name: str(value)}) + else: + try: + d.update({name: JsonObj(value)}) + except Exception: + d.update({name: value.__str__()}) + return d + + def __str__(self): + return json.dumps(self.__json__()) + + def toJson(self): + return self.__json__() + + def json(self): + return self.__json__() + + +def isArgsThisClass(self, args): + return (len(args) == 1 and type(args[0]).__name__ == type(self).__name__) diff --git a/dpaycligraphenebase/objecttypes.py b/dpaycligraphenebase/objecttypes.py new file mode 100755 index 0000000..244a23b --- /dev/null +++ b/dpaycligraphenebase/objecttypes.py @@ -0,0 +1,6 @@ +#: Object types for object ids +object_type = {} +object_type["null"] = 0 +object_type["base"] = 1 +object_type["account"] = 2 +object_type["OBJECT_TYPE_COUNT"] = 3 diff --git a/dpaycligraphenebase/operationids.py b/dpaycligraphenebase/operationids.py new file mode 100755 index 0000000..fc0a5dd --- /dev/null +++ b/dpaycligraphenebase/operationids.py @@ -0,0 +1,3 @@ +#: Operation ids +operations = {} +operations["demooepration"] = 0 diff --git a/dpaycligraphenebase/operations.py b/dpaycligraphenebase/operations.py new file mode 100755 index 0000000..8bf23dd --- /dev/null +++ b/dpaycligraphenebase/operations.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from collections import OrderedDict +import json +from .types import ( + Uint8, Int16, Uint16, Uint32, Uint64, + Varint32, Int64, String, Bytes, Void, + Array, PointInTime, Signature, Bool, + Set, Fixed_array, Optional, Static_variant, + Map, Id, +) +from .objects import GrapheneObject, isArgsThisClass +from .account import PublicKey +from .chains import default_prefix +from .objects import Operation +from .operationids import operations + + +class Demooepration(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super(Demooepration, self).__init__(OrderedDict([ + ('string', String(kwargs["string"], "account")), + ('extensions', Set([])), + ])) diff --git a/dpaycligraphenebase/py23.py b/dpaycligraphenebase/py23.py new file mode 100755 index 0000000..20ab449 --- /dev/null +++ b/dpaycligraphenebase/py23.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, int, str, chr +import sys + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + + +if PY3: + bytes_types = bytes, + string_types = str, + integer_types = int, + text_type = str + binary_type = bytes +else: + bytes_types = bytes, + string_types = basestring, # noqa: F821 + integer_types = (int, long) # noqa: F821 + text_type = unicode # noqa: F821 + binary_type = str + + +def py23_bytes(item=None, encoding=None): + if item is None: + return b'' + if hasattr(item, '__bytes__'): + return item.__bytes__() + else: + if encoding: + return bytes(item, encoding) + else: + return bytes(item) + + +def py23_chr(item): + if PY2: + return chr(item) + else: + return bytes([item]) diff --git a/dpaycligraphenebase/signedtransactions.py b/dpaycligraphenebase/signedtransactions.py new file mode 100755 index 0000000..2cd7eed --- /dev/null +++ b/dpaycligraphenebase/signedtransactions.py @@ -0,0 +1,212 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes, str, int +from dpaycligraphenebase.py23 import py23_bytes, bytes_types +import ecdsa +import hashlib +from binascii import hexlify, unhexlify +from collections import OrderedDict + +from .account import PublicKey +from .types import ( + Array, + Set, + Signature, + PointInTime, + Uint16, + Uint32, +) +from .objects import GrapheneObject, isArgsThisClass +from .operations import Operation +from .chains import known_chains +from .ecdsasig import sign_message, verify_message +import logging +log = logging.getLogger(__name__) + +try: + import secp256k1 + USE_SECP256K1 = True + log.debug("Loaded secp256k1 binding.") +except Exception: + USE_SECP256K1 = False + log.debug("To speed up transactions signing install \n" + " pip install secp256k1") + + +class Signed_Transaction(GrapheneObject): + """ Create a signed transaction and offer method to create the + signature + + :param num refNum: parameter ref_block_num (see ``getBlockParams``) + :param num refPrefix: parameter ref_block_prefix (see ``getBlockParams``) + :param str expiration: expiration date + :param Array operations: array of operations + """ + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + prefix = kwargs.pop("prefix", "DWB") + if "extensions" not in kwargs: + kwargs["extensions"] = Set([]) + elif not kwargs.get("extensions"): + kwargs["extensions"] = Set([]) + if "signatures" not in kwargs: + kwargs["signatures"] = Array([]) + else: + kwargs["signatures"] = Array([Signature(unhexlify(a)) for a in kwargs["signatures"]]) + + if "operations" in kwargs: + opklass = self.getOperationKlass() + if all([not isinstance(a, opklass) for a in kwargs["operations"]]): + kwargs['operations'] = Array([opklass(a, prefix=prefix) for a in kwargs["operations"]]) + else: + kwargs['operations'] = Array(kwargs["operations"]) + + super(Signed_Transaction, self).__init__(OrderedDict([ + ('ref_block_num', Uint16(kwargs['ref_block_num'])), + ('ref_block_prefix', Uint32(kwargs['ref_block_prefix'])), + ('expiration', PointInTime(kwargs['expiration'])), + ('operations', kwargs['operations']), + ('extensions', kwargs['extensions']), + ('signatures', kwargs['signatures']), + ])) + + @property + def id(self): + """ The transaction id of this transaction + """ + # Store signatures temporarily since they are not part of + # transaction id + sigs = self.data["signatures"] + self.data.pop("signatures", None) + + # Generage Hash of the seriliazed version + h = hashlib.sha256(py23_bytes(self)).digest() + + # recover signatures + self.data["signatures"] = sigs + + # Return properly truncated tx hash + return hexlify(h[:20]).decode("ascii") + + def getOperationKlass(self): + return Operation + + def derSigToHexSig(self, s): + """ Format DER to HEX signature + """ + s, junk = ecdsa.der.remove_sequence(unhexlify(s)) + if junk: + log.debug('JUNK: %s', hexlify(junk).decode('ascii')) + if not (junk == b''): + raise AssertionError() + x, s = ecdsa.der.remove_integer(s) + y, s = ecdsa.der.remove_integer(s) + return '%064x%064x' % (x, y) + + def getKnownChains(self): + return known_chains + + def getChainParams(self, chain): + # Which network are we on: + chains = self.getKnownChains() + if isinstance(chain, str) and chain in chains: + chain_params = chains[chain] + elif isinstance(chain, dict): + chain_params = chain + else: + raise Exception("sign() only takes a string or a dict as chain!") + if "chain_id" not in chain_params: + raise Exception("sign() needs a 'chain_id' in chain params!") + return chain_params + + def deriveDigest(self, chain): + chain_params = self.getChainParams(chain) + # Chain ID + self.chainid = chain_params["chain_id"] + + # Do not serialize signatures + sigs = self.data["signatures"] + self.data["signatures"] = [] + + # Get message to sign + # bytes(self) will give the wire formated data according to + # GrapheneObject and the data given in __init__() + self.message = unhexlify(self.chainid) + py23_bytes(self) + self.digest = hashlib.sha256(self.message).digest() + + # restore signatures + self.data["signatures"] = sigs + + def verify(self, pubkeys=[], chain=None, recover_parameter=False): + """Returned pubkeys have to be checked if they are existing""" + if not chain: + raise + chain_params = self.getChainParams(chain) + self.deriveDigest(chain) + signatures = self.data["signatures"].data + pubKeysFound = [] + + for signature in signatures: + if recover_parameter: + p = verify_message( + self.message, + py23_bytes(signature) + ) + else: + p = None + if p is None: + for i in range(4): + try: + p = verify_message( + self.message, + py23_bytes(signature), + recover_parameter=i + ) + phex = hexlify(p).decode('ascii') + pubKeysFound.append(phex) + except Exception: + p = None + else: + phex = hexlify(p).decode('ascii') + pubKeysFound.append(phex) + + for pubkey in pubkeys: + if not isinstance(pubkey, PublicKey): + raise Exception("Pubkeys must be array of 'PublicKey'") + + k = pubkey.unCompressed()[2:] + if k not in pubKeysFound and repr(pubkey) not in pubKeysFound: + k = PublicKey(PublicKey(k).compressed()) + f = format(k, chain_params["prefix"]) + raise Exception("Signature for %s missing!" % f) + return pubKeysFound + + def sign(self, wifkeys, chain=None): + """ Sign the transaction with the provided private keys. + + :param array wifkeys: Array of wif keys + :param str chain: identifier for the chain + + """ + if not chain: + raise Exception("Chain needs to be provided!") + self.deriveDigest(chain) + + # Get Unique private keys + self.privkeys = [] + [self.privkeys.append(item) for item in wifkeys if item not in self.privkeys] + + # Sign the message with every private key given! + sigs = [] + for wif in self.privkeys: + signature = sign_message(self.message, wif) + sigs.append(Signature(signature)) + + self.data["signatures"] = Array(sigs) + return self diff --git a/dpaycligraphenebase/types.py b/dpaycligraphenebase/types.py new file mode 100755 index 0000000..1e08d20 --- /dev/null +++ b/dpaycligraphenebase/types.py @@ -0,0 +1,416 @@ +"""types.""" +# encoding=utf8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from builtins import bytes +from builtins import str +from builtins import object +from builtins import int +import json +import struct +import sys +import time +from calendar import timegm +from binascii import hexlify, unhexlify +from datetime import datetime +from collections import OrderedDict +from .objecttypes import object_type + +from future.utils import python_2_unicode_compatible +from .py23 import py23_bytes + +timeformat = '%Y-%m-%dT%H:%M:%S%Z' + + +def varint(n): + """Varint encoding.""" + data = b'' + while n >= 0x80: + data += bytes([(n & 0x7f) | 0x80]) + n >>= 7 + data += bytes([n]) + return data + + +def varintdecode(data): + """Varint decoding.""" + shift = 0 + result = 0 + for b in bytes(data): + result |= ((b & 0x7f) << shift) + if not (b & 0x80): + break + shift += 7 + return result + + +def variable_buffer(s): + """Encodes variable length buffer.""" + return varint(len(s)) + s + + +def JsonObj(data): + """Returns json object from data.""" + return json.loads(str(data)) + + +@python_2_unicode_compatible +class Uint8(object): + """Uint8.""" + + def __init__(self, d): + """init.""" + self.data = int(d) + + def __bytes__(self): + """Returns bytes.""" + return struct.pack(" 13 and o < 32): + r.append("u%04x" % o) + elif o == 8: + r.append("b") + elif o == 9: + r.append("\t") + elif o == 10: + r.append("\n") + elif o == 12: + r.append("f") + elif o == 13: + r.append("\r") + else: + r.append(s) + return bytes("".join(r), "utf-8") + + +@python_2_unicode_compatible +class Bytes(object): + def __init__(self, d): + self.data = d + + def __bytes__(self): + """Returns data as bytes.""" + d = unhexlify(bytes(self.data, 'utf-8')) + return varint(len(d)) + d + + def __str__(self): + """Returns data as string.""" + return str(self.data) + + +@python_2_unicode_compatible +class Void(object): + def __init__(self): + pass + + def __bytes__(self): + """Returns bytes representation.""" + return b'' + + def __str__(self): + """Returns data as string.""" + return "" + + +@python_2_unicode_compatible +class Array(object): + def __init__(self, d): + self.data = d + self.length = Varint32(len(self.data)) + + def __bytes__(self): + """Returns bytes representation.""" + return py23_bytes(self.length) + b"".join([py23_bytes(a) for a in self.data]) + + def __str__(self): + """Returns data as string.""" + r = [] + for a in self.data: + try: + if isinstance(a, String): + r.append(str(a)) + else: + r.append(JsonObj(a)) + except Exception: + r.append(str(a)) + return json.dumps(r) + + +@python_2_unicode_compatible +class PointInTime(object): + def __init__(self, d): + self.data = d + + def __bytes__(self): + """Returns bytes representation.""" + if isinstance(self.data, datetime): + unixtime = timegm(self.data.timetuple()) + elif sys.version > '3': + unixtime = timegm(time.strptime((self.data + "UTC"), timeformat)) + else: + unixtime = timegm(time.strptime((self.data + "UTC"), timeformat.encode("utf-8"))) + if unixtime < 0: + return struct.pack(" stopTime: + total_duration = formatTimedelta(datetime.now() - startTime) + last_block_id = block_no + avtran = total_transaction / (last_block_id - 19273700) + print("* HOUR mark: Processed %d blockchain hours in %s" % (how_many_hours, total_duration)) + print("* Blocks %d, Transactions %d (Avg. per Block %f)" % ((last_block_id - 19273700), total_transaction, avtran)) + break + + if block_no != last_block_id: + cnt += 1 + last_block_id = block_no + if last_block_id % 100 == 0: + now = time.time() + duration = now - ltime + total_duration = now - start_time + speed = int(100000.0 / duration) * 1.0 / 1000 + avspeed = int((last_block_id - 19273700) * 1000 / total_duration) * 1.0 / 1000 + avtran = total_transaction / (last_block_id - 19273700) + ltime = now + if last_node != blockchain.dpay.rpc.url: + last_node = blockchain.dpay.rpc.url + print("Current node:", last_node) + print("* 100 blocks processed in %.2f seconds. Speed %.2f. Avg: %.2f. Avg.Trans:" + "%.2f Count: %d Block minutes: %d" % (duration, speed, avspeed, avtran, cnt, cnt * 3 / 60)) diff --git a/examples/benchmark_nodes.py b/examples/benchmark_nodes.py new file mode 100755 index 0000000..70b6222 --- /dev/null +++ b/examples/benchmark_nodes.py @@ -0,0 +1,96 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging +from prettytable import PrettyTable +from dpaycli.blockchain import Blockchain +from dpaycli.account import Account +from dpaycli.block import Block +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycli.nodelist import NodeList +from dpaycliapi.exceptions import NumRetriesReached +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +if __name__ == "__main__": + how_many_minutes = 10 + how_many_virtual_op = 10000 + max_batch_size = None + threading = False + thread_num = 16 + nodelist = NodeList() + nodes = nodelist.get_nodes(normal=True, appbase=True, dev=True) + t = PrettyTable(["node", "10 blockchain minutes", "10000 virtual account op", "version"]) + t.align = "l" + for node in nodes: + print("Current node:", node) + try: + stm = DPay(node=node, num_retries=3) + blockchain = Blockchain(dpay_instance=stm) + account = Account("gtg", dpay_instance=stm) + virtual_op_count = account.virtual_op_count() + blockchain_version = stm.get_blockchain_version() + + last_block_id = 19273700 + last_block = Block(last_block_id, dpay_instance=stm) + startTime = datetime.now() + + stopTime = last_block.time() + timedelta(seconds=how_many_minutes * 60) + ltime = time.time() + cnt = 0 + total_transaction = 0 + + start_time = time.time() + last_node = blockchain.dpay.rpc.url + + for entry in blockchain.blocks(start=last_block_id, max_batch_size=max_batch_size, threading=threading, thread_num=thread_num): + block_no = entry.identifier + if "block" in entry: + trxs = entry["block"]["transactions"] + else: + trxs = entry["transactions"] + + for tx in trxs: + for op in tx["operations"]: + total_transaction += 1 + if "block" in entry: + block_time = parse_time(entry["block"]["timestamp"]) + else: + block_time = parse_time(entry["timestamp"]) + + if block_time > stopTime: + total_duration = formatTimedelta(datetime.now() - startTime) + last_block_id = block_no + avtran = total_transaction / (last_block_id - 19273700) + break + start_time = time.time() + + stopOP = virtual_op_count - how_many_virtual_op + 1 + i = 0 + for acc_op in account.history_reverse(stop=stopOP): + i += 1 + total_duration_acc = formatTimedelta(datetime.now() - startTime) + + print("* Processed %d blockchain minutes in %s" % (how_many_minutes, total_duration)) + print("* Processed %d account ops in %s" % (i, total_duration_acc)) + print("* blockchain version: %s" % (blockchain_version)) + t.add_row([ + node, + total_duration, + total_duration_acc, + blockchain_version + ]) + except NumRetriesReached: + print("NumRetriesReached") + continue + except Exception as e: + print("Error: " + str(e)) + continue + print(t) diff --git a/examples/benchmark_nodes2.py b/examples/benchmark_nodes2.py new file mode 100755 index 0000000..e91f750 --- /dev/null +++ b/examples/benchmark_nodes2.py @@ -0,0 +1,206 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +from timeit import default_timer as timer +import logging +from prettytable import PrettyTable +from dpaycli.blockchain import Blockchain +from dpaycli.account import Account +from dpaycli.block import Block +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta, construct_authorperm, resolve_authorperm, resolve_authorpermvoter, construct_authorpermvoter, formatTimeString +from dpaycli.comment import Comment +from dpaycli.nodelist import NodeList +from dpaycli.vote import Vote +from dpaycliapi.exceptions import NumRetriesReached +FUTURES_MODULE = None +if not FUTURES_MODULE: + try: + from concurrent.futures import ThreadPoolExecutor, wait, as_completed + FUTURES_MODULE = "futures" + except ImportError: + FUTURES_MODULE = None +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) +quit_thread = False + + +def benchmark_node(node, how_many_minutes=10, how_many_seconds=30): + block_count = 0 + history_count = 0 + access_time = 0 + follow_time = 0 + blockchain_version = u'0.0.0' + successful = True + error_msg = None + start_total = timer() + max_batch_size = None + threading = False + thread_num = 16 + + authorpermvoter = u"@gtg/dpay-pressure-4-need-for-speed|gandalf" + [author, permlink, voter] = resolve_authorpermvoter(authorpermvoter) + authorperm = construct_authorperm(author, permlink) + last_block_id = 19273700 + try: + stm = DPay(node=node, num_retries=3, num_retries_call=3, timeout=30) + blockchain = Blockchain(dpay_instance=stm) + blockchain_version = stm.get_blockchain_version() + + last_block = Block(last_block_id, dpay_instance=stm) + + stopTime = last_block.time() + timedelta(seconds=how_many_minutes * 60) + total_transaction = 0 + + start = timer() + for entry in blockchain.blocks(start=last_block_id, max_batch_size=max_batch_size, threading=threading, thread_num=thread_num): + block_no = entry.identifier + block_count += 1 + if "block" in entry: + trxs = entry["block"]["transactions"] + else: + trxs = entry["transactions"] + + for tx in trxs: + for op in tx["operations"]: + total_transaction += 1 + if "block" in entry: + block_time = parse_time(entry["block"]["timestamp"]) + else: + block_time = parse_time(entry["timestamp"]) + + if block_time > stopTime: + last_block_id = block_no + break + if timer() - start > how_many_seconds or quit_thread: + break + except NumRetriesReached: + error_msg = 'NumRetriesReached' + block_count = -1 + except KeyboardInterrupt: + error_msg = 'KeyboardInterrupt' + # quit = True + except Exception as e: + error_msg = str(e) + block_count = -1 + + try: + stm = DPay(node=node, num_retries=3, num_retries_call=3, timeout=30) + account = Account("gtg", dpay_instance=stm) + blockchain_version = stm.get_blockchain_version() + + start = timer() + for acc_op in account.history_reverse(batch_size=100): + history_count += 1 + if timer() - start > how_many_seconds or quit_thread: + break + except NumRetriesReached: + error_msg = 'NumRetriesReached' + history_count = -1 + successful = False + except KeyboardInterrupt: + error_msg = 'KeyboardInterrupt' + history_count = -1 + successful = False + # quit = True + except Exception as e: + error_msg = str(e) + history_count = -1 + successful = False + + try: + stm = DPay(node=node, num_retries=3, num_retries_call=3, timeout=30) + account = Account("gtg", dpay_instance=stm) + blockchain_version = stm.get_blockchain_version() + + start = timer() + Vote(authorpermvoter, dpay_instance=stm) + stop = timer() + vote_time = stop - start + start = timer() + Comment(authorperm, dpay_instance=stm) + stop = timer() + comment_time = stop - start + start = timer() + Account(author, dpay_instance=stm) + stop = timer() + account_time = stop - start + start = timer() + account.get_followers() + stop = timer() + follow_time = stop - start + access_time = (vote_time + comment_time + account_time + follow_time) / 4.0 + except NumRetriesReached: + error_msg = 'NumRetriesReached' + access_time = -1 + except KeyboardInterrupt: + error_msg = 'KeyboardInterrupt' + # quit = True + except Exception as e: + error_msg = str(e) + access_time = -1 + return {'successful': successful, 'node': node, 'error': error_msg, + 'total_duration': timer() - start_total, 'block_count': block_count, + 'history_count': history_count, 'access_time': access_time, 'follow_time': follow_time, + 'version': blockchain_version} + + +if __name__ == "__main__": + how_many_seconds = 30 + how_many_minutes = 10 + threading = True + set_default_nodes = False + quit_thread = False + benchmark_time = timer() + + nodelist = NodeList() + nodes = nodelist.get_nodes(normal=True, appbase=True, dev=True) + t = PrettyTable(["node", "N blocks", "N acc hist", "dur. call in s"]) + t.align = "l" + t2 = PrettyTable(["node", "version"]) + t2.align = "l" + working_nodes = [] + results = [] + if threading and FUTURES_MODULE: + pool = ThreadPoolExecutor(max_workers=len(nodes) + 1) + futures = [] + for node in nodes: + futures.append(pool.submit(benchmark_node, node, how_many_minutes, how_many_seconds)) + try: + results = [r.result() for r in as_completed(futures)] + except KeyboardInterrupt: + quit_thread = True + print("benchmark aborted.") + else: + for node in nodes: + print("Current node:", node) + result = benchmark_node(node, how_many_minutes, how_many_seconds) + results.append(result) + for result in results: + t2.add_row([result["node"], result["version"]]) + print(t2) + print("\n") + + sortedList = sorted(results, key=lambda self: self["history_count"], reverse=True) + for result in sortedList: + if result["successful"]: + t.add_row([ + result["node"], + result["block_count"], + result["history_count"], + ("%.2f" % (result["access_time"])) + ]) + working_nodes.append(result["node"]) + print(t) + print("\n") + print("Total benchmark time: %.2f s\n" % (timer() - benchmark_time)) + if set_default_nodes: + stm = DPay(offline=True) + stm.set_default_nodes(working_nodes) + else: + print("dpay set nodes " + str(working_nodes)) diff --git a/examples/cache_performance.py b/examples/cache_performance.py new file mode 100755 index 0000000..5208369 --- /dev/null +++ b/examples/cache_performance.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycli.nodelist import NodeList +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def stream_votes(stm, threading, thread_num): + b = Blockchain(dpay_instance=stm) + opcount = 0 + start_time = time.time() + for op in b.stream(start=23483000, stop=23485000, threading=threading, thread_num=thread_num, + opNames=['vote']): + sys.stdout.write("\r%s" % op['block_num']) + opcount += 1 + now = time.time() + total_duration = now - start_time + print(" votes: %d, time %.2f" % (opcount, total_duration)) + return opcount, total_duration + + +if __name__ == "__main__": + node_setup = 1 + threading = True + thread_num = 8 + timeout = 10 + nodes = NodeList() + nodes.update_nodes(weights={"block": 1}) + node_list = nodes.get_nodes()[:5] + + vote_result = [] + duration = [] + + stm = DPay(node=node_list, timeout=timeout) + b = Blockchain(dpay_instance=stm) + block = b.get_current_block() + block.set_cache_auto_clean(False) + opcount, total_duration = stream_votes(stm, threading, thread_num) + print("Finished!") + block.set_cache_auto_clean(True) + cache_len = len(list(block._cache)) + start_time = time.time() + block.clear_cache_from_expired_items() + clear_duration = time.time() - start_time + time.sleep(5) + cache_len_after = len(list(block._cache)) + start_time = time.time() + print(str(block._cache)) + clear_duration2 = time.time() - start_time + print("Results:") + print("%d Threads with https duration: %.2f s - votes: %d" % (thread_num, total_duration, opcount)) + print("Clear %d items in %.3f s (%.3f s) (%d remaining)" % (cache_len, clear_duration, clear_duration2, cache_len_after)) diff --git a/examples/compare_transactions_speed_with_steem.py b/examples/compare_transactions_speed_with_steem.py new file mode 100755 index 0000000..964e4eb --- /dev/null +++ b/examples/compare_transactions_speed_with_steem.py @@ -0,0 +1,128 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import chr +from builtins import range +from builtins import super +import random +from pprint import pprint +from binascii import hexlify +from collections import OrderedDict + +from dpayclibase import ( + transactions, + memo, + operations, + objects +) +from dpayclibase.objects import Operation +from dpayclibase.signedtransactions import Signed_Transaction +from dpaycligraphenebase.account import PrivateKey +from dpaycligraphenebase import account +from dpayclibase.operationids import getOperationNameForId +from dpaycligraphenebase.py23 import py23_bytes, bytes_types +from dpaycli.amount import Amount +from dpaycli.asset import Asset +from dpaycli.dpay import DPay +import time + +from dpay import DPay as dpayDPay +from dpaybase.account import PrivateKey as dpayPrivateKey +from dpaybase.transactions import SignedTransaction as dpaySignedTransaction +from dpaybase import operations as dpayOperations +from timeit import default_timer as timer + + +class DPayCliTest(object): + + def setup(self): + self.prefix = u"BEX" + self.default_prefix = u"DWB" + self.wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + self.ref_block_num = 34294 + self.ref_block_prefix = 3707022213 + self.expiration = "2016-04-06T08:29:27" + self.stm = DPay(offline=True) + + def doit(self, printWire=False, ops=None): + ops = [Operation(ops)] + tx = Signed_Transaction(ref_block_num=self.ref_block_num, + ref_block_prefix=self.ref_block_prefix, + expiration=self.expiration, + operations=ops) + start = timer() + tx = tx.sign([self.wif], chain=self.prefix) + end1 = timer() + tx.verify([PrivateKey(self.wif, prefix=u"DWB").pubkey], self.prefix) + end2 = timer() + return end2 - end1, end1 - start + + +class DPayTest(object): + + def setup(self): + self.prefix = u"BEX" + self.default_prefix = u"DWB" + self.wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + self.ref_block_num = 34294 + self.ref_block_prefix = 3707022213 + self.expiration = "2016-04-06T08:29:27" + + def doit(self, printWire=False, ops=None): + ops = [dpayOperations.Operation(ops)] + tx = dpaySignedTransaction(ref_block_num=self.ref_block_num, + ref_block_prefix=self.ref_block_prefix, + expiration=self.expiration, + operations=ops) + start = timer() + tx = tx.sign([self.wif], chain=self.prefix) + end1 = timer() + tx.verify([dpayPrivateKey(self.wif, prefix=u"DWB").pubkey], self.prefix) + end2 = timer() + return end2 - end1, end1 - start + + +if __name__ == "__main__": + dpay_test = DPayTest() + dpaycli_test = DPayCliTest() + dpay_test.setup() + dpaycli_test.setup() + dpay_times = [] + dpaycli_times = [] + loops = 50 + for i in range(0, loops): + print(i) + opDPay = dpayOperations.Transfer(**{ + "from": "foo", + "to": "baar", + "amount": "111.110 BEX", + "memo": "Fooo" + }) + opDPayCli = operations.Transfer(**{ + "from": "foo", + "to": "baar", + "amount": Amount("111.110 BEX", dpay_instance=DPay(offline=True)), + "memo": "Fooo" + }) + + t_s, t_v = dpay_test.doit(ops=opDPay) + dpay_times.append([t_s, t_v]) + + t_s, t_v = dpaycli_test.doit(ops=opDPayCli) + dpaycli_times.append([t_s, t_v]) + + dpay_dt = [0, 0] + dpaycli_dt = [0, 0] + for i in range(0, loops): + dpay_dt[0] += dpay_times[i][0] + dpay_dt[1] += dpay_times[i][1] + dpaycli_dt[0] += dpaycli_times[i][0] + dpaycli_dt[1] += dpaycli_times[i][1] + print("dpay vs dpaycli:\n") + print("dpay: sign: %.2f s, verification %.2f s" % (dpay_dt[0] / loops, dpay_dt[1] / loops)) + print("dpaycli: sign: %.2f s, verification %.2f s" % (dpaycli_dt[0] / loops, dpaycli_dt[1] / loops)) + print("------------------------------------") + print("dpaycli is %.2f %% (sign) and %.2f %% (verify) faster than dpay" % + (dpay_dt[0] / dpaycli_dt[0] * 100, dpay_dt[1] / dpaycli_dt[1] * 100)) diff --git a/examples/compare_with_steem_python_account.py b/examples/compare_with_steem_python_account.py new file mode 100755 index 0000000..450621f --- /dev/null +++ b/examples/compare_with_steem_python_account.py @@ -0,0 +1,95 @@ +from __future__ import print_function +import sys +from datetime import timedelta +import time +import io +from dpaycli import DPay +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycli.utils import parse_time +from dpay.account import Account as dpayAccount +from dpay.post import Post as dpayPost +from dpay import DPay as dpayDPay +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +if __name__ == "__main__": + stm = DPay("https://api.dpays.io") + dpaycli_acc = Account("whitehorse", dpay_instance=stm) + stm2 = dpayDPay(nodes=["https://api.dpays.io"]) + dpay_acc = dpayAccount("whitehorse", dpayd_instance=stm2) + + # profile + print("dpaycli_acc.profile {}".format(dpaycli_acc.profile)) + print("dpay_acc.profile {}".format(dpay_acc.profile)) + # bp + print("dpaycli_acc.bp {}".format(dpaycli_acc.bp)) + print("dpay_acc.bp {}".format(dpay_acc.bp)) + # rep + print("dpaycli_acc.rep {}".format(dpaycli_acc.rep)) + print("dpay_acc.rep {}".format(dpay_acc.rep)) + # balances + print("dpaycli_acc.balances {}".format(dpaycli_acc.balances)) + print("dpay_acc.balances {}".format(dpay_acc.balances)) + # get_balances() + print("dpaycli_acc.get_balances() {}".format(dpaycli_acc.get_balances())) + print("dpay_acc.get_balances() {}".format(dpay_acc.get_balances())) + # reputation() + print("dpaycli_acc.get_reputation() {}".format(dpaycli_acc.get_reputation())) + print("dpay_acc.reputation() {}".format(dpay_acc.reputation())) + # voting_power() + print("dpaycli_acc.get_voting_power() {}".format(dpaycli_acc.get_voting_power())) + print("dpay_acc.voting_power() {}".format(dpay_acc.voting_power())) + # get_followers() + print("dpaycli_acc.get_followers() {}".format(dpaycli_acc.get_followers())) + print("dpay_acc.get_followers() {}".format(dpay_acc.get_followers())) + # get_following() + print("dpaycli_acc.get_following() {}".format(dpaycli_acc.get_following())) + print("dpay_acc.get_following() {}".format(dpay_acc.get_following())) + # has_voted() + print("dpaycli_acc.has_voted() {}".format(dpaycli_acc.has_voted("@holger80/api-methods-list-for-appbase"))) + print("dpay_acc.has_voted() {}".format(dpay_acc.has_voted(dpayPost("@holger80/api-methods-list-for-appbase")))) + # curation_stats() + print("dpaycli_acc.curation_stats() {}".format(dpaycli_acc.curation_stats())) + print("dpay_acc.curation_stats() {}".format(dpay_acc.curation_stats())) + # virtual_op_count + print("dpaycli_acc.virtual_op_count() {}".format(dpaycli_acc.virtual_op_count())) + print("dpay_acc.virtual_op_count() {}".format(dpay_acc.virtual_op_count())) + # get_account_votes + print("dpaycli_acc.get_account_votes() {}".format(dpaycli_acc.get_account_votes())) + print("dpay_acc.get_account_votes() {}".format(dpay_acc.get_account_votes())) + # get_withdraw_routes + print("dpaycli_acc.get_withdraw_routes() {}".format(dpaycli_acc.get_withdraw_routes())) + print("dpay_acc.get_withdraw_routes() {}".format(dpay_acc.get_withdraw_routes())) + # get_conversion_requests + print("dpaycli_acc.get_conversion_requests() {}".format(dpaycli_acc.get_conversion_requests())) + print("dpay_acc.get_conversion_requests() {}".format(dpay_acc.get_conversion_requests())) + # export + # history + dpaycli_hist = [] + for h in dpaycli_acc.history(only_ops=["transfer"]): + dpaycli_hist.append(h) + if len(dpaycli_hist) >= 10: + break + dpay_hist = [] + for h in dpay_acc.history(filter_by="transfer", start=0): + dpay_hist.append(h) + if len(dpay_hist) >= 10: + break + print("dpaycli_acc.history() {}".format(dpaycli_hist)) + print("dpay_acc.history() {}".format(dpay_hist)) + # history_reverse + dpaycli_hist = [] + for h in dpaycli_acc.history_reverse(only_ops=["transfer"]): + dpaycli_hist.append(h) + if len(dpaycli_hist) >= 10: + break + dpay_hist = [] + for h in dpay_acc.history_reverse(filter_by="transfer"): + dpay_hist.append(h) + if len(dpay_hist) >= 10: + break + print("dpaycli_acc.history_reverse() {}".format(dpaycli_hist)) + print("dpay_acc.history_reverse() {}".format(dpay_hist)) diff --git a/examples/hf20_testnet.py b/examples/hf20_testnet.py new file mode 100755 index 0000000..fec301c --- /dev/null +++ b/examples/hf20_testnet.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycliapi.exceptions import NumRetriesReached +from dpaycli.nodelist import NodeList +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +if __name__ == "__main__": + # stm = DPay(node="https://testnet.timcliff.com/") + # stm = DPay(node="https://testnet.dpaydev.com") + stm = DPay(node="https://api.dpays.io") + stm.wallet.unlock(pwd="pwd123") + + account = Account("dpayclibot", dpay_instance=stm) + print(account.get_voting_power()) + + account.transfer("holger80", 0.001, "BBD", "test") diff --git a/examples/login_app/app.py b/examples/login_app/app.py new file mode 100755 index 0000000..50fece9 --- /dev/null +++ b/examples/login_app/app.py @@ -0,0 +1,38 @@ +from flask import Flask, request +from dpaycli.dpayid import DPayID +import getpass + +app = Flask(__name__) + + +c = DPayID(client_id="dpaycli.app", scope="login,vote,custom_json", get_refresh_token=False) +# replace test with our wallet password +wallet_password = getpass.getpass('Wallet-Password:') +c.dpay.wallet.unlock(wallet_password) + + +@app.route('/') +def index(): + login_url = c.get_login_url( + "http://localhost:5000/welcome", + ) + return "Login with DPayID" % login_url + + +@app.route('/welcome') +def welcome(): + access_token = request.args.get("access_token", None) + name = request.args.get("username", None) + if c.get_refresh_token: + code = request.args.get("code") + refresh_token = c.get_access_token(code) + access_token = refresh_token["access_token"] + name = refresh_token["username"] + elif name is None: + c.set_access_token(access_token) + name = c.me()["name"] + + if name in c.dpay.wallet.getPublicNames(): + c.dpay.wallet.removeTokenFromPublicName(name) + c.dpay.wallet.addToken(name, access_token) + return "Welcome %s!" % name diff --git a/examples/memory_profiler1.py b/examples/memory_profiler1.py new file mode 100755 index 0000000..8a6151c --- /dev/null +++ b/examples/memory_profiler1.py @@ -0,0 +1,49 @@ +from __future__ import print_function +from memory_profiler import profile +import sys +from datetime import datetime, timedelta +import time +import io +from dpaycli.dpay import DPay +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycli.blockchain import Blockchain +from dpaycli.utils import parse_time +from dpaycli.instance import set_shared_dpay_instance +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +@profile +def profiling(name_list): + stm = DPay() + set_shared_dpay_instance(stm) + del stm + print("start") + for name in name_list: + print("account: %s" % (name)) + acc = Account(name) + max_index = acc.virtual_op_count() + print(max_index) + stopTime = datetime(2018, 4, 22, 0, 0, 0) + hist_elem = None + for h in acc.history_reverse(stop=stopTime): + hist_elem = h + print(hist_elem) + print("blockchain") + blockchain_object = Blockchain() + current_num = blockchain_object.get_current_block_num() + startBlockNumber = current_num - 20 + endBlockNumber = current_num + block_elem = None + for o in blockchain_object.stream(start=startBlockNumber, stop=endBlockNumber): + print("block %d" % (o["block_num"])) + block_elem = o + print(block_elem) + + +if __name__ == "__main__": + + account_list = ["whitehorse", "whitehorse2", "whitehorse3", "whitehorse4", "whitehorse5", "whitehorse6"] + profiling(account_list) diff --git a/examples/memory_profiler2.py b/examples/memory_profiler2.py new file mode 100755 index 0000000..75f3193 --- /dev/null +++ b/examples/memory_profiler2.py @@ -0,0 +1,52 @@ +from __future__ import print_function +from memory_profiler import profile +import sys +from dpaycli.dpay import DPay +from dpaycli.account import Account +from dpaycli.blockchain import Blockchain +from dpaycli.instance import set_shared_dpay_instance, clear_cache +from dpaycli.storage import configStorage as config +from dpaycliapi.graphenerpc import GrapheneRPC +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +@profile +def profiling(node, name_list, shared_instance=True, clear_acc_cache=False, clear_all_cache=True): + print("shared_instance %d clear_acc_cache %d clear_all_cache %d" % + (shared_instance, clear_acc_cache, clear_all_cache)) + if not shared_instance: + stm = DPay(node=node) + print(str(stm)) + else: + stm = None + acc_dict = {} + for name in name_list: + acc = Account(name, dpay_instance=stm) + acc_dict[name] = acc + if clear_acc_cache: + acc.clear_cache() + acc_dict = {} + if clear_all_cache: + clear_cache() + if not shared_instance: + del stm.rpc + + +if __name__ == "__main__": + stm = DPay() + print("Shared instance: " + str(stm)) + set_shared_dpay_instance(stm) + b = Blockchain() + account_list = [] + for a in b.get_all_accounts(limit=500): + account_list.append(a) + shared_instance = False + clear_acc_cache = False + clear_all_cache = False + node = "https://api.dpays.io" + n = 3 + for i in range(1, n + 1): + print("%d of %d" % (i, n)) + profiling(node, account_list, shared_instance=shared_instance, clear_acc_cache=clear_acc_cache, clear_all_cache=clear_all_cache) diff --git a/examples/next_witness_block_coundown.py b/examples/next_witness_block_coundown.py new file mode 100755 index 0000000..d9d2ba8 --- /dev/null +++ b/examples/next_witness_block_coundown.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +import sys +from dpaycli import DPay +from dpaycli.witness import Witness, WitnessesRankedByVote +from time import sleep + + +def convert_block_diff_to_time_string(block_diff_est): + next_block_s = int(block_diff_est) * 3 + next_block_min = next_block_s / 60 + next_block_h = next_block_min / 60 + next_block_d = next_block_h / 24 + time_diff_est = "" + if next_block_d > 1: + time_diff_est = "%.2f days" % next_block_d + elif next_block_h > 1: + time_diff_est = "%.2f hours" % next_block_h + elif next_block_min > 1: + time_diff_est = "%.2f minutes" % next_block_min + else: + time_diff_est = "%.2f seconds" % next_block_s + return time_diff_est + + +if __name__ == "__main__": + if len(sys.argv) != 2: + witness = "holger80" + else: + witness = sys.argv[1] + stm = DPay() + witness = Witness(witness, dpay_instance=stm) + + witness_schedule = stm.get_witness_schedule() + config = stm.get_config() + if "VIRTUAL_SCHEDULE_LAP_LENGTH2" in config: + lap_length = int(config["VIRTUAL_SCHEDULE_LAP_LENGTH2"]) + else: + lap_length = int(config["DPAY_VIRTUAL_SCHEDULE_LAP_LENGTH2"]) + witnesses = WitnessesRankedByVote(limit=250, dpay_instance=stm) + vote_sum = witnesses.get_votes_sum() + + virtual_time_to_block_num = int(witness_schedule["num_scheduled_witnesses"]) / (lap_length / (vote_sum + 1)) + while True: + witness.refresh() + witness_schedule = stm.get_witness_schedule(use_stored_data=False) + + witness_json = witness.json() + virtual_diff = int(witness_json["virtual_scheduled_time"]) - int(witness_schedule['current_virtual_time']) + block_diff_est = virtual_diff * virtual_time_to_block_num + + time_diff_est = convert_block_diff_to_time_string(block_diff_est) + + sys.stdout.write("\r Next block for %s in %s" % (witness["owner"], time_diff_est)) + sleep(30) diff --git a/examples/op_on_testnet.py b/examples/op_on_testnet.py new file mode 100755 index 0000000..98c5c16 --- /dev/null +++ b/examples/op_on_testnet.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycliapi.exceptions import NumRetriesReached +from dpaycli.nodelist import NodeList +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +password = "secretPassword" +username = "dpaycli5" +useWallet = False +walletpassword = "123" + +if __name__ == "__main__": + testnet_node = "https://testnet.dpay.vc" + stm = DPay(node=testnet_node) + prefix = stm.prefix + # curl --data "username=username&password=secretPassword" https://testnet.dpay.vc/create + if useWallet: + stm.wallet.wipe(True) + stm.wallet.create(walletpassword) + stm.wallet.unlock(walletpassword) + active_key = PasswordKey(username, password, role="active", prefix=prefix) + owner_key = PasswordKey(username, password, role="owner", prefix=prefix) + posting_key = PasswordKey(username, password, role="posting", prefix=prefix) + memo_key = PasswordKey(username, password, role="memo", prefix=prefix) + active_pubkey = active_key.get_public_key() + owner_pubkey = owner_key.get_public_key() + posting_pubkey = posting_key.get_public_key() + memo_pubkey = memo_key.get_public_key() + active_privkey = active_key.get_private_key() + posting_privkey = posting_key.get_private_key() + owner_privkey = owner_key.get_private_key() + memo_privkey = memo_key.get_private_key() + if useWallet: + stm.wallet.addPrivateKey(owner_privkey) + stm.wallet.addPrivateKey(active_privkey) + stm.wallet.addPrivateKey(memo_privkey) + stm.wallet.addPrivateKey(posting_privkey) + else: + stm = DPay(node=testnet_node, + wif={'active': str(active_privkey), + 'posting': str(posting_privkey), + 'memo': str(memo_privkey)}) + account = Account(username, dpay_instance=stm) + if account["name"] == "dpaycli": + account.disallow("dpaycli1", permission='posting') + account.allow('dpaycli1', weight=1, permission='posting', account=None) + account.follow("dpaycli1") + elif account["name"] == "dpaycli5": + account.allow('dpaycli4', weight=2, permission='active', account=None) + if useWallet: + stm.wallet.getAccountFromPrivateKey(str(active_privkey)) + + # stm.create_account("dpaycli1", creator=account, password=password1) + + account1 = Account("dpaycli1", dpay_instance=stm) + b = Blockchain(dpay_instance=stm) + blocknum = b.get_current_block().identifier + + account.transfer("dpaycli1", 1, "BBD", "test") + b1 = Block(blocknum, dpay_instance=stm) diff --git a/examples/post_to_html.py b/examples/post_to_html.py new file mode 100755 index 0000000..f0c1006 --- /dev/null +++ b/examples/post_to_html.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +import argparse +import sys +import jinja2 +import markdown +import pytz +from datetime import datetime, timedelta +from dpaycli.blockchain import Blockchain +from dpaycli.comment import Comment +from dpaycli.utils import formatTimeString, formatTimedelta, remove_from_dict, reputation_to_score, parse_time +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +TEMPLATE = """ + + + + + {{title}} + + +

+{{content}} +
+ + +""" + + +def parse_args(args=None): + d = 'Make a complete, styled HTML document from a Markdown file.' + parser = argparse.ArgumentParser(description=d) + parser.add_argument('authorperm', type=str, nargs='?', + default=sys.stdin, + help='Authorperm to read. Defaults to stdin.') + parser.add_argument('-o', '--out', type=argparse.FileType('w'), + default=sys.stdout, + help='Output file name. Defaults to stdout.') + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + authorperm = args.authorperm + comment = Comment(authorperm) + title = comment["title"] + author = comment["author"] + rep = reputation_to_score(comment["author_reputation"]) + time_created = comment["created"] + utc = pytz.timezone('UTC') + td_created = utc.localize(datetime.utcnow()) - time_created + md = '# ' + title + '\n' + author + md += '(%.2f) ' % (rep) + md += formatTimedelta(td_created) + '\n\n' + md += comment["body"] + extensions = ['extra', 'smarty'] + html = markdown.markdown(md, extensions=extensions, output_format='html5') + doc = jinja2.Template(TEMPLATE).render(content=html, title=title) + args.out.write(doc) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/examples/post_to_md.py b/examples/post_to_md.py new file mode 100755 index 0000000..d1207d7 --- /dev/null +++ b/examples/post_to_md.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +import argparse +import sys +import pytz +import markdown +from datetime import datetime, timedelta +from dpaycli.blockchain import Blockchain +from dpaycli.comment import Comment +from dpaycli.utils import formatTimeString, formatTimedelta, remove_from_dict, reputation_to_score, parse_time +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def parse_args(args=None): + d = 'Save post as markdown.' + parser = argparse.ArgumentParser(description=d) + parser.add_argument('authorperm', type=str, nargs='?', + default=sys.stdin, + help='Authorperm to read. Defaults to stdin.') + parser.add_argument('-o', '--out', type=argparse.FileType('w'), + default=sys.stdout, + help='Output file name. Defaults to stdout.') + return parser.parse_args(args) + + +def main(args=None): + """pandoc -s test.md --from markdown-blank_before_header-blank_before_blockquote+lists_without_preceding_blankline -o test.pdf""" + args = parse_args(args) + authorperm = args.authorperm + comment = Comment(authorperm) + title = comment["title"] + author = comment["author"] + rep = reputation_to_score(comment["author_reputation"]) + time_created = comment["created"] + if True: + md = '% Title:\t ' + title + '\n' + md += '% Author:\t' + author + '(%.2f) ' % (rep) + '\n' + md += '% Date:\t' + time_created.strftime("%d.%m.%Y") + '\n' + md += '% Comment:\n' + else: + md = '# ' + title + '\n' + md += author + '(%.2f) ' % (rep) + ' ' + time_created.strftime("%d.%m.%Y") + '\n\n' + md += comment["body"] + + args.out.write(md) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/examples/print_appbase_calls.py b/examples/print_appbase_calls.py new file mode 100755 index 0000000..a96b5e7 --- /dev/null +++ b/examples/print_appbase_calls.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import int, str +import sys +from datetime import timedelta +import time +import io +from dpaycli.dpay import DPay +import logging +from prettytable import PrettyTable +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +if __name__ == "__main__": + stm = DPay(node="https://api.dpays.io") + all_calls = stm.rpc.get_methods(api="jsonrpc") + t = PrettyTable(["method", "args", "ret"]) + t.align = "l" + t_condenser = PrettyTable(["method", "args", "ret"]) + t_condenser.align = "l" + for call in all_calls: + if "condenser" not in call: + ret = stm.rpc.get_signature({'method': call}, api="jsonrpc") + t.add_row([ + call, + ret['args'], + ret['ret'] + ]) + else: + ret = stm.rpc.get_signature({'method': call}, api="jsonrpc") + t_condenser.add_row([ + call, + ret['args'], + ret['ret'] + ]) + print("Finished. Write results...") + with open('print_appbase.txt', 'w') as w: + w.write(str(t)) + with open('print_appbase.html', 'w') as w: + w.write(str(t.get_html_string())) + with open('print_appbase_condenser.txt', 'w') as w: + w.write(str(t_condenser)) + with open('print_appbase_condenser.html', 'w') as w: + w.write(str(t_condenser.get_html_string())) diff --git a/examples/print_comments.py b/examples/print_comments.py new file mode 100755 index 0000000..7ea93cc --- /dev/null +++ b/examples/print_comments.py @@ -0,0 +1,26 @@ +from __future__ import print_function +import sys +from datetime import timedelta +import time +import io +from dpaycli.blockchain import Blockchain +from dpaycli.utils import parse_time +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class DemoBot(object): + def comment(self, comment_event): + print('Comment by {} on post {} by {}:'.format(comment_event['author'], + comment_event['parent_permlink'], + comment_event['parent_author'])) + print(comment_event['body']) + print() + + +if __name__ == "__main__": + tb = DemoBot() + blockchain = Blockchain() + for vote in blockchain.stream(opNames=["comment"]): + tb.comment(vote) diff --git a/examples/print_votes.py b/examples/print_votes.py new file mode 100755 index 0000000..1d4223a --- /dev/null +++ b/examples/print_votes.py @@ -0,0 +1,29 @@ +from __future__ import print_function +import sys +from datetime import timedelta +import time +import io +from dpaycli.blockchain import Blockchain +from dpaycli.utils import parse_time +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class DemoBot(object): + def vote(self, vote_event): + w = vote_event["weight"] + if w > 0: + print("Vote by", vote_event["voter"], "for", vote_event["author"]) + else: + if w < 0: + print("Downvote by", vote_event["voter"], "for", vote_event["author"]) + else: + print("(Down)vote by", vote_event["voter"], "for", vote_event["author"], "CANCELED") + + +if __name__ == "__main__": + tb = DemoBot() + blockchain = Blockchain() + for vote in blockchain.stream(opNames=["vote"]): + tb.vote(vote) diff --git a/examples/print_votes_notify.py b/examples/print_votes_notify.py new file mode 100755 index 0000000..9dce472 --- /dev/null +++ b/examples/print_votes_notify.py @@ -0,0 +1,71 @@ +from __future__ import print_function +import sys +from datetime import timedelta +import time +import io +from dpaycli.notify import Notify +from dpaycli.utils import parse_time +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class TestBot: + def __init__(self): + self.notify = None + self.blocks = 0 + self.hourcount = 0 + self.start = time.time() + self.last = time.time() + self.total_transaction = 0 + + def new_block(self, block): + if "block" in block: + trxs = block["block"]["transactions"] + else: + trxs = block["transactions"] + for tx in trxs: + for op in tx["operations"]: + self.total_transaction += 1 + if op[0] == 'vote': + self.vote(op[1]) + chunk = 100 + self.blocks = self.blocks + 1 + if self.blocks % chunk == 0: + now = time.time() + duration = now - self.last + total_duration = now - self.start + speed = int(chunk * 1000.0 / duration) * 1.0 / 1000 + avspeed = int(self.blocks * 1000 / total_duration) * 1.0 / 1000 + avtran = self.total_transaction / self.blocks + self.last = now + print("* 100 blocks processed in %.2f seconds. Speed %.2f. Avg: %.2f. Avg.Trans:" + "%.2f Count: %d Block minutes: %d" % (duration, speed, avspeed, avtran, self.blocks, self.blocks * 3 / 60)) + if self.blocks % 1200 == 0: + self.hour() + + def vote(self, vote_event): + w = vote_event["weight"] + if w > 0: + print("Vote by", vote_event["voter"], "for", vote_event["author"]) + else: + if w < 0: + print("Downvote by", vote_event["voter"], "for", vote_event["author"]) + else: + print("(Down)vote by", vote_event["voter"], "for", vote_event["author"], "CANCELED") + + def hour(self): + self.hourcount = self.hourcount + 1 + now = time.time() + total_duration = str(timedelta(seconds=now - self.start)) + print("* HOUR mark: Processed " + str(self.hourcount) + " blockchain hours in " + total_duration) + if self.hourcount == 1 * 24: + print("Ending eventloop") + self.notify.close() + + +if __name__ == "__main__": + tb = TestBot() + notify = Notify(on_block=tb.new_block) + tb.notify = notify + notify.listen() diff --git a/examples/stream_threading_performance.py b/examples/stream_threading_performance.py new file mode 100755 index 0000000..c7fa3cf --- /dev/null +++ b/examples/stream_threading_performance.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycli.nodelist import NodeList +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def stream_votes(stm, threading, thread_num): + b = Blockchain(dpay_instance=stm) + opcount = 0 + start_time = time.time() + for op in b.stream(start=23483000, stop=23483200, threading=threading, thread_num=thread_num, + opNames=['vote']): + sys.stdout.write("\r%s" % op['block_num']) + opcount += 1 + now = time.time() + total_duration = now - start_time + print(" votes: %d, time %.2f" % (opcount, total_duration)) + return opcount, total_duration + + +if __name__ == "__main__": + node_setup = 1 + threading = True + thread_num = 8 + timeout = 10 + nodes = NodeList() + nodes.update_nodes(weights={"block": 1}) + node_list_wss = nodes.get_nodes(https=False)[:5] + node_list_https = nodes.get_nodes(wss=False)[:5] + + vote_result = [] + duration = [] + stm_wss = DPay(node=node_list_wss, timeout=timeout) + stm_https = DPay(node=node_list_https, timeout=timeout) + print("Without threading wss") + opcount_wot_wss, total_duration_wot_wss = stream_votes(stm_wss, False, 8) + print("Without threading https") + opcount_wot_https, total_duration_wot_https = stream_votes(stm_https, False, 8) + if threading: + print("\n Threading with %d threads is activated now." % thread_num) + + stm = DPay(node=node_list_wss, timeout=timeout) + opcount_wss, total_duration_wss = stream_votes(stm, threading, thread_num) + opcount_https, total_duration_https = stream_votes(stm, threading, thread_num) + print("Finished!") + + print("Results:") + print("No Threads with wss duration: %.2f s - votes: %d" % (total_duration_wot_wss, opcount_wot_wss)) + print("No Threads with https duration: %.2f s - votes: %d" % (total_duration_wot_https, opcount_wot_https)) + print("%d Threads with wss duration: %.2f s - votes: %d" % (thread_num, total_duration_wss, opcount_wss)) + print("%d Threads with https duration: %.2f s - votes: %d" % (thread_num, total_duration_https, opcount_https)) diff --git a/examples/using_custom_chain.py b/examples/using_custom_chain.py new file mode 100755 index 0000000..e078c94 --- /dev/null +++ b/examples/using_custom_chain.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycliapi.exceptions import NumRetriesReached +from dpaycli.nodelist import NodeList +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +if __name__ == "__main__": + stm = DPay(node=["https://testnet.dpaydev.com"], + custom_chains={"TESTNETHF20": + {'chain_assets': + [ + {"asset": "@@000000013", "symbol": "BBD", "precision": 3, "id": 0}, + {"asset": "@@000000021", "symbol": "BEX", "precision": 3, "id": 1}, + {"asset": "@@000000037", "symbol": "VESTS", "precision": 6, "id": 2} + ], + 'chain_id': '46d82ab7d8db682eb1959aed0ada039a6d49afa1602491f93dde9cac3e8e6c32', + 'min_version': '0.20.0', + 'prefix': 'TST'}}) + print(stm.get_blockchain_version()) + print(stm.get_config()["DPAY_CHAIN_ID"]) diff --git a/examples/using_steem_offline.py b/examples/using_steem_offline.py new file mode 100755 index 0000000..acc9eaf --- /dev/null +++ b/examples/using_steem_offline.py @@ -0,0 +1,47 @@ +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import sys +from datetime import datetime, timedelta +import time +import io +import logging + +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycli.witness import Witness +from dpayclibase import operations +from dpaycli.transactionbuilder import TransactionBuilder +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.dpay import DPay +from dpaycli.utils import parse_time, formatTimedelta +from dpaycliapi.exceptions import NumRetriesReached +from dpaycli.nodelist import NodeList +from dpayclibase.transactions import getBlockParams +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# example wif +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +if __name__ == "__main__": + stm_online = DPay() + ref_block_num, ref_block_prefix = getBlockParams(stm_online) + print("ref_block_num %d - ref_block_prefix %d" % (ref_block_num, ref_block_prefix)) + + stm = DPay(offline=True) + + op = operations.Transfer({'from': 'dpayclibot', + 'to': 'holger80', + 'amount': "0.001 BBD", + 'memo': ""}) + tb = TransactionBuilder(dpay_instance=stm) + + tb.appendOps([op]) + tb.appendWif(wif) + tb.constructTx(ref_block_num=ref_block_num, ref_block_prefix=ref_block_prefix) + tx = tb.sign(reconstruct_tx=False) + print(tx.json()) diff --git a/examples/waitForRecharge.py b/examples/waitForRecharge.py new file mode 100755 index 0000000..0d8db6b --- /dev/null +++ b/examples/waitForRecharge.py @@ -0,0 +1,33 @@ +from win10toast import ToastNotifier +from dpaycli.account import Account +import time + +if __name__ == "__main__": + toaster = ToastNotifier() + + randowhale = Account("randowhale") + randowhale.refresh() + + try: + while True: + time.sleep(15) + randowhale.refresh() + if randowhale.profile["name"] == "Rando Is Sleeping": + # print(randowhale) + print("still sleeping, awake in " + randowhale.get_recharge_time_str(99)) + else: + toaster.show_toast(randowhale.profile["name"], + randowhale.profile["about"], + icon_path=None, + duration=5, + threaded=True) + # Wait for threaded notification to finish + while toaster.notification_active(): + time.sleep(0.1) + + except KeyboardInterrupt: + pass + + # Wait for threaded notification to finish + while toaster.notification_active(): + time.sleep(0.1) diff --git a/examples/watching_the_watchers.py b/examples/watching_the_watchers.py new file mode 100755 index 0000000..42e1d19 --- /dev/null +++ b/examples/watching_the_watchers.py @@ -0,0 +1,154 @@ +from __future__ import print_function +import sys +from datetime import datetime, timedelta +import time +import io +from dpaycli.blockchain import Blockchain +from dpaycli.comment import Comment +from dpaycli.account import Account +from dpaycli.utils import parse_time, construct_authorperm +from dpaycli import exceptions +import logging +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class WatchingTheWatchers: + def __init__(self): + self.dvcount = 0 + self.account_type = dict() + self.account_relations = dict() + self.by_voter = dict() + self.by_target = dict() + self.by_pair = dict() + + def update(self, downvoter, downvoted, dvpower, flagpower): + pair = downvoter + "/" + downvoted + if downvoter not in self.by_voter: + self.by_voter[downvoter] = [0.0, 0.0] + if downvoted not in self.by_target: + self.by_target[downvoted] = [0.0, 0.0] + if pair not in self.by_pair: + self.by_pair[pair] = [0.0, 0.0] + self.by_voter[downvoter][0] = self.by_voter[downvoter][0] + dvpower + self.by_voter[downvoter][1] = self.by_voter[downvoter][1] + flagpower + self.by_target[downvoted][0] = self.by_target[downvoted][0] + dvpower + self.by_target[downvoted][1] = self.by_target[downvoted][1] + flagpower + self.by_pair[pair][0] = self.by_pair[pair][0] + dvpower + self.by_pair[pair][1] = self.by_pair[pair][1] + flagpower + self.dvcount = self.dvcount + 1 + if self.dvcount % 100 == 0: + print(self.dvcount, "downvotes so far.") + + def set_account_info(self, account, fish, related): + self.account_type[account] = fish + if len(related) > 0: + self.account_relations[account] = related + + def report(self): + print("[REPORT]") + print(" * account_type :", self.account_type) + print() + print(" * account_relations :", self.account_relations) + print() + print(" * by voter :", self.by_voter) + print() + print(" * by target :", self.by_target) + print() + print(" * by pair :", self.by_pair) + print() + self.dvcount = 0 + self.account_type = dict() + self.account_relations = dict() + self.by_voter = dict() + self.by_target = dict() + self.by_pair = dict() + + +class WatchingTheWatchersBot: + def __init__(self, wtw): + self.stopped = None + self.wtw = wtw + self.looked_up = set() + + def vote(self, vote_event): + def process_vote_content(event): + start_rshares = 0.0 + for vote in event["active_votes"]: + if vote["voter"] == vote_event["voter"] and float(vote["rshares"]) < 0: + if start_rshares + float(vote["rshares"]) < 0: + flag_power = 0 - start_rshares - float(vote["rshares"]) + else: + flag_power = 0 + downvote_power = 0 - vote["rshares"] - flag_power + self.wtw.update(vote["voter"], vote_event["author"], downvote_power, flag_power) + + def lookup_accounts(acclist): + def user_info(accounts): + if len(acclist) != len(accounts): + print("OOPS:", len(acclist), len(accounts), acclist) + for index in range(0, len(accounts)): + a = accounts[index] + account = acclist[index] + vp = (a["vesting_shares"].amount + + a["received_vesting_shares"].amount - + a["delegated_vesting_shares"].amount) / 1000000.0 + fish = "redfish" + if vp >= 1.0: + fish = "minnow" + if vp >= 10.0: + fish = "dolphin" + if vp >= 100: + fish = "orca" + if vp > 1000: + fish = "whale" + racc = None + proxy = None + related = list() + if a["recovery_account"] != "whitehorse" and a["recovery_account"] != "": + related.append(a["recovery_account"]) + if a["proxy"] != "": + related.append(a["proxy"]) + self.wtw.set_account_info(account, fish, related) + accl2 = list() + if racc is not None and racc not in self.looked_up: + accl2.append(racc) + if proxy is not None and proxy not in self.looked_up: + accl2.append(proxy) + if len(accl2) > 0: + lookup_accounts(accl2) + accounts = [] + for a in acclist: + accounts.append(Account(a)) + user_info(accounts) + if vote_event["weight"] < 0: + authorperm = construct_authorperm(vote_event["author"], vote_event["permlink"]) + # print(authorperm) + try: + process_vote_content(Comment(authorperm)) + except exceptions.ContentDoesNotExistsException: + print("Could not find Comment: %s" % (authorperm)) + al = list() + if not vote_event["voter"] in self.looked_up: + al.append(vote_event["voter"]) + self.looked_up.add(vote_event["voter"]) + if not vote_event["author"] in self.looked_up: + al.append(vote_event["author"]) + self.looked_up.add(vote_event["author"]) + if len(al) > 0: + lookup_accounts(al) + + +if __name__ == "__main__": + wtw = WatchingTheWatchers() + tb = WatchingTheWatchersBot(wtw) + blockchain = Blockchain() + threading = True + thread_num = 16 + cur_block = blockchain.get_current_block() + stop = cur_block.identifier + startdate = cur_block.time() - timedelta(days=1) + start = blockchain.get_estimated_block_num(startdate, accurate=True) + for vote in blockchain.stream(opNames=["vote"], start=start, stop=stop, threading=threading, thread_num=thread_num): + tb.vote(vote) + wtw.report() diff --git a/examples/write_blocks_to_file.py b/examples/write_blocks_to_file.py new file mode 100755 index 0000000..c84d717 --- /dev/null +++ b/examples/write_blocks_to_file.py @@ -0,0 +1,76 @@ +from __future__ import print_function +import sys +from datetime import datetime, timedelta +import time +import io +import re +import gzip +import json +from dpaycli.blockchain import Blockchain +from dpaycli.comment import Comment +from dpaycli.account import Account +from dpaycli.utils import parse_time, construct_authorperm +from dpaycli import exceptions +import logging +from binascii import hexlify, unhexlify +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +try: + from cPickle import dumps, loads +except ImportError: + from pickle import dumps, loads + + +def s_dump_text(elt_to_pickle, file_obj): + '''dumps one element to file_obj, a file opened in write mode''' + pickled_elt_str = dumps(elt_to_pickle) + file_obj.write(hexlify(pickled_elt_str).decode("utf-8")) + # record separator is a blank line + file_obj.write('\n') + + +def s_dump_binary(elt_to_pickle, file_obj): + '''dumps one element to file_obj, a file opened in binary write mode''' + pickled_elt_str = dumps(elt_to_pickle) + file_obj.write(hexlify(pickled_elt_str)) + # record separator is a blank line + file_obj.write(bytes('\n'.encode("latin1"))) + + +def s_load_text(file_obj): + '''load contents from file_obj, returning a generator that yields one + element at a time''' + for line in file_obj: + elt = loads(unhexlify(line[:-1].encode("latin"))) + yield elt + + +def s_load_binary(file_obj): + '''load contents from file_obj, returning a generator that yields one + element at a time''' + for line in file_obj: + elt = loads(unhexlify(line[:-1])) + yield elt + + +if __name__ == "__main__": + + blockchain = Blockchain() + threading = True + thread_num = 8 + cur_block = blockchain.get_current_block() + stop = cur_block.identifier + startdate = cur_block.time() - timedelta(seconds=3600) + start = blockchain.get_estimated_block_num(startdate, accurate=True) + outf = gzip.open('blocks1.pkl', 'w') + blocks = 0 + for block in blockchain.stream(opNames=[], start=start, stop=stop, threading=threading, thread_num=thread_num): + s_dump_binary(block, outf) + blocks = blocks + 1 + if blocks % 200 == 0: + print(blocks, "blocks streamed") + outf.close() + + for block in s_load_binary(gzip.open('blocks1.pkl')): + print(block) diff --git a/package-linux.sh b/package-linux.sh new file mode 100755 index 0000000..2eddba9 --- /dev/null +++ b/package-linux.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +COMM_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) +COMM_COUNT=$(git rev-list --count HEAD) +BUILD="dPay-${COMM_TAG}-${COMM_COUNT}_linux.tar.gz" + + +rm -rf dist build locale +pip install +python setup.py clean +python setup.py build_ext +# python setup.py build_locales +pip install pyinstaller +pyinstaller dpay-onedir.spec + +cd dist + +tar -zcvf ${BUILD} dPay +if [ -n "$UPLOAD_LINUX" ] +then + curl --upload-file ${BUILD} https://transfer.sh/ + # Required for a newline between the outputs + echo -e "\n" + md5sum ${BUILD} + echo -e "\n" + sha256sum ${BUILD} +fi diff --git a/package-osx.sh b/package-osx.sh new file mode 100755 index 0000000..42c48fb --- /dev/null +++ b/package-osx.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +VERSION=$(python -c 'import dpaycli; print(dpaycli.__version__)') +COMM_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) +COMM_COUNT=$(git rev-list --count HEAD) +BUILD="dpay-${COMM_TAG}-${COMM_COUNT}_osx.dmg" + +rm -rf dist build locale +pip install +python setup.py clean +python setup.py build_ext +# python setup.py build_locales +pip install pyinstaller +pyinstaller dpay-onedir.spec + +cd dist +ditto -rsrc --arch x86_64 'dpay.app' 'dpay.tmp' +rm -r 'dpay.app' +mv 'dpay.tmp' 'dpay.app' +hdiutil create -volname "dPay $VERSION" -srcfolder 'dpay.app' -ov -format UDBZ "$BUILD" +if [ -n "$UPLOAD_OSX" ] +then + curl --upload-file "$BUILD" https://transfer.sh/ + # Required for a newline between the outputs + echo -e "\n" + md5 -r "$BUILD" + echo -e "\n" + shasum -a 256 "$BUILD" +fi diff --git a/pytest.ini b/pytest.ini new file mode 100755 index 0000000..908b333 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +norecursedirs = .git .* *.egg* old docs dist build examples +addopts = -rw --doctest-modules diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100755 index 0000000..cee15a4 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,32 @@ +pip +setuptools +wheel +future==0.16.0 +ecdsa==0.13 +requests==2.19.1 +websocket-client==0.53.0 +pytz==2018.5 +pycryptodomex==3.6.6 +scrypt==0.8.6 +Events==0.3 +secp256k1==0.13.2 +cryptography==2.3.1 +pyyaml==3.13 +mock==2.0.0 +appdirs==1.4.3 +Click==7.0 +prettytable +pycodestyle==2.4.0 +pyflakes==2.0.0 +pylibscrypt==1.7.1 +six==1.11.0 +pytest +pytest-mock +pytest-cov +coverage +parameterized +tox +codacy-coverage +virtualenv +codecov + diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 0000000..3a5c327 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[metadata] +description-file = README.rst +license_file = LICENSE.txt + +[aliases] +test=pytest + +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..20af0d9 --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Packaging logic for dpaycli.""" +import codecs +import io +import os +import sys + +from setuptools import setup + +# Work around mbcs bug in distutils. +# http://bugs.python.org/issue10945 + +try: + codecs.lookup('mbcs') +except LookupError: + ascii = codecs.lookup('ascii') + codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs')) + +VERSION = '0.02.0' + +tests_require = ['mock >= 2.0.0', 'pytest', 'pytest-mock', 'parameterized'] + +requires = [ + "future", + "ecdsa", + "requests", + "websocket-client", + "appdirs", + "Events", + "scrypt", + "pylibscrypt", + "pycryptodomex", + "pytz", + "Click", + "prettytable" +] + + +def write_version_py(filename): + """Write version.""" + cnt = """\"""THIS FILE IS GENERATED FROM dpaycli SETUP.PY.\""" +version = '%(version)s' +""" + with open(filename, 'w') as a: + a.write(cnt % {'version': VERSION}) + + +def get_long_description(): + """Generate a long description from the README file.""" + descr = [] + for fname in ('README.rst',): + with io.open(fname, encoding='utf-8') as f: + descr.append(f.read()) + return '\n\n'.join(descr) + + +if __name__ == '__main__': + + # Rewrite the version file everytime + write_version_py('dpaycli/version.py') + write_version_py('dpayclibase/version.py') + write_version_py('dpaycliapi/version.py') + write_version_py('dpaycligraphenebase/version.py') + + setup( + name='dpaycli', + version=VERSION, + description='Unofficial Python library for dPay', + long_description=get_long_description(), + download_url='https://github.com/holgern/dpaycli/tarball/' + VERSION, + author='Holger Nahrstaedt', + author_email='holger@nahrstaedt.de', + maintainer='Holger Nahrstaedt', + maintainer_email='holger@nahrstaedt.de', + url='http://www.github.com/holgern/dpaycli', + keywords=['dpay', 'library', 'api', 'rpc'], + packages=[ + "dpaycli", + "dpaycliapi", + "dpayclibase", + "dpaycligraphenebase", + "dpaycligrapheneapi" + ], + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Financial and Insurance Industry', + 'Topic :: Office/Business :: Financial', + ], + install_requires=requires, + entry_points={ + 'console_scripts': [ + 'dpay=dpaycli.cli:cli', + ], + }, + setup_requires=['pytest-runner'], + tests_require=tests_require, + include_package_data=True, + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100755 index 0000000..e0310a0 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/dpaycli/__init__.py b/tests/dpaycli/__init__.py new file mode 100755 index 0000000..e0310a0 --- /dev/null +++ b/tests/dpaycli/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/dpaycli/test_account.py b/tests/dpaycli/test_account.py new file mode 100755 index 0000000..cb521f3 --- /dev/null +++ b/tests/dpaycli/test_account.py @@ -0,0 +1,517 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import super +import unittest +import mock +import pytz +from datetime import datetime, timedelta +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.account import Account +from dpaycli.block import Block +from dpaycli.amount import Amount +from dpaycli.asset import Asset +from dpaycli.utils import formatTimeString +from dpaycli.nodelist import NodeList +from dpaycli.instance import set_shared_dpay_instance + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + bundle=False, + unsigned=True, + # Overwrite wallet to use this list of wifs only + keys={"active": wif}, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + bundle=False, + unsigned=True, + # Overwrite wallet to use this list of wifs only + keys={"active": wif}, + num_retries=10 + ) + cls.account = Account("dpayclibot", full=True, dpay_instance=cls.bts) + set_shared_dpay_instance(cls.bts) + + def test_account(self): + stm = self.bts + account = self.account + Account("dpayclibot", dpay_instance=stm) + with self.assertRaises( + exceptions.AccountDoesNotExistsException + ): + Account("DoesNotExistsXXX", dpay_instance=stm) + # asset = Asset("1.3.0") + # symbol = asset["symbol"] + self.assertEqual(account.name, "dpayclibot") + self.assertEqual(account["name"], account.name) + self.assertIsInstance(account.get_balance("available", "BBD"), Amount) + account.print_info() + # self.assertIsInstance(account.balance({"symbol": symbol}), Amount) + self.assertIsInstance(account.available_balances, list) + self.assertTrue(account.virtual_op_count() > 0) + + # BlockchainObjects method + account.cached = False + self.assertTrue(list(account.items())) + account.cached = False + self.assertIn("id", account) + account.cached = False + # self.assertEqual(account["id"], "1.2.1") + self.assertEqual(str(account), "") + self.assertIsInstance(Account(account), Account) + + def test_history(self): + account = self.account + zero_element = 0 + h_all_raw = [] + for h in account.history_reverse(raw_output=True): + h_all_raw.append(h) + # h_all_raw = h_all_raw[zero_element:] + zero_element = h_all_raw[-1][0] + h_list = [] + for h in account.history(stop=10, use_block_num=False, batch_size=10, raw_output=True): + h_list.append(h) + # self.assertEqual(h_list[0][0], zero_element) + self.assertEqual(h_list[-1][0], 10) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-1][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-11 + zero_element][1]['block']) + h_list = [] + for h in account.history(start=1, stop=9, use_block_num=False, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 1) + self.assertEqual(h_list[-1][0], 9) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + start = formatTimeString(h_list[0][1]["timestamp"]) + stop = formatTimeString(h_list[-1][1]["timestamp"]) + h_list = [] + for h in account.history(start=start, stop=stop, use_block_num=False, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 1) + self.assertEqual(h_list[-1][0], 9) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + h_list = [] + for h in account.history_reverse(start=10, stop=0, use_block_num=False, batch_size=10, raw_output=False): + h_list.append(h) + # zero_element = h_list[-1]['index'] + self.assertEqual(h_list[0]['index'], 10) + # self.assertEqual(h_list[-1]['index'], zero_element) + self.assertEqual(h_list[0]['block'], h_all_raw[-11 + zero_element][1]['block']) + self.assertEqual(h_list[-1]['block'], h_all_raw[-1][1]['block']) + h_list = [] + for h in account.history_reverse(start=9, stop=1, use_block_num=False, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 9) + self.assertEqual(h_list[-1][0], 1) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + start = formatTimeString(h_list[0][1]["timestamp"]) + stop = formatTimeString(h_list[-1][1]["timestamp"]) + h_list = [] + for h in account.history_reverse(start=start, stop=stop, use_block_num=False, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 9) + self.assertEqual(h_list[-1][0], 1) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=False, order=1, raw_output=True): + h_list.append(h) + # self.assertEqual(h_list[0][0], zero_element) + self.assertEqual(h_list[-1][0], 10) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-1][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-11 + zero_element][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=False, start=1, stop=9, order=1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 1) + self.assertEqual(h_list[-1][0], 9) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + start = formatTimeString(h_list[0][1]["timestamp"]) + stop = formatTimeString(h_list[-1][1]["timestamp"]) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=False, start=start, stop=stop, order=1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 1) + self.assertEqual(h_list[-1][0], 9) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=False, order=-1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 10) + # self.assertEqual(h_list[-1][0], zero_element) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-11 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-1][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=False, start=9, stop=1, order=-1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 9) + self.assertEqual(h_list[-1][0], 1) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + start = formatTimeString(h_list[0][1]["timestamp"]) + stop = formatTimeString(h_list[-1][1]["timestamp"]) + h_list = [] + for h in account.get_account_history(10, 10, start=start, stop=stop, order=-1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 9) + self.assertEqual(h_list[-1][0], 1) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + + def test_history2(self): + stm = self.bts + account = Account("dpayclibot", dpay_instance=stm) + h_list = [] + max_index = account.virtual_op_count() + for h in account.history(start=max_index - 4, stop=max_index, use_block_num=False, batch_size=2, raw_output=False): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i]["index"] - h_list[i - 1]["index"], 1) + + h_list = [] + for h in account.history(start=max_index - 4, stop=max_index, use_block_num=False, batch_size=6, raw_output=False): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i]["index"] - h_list[i - 1]["index"], 1) + + h_list = [] + for h in account.history(start=max_index - 4, stop=max_index, use_block_num=False, batch_size=2, raw_output=True): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i][0] - h_list[i - 1][0], 1) + + h_list = [] + for h in account.history(start=max_index - 4, stop=max_index, use_block_num=False, batch_size=6, raw_output=True): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i][0] - h_list[i - 1][0], 1) + + def test_history_reverse2(self): + stm = self.bts + account = Account("dpayclibot", dpay_instance=stm) + h_list = [] + max_index = account.virtual_op_count() + for h in account.history_reverse(start=max_index, stop=max_index - 4, use_block_num=False, batch_size=2, raw_output=False): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i]["index"] - h_list[i - 1]["index"], -1) + + h_list = [] + for h in account.history_reverse(start=max_index, stop=max_index - 4, use_block_num=False, batch_size=6, raw_output=False): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i]["index"] - h_list[i - 1]["index"], -1) + + h_list = [] + for h in account.history_reverse(start=max_index, stop=max_index - 4, use_block_num=False, batch_size=6, raw_output=True): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i][0] - h_list[i - 1][0], -1) + + h_list = [] + for h in account.history_reverse(start=max_index, stop=max_index - 4, use_block_num=False, batch_size=2, raw_output=True): + h_list.append(h) + self.assertEqual(len(h_list), 5) + for i in range(1, 5): + self.assertEqual(h_list[i][0] - h_list[i - 1][0], -1) + + def test_history_block_num(self): + stm = self.bts + zero_element = 0 + account = Account("fullnodeupdate", dpay_instance=stm) + h_all_raw = [] + for h in account.history_reverse(raw_output=True): + h_all_raw.append(h) + h_list = [] + for h in account.history(start=h_all_raw[-1][1]["block"], stop=h_all_raw[-11 + zero_element][1]["block"], use_block_num=True, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], zero_element) + self.assertEqual(h_list[-1][0], 10) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-1][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-11 + zero_element][1]['block']) + h_list = [] + for h in account.history_reverse(start=h_all_raw[-11 + zero_element][1]["block"], stop=h_all_raw[-1][1]["block"], use_block_num=True, batch_size=10, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 10) + self.assertEqual(h_list[-1][0], zero_element) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-11 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-1][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=True, start=h_all_raw[-2 + zero_element][1]["block"], stop=h_all_raw[-10 + zero_element][1]["block"], order=1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 1) + self.assertEqual(h_list[-1][0], 9) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + h_list = [] + for h in account.get_account_history(10, 10, use_block_num=True, start=h_all_raw[-10 + zero_element][1]["block"], stop=h_all_raw[-2 + zero_element][1]["block"], order=-1, raw_output=True): + h_list.append(h) + self.assertEqual(h_list[0][0], 9) + self.assertEqual(h_list[-1][0], 1) + self.assertEqual(h_list[0][1]['block'], h_all_raw[-10 + zero_element][1]['block']) + self.assertEqual(h_list[-1][1]['block'], h_all_raw[-2 + zero_element][1]['block']) + + def test_account_props(self): + account = self.account + rep = account.get_reputation() + self.assertTrue(isinstance(rep, float)) + vp = account.get_voting_power() + self.assertTrue(vp >= 0) + self.assertTrue(vp <= 100) + bp = account.get_dpay_power() + self.assertTrue(bp >= 0) + vv = account.get_voting_value_BBD() + self.assertTrue(vv >= 0) + bw = account.get_bandwidth() + self.assertTrue(bw['used'] <= bw['allocated']) + followers = account.get_followers() + self.assertTrue(isinstance(followers, list)) + following = account.get_following() + self.assertTrue(isinstance(following, list)) + count = account.get_follow_count() + self.assertEqual(count['follower_count'], len(followers)) + self.assertEqual(count['following_count'], len(following)) + + def test_MissingKeyError(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.convert("1 BBD") + with self.assertRaises( + exceptions.MissingKeyError + ): + tx.sign() + + def test_withdraw_vesting(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.withdraw_vesting("100 VESTS") + self.assertEqual( + (tx["operations"][0][0]), + "withdraw_vesting" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["account"]) + + def test_delegate_vesting_shares(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.delegate_vesting_shares("test1", "100 VESTS") + self.assertEqual( + (tx["operations"][0][0]), + "delegate_vesting_shares" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["delegator"]) + + def test_claim_reward_balance(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.claim_reward_balance() + self.assertEqual( + (tx["operations"][0][0]), + "claim_reward_balance" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["account"]) + + def test_cancel_transfer_from_savings(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.cancel_transfer_from_savings(0) + self.assertEqual( + (tx["operations"][0][0]), + "cancel_transfer_from_savings" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["from"]) + + def test_transfer_from_savings(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.transfer_from_savings(1, "BEX", "") + self.assertEqual( + (tx["operations"][0][0]), + "transfer_from_savings" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["from"]) + + def test_transfer_to_savings(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.transfer_to_savings(1, "BEX", "") + self.assertEqual( + (tx["operations"][0][0]), + "transfer_to_savings" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["from"]) + + def test_convert(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.convert("1 BBD") + self.assertEqual( + (tx["operations"][0][0]), + "convert" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["owner"]) + + def test_transfer_to_vesting(self): + w = self.account + w.dpay.txbuffer.clear() + tx = w.transfer_to_vesting("1 BEX") + self.assertEqual( + (tx["operations"][0][0]), + "transfer_to_vesting" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpayclibot", + op["from"]) + + def test_json_export(self): + account = self.account + if account.dpay.rpc.get_use_appbase(): + content = self.bts.rpc.find_accounts({'accounts': [account["name"]]}, api="database")["accounts"][0] + else: + content = self.bts.rpc.get_accounts([account["name"]])[0] + keys = list(content.keys()) + json_content = account.json() + exclude_list = ['owner_challenged', 'average_bandwidth'] # ['json_metadata', 'reputation', 'active_votes', 'savings_bbd_seconds'] + for k in keys: + if k not in exclude_list: + if isinstance(content[k], dict) and isinstance(json_content[k], list): + content_list = [content[k]["amount"], content[k]["precision"], content[k]["nai"]] + self.assertEqual(content_list, json_content[k]) + else: + self.assertEqual(content[k], json_content[k]) + + def test_estimate_virtual_op_num(self): + stm = self.bts + account = Account("gtg", dpay_instance=stm) + block_num = 21248120 + block = Block(block_num, dpay_instance=stm) + op_num1 = account.estimate_virtual_op_num(block.time(), stop_diff=1, max_count=100) + op_num2 = account.estimate_virtual_op_num(block_num, stop_diff=1, max_count=100) + op_num3 = account.estimate_virtual_op_num(block_num, stop_diff=100, max_count=100) + op_num4 = account.estimate_virtual_op_num(block_num, stop_diff=0.00001, max_count=100) + self.assertTrue(abs(op_num1 - op_num2) < 2) + self.assertTrue(abs(op_num1 - op_num4) < 2) + self.assertTrue(abs(op_num1 - op_num3) < 200) + block_diff1 = 0 + block_diff2 = 0 + for h in account.get_account_history(op_num4 - 1, 0): + block_diff1 = (block_num - h["block"]) + for h in account.get_account_history(op_num4 + 1, 0): + block_diff2 = (block_num - h["block"]) + self.assertTrue(block_diff1 > 0) + self.assertTrue(block_diff2 <= 0) + + def test_estimate_virtual_op_num2(self): + account = self.account + h_all_raw = [] + for h in account.history(raw_output=False): + h_all_raw.append(h) + last_block = h_all_raw[0]["block"] + i = 1 + for op in h_all_raw[1:]: + new_block = op["block"] + block_num = last_block + int((new_block - last_block) / 2) + op_num = account.estimate_virtual_op_num(block_num, stop_diff=0.1, max_count=100) + if op_num > 0: + op_num -= 1 + self.assertTrue(op_num <= i) + i += 1 + last_block = new_block + + def test_history_votes(self): + stm = self.bts + account = Account("gtg", dpay_instance=stm) + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=2) + votes_list = [] + for v in account.history(start=limit_time, only_ops=["vote"]): + votes_list.append(v) + start_num = votes_list[0]["block"] + votes_list2 = [] + for v in account.history(start=start_num, only_ops=["vote"]): + votes_list2.append(v) + self.assertTrue(abs(len(votes_list) - len(votes_list2)) < 2) + + def test_comment_history(self): + account = self.account + comments = [] + for c in account.comment_history(limit=1): + comments.append(c) + self.assertEqual(len(comments), 1) + self.assertEqual(comments[0]["author"], account["name"]) + self.assertTrue(comments[0].is_comment()) + self.assertTrue(comments[0].depth > 0) + + def test_blog_history(self): + account = Account("holger80", dpay_instance=self.bts) + posts = [] + for p in account.blog_history(limit=1): + posts.append(p) + self.assertEqual(len(posts), 1) + self.assertEqual(posts[0]["author"], account["name"]) + self.assertTrue(posts[0].is_main_post()) + self.assertTrue(posts[0].depth == 0) + + def test_reply_history(self): + account = self.account + replies = [] + for r in account.reply_history(limit=1): + replies.append(r) + self.assertEqual(len(replies), 1) + self.assertTrue(replies[0].is_comment()) + self.assertTrue(replies[0].depth > 0) + + def test_get_vote_pct_for_BBD(self): + account = self.account + for vote_pwr in range(5, 100, 5): + self.assertTrue(9900 <= account.get_vote_pct_for_BBD(account.get_voting_value_BBD(voting_power=vote_pwr), voting_power=vote_pwr) <= 11000) diff --git a/tests/dpaycli/test_aes.py b/tests/dpaycli/test_aes.py new file mode 100755 index 0000000..2d5b197 --- /dev/null +++ b/tests/dpaycli/test_aes.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import range +from builtins import super +import string +import random +import unittest +import base64 +from pprint import pprint +from dpaycli.aes import AESCipher + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.aes = AESCipher("Foobar") + + def test_str(self): + self.assertIsInstance(AESCipher.str_to_bytes("foobar"), bytes) + self.assertIsInstance(AESCipher.str_to_bytes(b"foobar"), bytes) + + def test_key(self): + self.assertEqual( + base64.b64encode(self.aes.key), + b"6BGBj4DZw8ItV3uoPWGWeI5VO7QIU1u0IQXN/3JqYKs=" + ) + + def test_pad(self): + self.assertEqual( + base64.b64encode(self.aes._pad(b"123456")), + b"MTIzNDU2GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGho=" + ) + + def test_unpad(self): + self.assertEqual( + self.aes._unpad(base64.b64decode(b"MTIzNDU2GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGho=")), + b"123456" + ) + + def test_padding(self): + for n in range(1, 64): + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(n)) + self.assertEqual( + self.aes._unpad(self.aes._pad( + bytes(name, "utf-8"))), + bytes(name, "utf-8") + ) + + def test_encdec(self): + for n in range(1, 16): + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(64)) + self.assertEqual( + self.aes.decrypt(self.aes.encrypt(name)), + name) diff --git a/tests/dpaycli/test_amount.py b/tests/dpaycli/test_amount.py new file mode 100755 index 0000000..f13af83 --- /dev/null +++ b/tests/dpaycli/test_amount.py @@ -0,0 +1,262 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from dpaycli import DPay +from dpaycli.amount import Amount +from dpaycli.asset import Asset +from dpaycli.nodelist import NodeList +from dpaycli.instance import set_shared_dpay_instance, SharedInstance + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(appbase=False), + nobroadcast=True, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + use_condenser=False, + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + cls.asset = Asset("BBD") + cls.symbol = cls.asset["symbol"] + cls.precision = cls.asset["precision"] + cls.asset2 = Asset("BEX") + + def dotest(self, ret, amount, symbol): + self.assertEqual(float(ret), float(amount)) + self.assertEqual(ret["symbol"], symbol) + self.assertIsInstance(ret["asset"], dict) + self.assertIsInstance(ret["amount"], float) + + def test_init(self): + stm = self.bts + # String init + asset = Asset("BBD", dpay_instance=stm) + symbol = asset["symbol"] + precision = asset["precision"] + amount = Amount("1 {}".format(symbol), dpay_instance=stm) + self.dotest(amount, 1, symbol) + + # Amount init + amount = Amount(amount, dpay_instance=stm) + self.dotest(amount, 1, symbol) + + # blockchain dict init + amount = Amount({ + "amount": 1 * 10 ** precision, + "asset_id": asset["id"] + }, dpay_instance=stm) + self.dotest(amount, 1, symbol) + + # API dict init + amount = Amount({ + "amount": 1.3 * 10 ** precision, + "asset": asset["id"] + }, dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + # Asset as symbol + amount = Amount(1.3, Asset("BBD"), dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + # Asset as symbol + amount = Amount(1.3, symbol, dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=Asset("BBD", dpay_instance=stm), dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=dict(Asset("BBD", dpay_instance=stm)), dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=symbol, dpay_instance=stm) + self.dotest(amount, 1.3, symbol) + + def test_copy(self): + amount = Amount("1", self.symbol) + self.dotest(amount.copy(), 1, self.symbol) + + def test_properties(self): + amount = Amount("1", self.symbol) + self.assertEqual(amount.amount, 1.0) + self.assertEqual(amount.symbol, self.symbol) + self.assertIsInstance(amount.asset, Asset) + self.assertEqual(amount.asset["symbol"], self.symbol) + + def test_tuple(self): + amount = Amount("1", self.symbol) + self.assertEqual( + amount.tuple(), + (1.0, self.symbol)) + + def test_json_appbase(self): + asset = Asset("BBD", dpay_instance=self.bts) + amount = Amount("1", asset, new_appbase_format=False, dpay_instance=self.bts) + if self.bts.rpc.get_use_appbase(): + self.assertEqual( + amount.json(), + [str(1 * 10 ** asset.precision), asset.precision, asset.asset]) + else: + self.assertEqual(amount.json(), "1.000 BBD") + + def test_json_appbase2(self): + asset = Asset("BBD", dpay_instance=self.bts) + amount = Amount("1", asset, new_appbase_format=True, dpay_instance=self.bts) + if self.bts.rpc.get_use_appbase(): + self.assertEqual( + amount.json(), + {'amount': str(1 * 10 ** asset.precision), 'nai': asset.asset, 'precision': asset.precision}) + else: + self.assertEqual(amount.json(), "1.000 BBD") + + def test_string(self): + self.assertEqual( + str(Amount("10000", self.symbol)), + "10000.000 {}".format(self.symbol)) + + def test_int(self): + self.assertEqual( + int(Amount("1", self.symbol)), + 1000) + + def test_float(self): + self.assertEqual( + float(Amount("1", self.symbol)), + 1.00000) + + def test_plus(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 + a2, 3, self.symbol) + self.dotest(a1 + 2, 3, self.symbol) + with self.assertRaises(Exception): + a1 + Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 += a1 + self.dotest(a2, 3, self.symbol) + a2 += 5 + self.dotest(a2, 8, self.symbol) + with self.assertRaises(Exception): + a1 += Amount(1, asset=self.asset2) + + def test_minus(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 - a2, -1, self.symbol) + self.dotest(a1 - 5, -4, self.symbol) + with self.assertRaises(Exception): + a1 - Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 -= a1 + self.dotest(a2, 1, self.symbol) + a2 -= 1 + self.dotest(a2, 0, self.symbol) + self.dotest(a2 - 2, -2, self.symbol) + with self.assertRaises(Exception): + a1 -= Amount(1, asset=self.asset2) + + def test_mul(self): + a1 = Amount(5, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 * a2, 10, self.symbol) + self.dotest(a1 * 3, 15, self.symbol) + with self.assertRaises(Exception): + a1 * Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 *= 5 + self.dotest(a2, 10, self.symbol) + a2 = Amount(2, self.symbol) + a2 *= a1 + self.dotest(a2, 10, self.symbol) + with self.assertRaises(Exception): + a1 *= Amount(2, asset=self.asset2) + + def test_div(self): + a1 = Amount(15, self.symbol) + self.dotest(a1 / 3, 5, self.symbol) + self.dotest(a1 // 2, 7, self.symbol) + with self.assertRaises(Exception): + a1 / Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 /= 3 + self.dotest(a2, 5, self.symbol) + a2 = a1.copy() + a2 //= 2 + self.dotest(a2, 7, self.symbol) + with self.assertRaises(Exception): + a1 *= Amount(2, asset=self.asset2) + + def test_mod(self): + a1 = Amount(15, self.symbol) + a2 = Amount(3, self.symbol) + self.dotest(a1 % 3, 0, self.symbol) + self.dotest(a1 % a2, 0, self.symbol) + self.dotest(a1 % 2, 1, self.symbol) + with self.assertRaises(Exception): + a1 % Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 %= 3 + self.dotest(a2, 0, self.symbol) + with self.assertRaises(Exception): + a1 %= Amount(2, asset=self.asset2) + + def test_pow(self): + a1 = Amount(15, self.symbol) + a2 = Amount(3, self.symbol) + self.dotest(a1 ** 3, 15 ** 3, self.symbol) + self.dotest(a1 ** a2, 15 ** 3, self.symbol) + self.dotest(a1 ** 2, 15 ** 2, self.symbol) + with self.assertRaises(Exception): + a1 ** Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 **= 3 + self.dotest(a2, 15 ** 3, self.symbol) + with self.assertRaises(Exception): + a1 **= Amount(2, asset=self.asset2) + + def test_ltge(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.assertTrue(a1 < a2) + self.assertTrue(a2 > a1) + self.assertTrue(a2 > 1) + self.assertTrue(a1 < 5) + + def test_leeq(self): + a1 = Amount(1, self.symbol) + a2 = Amount(1, self.symbol) + self.assertTrue(a1 <= a2) + self.assertTrue(a1 >= a2) + self.assertTrue(a1 <= 1) + self.assertTrue(a1 >= 1) + + def test_ne(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.assertTrue(a1 != a2) + self.assertTrue(a1 != 5) + a1 = Amount(1, self.symbol) + a2 = Amount(1, self.symbol) + self.assertTrue(a1 == a2) + self.assertTrue(a1 == 1) diff --git a/tests/dpaycli/test_asciichart.py b/tests/dpaycli/test_asciichart.py new file mode 100755 index 0000000..24810de --- /dev/null +++ b/tests/dpaycli/test_asciichart.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import range +from builtins import super +import string +import random +import unittest +import base64 +from pprint import pprint +from dpaycli.asciichart import AsciiChart + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.curve = [1.2, 4.3, 2.0, -1.3, 6.4, 0.] + + def test_plot(self): + ac = AsciiChart(height=3, width=3) + self.assertEqual(len(ac.canvas), 0) + ret = ac.plot(self.curve, return_str=True) + ac.plot(self.curve, return_str=False) + self.assertTrue(len(ret) > 0) + ac.clear_data() + self.assertEqual(len(ac.canvas), 0) + + def test_plot2(self): + ac = AsciiChart(height=3, width=3) + ac.clear_data() + ac.adapt_on_series(self.curve) + self.assertEqual(ac.maximum, max(self.curve)) + self.assertEqual(ac.minimum, min(self.curve)) + self.assertEqual(ac.n, len(self.curve)) + ac.new_chart() + ac.add_axis() + ac.add_curve(self.curve) diff --git a/tests/dpaycli/test_asset.py b/tests/dpaycli/test_asset.py new file mode 100755 index 0000000..ff59f94 --- /dev/null +++ b/tests/dpaycli/test_asset.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import super +import unittest +from parameterized import parameterized +from dpaycli import DPay +from dpaycli.asset import Asset +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.exceptions import AssetDoesNotExistsException +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_assert(self, node_param): + if node_param == "normal": + stm = self.bts + else: + stm = self.testnet + with self.assertRaises(AssetDoesNotExistsException): + Asset("FOObarNonExisting", full=False, dpay_instance=stm) + + @parameterized.expand([ + ("normal", "BBD", "BBD", 3, "@@000000013"), + ("normal", "BEX", "BEX", 3, "@@000000021"), + ("normal", "VESTS", "VESTS", 6, "@@000000037"), + ("normal", "@@000000013", "BBD", 3, "@@000000013"), + ("normal", "@@000000021", "BEX", 3, "@@000000021"), + ("normal", "@@000000037", "VESTS", 6, "@@000000037"), + ]) + def test_properties(self, node_param, data, symbol_str, precision, asset_str): + if node_param == "normal": + stm = self.bts + else: + stm = self.testnet + asset = Asset(data, full=False, dpay_instance=stm) + self.assertEqual(asset.symbol, symbol_str) + self.assertEqual(asset.precision, precision) + self.assertEqual(asset.asset, asset_str) + + def test_assert_equal(self): + stm = self.bts + asset1 = Asset("BBD", full=False, dpay_instance=stm) + asset2 = Asset("BBD", full=False, dpay_instance=stm) + self.assertTrue(asset1 == asset2) + self.assertTrue(asset1 == "BBD") + self.assertTrue(asset2 == "BBD") + asset3 = Asset("BEX", full=False, dpay_instance=stm) + self.assertTrue(asset1 != asset3) + self.assertTrue(asset3 != "BBD") + self.assertTrue(asset1 != "BEX") + + a = {'asset': '@@000000021', 'precision': 3, 'id': 'BEX', 'symbol': 'BEX'} + b = {'asset': '@@000000021', 'precision': 3, 'id': '@@000000021', 'symbol': 'BEX'} + self.assertTrue(Asset(a, dpay_instance=stm) == Asset(b, dpay_instance=stm)) + + """ + # Mocker comes from pytest-mock, providing an easy way to have patched objects + # for the life of the test. + def test_calls(mocker): + asset = Asset("USD", lazy=True, dpay_instance=DPay(offline=True)) + method = mocker.patch.object(Asset, 'get_call_orders') + asset.calls + method.assert_called_with(10) + """ diff --git a/tests/dpaycli/test_base_objects.py b/tests/dpaycli/test_base_objects.py new file mode 100755 index 0000000..e6bf528 --- /dev/null +++ b/tests/dpaycli/test_base_objects.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from dpaycli import DPay, exceptions +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.account import Account +from dpaycli.witness import Witness +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + + def test_Account(self): + with self.assertRaises( + exceptions.AccountDoesNotExistsException + ): + Account("FOObarNonExisting") + + c = Account("test") + self.assertEqual(c["name"], "test") + self.assertIsInstance(c, Account) + + def test_Witness(self): + with self.assertRaises( + exceptions.WitnessDoesNotExistsException + ): + Witness("FOObarNonExisting") + + c = Witness("jesta") + self.assertEqual(c["owner"], "jesta") + self.assertIsInstance(c.account, Account) diff --git a/tests/dpaycli/test_block.py b/tests/dpaycli/test_block.py new file mode 100755 index 0000000..5ad913b --- /dev/null +++ b/tests/dpaycli/test_block.py @@ -0,0 +1,147 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.block import Block, BlockHeader +from datetime import datetime +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + cls.test_block_id = 19273700 + cls.test_block_id_testnet = 10000 + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_block(self, node_param): + if node_param == "normal": + bts = self.bts + test_block_id = self.test_block_id + else: + bts = self.testnet + test_block_id = self.test_block_id_testnet + block = Block(test_block_id, dpay_instance=bts) + self.assertEqual(block.identifier, test_block_id) + self.assertTrue(isinstance(block.time(), datetime)) + self.assertTrue(isinstance(block, dict)) + + self.assertTrue(len(block.operations)) + self.assertTrue(isinstance(block.ops_statistics(), dict)) + + block2 = Block(test_block_id + 1, dpay_instance=bts) + self.assertTrue(block2.time() > block.time()) + with self.assertRaises( + exceptions.BlockDoesNotExistsException + ): + Block(0, dpay_instance=bts) + + def test_block_only_ops(self): + bts = self.bts + test_block_id = self.test_block_id + block = Block(test_block_id, only_ops=True, dpay_instance=bts) + self.assertEqual(block.identifier, test_block_id) + self.assertTrue(isinstance(block.time(), datetime)) + self.assertTrue(isinstance(block, dict)) + + self.assertTrue(len(block.operations)) + self.assertTrue(isinstance(block.ops_statistics(), dict)) + + block2 = Block(test_block_id + 1, dpay_instance=bts) + self.assertTrue(block2.time() > block.time()) + with self.assertRaises( + exceptions.BlockDoesNotExistsException + ): + Block(0, dpay_instance=bts) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_block_header(self, node_param): + if node_param == "normal": + bts = self.bts + test_block_id = self.test_block_id + else: + bts = self.testnet + test_block_id = self.test_block_id_testnet + block = BlockHeader(test_block_id, dpay_instance=bts) + self.assertEqual(block.identifier, test_block_id) + self.assertTrue(isinstance(block.time(), datetime)) + self.assertTrue(isinstance(block, dict)) + + block2 = BlockHeader(test_block_id + 1, dpay_instance=bts) + self.assertTrue(block2.time() > block.time()) + with self.assertRaises( + exceptions.BlockDoesNotExistsException + ): + BlockHeader(0, dpay_instance=bts) + + def test_export(self): + bts = self.bts + block_num = 2000000 + + if bts.rpc.get_use_appbase(): + block = bts.rpc.get_block({"block_num": block_num}, api="block") + if block and "block" in block: + block = block["block"] + else: + block = bts.rpc.get_block(block_num) + + b = Block(block_num, dpay_instance=bts) + keys = list(block.keys()) + json_content = b.json() + + for k in keys: + if k not in "json_metadata": + if isinstance(block[k], dict) and isinstance(json_content[k], list): + self.assertEqual(list(block[k].values()), json_content[k]) + else: + self.assertEqual(block[k], json_content[k]) + + if bts.rpc.get_use_appbase(): + block = bts.rpc.get_block_header({"block_num": block_num}, api="block") + if "header" in block: + block = block["header"] + else: + block = bts.rpc.get_block_header(block_num) + + b = BlockHeader(block_num, dpay_instance=bts) + keys = list(block.keys()) + json_content = b.json() + + for k in keys: + if k not in "json_metadata": + if isinstance(block[k], dict) and isinstance(json_content[k], list): + self.assertEqual(list(block[k].values()), json_content[k]) + else: + self.assertEqual(block[k], json_content[k]) diff --git a/tests/dpaycli/test_blockchain.py b/tests/dpaycli/test_blockchain.py new file mode 100755 index 0000000..f43605c --- /dev/null +++ b/tests/dpaycli/test_blockchain.py @@ -0,0 +1,254 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from datetime import datetime, timedelta +import pytz +import time +from pprint import pprint +from dpaycli import DPay +from dpaycli.blockchain import Blockchain +from dpaycli.exceptions import BlockWaitTimeExceeded +from dpaycli.block import Block +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList +from dpayclibase.signedtransactions import Signed_Transaction + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +nodes_appbase = ["https://api.dpays.io", "https://dpayapi.com", "https://api.dpayjs.com"] + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + b = Blockchain(dpay_instance=cls.bts) + num = b.get_current_block_num() + cls.start = num - 25 + cls.stop = num + + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + b = Blockchain(dpay_instance=cls.testnet) + num = b.get_current_block_num() + cls.start_testnet = num - 25 + cls.stop_testnet = num + + def test_blockchain(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + num = b.get_current_block_num() + self.assertTrue(num > 0) + self.assertTrue(isinstance(num, int)) + block = b.get_current_block() + self.assertTrue(isinstance(block, Block)) + self.assertTrue((num - block.identifier) < 3) + block_time = b.block_time(block.identifier) + self.assertEqual(block.time(), block_time) + block_timestamp = b.block_timestamp(block.identifier) + timestamp = int(time.mktime(block.time().timetuple())) + self.assertEqual(block_timestamp, timestamp) + + def test_estimate_block_num(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + last_block = b.get_current_block() + num = last_block.identifier + old_block = Block(num - 60, dpay_instance=bts) + date = old_block.time() + est_block_num = b.get_estimated_block_num(date, accurate=False) + self.assertTrue((est_block_num - (old_block.identifier)) < 10) + est_block_num = b.get_estimated_block_num(date, accurate=True) + self.assertTrue((est_block_num - (old_block.identifier)) < 2) + est_block_num = b.get_estimated_block_num(date, estimateForwards=True, accurate=True) + self.assertTrue((est_block_num - (old_block.identifier)) < 2) + est_block_num = b.get_estimated_block_num(date, estimateForwards=True, accurate=False) + + def test_get_all_accounts(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + accounts = [] + limit = 200 + for acc in b.get_all_accounts(steps=100, limit=limit): + accounts.append(acc) + self.assertEqual(len(accounts), limit) + self.assertEqual(len(set(accounts)), limit) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_awaitTX(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + b = Blockchain(dpay_instance=bts) + trans = {'ref_block_num': 3855, 'ref_block_prefix': 1730859721, + 'expiration': '2018-03-09T06:21:06', 'operations': [], + 'extensions': [], 'signatures': + ['2033a872a8ad33c7d5b946871e4c9cc8f08a5809258355fc909058eac83' + '20ac2a872517a52b51522930d93dd2c1d5eb9f90b070f75f838c881ff29b11af98d6a1b']} + with self.assertRaises( + Exception + ): + b.awaitTxConfirmation(trans) + + def test_stream(self): + bts = self.bts + start = self.start + stop = self.stop + b = Blockchain(dpay_instance=bts) + ops_stream = [] + opNames = ["transfer", "vote"] + for op in b.stream(opNames=opNames, start=start, stop=stop): + ops_stream.append(op) + self.assertTrue(len(ops_stream) > 0) + + ops_raw_stream = [] + opNames = ["transfer", "vote"] + for op in b.stream(opNames=opNames, raw_ops=True, start=start, stop=stop): + ops_raw_stream.append(op) + self.assertTrue(len(ops_raw_stream) > 0) + + only_ops_stream = [] + opNames = ["transfer", "vote"] + for op in b.stream(opNames=opNames, start=start, stop=stop, only_ops=True): + only_ops_stream.append(op) + self.assertTrue(len(only_ops_stream) > 0) + + only_ops_raw_stream = [] + opNames = ["transfer", "vote"] + for op in b.stream(opNames=opNames, raw_ops=True, start=start, stop=stop, only_ops=True): + only_ops_raw_stream.append(op) + self.assertTrue(len(only_ops_raw_stream) > 0) + + op_stat = b.ops_statistics(start=start, stop=stop) + op_stat2 = {"transfer": 0, "vote": 0} + for op in ops_stream: + self.assertIn(op["type"], opNames) + op_stat2[op["type"]] += 1 + self.assertTrue(op["block_num"] >= start) + self.assertTrue(op["block_num"] <= stop) + self.assertEqual(op_stat["transfer"], op_stat2["transfer"]) + self.assertEqual(op_stat["vote"], op_stat2["vote"]) + + op_stat3 = {"transfer": 0, "vote": 0} + for op in ops_raw_stream: + self.assertIn(op["op"][0], opNames) + op_stat3[op["op"][0]] += 1 + self.assertTrue(op["block_num"] >= start) + self.assertTrue(op["block_num"] <= stop) + self.assertEqual(op_stat["transfer"], op_stat3["transfer"]) + self.assertEqual(op_stat["vote"], op_stat3["vote"]) + + op_stat5 = {"transfer": 0, "vote": 0} + for op in only_ops_stream: + self.assertIn(op["type"], opNames) + op_stat5[op["type"]] += 1 + self.assertTrue(op["block_num"] >= start) + self.assertTrue(op["block_num"] <= stop) + self.assertEqual(op_stat["transfer"], op_stat5["transfer"]) + self.assertEqual(op_stat["vote"], op_stat5["vote"]) + + op_stat6 = {"transfer": 0, "vote": 0} + for op in only_ops_raw_stream: + self.assertIn(op["op"][0], opNames) + op_stat6[op["op"][0]] += 1 + self.assertTrue(op["block_num"] >= start) + self.assertTrue(op["block_num"] <= stop) + self.assertEqual(op_stat["transfer"], op_stat6["transfer"]) + self.assertEqual(op_stat["vote"], op_stat6["vote"]) + + ops_blocks = [] + for op in b.blocks(start=start, stop=stop): + ops_blocks.append(op) + op_stat4 = {"transfer": 0, "vote": 0} + self.assertTrue(len(ops_blocks) > 0) + for block in ops_blocks: + for tran in block["transactions"]: + for op in tran['operations']: + if isinstance(op, list) and op[0] in opNames: + op_stat4[op[0]] += 1 + elif isinstance(op, dict): + op_type = op["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + if op_type in opNames: + op_stat4[op_type] += 1 + self.assertTrue(block.identifier >= start) + self.assertTrue(block.identifier <= stop) + self.assertEqual(op_stat["transfer"], op_stat4["transfer"]) + self.assertEqual(op_stat["vote"], op_stat4["vote"]) + + ops_blocks = [] + for op in b.blocks(): + ops_blocks.append(op) + break + self.assertTrue(len(ops_blocks) == 1) + + def test_stream2(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + stop_block = b.get_current_block_num() + start_block = stop_block - 10 + ops_stream = [] + for op in b.stream(start=start_block, stop=stop_block): + ops_stream.append(op) + self.assertTrue(len(ops_stream) > 0) + + def test_wait_for_and_get_block(self): + bts = self.bts + b = Blockchain(dpay_instance=bts, max_block_wait_repetition=18) + start_num = b.get_current_block_num() + blocknum = start_num + last_fetched_block_num = None + for i in range(3): + block = b.wait_for_and_get_block(blocknum) + last_fetched_block_num = block.block_num + blocknum = last_fetched_block_num + 1 + self.assertEqual(last_fetched_block_num, start_num + 2) + + b2 = Blockchain(dpay_instance=bts, max_block_wait_repetition=1) + with self.assertRaises( + BlockWaitTimeExceeded + ): + for i in range(300): + block = b2.wait_for_and_get_block(blocknum) + last_fetched_block_num = block.block_num + blocknum = last_fetched_block_num + 2 + + def test_hash_op(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + op1 = {'type': 'vote_operation', 'value': {'voter': 'ubg', 'author': 'yesslife', 'permlink': 'dsite-sandwich-contest-week-25-2da-entry', 'weight': 100}} + op2 = ['vote', {'voter': 'ubg', 'author': 'yesslife', 'permlink': 'dsite-sandwich-contest-week-25-2da-entry', 'weight': 100}] + hash1 = b.hash_op(op1) + hash2 = b.hash_op(op2) + self.assertEqual(hash1, hash2) + + def test_signing_appbase(self): + b = Blockchain(dpay_instance=self.bts) + st = None + for block in b.blocks(start=25304468, stop=25304468): + for trx in block.transactions: + st = Signed_Transaction(trx.copy()) + self.assertTrue(st is not None) diff --git a/tests/dpaycli/test_blockchain_batch.py b/tests/dpaycli/test_blockchain_batch.py new file mode 100755 index 0000000..7d38663 --- /dev/null +++ b/tests/dpaycli/test_blockchain_batch.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from datetime import datetime, timedelta +import pytz +import time +from pprint import pprint +from dpaycli import DPay +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.utils import formatTimeString +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10, + timeout=30, + use_condenser=False, + keys={"active": wif}, + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + b = Blockchain(dpay_instance=cls.bts) + num = b.get_current_block_num() + cls.start = num - 100 + cls.stop = num + cls.max_batch_size = 1 # appbase does not support batch rpc calls at the momement (internal error) + + def test_stream_batch(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + ops_stream = [] + opNames = ["transfer", "vote"] + for op in b.stream(opNames=opNames, start=self.start, stop=self.stop, max_batch_size=self.max_batch_size, threading=False): + ops_stream.append(op) + self.assertTrue(ops_stream[0]["block_num"] >= self.start) + self.assertTrue(ops_stream[-1]["block_num"] <= self.stop) + op_stat = b.ops_statistics(start=self.start, stop=self.stop) + self.assertEqual(op_stat["vote"] + op_stat["transfer"], len(ops_stream)) + ops_blocks = [] + for op in b.blocks(start=self.start, stop=self.stop, max_batch_size=self.max_batch_size, threading=False): + ops_blocks.append(op) + op_stat4 = {"transfer": 0, "vote": 0} + self.assertTrue(len(ops_blocks) > 0) + for block in ops_blocks: + for tran in block["transactions"]: + for op in tran['operations']: + if isinstance(op, dict) and "type" in op and "value" in op: + op_type = op["type"] + if len(op_type) > 10 and op_type[len(op_type) - 10:] == "_operation": + op_type = op_type[:-10] + if op_type in opNames: + op_stat4[op_type] += 1 + elif op[0] in opNames: + op_stat4[op[0]] += 1 + self.assertTrue(block.identifier >= self.start) + self.assertTrue(block.identifier <= self.stop) + self.assertEqual(op_stat["transfer"], op_stat4["transfer"]) + self.assertEqual(op_stat["vote"], op_stat4["vote"]) + + def test_stream_batch2(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + ops_stream = [] + start_block = 25097000 + stop_block = 25097100 + opNames = ["account_create", "custom_json"] + for op in b.stream(start=int(start_block), stop=int(stop_block), opNames=opNames, max_batch_size=50, threading=False, thread_num=8): + ops_stream.append(op) + self.assertTrue(ops_stream[0]["block_num"] >= start_block) + self.assertTrue(ops_stream[-1]["block_num"] <= stop_block) + op_stat = b.ops_statistics(start=start_block, stop=stop_block) + self.assertEqual(op_stat["account_create"] + op_stat["custom_json"], len(ops_stream)) diff --git a/tests/dpaycli/test_blockchain_threading.py b/tests/dpaycli/test_blockchain_threading.py new file mode 100755 index 0000000..13b7ff7 --- /dev/null +++ b/tests/dpaycli/test_blockchain_threading.py @@ -0,0 +1,100 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from datetime import datetime, timedelta +import pytz +import time +from pprint import pprint +from dpaycli import DPay +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + timeout=30, + num_retries=30, + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + b = Blockchain(dpay_instance=cls.bts) + num = b.get_current_block_num() + # num = 23346630 + cls.start = num - 25 + cls.stop = num + # cls.N_transfer = 121 + # cls.N_vote = 2825 + + def test_block_threading(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + blocks_no_threading = [] + for block in b.blocks(start=self.start, stop=self.stop, threading=False, thread_num=8): + blocks_no_threading.append(block) + + for n in range(5): + blocks = [] + for block in b.blocks(start=self.start, stop=self.stop, threading=True, thread_num=8): + blocks.append(block) + + for i in range(min(len(blocks), len(blocks_no_threading))): + self.assertEqual(blocks[i]["block_id"], blocks_no_threading[i]["block_id"]) + self.assertEqual(len(blocks_no_threading), len(blocks)) + + def test_stream_threading(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + + ops_stream_no_threading = [] + opNames = ["transfer", "vote"] + + block_num_list2 = [] + for op in b.stream(opNames=opNames, start=self.start, stop=self.stop, threading=False): + ops_stream_no_threading.append(op) + if op["block_num"] not in block_num_list2: + block_num_list2.append(op["block_num"]) + for n in range(5): + ops_stream = [] + block_num_list = [] + for op in b.stream(opNames=opNames, start=self.start, stop=self.stop, threading=True, thread_num=8): + ops_stream.append(op) + if op["block_num"] not in block_num_list: + block_num_list.append(op["block_num"]) + + self.assertEqual(ops_stream[0]["block_num"], ops_stream_no_threading[0]["block_num"]) + self.assertEqual(ops_stream[-1]["block_num"], ops_stream_no_threading[-1]["block_num"]) + self.assertEqual(len(ops_stream_no_threading), len(ops_stream)) + + self.assertEqual(len(block_num_list), len(block_num_list2)) + for i in range(len(block_num_list)): + self.assertEqual(block_num_list[i], block_num_list2[i]) + + def test_stream_threading2(self): + bts = self.bts + b = Blockchain(dpay_instance=bts) + + ops_stream = [] + start_block = 25097000 + stop_block = 25097100 + opNames = ["account_create", "custom_json"] + for op in b.stream(start=int(start_block), stop=int(stop_block), opNames=opNames, threading=True, thread_num=8): + ops_stream.append(op) + self.assertTrue(ops_stream[0]["block_num"] >= start_block) + self.assertTrue(ops_stream[-1]["block_num"] <= stop_block) + op_stat = b.ops_statistics(start=start_block, stop=stop_block) + self.assertEqual(op_stat["account_create"] + op_stat["custom_json"], len(ops_stream)) diff --git a/tests/dpaycli/test_cli.py b/tests/dpaycli/test_cli.py new file mode 100755 index 0000000..96bca57 --- /dev/null +++ b/tests/dpaycli/test_cli.py @@ -0,0 +1,506 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import super +import unittest +import mock +import click +from click.testing import CliRunner +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycligraphenebase.account import PrivateKey +from dpaycli.cli import cli, balance +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance +from dpayclibase.operationids import getOperationNameForId +from dpaycli.nodelist import NodeList + +wif = "5Jt2wTfhUt5GkZHV1HYVfkEaJ6XnY8D2iA4qjtK9nnGXAhThM3w" +posting_key = "5Jh1Gtu2j4Yi16TfhoDmg8Qj3ULcgRi7A49JXdfUUTVPkaFaRKz" +memo_key = "5KPbCuocX26aMxN9CDPdUex4wCbfw9NoT5P7UhcqgDwxXa47bit" +pub_key = "STX52xMqKegLk4tdpNcUXU9Rw5DtdM9fxf3f12Gp55v1UjLX3ELZf" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.nodelist = NodeList() + cls.nodelist.update_nodes() + cls.nodelist.update_nodes(dpay_instance=DPay(node=cls.nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + # stm = shared_dpay_instance() + # stm.config.refreshBackup() + runner = CliRunner() + result = runner.invoke(cli, ['-o', 'set', 'default_vote_weight', '100']) + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['-o', 'set', 'default_account', 'dpaycli']) + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['-o', 'set', 'nodes', str(cls.nodelist.get_testnet())]) + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['createwallet', '--wipe'], input="test\ntest\n") + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['addkey'], input="test\n" + wif + "\n") + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['addkey'], input="test\n" + posting_key + "\n") + if result.exit_code != 0: + raise AssertionError(str(result)) + result = runner.invoke(cli, ['addkey'], input="test\n" + memo_key + "\n") + if result.exit_code != 0: + raise AssertionError(str(result)) + + @classmethod + def tearDownClass(cls): + stm = shared_dpay_instance() + stm.config.recover_with_latest_backup() + + def test_balance(self): + runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['balance', 'dpaycli', 'dpaycli1']) + self.assertEqual(result.exit_code, 0) + + def test_interest(self): + runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['interest', 'dpaycli', 'dpaycli1']) + self.assertEqual(result.exit_code, 0) + + def test_config(self): + runner = CliRunner() + result = runner.invoke(cli, ['config']) + self.assertEqual(result.exit_code, 0) + + def test_addkey(self): + runner = CliRunner() + result = runner.invoke(cli, ['createwallet', '--wipe'], input="test\ntest\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['addkey'], input="test\n" + wif + "\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['addkey'], input="test\n" + posting_key + "\n") + self.assertEqual(result.exit_code, 0) + + def test_parsewif(self): + runner = CliRunner() + result = runner.invoke(cli, ['parsewif'], input=wif + "\nexit\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['parsewif', '--unsafe-import-key', wif]) + self.assertEqual(result.exit_code, 0) + + def test_delkey(self): + runner = CliRunner() + result = runner.invoke(cli, ['delkey', '--confirm', pub_key], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['addkey'], input="test\n" + wif + "\n") + self.assertEqual(result.exit_code, 0) + + def test_listkeys(self): + runner = CliRunner() + result = runner.invoke(cli, ['listkeys']) + self.assertEqual(result.exit_code, 0) + + def test_listaccounts(self): + runner = CliRunner() + result = runner.invoke(cli, ['listaccounts']) + self.assertEqual(result.exit_code, 0) + + def test_info(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['info']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', 'dpaycli']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', '100']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', '--', '-1']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', pub_key]) + self.assertEqual(result.exit_code, 0) + + def test_info2(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['info', '--', '-1:1']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', 'gtg']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['info', "@gtg/witness-gtg-log"]) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_changepassword(self): + runner = CliRunner() + result = runner.invoke(cli, ['changewalletpassphrase'], input="test\ntest\ntest\n") + self.assertEqual(result.exit_code, 0) + + def test_walletinfo(self): + runner = CliRunner() + result = runner.invoke(cli, ['walletinfo']) + self.assertEqual(result.exit_code, 0) + + def test_keygen(self): + runner = CliRunner() + result = runner.invoke(cli, ['keygen']) + self.assertEqual(result.exit_code, 0) + + def test_set(self): + runner = CliRunner() + result = runner.invoke(cli, ['-o', 'set', 'set_default_vote_weight', '100']) + self.assertEqual(result.exit_code, 0) + + def test_upvote(self): + runner = CliRunner() + result = runner.invoke(cli, ['-o', 'upvote', '@test/abcd'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-o', 'upvote', '@test/abcd', '100'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-o', 'upvote', '--weight', '100', '@test/abcd'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_downvote(self): + runner = CliRunner() + result = runner.invoke(cli, ['-o', 'downvote', '@test/abcd'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-o', 'downvote', '@test/abcd', '100'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-o', 'downvote', '--weight', '100', '@test/abcd'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_transfer(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['transfer', 'dpaycli1', '1', 'BBD', 'test'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_powerdownroute(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['powerdownroute', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_convert(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['convert', '1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_powerup(self): + runner = CliRunner() + result = runner.invoke(cli, ['powerup', '1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_powerdown(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'powerdown', '1e3'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', 'powerdown', '0'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_updatememokey(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'updatememokey'], input="test\ntest\ntest\n") + self.assertEqual(result.exit_code, 0) + + def test_permissions(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['permissions', 'dpaycli']) + self.assertEqual(result.exit_code, 0) + + def test_follower(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['follower', 'dpaycli1']) + self.assertEqual(result.exit_code, 0) + + def test_following(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['following', 'dpaycli']) + self.assertEqual(result.exit_code, 0) + + def test_muter(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['muter', 'dpaycli1']) + self.assertEqual(result.exit_code, 0) + + def test_muting(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['muting', 'dpaycli']) + self.assertEqual(result.exit_code, 0) + + def test_allow_disallow(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'allow', '--account', 'dpaycli', '--permission', 'posting', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', 'disallow', '--account', 'dpaycli', '--permission', 'posting', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_witnesses(self): + runner = CliRunner() + result = runner.invoke(cli, ['witnesses']) + self.assertEqual(result.exit_code, 0) + + def test_votes(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['votes', '--direction', 'out', 'test']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['votes', '--direction', 'in', 'test']) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_approvewitness(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-o', 'approvewitness', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_disapprovewitness(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-o', 'disapprovewitness', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_newaccount(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'newaccount', 'dpaycli3'], input="test\ntest\ntest\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', 'newaccount', 'dpaycli3'], input="test\ntest\ntest\n") + self.assertEqual(result.exit_code, 0) + + def test_importaccount(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['importaccount', '--roles', '["owner", "active", "posting", "memo"]', 'dpaycli2'], input="test\numybjvCafrt8LdoCjEimQiQ4\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['delkey', '--confirm', 'STX7mLs2hns87f7kbf3o2HBqNoEaXiTeeU89eVF6iUCrMQJFzBsPo'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['delkey', '--confirm', 'STX7rUmnpnCp9oZqMQeRKDB7GvXTM9KFvhzbA3AKcabgTBfQZgHZp'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['delkey', '--confirm', 'STX6qGWHsCpmHbphnQbS2yfhvhJXDUVDwnsbnrMZkTqfnkNEZRoLP'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['delkey', '--confirm', 'STX8Wvi74GYzBKgnUmiLvptzvxmPtXfjGPJL8QY3rebecXaxGGQyV'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_orderbook(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['orderbook']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['orderbook', '--show-date']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['orderbook', '--chart']) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_buy(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['-d', '-x', 'buy', '1', 'BEX', '2.2'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'buy', '1', 'BEX'], input="y\ntest\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'buy', '1', 'BBD', '2.2'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'buy', '1', 'BBD'], input="y\ntest\n") + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_sell(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['-d', '-x', 'sell', '1', 'BEX', '2.2'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'sell', '1', 'BBD', '2.2'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'sell', '1', 'BEX'], input="y\ntest\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', '-x', 'sell', '1', 'BBD'], input="y\ntest\n") + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_cancel(self): + runner = CliRunner() + result = runner.invoke(cli, ['-d', 'cancel', '5'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_openorders(self): + runner = CliRunner() + result = runner.invoke(cli, ['openorders']) + self.assertEqual(result.exit_code, 0) + + def test_repost(self): + runner = CliRunner() + result = runner.invoke(cli, ['-o', 'repost', '@test/abcde'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_follow_unfollow(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'follow', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', 'unfollow', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_mute_unmute(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'mute', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-d', 'unfollow', 'dpaycli1'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_witnesscreate(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + result = runner.invoke(cli, ['-d', 'witnesscreate', 'dpaycli', pub_key], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_witnessupdate(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['-o', 'nextnode']) + runner.invoke(cli, ['-o', 'witnessupdate', 'gtg', '--maximum_block_size', 65000, '--account_creation_fee', 0.1, '--bbd_interest_rate', 0, '--url', 'https://google.de', '--signing_key', wif]) + self.assertEqual(result.exit_code, 0) + + def test_profile(self): + runner = CliRunner() + result = runner.invoke(cli, ['setprofile', 'url', 'https://google.de'], input="test\n") + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['delprofile', 'url'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_claimreward(self): + runner = CliRunner() + result = runner.invoke(cli, ['-d', 'claimreward'], input="test\n") + result = runner.invoke(cli, ['-d', 'claimreward', '--claim_all_dpay'], input="test\n") + result = runner.invoke(cli, ['-d', 'claimreward', '--claim_all_bbd'], input="test\n") + result = runner.invoke(cli, ['-d', 'claimreward', '--claim_all_vests'], input="test\n") + self.assertEqual(result.exit_code, 0) + + def test_power(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['power']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_nextnode(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['-o', 'nextnode']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_pingnode(self): + runner = CliRunner() + result = runner.invoke(cli, ['pingnode']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pingnode', '--raw']) + self.assertEqual(result.exit_code, 0) + + def test_updatenodes(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['updatenodes', '--test']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_currentnode(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['currentnode']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['currentnode', '--url']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['currentnode', '--version']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_ticker(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['ticker']) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_pricehistory(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['pricehistory']) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_pending(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['pending', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--permlink', '--days', '1', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--author', '--days', '1', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--author', '--title', '--days', '1', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--author', '--permlink', '--length', '30', '--days', '1', 'holger80']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_rewards(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['rewards', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', '--permlink', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', '--author', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', '--author', '--title', 'holger80']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['rewards', '--post', '--comment', '--curation', '--author', '--permlink', '--length', '30', 'holger80']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + + def test_curation(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['curation', "@gtg/witness-gtg-log"]) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_verify(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes(normal=False, appbase=True)]) + result = runner.invoke(cli, ['verify', '--trx', '3', '25304468']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['verify', '--trx', '5', '25304468']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['verify', '--trx', '0']) + self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) + + def test_tradehistory(self): + runner = CliRunner() + runner.invoke(cli, ['-o', 'set', 'nodes', self.nodelist.get_nodes()]) + result = runner.invoke(cli, ['tradehistory']) + runner.invoke(cli, ['-o', 'set', 'nodes', str(self.nodelist.get_testnet())]) + self.assertEqual(result.exit_code, 0) diff --git a/tests/dpaycli/test_comment.py b/tests/dpaycli/test_comment.py new file mode 100755 index 0000000..bad9d09 --- /dev/null +++ b/tests/dpaycli/test_comment.py @@ -0,0 +1,245 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super, str +import unittest +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.comment import Comment, RecentReplies, RecentByPath +from dpaycli.vote import Vote +from dpaycli.account import Account +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.utils import resolve_authorperm +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + use_condenser=True, + nobroadcast=True, + unsigned=True, + keys={"active": wif}, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + unsigned=True, + keys={"active": wif}, + num_retries=10 + ) + acc = Account("holger80", dpay_instance=cls.bts) + comment = acc.get_feed(limit=20)[-1] + cls.authorperm = comment.authorperm + [author, permlink] = resolve_authorperm(cls.authorperm) + cls.author = author + cls.permlink = permlink + cls.category = comment.category + cls.title = comment.title + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + # set_shared_dpay_instance(cls.bts) + # cls.bts.set_default_account("test") + + def test_comment(self): + bts = self.bts + with self.assertRaises( + exceptions.ContentDoesNotExistsException + ): + Comment("@abcdef/abcdef", dpay_instance=bts) + title = '' + cnt = 0 + while title == '' and cnt < 5: + c = Comment(self.authorperm, dpay_instance=bts) + title = c.title + cnt += 1 + if title == '': + c.dpay.rpc.next() + c.refresh() + title = c.title + self.assertTrue(isinstance(c.id, int)) + self.assertTrue(c.id > 0) + self.assertEqual(c.author, self.author) + self.assertEqual(c.permlink, self.permlink) + self.assertEqual(c.authorperm, self.authorperm) + self.assertEqual(c.category, self.category) + self.assertEqual(c.parent_author, '') + self.assertEqual(c.parent_permlink, self.category) + self.assertEqual(c.title, self.title) + self.assertTrue(len(c.body) > 0) + self.assertTrue(isinstance(c.json_metadata, dict)) + self.assertTrue(c.is_main_post()) + self.assertFalse(c.is_comment()) + if c.is_pending(): + self.assertFalse((c.time_elapsed().total_seconds() / 60 / 60 / 24) > 7.0) + else: + self.assertTrue((c.time_elapsed().total_seconds() / 60 / 60 / 24) > 7.0) + self.assertTrue(isinstance(c.get_reblogged_by(), list)) + self.assertTrue(len(c.get_reblogged_by()) > 0) + self.assertTrue(isinstance(c.get_votes(), list)) + self.assertTrue(len(c.get_votes()) > 0) + self.assertTrue(isinstance(c.get_votes()[0], Vote)) + + def test_comment_dict(self): + bts = self.bts + title = '' + cnt = 0 + while title == '' and cnt < 5: + c = Comment({'author': self.author, 'permlink': self.permlink}, dpay_instance=bts) + c.refresh() + title = c.title + cnt += 1 + if title == '': + c.dpay.rpc.next() + c.refresh() + title = c.title + + self.assertEqual(c.author, self.author) + self.assertEqual(c.permlink, self.permlink) + self.assertEqual(c.authorperm, self.authorperm) + self.assertEqual(c.category, self.category) + self.assertEqual(c.parent_author, '') + self.assertEqual(c.parent_permlink, self.category) + self.assertEqual(c.title, self.title) + + def test_vote(self): + bts = self.bts + c = Comment(self.authorperm, dpay_instance=bts) + bts.txbuffer.clear() + tx = c.vote(100, account="test") + self.assertEqual( + (tx["operations"][0][0]), + "vote" + ) + op = tx["operations"][0][1] + self.assertIn( + "test", + op["voter"]) + c.dpay.txbuffer.clear() + tx = c.upvote(weight=150, voter="test") + op = tx["operations"][0][1] + self.assertEqual(op["weight"], 10000) + c.dpay.txbuffer.clear() + tx = c.upvote(weight=99.9, voter="test") + op = tx["operations"][0][1] + self.assertEqual(op["weight"], 9990) + c.dpay.txbuffer.clear() + tx = c.downvote(weight=-150, voter="test") + op = tx["operations"][0][1] + self.assertEqual(op["weight"], -10000) + c.dpay.txbuffer.clear() + tx = c.downvote(weight=-99.9, voter="test") + op = tx["operations"][0][1] + self.assertEqual(op["weight"], -9990) + + def test_export(self): + bts = self.bts + + if bts.rpc.get_use_appbase(): + content = bts.rpc.get_discussion({'author': self.author, 'permlink': self.permlink}, api="tags") + else: + content = bts.rpc.get_content(self.author, self.permlink) + + c = Comment(self.authorperm, dpay_instance=bts) + keys = list(content.keys()) + json_content = c.json() + exclude_list = ["json_metadata", "reputation", "active_votes"] + for k in keys: + if k not in exclude_list: + if isinstance(content[k], dict) and isinstance(json_content[k], list): + self.assertEqual(list(content[k].values()), json_content[k]) + elif isinstance(content[k], str) and isinstance(json_content[k], str): + self.assertEqual(content[k].encode('utf-8'), json_content[k].encode('utf-8')) + else: + self.assertEqual(content[k], json_content[k]) + + def test_repost(self): + bts = self.bts + bts.txbuffer.clear() + c = Comment(self.authorperm, dpay_instance=bts) + tx = c.repost(account="test") + self.assertEqual( + (tx["operations"][0][0]), + "custom_json" + ) + + def test_reply(self): + bts = self.bts + bts.txbuffer.clear() + c = Comment(self.authorperm, dpay_instance=bts) + tx = c.reply(body="Good post!", author="test") + self.assertEqual( + (tx["operations"][0][0]), + "comment" + ) + op = tx["operations"][0][1] + self.assertIn( + "test", + op["author"]) + + def test_delete(self): + bts = self.bts + bts.txbuffer.clear() + c = Comment(self.authorperm, dpay_instance=bts) + tx = c.delete(account="test") + self.assertEqual( + (tx["operations"][0][0]), + "delete_comment" + ) + op = tx["operations"][0][1] + self.assertIn( + self.author, + op["author"]) + + def test_edit(self): + bts = self.bts + bts.txbuffer.clear() + c = Comment(self.authorperm, dpay_instance=bts) + c.edit(c.body, replace=False) + body = c.body + "test" + tx = c.edit(body, replace=False) + self.assertEqual( + (tx["operations"][0][0]), + "comment" + ) + op = tx["operations"][0][1] + self.assertIn( + self.author, + op["author"]) + + def test_edit_replace(self): + bts = self.bts + bts.txbuffer.clear() + c = Comment(self.authorperm, dpay_instance=bts) + body = c.body + "test" + tx = c.edit(body, meta=c["json_metadata"], replace=True) + self.assertEqual( + (tx["operations"][0][0]), + "comment" + ) + op = tx["operations"][0][1] + self.assertIn( + self.author, + op["author"]) + self.assertEqual(body, op["body"]) + + def test_recent_replies(self): + bts = self.bts + r = RecentReplies(self.author, skip_own=True, dpay_instance=bts) + self.assertTrue(len(r) > 0) + self.assertTrue(r[0] is not None) + + def test_recent_by_path(self): + bts = self.bts + r = RecentByPath(path="hot", dpay_instance=bts) + self.assertTrue(len(r) > 0) + self.assertTrue(r[0] is not None) diff --git a/tests/dpaycli/test_connection.py b/tests/dpaycli/test_connection.py new file mode 100755 index 0000000..f359e45 --- /dev/null +++ b/tests/dpaycli/test_connection.py @@ -0,0 +1,50 @@ +import unittest +from dpaycli import DPay +from dpaycli.account import Account +from dpaycli.instance import set_shared_dpay_instance, SharedInstance +from dpaycli.blockchainobject import BlockchainObject +from dpaycli.nodelist import NodeList + +import logging +log = logging.getLogger() + + +class Testcases(unittest.TestCase): + + def test_stm1stm2(self): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + b1 = DPay( + node=nodelist.get_testnet(testnet=True, testnetdev=False), + nobroadcast=True, + num_retries=10 + ) + + b2 = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10 + ) + + self.assertNotEqual(b1.rpc.url, b2.rpc.url) + + def test_default_connection(self): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + b1 = DPay( + node=nodelist.get_testnet(testnet=True, testnetdev=False), + nobroadcast=True, + ) + set_shared_dpay_instance(b1) + test = Account("dpaycli") + + b2 = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + ) + set_shared_dpay_instance(b2) + + bts = Account("dpaycli") + + self.assertEqual(test.dpay.prefix, "STX") + self.assertEqual(bts.dpay.prefix, "DWB") diff --git a/tests/dpaycli/test_constants.py b/tests/dpaycli/test_constants.py new file mode 100755 index 0000000..e940daa --- /dev/null +++ b/tests/dpaycli/test_constants.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import super +import unittest +import mock +import pytz +from datetime import datetime, timedelta +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay, exceptions, constants +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(appbase=False), + nobroadcast=True, + bundle=False, + # Overwrite wallet to use this list of wifs only + keys={"active": wif}, + num_retries=10 + ) + cls.appbase = DPay( + node=nodelist.get_nodes(appbase=True, dev=True), + nobroadcast=True, + bundle=False, + # Overwrite wallet to use this list of wifs only + keys={"active": wif}, + num_retries=10 + ) + + @parameterized.expand([ + ("non_appbase"), + ("appbase"), + ]) + def test_constants(self, node_param): + if node_param == "non_appbase": + stm = self.bts + else: + stm = self.appbase + dpay_conf = stm.get_config() + if "DPAY_100_PERCENT" in dpay_conf: + DPAY_100_PERCENT = dpay_conf['DPAY_100_PERCENT'] + else: + DPAY_100_PERCENT = dpay_conf['DPAY_100_PERCENT'] + self.assertEqual(constants.DPAY_100_PERCENT, DPAY_100_PERCENT) + + if "DPAY_1_PERCENT" in dpay_conf: + DPAY_1_PERCENT = dpay_conf['DPAY_1_PERCENT'] + else: + DPAY_1_PERCENT = dpay_conf['DPAY_1_PERCENT'] + self.assertEqual(constants.DPAY_1_PERCENT, DPAY_1_PERCENT) + + if "DPAY_REVERSE_AUCTION_WINDOW_SECONDS" in dpay_conf: + DPAY_REVERSE_AUCTION_WINDOW_SECONDS = dpay_conf['DPAY_REVERSE_AUCTION_WINDOW_SECONDS'] + elif "DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6" in dpay_conf: + DPAY_REVERSE_AUCTION_WINDOW_SECONDS = dpay_conf['DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6'] + else: + DPAY_REVERSE_AUCTION_WINDOW_SECONDS = dpay_conf['DPAY_REVERSE_AUCTION_WINDOW_SECONDS'] + self.assertEqual(constants.DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF6, DPAY_REVERSE_AUCTION_WINDOW_SECONDS) + + if "DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20" in dpay_conf: + self.assertEqual(constants.DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20, dpay_conf["DPAY_REVERSE_AUCTION_WINDOW_SECONDS_HF20"]) + + if "DPAY_VOTE_DUST_THRESHOLD" in dpay_conf: + self.assertEqual(constants.DPAY_VOTE_DUST_THRESHOLD, dpay_conf["DPAY_VOTE_DUST_THRESHOLD"]) + + if "DPAY_VOTE_REGENERATION_SECONDS" in dpay_conf: + DPAY_VOTE_REGENERATION_SECONDS = dpay_conf['DPAY_VOTE_REGENERATION_SECONDS'] + self.assertEqual(constants.DPAY_VOTE_REGENERATION_SECONDS, DPAY_VOTE_REGENERATION_SECONDS) + elif "DPAY_VOTING_MANA_REGENERATION_SECONDS" in dpay_conf: + DPAY_VOTING_MANA_REGENERATION_SECONDS = dpay_conf["DPAY_VOTING_MANA_REGENERATION_SECONDS"] + self.assertEqual(constants.DPAY_VOTING_MANA_REGENERATION_SECONDS, DPAY_VOTING_MANA_REGENERATION_SECONDS) + else: + DPAY_VOTE_REGENERATION_SECONDS = dpay_conf['DPAY_VOTE_REGENERATION_SECONDS'] + self.assertEqual(constants.DPAY_VOTE_REGENERATION_SECONDS, DPAY_VOTE_REGENERATION_SECONDS) + + if "DPAY_ROOT_POST_PARENT" in dpay_conf: + DPAY_ROOT_POST_PARENT = dpay_conf['DPAY_ROOT_POST_PARENT'] + else: + DPAY_ROOT_POST_PARENT = dpay_conf['DPAY_ROOT_POST_PARENT'] + self.assertEqual(constants.DPAY_ROOT_POST_PARENT, DPAY_ROOT_POST_PARENT) diff --git a/tests/dpaycli/test_conveyor.py b/tests/dpaycli/test_conveyor.py new file mode 100755 index 0000000..c1f57d1 --- /dev/null +++ b/tests/dpaycli/test_conveyor.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import unittest +from dpaycli import DPay +from dpaycli.conveyor import Conveyor +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = '5Jh1Gtu2j4Yi16TfhoDmg8Qj3ULcgRi7A49JXdfUUTVPkaFaRKz' + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), nobroadcast=True, + num_retries=10, expiration=120, keys=wif) + set_shared_dpay_instance(stm) + + def test_healthcheck(self): + health = Conveyor().healthcheck() + self.assertTrue('version' in health) + self.assertTrue('ok' in health) + self.assertTrue('date' in health) + + def test_get_user_data(self): + c = Conveyor() + userdata = c.get_user_data('dpaycli') + self.assertTrue('jsonrpc' in userdata) + self.assertTrue('error' in userdata) + self.assertTrue('code' in userdata['error']) + # error 401 -> unauthorized, but proper format + self.assertTrue(userdata['error']['code'] == 401) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/dpaycli/test_discussions.py b/tests/dpaycli/test_discussions.py new file mode 100755 index 0000000..3bc1954 --- /dev/null +++ b/tests/dpaycli/test_discussions.py @@ -0,0 +1,140 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay +from dpaycli.discussions import ( + Query, Discussions_by_trending, Comment_discussions_by_payout, + Post_discussions_by_payout, Discussions_by_created, Discussions_by_active, + Discussions_by_cashout, Discussions_by_votes, + Discussions_by_children, Discussions_by_hot, Discussions_by_feed, Discussions_by_blog, + Discussions_by_comments, Discussions_by_promoted, Discussions +) +from datetime import datetime +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + use_condenser=True, + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + def test_trending(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_trending(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_comment_payout(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Comment_discussions_by_payout(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_post_payout(self): + bts = self.bts + + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Post_discussions_by_payout(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_created(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_created(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_active(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_active(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_cashout(self): + bts = self.bts + query = Query(limit=10) + Discussions_by_cashout(query, dpay_instance=bts) + # self.assertEqual(len(d), 10) + + def test_votes(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_votes(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_children(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_children(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_feed(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "gtg" + d = Discussions_by_feed(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_blog(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "gtg" + d = Discussions_by_blog(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_comments(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["filter_tags"] = ["gtg"] + query["start_author"] = "gtg" + d = Discussions_by_comments(query, dpay_instance=bts) + self.assertEqual(len(d), 10) + + def test_promoted(self): + bts = self.bts + query = Query() + query["limit"] = 10 + query["tag"] = "dsocial" + d = Discussions_by_promoted(query, dpay_instance=bts) + discussions = Discussions(dpay_instance=bts) + d2 = [] + for dd in discussions.get_discussions("promoted", query, limit=10): + d2.append(dd) + self.assertEqual(len(d), 10) + self.assertEqual(len(d2), 10) diff --git a/tests/dpaycli/test_instance.py b/tests/dpaycli/test_instance.py new file mode 100755 index 0000000..87281c9 --- /dev/null +++ b/tests/dpaycli/test_instance.py @@ -0,0 +1,358 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +import random +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay +from dpaycli.amount import Amount +from dpaycli.witness import Witness +from dpaycli.account import Account +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance, set_shared_config +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.market import Market +from dpaycli.price import Price +from dpaycli.comment import Comment +from dpaycli.vote import Vote +from dpaycliapi.exceptions import RPCConnection +from dpaycli.wallet import Wallet +from dpaycli.transactionbuilder import TransactionBuilder +from dpayclibase.operations import Transfer +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.utils import parse_time, formatTimedelta +from dpaycli.nodelist import NodeList + +# Py3 compatibility +import sys + +core_unit = "DWB" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.nodelist = NodeList() + cls.nodelist.update_nodes(dpay_instance=DPay(node=cls.nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + stm = DPay(node=cls.nodelist.get_nodes()) + stm.config.refreshBackup() + stm.set_default_nodes(["xyz"]) + del stm + + cls.urls = cls.nodelist.get_nodes() + cls.bts = DPay( + node=cls.urls, + nobroadcast=True, + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + acc = Account("holger80", dpay_instance=cls.bts) + comment = acc.get_blog(limit=20)[-1] + cls.authorperm = comment.authorperm + votes = acc.get_account_votes() + last_vote = votes[-1] + cls.authorpermvoter = '@' + last_vote['authorperm'] + '|' + acc["name"] + + @classmethod + def tearDownClass(cls): + stm = DPay(node=cls.nodelist.get_nodes()) + stm.config.recover_with_latest_backup() + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_account(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + acc = Account("test") + self.assertIn(acc.dpay.rpc.url, self.urls) + self.assertIn(acc["balance"].dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Account("test", dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + acc = Account("test", dpay_instance=stm) + self.assertIn(acc.dpay.rpc.url, self.urls) + self.assertIn(acc["balance"].dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Account("test") + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_amount(self, node_param): + if node_param == "instance": + stm = DPay(node="https://abc.d", autoconnect=False, num_retries=1) + set_shared_dpay_instance(self.bts) + o = Amount("1 BBD") + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Amount("1 BBD", dpay_instance=stm) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Amount("1 BBD", dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Amount("1 BBD") + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_block(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Block(1) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Block(1, dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Block(1, dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Block(1) + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_blockchain(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Blockchain() + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Blockchain(dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Blockchain(dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Blockchain() + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_comment(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Comment(self.authorperm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Comment(self.authorperm, dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Comment(self.authorperm, dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Comment(self.authorperm) + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_market(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Market() + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Market(dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Market(dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Market() + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_price(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Price(10.0, "BEX/BBD") + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Price(10.0, "BEX/BBD", dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Price(10.0, "BEX/BBD", dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Price(10.0, "BEX/BBD") + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_vote(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Vote(self.authorpermvoter) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Vote(self.authorpermvoter, dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Vote(self.authorpermvoter, dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Vote(self.authorpermvoter) + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_wallet(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Wallet() + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + o = Wallet(dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + o.dpay.get_config() + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Wallet(dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + o = Wallet() + o.dpay.get_config() + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_witness(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = Witness("gtg") + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Witness("gtg", dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = Witness("gtg", dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + Witness("gtg") + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_transactionbuilder(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = TransactionBuilder() + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + o = TransactionBuilder(dpay_instance=DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + o.dpay.get_config() + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = TransactionBuilder(dpay_instance=stm) + self.assertIn(o.dpay.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + o = TransactionBuilder() + o.dpay.get_config() + + @parameterized.expand([ + ("instance"), + ("dpay") + ]) + def test_dpay(self, node_param): + if node_param == "instance": + set_shared_dpay_instance(self.bts) + o = DPay(node=self.urls) + o.get_config() + self.assertIn(o.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + stm = DPay(node="https://abc.d", autoconnect=False, num_retries=1) + stm.get_config() + else: + set_shared_dpay_instance(DPay(node="https://abc.d", autoconnect=False, num_retries=1)) + stm = self.bts + o = stm + o.get_config() + self.assertIn(o.rpc.url, self.urls) + with self.assertRaises( + RPCConnection + ): + stm = shared_dpay_instance() + stm.get_config() + + def test_config(self): + set_shared_config({"node": self.urls}) + set_shared_dpay_instance(None) + o = shared_dpay_instance() + self.assertIn(o.rpc.url, self.urls) diff --git a/tests/dpaycli/test_market.py b/tests/dpaycli/test_market.py new file mode 100755 index 0000000..3ade5ad --- /dev/null +++ b/tests/dpaycli/test_market.py @@ -0,0 +1,180 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay +from dpaycli.market import Market +from dpaycli.price import Price +from dpaycli.asset import Asset +from dpaycli.amount import Amount +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + keys={"active": wif}, + num_retries=10 + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + def test_market(self): + bts = self.bts + m1 = Market(u'BEX', u'BBD', dpay_instance=bts) + self.assertEqual(m1.get_string(), u'BBD:BEX') + m2 = Market(dpay_instance=bts) + self.assertEqual(m2.get_string(), u'BBD:BEX') + m3 = Market(u'BEX:BBD', dpay_instance=bts) + self.assertEqual(m3.get_string(), u'BEX:BBD') + self.assertTrue(m1 == m2) + + base = Asset("BBD", dpay_instance=bts) + quote = Asset("BEX", dpay_instance=bts) + m = Market(base, quote, dpay_instance=bts) + self.assertEqual(m.get_string(), u'BEX:BBD') + + def test_ticker(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + ticker = m.ticker() + self.assertEqual(len(ticker), 6) + self.assertEqual(ticker['dpay_volume']["symbol"], u'BEX') + self.assertEqual(ticker['bbd_volume']["symbol"], u'BBD') + + def test_volume(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + volume = m.volume24h() + self.assertEqual(volume['BEX']["symbol"], u'BEX') + self.assertEqual(volume['BBD']["symbol"], u'BBD') + + def test_orderbook(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + orderbook = m.orderbook(limit=10) + self.assertEqual(len(orderbook['asks_date']), 10) + self.assertEqual(len(orderbook['asks']), 10) + self.assertEqual(len(orderbook['bids_date']), 10) + self.assertEqual(len(orderbook['bids']), 10) + + def test_recenttrades(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + recenttrades = m.recent_trades(limit=10) + recenttrades_raw = m.recent_trades(limit=10, raw_data=True) + self.assertEqual(len(recenttrades), 10) + self.assertEqual(len(recenttrades_raw), 10) + + def test_trades(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + trades = m.trades(limit=10) + trades_raw = m.trades(limit=10, raw_data=True) + trades_history = m.trade_history(limit=10) + self.assertEqual(len(trades), 10) + self.assertTrue(len(trades_history) > 0) + self.assertEqual(len(trades_raw), 10) + + def test_market_history(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + buckets = m.market_history_buckets() + history = m.market_history(buckets[2]) + self.assertTrue(len(history) > 0) + + def test_accountopenorders(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + openOrder = m.accountopenorders("test") + self.assertTrue(isinstance(openOrder, list)) + + def test_buy(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + bts.txbuffer.clear() + tx = m.buy(5, 0.1, account="test") + self.assertEqual( + (tx["operations"][0][0]), + "limit_order_create" + ) + op = tx["operations"][0][1] + self.assertIn("test", op["owner"]) + self.assertEqual(str(Amount('0.100 BEX', dpay_instance=bts)), op["min_to_receive"]) + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["amount_to_sell"]) + + p = Price(5, u"BBD:BEX", dpay_instance=bts) + tx = m.buy(p, 0.1, account="test") + op = tx["operations"][0][1] + self.assertEqual(str(Amount('0.100 BEX', dpay_instance=bts)), op["min_to_receive"]) + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["amount_to_sell"]) + + p = Price(5, u"BBD:BEX", dpay_instance=bts) + a = Amount(0.1, "BEX", dpay_instance=bts) + tx = m.buy(p, a, account="test") + op = tx["operations"][0][1] + self.assertEqual(str(a), op["min_to_receive"]) + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["amount_to_sell"]) + + def test_sell(self): + bts = self.bts + bts.txbuffer.clear() + m = Market(u'BEX:BBD', dpay_instance=bts) + tx = m.sell(5, 0.1, account="test") + self.assertEqual( + (tx["operations"][0][0]), + "limit_order_create" + ) + op = tx["operations"][0][1] + self.assertIn("test", op["owner"]) + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["min_to_receive"]) + self.assertEqual(str(Amount('0.100 BEX', dpay_instance=bts)), op["amount_to_sell"]) + + p = Price(5, u"BBD:BEX") + tx = m.sell(p, 0.1, account="test") + op = tx["operations"][0][1] + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["min_to_receive"]) + self.assertEqual(str(Amount('0.100 BEX', dpay_instance=bts)), op["amount_to_sell"]) + + p = Price(5, u"BBD:BEX", dpay_instance=bts) + a = Amount(0.1, "BEX", dpay_instance=bts) + tx = m.sell(p, a, account="test") + op = tx["operations"][0][1] + self.assertEqual(str(Amount('0.500 BBD', dpay_instance=bts)), op["min_to_receive"]) + self.assertEqual(str(Amount('0.100 BEX', dpay_instance=bts)), op["amount_to_sell"]) + + def test_cancel(self): + bts = self.bts + bts.txbuffer.clear() + m = Market(u'BEX:BBD', dpay_instance=bts) + tx = m.cancel(5, account="test") + self.assertEqual( + (tx["operations"][0][0]), + "limit_order_cancel" + ) + op = tx["operations"][0][1] + self.assertIn( + "test", + op["owner"]) + + def test_dpay_usb_impied(self): + bts = self.bts + m = Market(u'BEX:BBD', dpay_instance=bts) + dpay_usd = m.dpay_usd_implied() + self.assertGreater(dpay_usd, 0) diff --git a/tests/dpaycli/test_message.py b/tests/dpaycli/test_message.py new file mode 100755 index 0000000..5c92767 --- /dev/null +++ b/tests/dpaycli/test_message.py @@ -0,0 +1,85 @@ +from builtins import super +import unittest +import mock +from dpaycli import DPay +from dpaycli.message import Message +from dpaycli.account import Account +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +core_unit = "DWB" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + keys=[wif], + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + + def test_sign_message(self): + def new_refresh(self): + dict.__init__( + self, { + "identifier": "test", + "name": "test", + "id_item": "name", + "memo_key": "DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + }) + + with mock.patch( + "dpaycli.account.Account.refresh", + new=new_refresh + ): + account = Account("test") + account["memo_key"] = "DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + p = Message("message foobar").sign(account=account) + Message(p).verify(account=account) + + def test_verify_message(self): + def new_refresh(self): + dict.__init__( + self, { + "identifier": "test", + "name": "test", + "id_item": "name", + "memo_key": "DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + }) + + with mock.patch( + "dpaycli.account.Account.refresh", + new=new_refresh + ): + Message( + "-----BEGIN BEX SIGNED MESSAGE-----\n" + "message foobar\n" + "-----BEGIN META-----\n" + "account=test\n" + "memokey=DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\n" + "block=19902522\n" + "timestamp=2018-02-15T22:00:54\n" + "-----BEGIN SIGNATURE-----\n" + "20093ef63f375b9aa8570188cae3aad953bf6393d43ce6f03bbbd1b429e48c6a587dc012922515f6d327158df5081ea2d595888225f9f1c6c3028781c8f9451fde\n" + "-----END BEX SIGNED MESSAGE-----\n" + ).verify() + + Message( + "-----BEGIN BEX SIGNED MESSAGE-----" + "message foobar\n" + "-----BEGIN META-----" + "account=test\n" + "memokey=DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\n" + "block=19902522\n" + "timestamp=2018-02-15T22:00:54\n" + "-----BEGIN SIGNATURE-----" + "20093ef63f375b9aa8570188cae3aad953bf6393d43ce6f03bbbd1b429e48c6a587dc012922515f6d327158df5081ea2d595888225f9f1c6c3028781c8f9451fde\n" + "-----END BEX SIGNED MESSAGE-----" + ).verify() diff --git a/tests/dpaycli/test_nodelist.py b/tests/dpaycli/test_nodelist.py new file mode 100755 index 0000000..30bff47 --- /dev/null +++ b/tests/dpaycli/test_nodelist.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from dpaycli import DPay, exceptions +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.account import Account +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10 + ) + set_shared_dpay_instance(cls.bts) + + def test_get_nodes(self): + nodelist = NodeList() + all_nodes = nodelist.get_nodes(normal=True, appbase=True, dev=True, testnet=True, testnetdev=True) + self.assertEqual(len(nodelist) - 11, len(all_nodes)) + https_nodes = nodelist.get_nodes(wss=False) + self.assertEqual(https_nodes[0][:5], 'https') + + def test_nodes_update(self): + nodelist = NodeList() + all_nodes = nodelist.get_nodes(normal=True, appbase=True, dev=True, testnet=True) + nodelist.update_nodes(dpay_instance=self.bts) + nodes = nodelist.get_nodes() + self.assertIn(nodes[0], all_nodes) diff --git a/tests/dpaycli/test_objectcache.py b/tests/dpaycli/test_objectcache.py new file mode 100755 index 0000000..7a2c733 --- /dev/null +++ b/tests/dpaycli/test_objectcache.py @@ -0,0 +1,77 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +from builtins import str +import time +import unittest +from dpaycli import DPay, exceptions +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.blockchainobject import ObjectCache +from dpaycli.account import Account +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + + def test_cache(self): + cache = ObjectCache(default_expiration=1, auto_clean=False) + self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=1)") + + # Data + cache["foo"] = "bar" + self.assertIn("foo", cache) + self.assertEqual(cache["foo"], "bar") + self.assertEqual(cache.get("foo", "New"), "bar") + + # Expiration + time.sleep(2) + self.assertNotIn("foo", cache) + self.assertEqual(str(cache), "ObjectCache(n=1, default_expiration=1)") + + # Get + self.assertEqual(cache.get("foo", "New"), "New") + + def test_cache_autoclean(self): + cache = ObjectCache(default_expiration=1, auto_clean=True) + self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=1)") + + # Data + cache["foo"] = "bar" + self.assertEqual(str(cache), "ObjectCache(n=1, default_expiration=1)") + self.assertIn("foo", cache) + self.assertEqual(cache["foo"], "bar") + self.assertEqual(cache.get("foo", "New"), "bar") + + # Expiration + time.sleep(2) + self.assertNotIn("foo", cache) + self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=1)") + self.assertEqual(len(list(cache)), 0) + + # Get + self.assertEqual(cache.get("foo", "New"), "New") + + def test_cache2(self): + cache = ObjectCache(default_expiration=3, auto_clean=True) + self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=3)") + + # Data + cache["foo"] = "bar" + self.assertEqual(str(cache), "ObjectCache(n=1, default_expiration=3)") + self.assertIn("foo", cache) + self.assertEqual(cache["foo"], "bar") + self.assertEqual(cache.get("foo", "New"), "bar") + time.sleep(1) + cache["foo2"] = "bar2" + time.sleep(1) + cache["foo3"] = "bar3" + self.assertEqual(str(cache), "ObjectCache(n=3, default_expiration=3)") + # Expiration + time.sleep(2) + self.assertNotIn("foo", cache) + self.assertEqual(str(cache), "ObjectCache(n=1, default_expiration=3)") + self.assertEqual(len(list(cache)), 1) + # Get + self.assertEqual(cache.get("foo", "New"), "New") diff --git a/tests/dpaycli/test_price.py b/tests/dpaycli/test_price.py new file mode 100755 index 0000000..59cb940 --- /dev/null +++ b/tests/dpaycli/test_price.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from dpaycli import DPay +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.amount import Amount +from dpaycli.price import Price, Order, FilledOrder +from dpaycli.asset import Asset +import unittest +from dpaycli.nodelist import NodeList + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + dpay = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + num_retries=10 + ) + set_shared_dpay_instance(dpay) + + def test_init(self): + # self.assertEqual(1, 1) + + Price("0.315 BEX/BBD") + Price(1.0, "BEX/BBD") + Price(0.315, base="BEX", quote="BBD") + Price(0.315, base=Asset("BEX"), quote=Asset("BBD")) + Price({ + "base": {"amount": 1, "asset_id": "BBD"}, + "quote": {"amount": 10, "asset_id": "BEX"}}) + Price("", quote="10 BBD", base="1 BEX") + Price("10 BBD", "1 BEX") + Price(Amount("10 BBD"), Amount("1 BEX")) + + def test_multiplication(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "VESTS/BEX") + p3 = p1 * p2 + p4 = p3.as_base("BBD") + p4_2 = p3.as_quote("VESTS") + + self.assertEqual(p4["quote"]["symbol"], "VESTS") + self.assertEqual(p4["base"]["symbol"], "BBD") + # 10 BEX/BBD * 0.2 VESTS/BEX = 50 VESTS/BBD = 0.02 BBD/VESTS + self.assertEqual(float(p4), 0.02) + self.assertEqual(p4_2["quote"]["symbol"], "VESTS") + self.assertEqual(p4_2["base"]["symbol"], "BBD") + self.assertEqual(float(p4_2), 0.02) + p3 = p1 * 5 + self.assertEqual(float(p3), 50) + + # Inline multiplication + p5 = Price(10.0, "BEX/BBD") + p5 *= p2 + p4 = p5.as_base("BBD") + self.assertEqual(p4["quote"]["symbol"], "VESTS") + self.assertEqual(p4["base"]["symbol"], "BBD") + # 10 BEX/BBD * 0.2 VESTS/BEX = 2 VESTS/BBD = 0.02 BBD/VESTS + self.assertEqual(float(p4), 0.02) + p6 = Price(10.0, "BEX/BBD") + p6 *= 5 + self.assertEqual(float(p6), 50) + + def test_div(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "BEX/VESTS") + + # 10 BEX/BBD / 5 BEX/VESTS = 2 VESTS/BBD + p3 = p1 / p2 + p4 = p3.as_base("VESTS") + self.assertEqual(p4["base"]["symbol"], "VESTS") + self.assertEqual(p4["quote"]["symbol"], "BBD") + # 10 BEX/BBD * 0.2 VESTS/BEX = 2 VESTS/BBD = 0.5 BBD/VESTS + self.assertEqual(float(p4), 2) + + def test_div2(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "BEX/BBD") + + # 10 BEX/BBD / 5 BEX/VESTS = 2 VESTS/BBD + p3 = p1 / p2 + self.assertTrue(isinstance(p3, (float, int))) + self.assertEqual(float(p3), 2.0) + p3 = p1 / 5 + self.assertEqual(float(p3), 2.0) + p3 = p1 / Amount("1 BBD") + self.assertEqual(float(p3), 0.1) + p3 = p1 + p3 /= p2 + self.assertEqual(float(p3), 2.0) + p3 = p1 + p3 /= 5 + self.assertEqual(float(p3), 2.0) + + def test_ltge(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "BEX/BBD") + + self.assertTrue(p1 > p2) + self.assertTrue(p2 < p1) + self.assertTrue(p1 > 5) + self.assertTrue(p2 < 10) + + def test_leeq(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "BEX/BBD") + + self.assertTrue(p1 >= p2) + self.assertTrue(p2 <= p1) + self.assertTrue(p1 >= 5) + self.assertTrue(p2 <= 10) + + def test_ne(self): + p1 = Price(10.0, "BEX/BBD") + p2 = Price(5.0, "BEX/BBD") + + self.assertTrue(p1 != p2) + self.assertTrue(p1 == p1) + self.assertTrue(p1 != 5) + self.assertTrue(p1 == 10) + + def test_order(self): + order = Order(Amount("2 BBD"), Amount("1 BEX")) + self.assertTrue(repr(order) is not None) + + def test_filled_order(self): + order = {"date": "1900-01-01T00:00:00", "current_pays": "2 BBD", "open_pays": "1 BEX"} + filledOrder = FilledOrder(order) + self.assertTrue(repr(filledOrder) is not None) + self.assertEqual(filledOrder.json()["current_pays"], Amount("2.000 BBD").json()) diff --git a/tests/dpaycli/test_profile.py b/tests/dpaycli/test_profile.py new file mode 100755 index 0000000..1109779 --- /dev/null +++ b/tests/dpaycli/test_profile.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import range +from builtins import super +import string +import random +import unittest +import base64 +import json +from pprint import pprint +from dpaycli.profile import Profile, DotDict + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def test_profile(self): + keys = ['profile.url', 'profile.img'] + values = ["http:", "foobar"] + profile = Profile(keys, values) + profile_ref = {'profile': {'url': 'http:', 'img': 'foobar'}} + self.assertTrue(profile, profile_ref) + self.assertTrue(json.loads(str(profile)), profile_ref) + profile.update(profile_ref) + self.assertTrue(profile, profile_ref) + profile.remove('img') + profile_ref = {'profile': {'url': 'http:'}} + self.assertTrue(profile, profile_ref) + profile = Profile({"foo": "bar"}) + self.assertTrue(profile, {"foo": "bar"}) diff --git a/tests/dpaycli/test_steem.py b/tests/dpaycli/test_steem.py new file mode 100755 index 0000000..d04f4ac --- /dev/null +++ b/tests/dpaycli/test_steem.py @@ -0,0 +1,523 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +from parameterized import parameterized +import random +import json +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.amount import Amount +from dpaycli.memo import Memo +from dpaycli.version import version as dpaycli_version +from dpaycli.wallet import Wallet +from dpaycli.witness import Witness +from dpaycli.account import Account +from dpaycligraphenebase.account import PrivateKey +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList +# Py3 compatibility +import sys + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.nodelist = NodeList() + cls.nodelist.update_nodes(dpay_instance=DPay(node=cls.nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=cls.nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + + cls.account = Account("test", full=True, dpay_instance=cls.bts) + cls.account_testnet = Account("test", full=True, dpay_instance=cls.testnet) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_transfer(self, node_param): + if node_param == "normal": + bts = self.bts + acc = self.account + elif node_param == "testnet": + bts = self.testnet + acc = self.account_testnet + acc.dpay.txbuffer.clear() + tx = acc.transfer( + "test", 1.33, "BBD", memo="Foobar", account="test1") + self.assertEqual( + tx["operations"][0][0], + "transfer" + ) + self.assertEqual(len(tx["operations"]), 1) + op = tx["operations"][0][1] + self.assertIn("memo", op) + self.assertEqual(op["memo"], "Foobar") + self.assertEqual(op["from"], "test1") + self.assertEqual(op["to"], "test") + amount = Amount(op["amount"], dpay_instance=bts) + self.assertEqual(float(amount), 1.33) + + def test_create_account(self): + bts = DPay(node=self.nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + core_unit = "DWB" + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) + key1 = PrivateKey() + key2 = PrivateKey() + key3 = PrivateKey() + key4 = PrivateKey() + key5 = PrivateKey() + bts.txbuffer.clear() + tx = bts.create_account( + name, + creator="test", # 1.2.7 + owner_key=format(key1.pubkey, core_unit), + active_key=format(key2.pubkey, core_unit), + posting_key=format(key3.pubkey, core_unit), + memo_key=format(key4.pubkey, core_unit), + additional_owner_keys=[format(key5.pubkey, core_unit)], + additional_active_keys=[format(key5.pubkey, core_unit)], + additional_posting_keys=[format(key5.pubkey, core_unit)], + additional_owner_accounts=["test1"], # 1.2.0 + additional_active_accounts=["test1"], + storekeys=False, + ) + self.assertEqual( + tx["operations"][0][0], + "account_create" + ) + op = tx["operations"][0][1] + role = "active" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "test1", + [x[0] for x in op[role]["account_auths"]]) + role = "posting" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "test1", + [x[0] for x in op[role]["account_auths"]]) + role = "owner" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "test1", + [x[0] for x in op[role]["account_auths"]]) + self.assertEqual( + op["creator"], + "test") + + def test_create_account_password(self): + bts = DPay(node=self.nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + core_unit = "DWB" + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) + key5 = PrivateKey() + bts.txbuffer.clear() + tx = bts.create_account( + name, + creator="test", # 1.2.7 + password="abcdefg", + additional_owner_keys=[format(key5.pubkey, core_unit)], + additional_active_keys=[format(key5.pubkey, core_unit)], + additional_posting_keys=[format(key5.pubkey, core_unit)], + additional_owner_accounts=["test1"], # 1.2.0 + additional_active_accounts=["test1"], + storekeys=False, + ) + self.assertEqual( + tx["operations"][0][0], + "account_create" + ) + op = tx["operations"][0][1] + role = "active" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "test1", + [x[0] for x in op[role]["account_auths"]]) + role = "owner" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "test1", + [x[0] for x in op[role]["account_auths"]]) + self.assertEqual( + op["creator"], + "test") + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_connect(self, node_param): + if node_param == "normal": + bts = self.bts + elif node_param == "testnet": + bts = self.testnet + bts.connect() + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_info(self, node_param): + if node_param == "normal": + bts = self.bts + elif node_param == "testnet": + bts = self.testnet + info = bts.info() + for key in ['current_witness', + 'head_block_id', + 'head_block_number', + 'id', + 'last_irreversible_block_num', + 'current_witness', + 'total_pow', + 'time']: + self.assertTrue(key in info) + + def test_finalizeOps(self): + bts = self.bts + acc = self.account + tx1 = bts.new_tx() + tx2 = bts.new_tx() + + acc.transfer("test1", 1, "BEX", append_to=tx1) + acc.transfer("test1", 2, "BEX", append_to=tx2) + acc.transfer("test1", 3, "BEX", append_to=tx1) + tx1 = tx1.json() + tx2 = tx2.json() + ops1 = tx1["operations"] + ops2 = tx2["operations"] + self.assertEqual(len(ops1), 2) + self.assertEqual(len(ops2), 1) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_weight_threshold(self, node_param): + if node_param == "normal": + bts = self.bts + pkey1 = 'DWB55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n' + pkey2 = 'DWB7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv' + elif node_param == "testnet": + bts = self.testnet + pkey1 = 'TST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n' + pkey2 = 'TST7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv' + + auth = {'account_auths': [['test', 1]], + 'extensions': [], + 'key_auths': [ + [pkey1, 1], + [pkey2, 1]], + 'weight_threshold': 3} # threshold fine + bts._test_weights_treshold(auth) + auth = {'account_auths': [['test', 1]], + 'extensions': [], + 'key_auths': [ + [pkey1, 1], + [pkey2, 1]], + 'weight_threshold': 4} # too high + + with self.assertRaises(ValueError): + bts._test_weights_treshold(auth) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_allow(self, node_param): + if node_param == "normal": + bts = self.bts + acc = self.account + prefix = "DWB" + wif = "DWB55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n" + elif node_param == "testnet": + bts = self.testnet + acc = self.account_testnet + prefix = "TST" + wif = "TST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n" + self.assertIn(bts.prefix, prefix) + tx = acc.allow( + wif, + account="test", + weight=1, + threshold=1, + permission="owner", + ) + self.assertEqual( + (tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertIn("owner", op) + self.assertIn( + [wif, '1'], + op["owner"]["key_auths"]) + self.assertEqual(op["owner"]["weight_threshold"], 1) + + def test_disallow(self): + acc = self.account + pkey1 = "DWB55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n" + pkey2 = "DWB6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + if sys.version > '3': + _assertRaisesRegex = self.assertRaisesRegex + else: + _assertRaisesRegex = self.assertRaisesRegexp + with _assertRaisesRegex(ValueError, ".*Changes nothing.*"): + acc.disallow( + pkey1, + weight=1, + threshold=1, + permission="owner" + ) + with _assertRaisesRegex(ValueError, ".*Changes nothing!.*"): + acc.disallow( + pkey2, + weight=1, + threshold=1, + permission="owner" + ) + + def test_update_memo_key(self): + acc = self.account + prefix = "DWB" + pkey = 'DWB55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n' + self.assertEqual(acc.dpay.prefix, prefix) + acc.dpay.txbuffer.clear() + tx = acc.update_memo_key(pkey) + self.assertEqual( + (tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertEqual( + op["memo_key"], + pkey) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_approvewitness(self, node_param): + if node_param == "normal": + w = self.account + elif node_param == "testnet": + w = self.account_testnet + w.dpay.txbuffer.clear() + tx = w.approvewitness("test1") + self.assertEqual( + (tx["operations"][0][0]), + "account_witness_vote" + ) + op = tx["operations"][0][1] + self.assertIn( + "test1", + op["witness"]) + + def test_post(self): + bts = self.bts + bts.txbuffer.clear() + tx = bts.post("title", "body", author="test", permlink=None, reply_identifier=None, + json_metadata=None, comment_options=None, community="test", tags=["a", "b", "c", "d", "e"], + beneficiaries=[{'account': 'test1', 'weight': 5000}, {'account': 'test2', 'weight': 5000}], self_vote=True) + self.assertEqual( + (tx["operations"][0][0]), + "comment" + ) + op = tx["operations"][0][1] + self.assertEqual(op["body"], "body") + self.assertEqual(op["title"], "title") + self.assertEqual(op["permlink"], "title") + self.assertEqual(op["parent_author"], "") + self.assertEqual(op["parent_permlink"], "a") + json_metadata = json.loads(op["json_metadata"]) + self.assertEqual(json_metadata["tags"], ["a", "b", "c", "d", "e"]) + self.assertEqual(json_metadata["app"], "dpaycli/%s" % (dpaycli_version)) + self.assertEqual( + (tx["operations"][1][0]), + "comment_options" + ) + op = tx["operations"][1][1] + self.assertEqual(len(op['extensions'][0][1]['beneficiaries']), 2) + + def test_comment_option(self): + bts = self.bts + bts.txbuffer.clear() + tx = bts.comment_options({}, "@gtg/witness-gtg-log", account="test") + self.assertEqual( + (tx["operations"][0][0]), + "comment_options" + ) + op = tx["operations"][0][1] + self.assertIn( + "gtg", + op["author"]) + self.assertEqual('1000000.000 BBD', op["max_accepted_payout"]) + self.assertEqual(10000, op["percent_dpay_dollars"]) + self.assertEqual(True, op["allow_votes"]) + self.assertEqual(True, op["allow_curation_rewards"]) + self.assertEqual("witness-gtg-log", op["permlink"]) + + def test_online(self): + bts = self.bts + self.assertFalse(bts.get_blockchain_version() == '0.0.0') + + def test_offline(self): + bts = DPay(node=self.nodelist.get_nodes(), + offline=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}) + bts.refresh_data() + self.assertTrue(bts.get_reserve_ratio(use_stored_data=False) is None) + self.assertTrue(bts.get_reserve_ratio(use_stored_data=True) is None) + self.assertTrue(bts.get_feed_history(use_stored_data=False) is None) + self.assertTrue(bts.get_feed_history(use_stored_data=True) is None) + self.assertTrue(bts.get_reward_funds(use_stored_data=False) is None) + self.assertTrue(bts.get_reward_funds(use_stored_data=True) is None) + self.assertTrue(bts.get_current_median_history(use_stored_data=False) is None) + self.assertTrue(bts.get_current_median_history(use_stored_data=True) is None) + self.assertTrue(bts.get_hardfork_properties(use_stored_data=False) is None) + self.assertTrue(bts.get_hardfork_properties(use_stored_data=True) is None) + self.assertTrue(bts.get_network(use_stored_data=False) is None) + self.assertTrue(bts.get_network(use_stored_data=True) is None) + self.assertTrue(bts.get_witness_schedule(use_stored_data=False) is None) + self.assertTrue(bts.get_witness_schedule(use_stored_data=True) is None) + self.assertTrue(bts.get_config(use_stored_data=False) is None) + self.assertTrue(bts.get_config(use_stored_data=True) is None) + self.assertEqual(bts.get_block_interval(), 3) + self.assertEqual(bts.get_blockchain_version(), '0.0.0') + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_properties(self, node_param): + if node_param == "normal": + bts = DPay(node=self.nodelist.get_nodes(), + nobroadcast=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + elif node_param == "testnet": + bts = DPay(node="https://testnet.dpaydev.com", + nobroadcast=True, + data_refresh_time_seconds=900, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10) + self.assertTrue(bts.get_reserve_ratio(use_stored_data=False) is not None) + self.assertTrue(bts.get_feed_history(use_stored_data=False) is not None) + self.assertTrue(bts.get_reward_funds(use_stored_data=False) is not None) + self.assertTrue(bts.get_current_median_history(use_stored_data=False) is not None) + self.assertTrue(bts.get_hardfork_properties(use_stored_data=False) is not None) + self.assertTrue(bts.get_network(use_stored_data=False) is not None) + self.assertTrue(bts.get_witness_schedule(use_stored_data=False) is not None) + self.assertTrue(bts.get_config(use_stored_data=False) is not None) + self.assertTrue(bts.get_block_interval() is not None) + self.assertTrue(bts.get_blockchain_version() is not None) + + def test_bp_to_rshares(self): + stm = self.bts + rshares = stm.bp_to_rshares(stm.vests_to_sp(1e6)) + self.assertTrue(abs(rshares - 20000000000.0) < 2) + + def test_rshares_to_vests(self): + stm = self.bts + rshares = stm.bp_to_rshares(stm.vests_to_sp(1e6)) + rshares2 = stm.vests_to_rshares(1e6) + self.assertTrue(abs(rshares - rshares2) < 2) + + def test_bp_to_bbd(self): + stm = self.bts + bp = 500 + ret = stm.bp_to_bbd(bp) + self.assertTrue(ret is not None) + + def test_bbd_to_rshares(self): + stm = self.bts + test_values = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7] + for v in test_values: + try: + bbd = round(stm.rshares_to_bbd(stm.bbd_to_rshares(v)), 5) + except ValueError: # Reward pool smaller than 1e7 BBD (e.g. caused by a very low BEX price) + continue + self.assertEqual(bbd, v) + + def test_rshares_to_vote_pct(self): + stm = self.bts + bp = 1000 + voting_power = 9000 + for vote_pct in range(500, 10000, 500): + rshares = stm.bp_to_rshares(bp, voting_power=voting_power, vote_pct=vote_pct) + vote_pct_ret = stm.rshares_to_vote_pct(rshares, dpay_power=bp, voting_power=voting_power) + self.assertEqual(vote_pct_ret, vote_pct) + + def test_sign(self): + bts = self.bts + with self.assertRaises( + exceptions.MissingKeyError + ): + bts.sign() + + def test_broadcast(self): + bts = self.bts + bts.txbuffer.clear() + tx = bts.comment_options({}, "@gtg/witness-gtg-log", account="test") + # tx = bts.sign() + with self.assertRaises( + exceptions.MissingKeyError + ): + bts.broadcast(tx=tx) diff --git a/tests/dpaycli/test_steemconnect.py b/tests/dpaycli/test_steemconnect.py new file mode 100755 index 0000000..1b34893 --- /dev/null +++ b/tests/dpaycli/test_steemconnect.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +from parameterized import parameterized +import random +import json +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.amount import Amount +from dpaycli.memo import Memo +from dpaycli.version import version as dpaycli_version +from dpaycli.wallet import Wallet +from dpaycli.witness import Witness +from dpaycli.account import Account +from dpaycligraphenebase.account import PrivateKey +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList +from dpaycli.dpayid import DPayID +# Py3 compatibility +import sys +core_unit = "DWB" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + num_retries=10) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + unsigned=True, + data_refresh_time_seconds=900, + num_retries=10) + + cls.account = Account("test", full=True, dpay_instance=cls.bts) + cls.account_testnet = Account("test", full=True, dpay_instance=cls.testnet) + + def test_transfer(self): + bts = self.bts + acc = self.account + acc.dpay.txbuffer.clear() + tx = acc.transfer( + "test1", 1.000, "BEX", memo="test") + dpid = DPayID(dpay_instance=bts) + url = dpid.url_from_tx(tx) + url_test = 'https://go.dpayid.io/sign/transfer?from=test&to=test1&amount=1.000+BEX&memo=test' + self.assertEqual(len(url), len(url_test)) + self.assertEqual(len(url.split('?')), 2) + self.assertEqual(url.split('?')[0], url_test.split('?')[0]) + + url_parts = (url.split('?')[1]).split('&') + url_test_parts = (url_test.split('?')[1]).split('&') + + self.assertEqual(len(url_parts), 4) + self.assertEqual(len(list(set(url_parts).intersection(set(url_test_parts)))), 4) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_login_url(self, node_param): + if node_param == "normal": + bts = self.bts + elif node_param == "testnet": + bts = self.testnet + dpid = DPayID(dpay_instance=bts) + url = dpid.get_login_url("localhost", scope="login,vote") + url_test = 'https://go.dpayid.io/oauth2/authorize?client_id=None&redirect_uri=localhost&scope=login,vote' + self.assertEqual(len(url), len(url_test)) + self.assertEqual(len(url.split('?')), 2) + self.assertEqual(url.split('?')[0], url_test.split('?')[0]) + + url_parts = (url.split('?')[1]).split('&') + url_test_parts = (url_test.split('?')[1]).split('&') + + self.assertEqual(len(url_parts), 3) + self.assertEqual(len(list(set(url_parts).intersection(set(url_test_parts)))), 3) diff --git a/tests/dpaycli/test_storage.py b/tests/dpaycli/test_storage.py new file mode 100755 index 0000000..d27a4ad --- /dev/null +++ b/tests/dpaycli/test_storage.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +from parameterized import parameterized +import random +import json +from pprint import pprint +from dpaycli import DPay +from dpaycli.amount import Amount +from dpaycli.memo import Memo +from dpaycli.version import version as dpaycli_version +from dpaycli.wallet import Wallet +from dpaycli.witness import Witness +from dpaycli.account import Account +from dpaycligraphenebase.account import PrivateKey +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance +from dpaycli.nodelist import NodeList +# Py3 compatibility +import sys +core_unit = "DWB" +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + stm = shared_dpay_instance() + stm.config.refreshBackup() + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + + cls.stm = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + # We want to bundle many operations into a single transaction + bundle=True, + num_retries=10 + # Overwrite wallet to use this list of wifs only + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + bundle=True, + num_retries=10 + ) + cls.stm.set_default_account("test") + set_shared_dpay_instance(cls.stm) + # self.stm.newWallet("TestingOneTwoThree") + + cls.wallet = Wallet(dpay_instance=cls.stm) + cls.wallet.wipe(True) + cls.wallet.newWallet(pwd="TestingOneTwoThree") + cls.wallet.unlock(pwd="TestingOneTwoThree") + cls.wallet.addPrivateKey(wif) + + @classmethod + def tearDownClass(cls): + stm = shared_dpay_instance() + stm.config.recover_with_latest_backup() + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_set_default_account(self, node_param): + if node_param == "normal": + stm = self.stm + elif node_param == "testnet": + stm = self.testnet + stm.set_default_account("test") + + self.assertEqual(stm.config["default_account"], "test") diff --git a/tests/dpaycli/test_testnet.py b/tests/dpaycli/test_testnet.py new file mode 100755 index 0000000..56572a1 --- /dev/null +++ b/tests/dpaycli/test_testnet.py @@ -0,0 +1,644 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +import random +from pprint import pprint +from dpaycli import DPay +from dpaycli.exceptions import ( + InsufficientAuthorityError, + MissingKeyError, + InvalidWifError, + WalletLocked +) +from dpaycliapi import exceptions +from dpaycli.amount import Amount +from dpaycli.witness import Witness +from dpaycli.account import Account +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance +from dpaycli.blockchain import Blockchain +from dpaycli.block import Block +from dpaycli.memo import Memo +from dpaycli.transactionbuilder import TransactionBuilder +from dpayclibase.operations import Transfer +from dpaycligraphenebase.account import PasswordKey, PrivateKey, PublicKey +from dpaycli.utils import parse_time, formatTimedelta +from dpaycliapi.rpcutils import NumRetriesReached +from dpaycli.nodelist import NodeList + +# Py3 compatibility +import sys + +core_unit = "STX" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + # stm = shared_dpay_instance() + # stm.config.refreshBackup() + cls.bts = DPay( + node=nodelist.get_testnet(), + nobroadcast=True, + num_retries=10, + expiration=120, + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + cls.bts.set_default_account("dpaycli") + + # Test account "dpaycli" + cls.active_key = "5Jt2wTfhUt5GkZHV1HYVfkEaJ6XnY8D2iA4qjtK9nnGXAhThM3w" + cls.posting_key = "5Jh1Gtu2j4Yi16TfhoDmg8Qj3ULcgRi7A49JXdfUUTVPkaFaRKz" + cls.memo_key = "5KPbCuocX26aMxN9CDPdUex4wCbfw9NoT5P7UhcqgDwxXa47bit" + + # Test account "dpaycli1" + cls.active_key1 = "5Jo9SinzpdAiCDLDJVwuN7K5JcusKmzFnHpEAtPoBHaC1B5RDUd" + cls.posting_key1 = "5JGNhDXuDLusTR3nbmpWAw4dcmE8WfSM8odzqcQ6mDhJHP8YkQo" + cls.memo_key1 = "5KA2ddfAffjfRFoe1UhQjJtKnGsBn9xcsdPQTfMt1fQuErDAkWr" + + cls.active_private_key_of_dpaycli4 = '5JkZZEUWrDsu3pYF7aknSo7BLJx7VfxB3SaRtQaHhsPouDYjxzi' + cls.active_private_key_of_dpaycli5 = '5Hvbm9VjRbd1B3ft8Lm81csaqQudwFwPGdiRKrCmTKcomFS3Z9J' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + stm = self.bts + stm.nobroadcast = True + stm.wallet.wipe(True) + stm.wallet.create("123") + stm.wallet.unlock("123") + + stm.wallet.addPrivateKey(self.active_key1) + stm.wallet.addPrivateKey(self.memo_key1) + stm.wallet.addPrivateKey(self.posting_key1) + + stm.wallet.addPrivateKey(self.active_key) + stm.wallet.addPrivateKey(self.memo_key) + stm.wallet.addPrivateKey(self.posting_key) + stm.wallet.addPrivateKey(self.active_private_key_of_dpaycli4) + stm.wallet.addPrivateKey(self.active_private_key_of_dpaycli5) + + @classmethod + def tearDownClass(cls): + stm = shared_dpay_instance() + stm.config.recover_with_latest_backup() + + def test_wallet_keys(self): + stm = self.bts + stm.wallet.unlock("123") + priv_key = stm.wallet.getPrivateKeyForPublicKey(str(PrivateKey(self.posting_key, prefix=stm.prefix).pubkey)) + self.assertEqual(str(priv_key), self.posting_key) + priv_key = stm.wallet.getKeyForAccount("dpaycli", "active") + self.assertEqual(str(priv_key), self.active_key) + priv_key = stm.wallet.getKeyForAccount("dpaycli1", "posting") + self.assertEqual(str(priv_key), self.posting_key1) + + priv_key = stm.wallet.getPrivateKeyForPublicKey(str(PrivateKey(self.active_private_key_of_dpaycli4, prefix=stm.prefix).pubkey)) + self.assertEqual(str(priv_key), self.active_private_key_of_dpaycli4) + priv_key = stm.wallet.getKeyForAccount("dpaycli4", "active") + self.assertEqual(str(priv_key), self.active_private_key_of_dpaycli4) + + priv_key = stm.wallet.getPrivateKeyForPublicKey(str(PrivateKey(self.active_private_key_of_dpaycli5, prefix=stm.prefix).pubkey)) + self.assertEqual(str(priv_key), self.active_private_key_of_dpaycli5) + priv_key = stm.wallet.getKeyForAccount("dpaycli5", "active") + self.assertEqual(str(priv_key), self.active_private_key_of_dpaycli5) + + def test_transfer(self): + bts = self.bts + bts.nobroadcast = False + bts.wallet.unlock("123") + # bts.wallet.addPrivateKey(self.active_key) + # bts.prefix ="STX" + acc = Account("dpaycli", dpay_instance=bts) + tx = acc.transfer( + "dpaycli1", 1.33, "BBD", memo="Foobar") + self.assertEqual( + tx["operations"][0][0], + "transfer" + ) + self.assertEqual(len(tx['signatures']), 1) + op = tx["operations"][0][1] + self.assertIn("memo", op) + self.assertEqual(op["from"], "dpaycli") + self.assertEqual(op["to"], "dpaycli1") + amount = Amount(op["amount"], dpay_instance=bts) + self.assertEqual(float(amount), 1.33) + bts.nobroadcast = True + + def test_transfer_memo(self): + bts = self.bts + bts.nobroadcast = False + bts.wallet.unlock("123") + acc = Account("dpaycli", dpay_instance=bts) + tx = acc.transfer( + "dpaycli1", 1.33, "BBD", memo="#Foobar") + self.assertEqual( + tx["operations"][0][0], + "transfer" + ) + op = tx["operations"][0][1] + self.assertIn("memo", op) + self.assertIn("#", op["memo"]) + m = Memo(from_account=op["from"], to_account=op["to"], dpay_instance=bts) + memo = m.decrypt(op["memo"]) + self.assertEqual(memo, "Foobar") + + self.assertEqual(op["from"], "dpaycli") + self.assertEqual(op["to"], "dpaycli1") + amount = Amount(op["amount"], dpay_instance=bts) + self.assertEqual(float(amount), 1.33) + bts.nobroadcast = True + + @unittest.skip + def test_transfer_1of1(self): + dpay = self.bts + dpay.nobroadcast = False + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli', + "to": 'dpaycli1', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '1 of 1 transaction'})) + self.assertEqual( + tx["operations"][0]["type"], + "transfer_operation" + ) + tx.appendWif(self.active_key) + tx.sign() + tx.sign() + self.assertEqual(len(tx['signatures']), 1) + tx.broadcast() + dpay.nobroadcast = True + + @unittest.skip + def test_transfer_2of2_simple(self): + # Send a 2 of 2 transaction from elf which needs dpaycli4's cosign to send funds + dpay = self.bts + dpay.nobroadcast = False + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli5', + "to": 'dpaycli1', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '2 of 2 simple transaction'})) + + tx.appendWif(self.active_private_key_of_dpaycli5) + tx.sign() + tx.clearWifs() + tx.appendWif(self.active_private_key_of_dpaycli4) + tx.sign(reconstruct_tx=False) + self.assertEqual(len(tx['signatures']), 2) + tx.broadcast() + dpay.nobroadcast = True + + @unittest.skip + def test_transfer_2of2_wallet(self): + # Send a 2 of 2 transaction from dpaycli5 which needs dpaycli4's cosign to send + # priv key of dpaycli5 and dpaycli4 are stored in the wallet + # appendSigner fetches both keys and signs automatically with both keys. + dpay = self.bts + dpay.nobroadcast = False + dpay.wallet.unlock("123") + + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli5', + "to": 'dpaycli1', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '2 of 2 serialized/deserialized transaction'})) + + tx.appendSigner("dpaycli5", "active") + tx.sign() + self.assertEqual(len(tx['signatures']), 2) + tx.broadcast() + dpay.nobroadcast = True + + @unittest.skip + def test_transfer_2of2_serialized_deserialized(self): + # Send a 2 of 2 transaction from dpaycli5 which needs dpaycli4's cosign to send + # funds but sign the transaction with dpaycli5's key and then serialize the transaction + # and deserialize the transaction. After that, sign with dpaycli4's key. + dpay = self.bts + dpay.nobroadcast = False + dpay.wallet.unlock("123") + # dpay.wallet.removeAccount("dpaycli4") + dpay.wallet.removePrivateKeyFromPublicKey(str(PublicKey(self.active_private_key_of_dpaycli4, prefix=core_unit))) + + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli5', + "to": 'dpaycli1', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '2 of 2 serialized/deserialized transaction'})) + + tx.appendSigner("dpaycli5", "active") + tx.addSigningInformation("dpaycli5", "active") + tx.sign() + tx.clearWifs() + self.assertEqual(len(tx['signatures']), 1) + # dpay.wallet.removeAccount("dpaycli5") + dpay.wallet.removePrivateKeyFromPublicKey(str(PublicKey(self.active_private_key_of_dpaycli5, prefix=core_unit))) + tx_json = tx.json() + del tx + new_tx = TransactionBuilder(tx=tx_json, dpay_instance=dpay) + self.assertEqual(len(new_tx['signatures']), 1) + dpay.wallet.addPrivateKey(self.active_private_key_of_dpaycli4) + new_tx.appendMissingSignatures() + new_tx.sign(reconstruct_tx=False) + self.assertEqual(len(new_tx['signatures']), 2) + new_tx.broadcast() + dpay.nobroadcast = True + + @unittest.skip + def test_transfer_2of2_offline(self): + # Send a 2 of 2 transaction from dpaycli5 which needs dpaycli4's cosign to send + # funds but sign the transaction with dpaycli5's key and then serialize the transaction + # and deserialize the transaction. After that, sign with dpaycli4's key. + dpay = self.bts + dpay.nobroadcast = False + dpay.wallet.unlock("123") + # dpay.wallet.removeAccount("dpaycli4") + dpay.wallet.removePrivateKeyFromPublicKey(str(PublicKey(self.active_private_key_of_dpaycli4, prefix=core_unit))) + + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli5', + "to": 'dpaycli', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '2 of 2 serialized/deserialized transaction'})) + + tx.appendSigner("dpaycli5", "active") + tx.addSigningInformation("dpaycli5", "active") + tx.sign() + tx.clearWifs() + self.assertEqual(len(tx['signatures']), 1) + # dpay.wallet.removeAccount("dpaycli5") + dpay.wallet.removePrivateKeyFromPublicKey(str(PublicKey(self.active_private_key_of_dpaycli5, prefix=core_unit))) + dpay.wallet.addPrivateKey(self.active_private_key_of_dpaycli4) + tx.appendMissingSignatures() + tx.sign(reconstruct_tx=False) + self.assertEqual(len(tx['signatures']), 2) + tx.broadcast() + dpay.nobroadcast = True + dpay.wallet.addPrivateKey(self.active_private_key_of_dpaycli5) + + @unittest.skip + def test_transfer_2of2_wif(self): + nodelist = NodeList() + # Send a 2 of 2 transaction from elf which needs dpaycli4's cosign to send + # funds but sign the transaction with elf's key and then serialize the transaction + # and deserialize the transaction. After that, sign with dpaycli4's key. + dpay = DPay( + node=nodelist.get_testnet(), + num_retries=10, + keys=[self.active_private_key_of_dpaycli5], + expiration=360, + ) + + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=dpay) + tx.appendOps(Transfer(**{"from": 'dpaycli5', + "to": 'dpaycli', + "amount": Amount("0.01 BEX", dpay_instance=dpay), + "memo": '2 of 2 serialized/deserialized transaction'})) + + tx.appendSigner("dpaycli5", "active") + tx.addSigningInformation("dpaycli5", "active") + tx.sign() + tx.clearWifs() + self.assertEqual(len(tx['signatures']), 1) + tx_json = tx.json() + del dpay + del tx + + dpay = DPay( + node=nodelist.get_testnet(), + num_retries=10, + keys=[self.active_private_key_of_dpaycli4], + expiration=360, + ) + new_tx = TransactionBuilder(tx=tx_json, dpay_instance=dpay) + new_tx.appendMissingSignatures() + new_tx.sign(reconstruct_tx=False) + self.assertEqual(len(new_tx['signatures']), 2) + new_tx.broadcast() + + @unittest.skip + def test_verifyAuthority(self): + stm = self.bts + stm.wallet.unlock("123") + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=stm) + tx.appendOps(Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1.300 BBD", dpay_instance=stm), + "memo": "Foobar"})) + account = Account("dpaycli", dpay_instance=stm) + tx.appendSigner(account, "active") + self.assertTrue(len(tx.wifs) > 0) + tx.sign() + tx.verify_authority() + self.assertTrue(len(tx["signatures"]) > 0) + + def test_create_account(self): + bts = self.bts + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) + key1 = PrivateKey() + key2 = PrivateKey() + key3 = PrivateKey() + key4 = PrivateKey() + key5 = PrivateKey() + tx = bts.create_account( + name, + creator="dpaycli", + owner_key=format(key1.pubkey, core_unit), + active_key=format(key2.pubkey, core_unit), + posting_key=format(key3.pubkey, core_unit), + memo_key=format(key4.pubkey, core_unit), + additional_owner_keys=[format(key5.pubkey, core_unit)], + additional_active_keys=[format(key5.pubkey, core_unit)], + additional_owner_accounts=["dpaycli1"], # 1.2.0 + additional_active_accounts=["dpaycli1"], + storekeys=False + ) + self.assertEqual( + tx["operations"][0][0], + "account_create" + ) + op = tx["operations"][0][1] + role = "active" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "dpaycli1", + [x[0] for x in op[role]["account_auths"]]) + role = "owner" + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key5.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "dpaycli1", + [x[0] for x in op[role]["account_auths"]]) + self.assertEqual( + op["creator"], + "dpaycli") + + def test_connect(self): + nodelist = NodeList() + self.bts.connect(node=nodelist.get_testnet()) + bts = self.bts + self.assertEqual(bts.prefix, "STX") + + def test_set_default_account(self): + self.bts.set_default_account("dpaycli") + + def test_info(self): + info = self.bts.info() + for key in ['current_witness', + 'head_block_id', + 'head_block_number', + 'id', + 'last_irreversible_block_num', + 'current_witness', + 'total_pow', + 'time']: + self.assertTrue(key in info) + + def test_finalizeOps(self): + bts = self.bts + tx1 = bts.new_tx() + tx2 = bts.new_tx() + + acc = Account("dpaycli", dpay_instance=bts) + acc.transfer("dpaycli1", 1, "BEX", append_to=tx1) + acc.transfer("dpaycli1", 2, "BEX", append_to=tx2) + acc.transfer("dpaycli1", 3, "BEX", append_to=tx1) + tx1 = tx1.json() + tx2 = tx2.json() + ops1 = tx1["operations"] + ops2 = tx2["operations"] + self.assertEqual(len(ops1), 2) + self.assertEqual(len(ops2), 1) + + def test_weight_threshold(self): + bts = self.bts + auth = {'account_auths': [['test', 1]], + 'extensions': [], + 'key_auths': [ + ['STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n', 1], + ['STX7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv', 1]], + 'weight_threshold': 3} # threshold fine + bts._test_weights_treshold(auth) + auth = {'account_auths': [['test', 1]], + 'extensions': [], + 'key_auths': [ + ['STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n', 1], + ['STX7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv', 1]], + 'weight_threshold': 4} # too high + + with self.assertRaises(ValueError): + bts._test_weights_treshold(auth) + + def test_allow(self): + bts = self.bts + self.assertIn(bts.prefix, "STX") + acc = Account("dpaycli", dpay_instance=bts) + self.assertIn(acc.dpay.prefix, "STX") + tx = acc.allow( + "STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", + account="dpaycli", + weight=1, + threshold=1, + permission="active", + ) + self.assertEqual( + (tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertIn("active", op) + self.assertIn( + ["STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", '1'], + op["active"]["key_auths"]) + self.assertEqual(op["active"]["weight_threshold"], 1) + + def test_disallow(self): + bts = self.bts + acc = Account("dpaycli", dpay_instance=bts) + if sys.version > '3': + _assertRaisesRegex = self.assertRaisesRegex + else: + _assertRaisesRegex = self.assertRaisesRegexp + with _assertRaisesRegex(ValueError, ".*Changes nothing.*"): + acc.disallow( + "STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", + weight=1, + threshold=1, + permission="active" + ) + with _assertRaisesRegex(ValueError, ".*Changes nothing!.*"): + acc.disallow( + "STX6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + weight=1, + threshold=1, + permission="active" + ) + + def test_update_memo_key(self): + bts = self.bts + bts.wallet.unlock("123") + self.assertEqual(bts.prefix, "STX") + acc = Account("dpaycli", dpay_instance=bts) + tx = acc.update_memo_key("STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") + self.assertEqual( + (tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertEqual( + op["memo_key"], + "STX55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") + + def test_approvewitness(self): + bts = self.bts + w = Account("dpaycli", dpay_instance=bts) + tx = w.approvewitness("dpaycli1") + self.assertEqual( + (tx["operations"][0][0]), + "account_witness_vote" + ) + op = tx["operations"][0][1] + self.assertIn( + "dpaycli1", + op["witness"]) + + def test_appendWif(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + nobroadcast=True, + expiration=120, + num_retries=10) + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=stm) + tx.appendOps(Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1 BEX", dpay_instance=stm), + "memo": ""})) + with self.assertRaises( + MissingKeyError + ): + tx.sign() + with self.assertRaises( + InvalidWifError + ): + tx.appendWif("abcdefg") + tx.appendWif(self.active_key) + tx.sign() + self.assertTrue(len(tx["signatures"]) > 0) + + def test_appendSigner(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + keys=[self.active_key], + nobroadcast=True, + expiration=120, + num_retries=10) + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=stm) + tx.appendOps(Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1 BEX", dpay_instance=stm), + "memo": ""})) + account = Account("dpaycli", dpay_instance=stm) + with self.assertRaises( + AssertionError + ): + tx.appendSigner(account, "abcdefg") + tx.appendSigner(account, "active") + self.assertTrue(len(tx.wifs) > 0) + tx.sign() + self.assertTrue(len(tx["signatures"]) > 0) + + @unittest.skip + def test_verifyAuthorityException(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + keys=[self.posting_key], + nobroadcast=True, + expiration=120, + num_retries=10) + tx = TransactionBuilder(use_condenser_api=True, dpay_instance=stm) + tx.appendOps(Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1 BEX", dpay_instance=stm), + "memo": ""})) + account = Account("dpaycli2", dpay_instance=stm) + tx.appendSigner(account, "active") + tx.appendWif(self.posting_key) + self.assertTrue(len(tx.wifs) > 0) + tx.sign() + with self.assertRaises( + exceptions.MissingRequiredActiveAuthority + ): + tx.verify_authority() + self.assertTrue(len(tx["signatures"]) > 0) + + def test_Transfer_broadcast(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + keys=[self.active_key], + nobroadcast=True, + expiration=120, + num_retries=10) + + tx = TransactionBuilder(use_condenser_api=True, expiration=10, dpay_instance=stm) + tx.appendOps(Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1 BEX", dpay_instance=stm), + "memo": ""})) + tx.appendSigner("dpaycli", "active") + tx.sign() + tx.broadcast() + + def test_TransactionConstructor(self): + stm = self.bts + opTransfer = Transfer(**{"from": "dpaycli", + "to": "dpaycli1", + "amount": Amount("1 BEX", dpay_instance=stm), + "memo": ""}) + tx1 = TransactionBuilder(use_condenser_api=True, dpay_instance=stm) + tx1.appendOps(opTransfer) + tx = TransactionBuilder(tx1, dpay_instance=stm) + self.assertFalse(tx.is_empty()) + self.assertTrue(len(tx.list_operations()) == 1) + self.assertTrue(repr(tx) is not None) + self.assertTrue(str(tx) is not None) + account = Account("dpaycli", dpay_instance=stm) + tx.appendSigner(account, "active") + self.assertTrue(len(tx.wifs) > 0) + tx.sign() + self.assertTrue(len(tx["signatures"]) > 0) + + def test_follow_active_key(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + keys=[self.active_key], + nobroadcast=True, + expiration=120, + num_retries=10) + account = Account("dpaycli", dpay_instance=stm) + account.follow("dpaycli1") + + def test_follow_posting_key(self): + nodelist = NodeList() + stm = DPay(node=nodelist.get_testnet(), + keys=[self.posting_key], + nobroadcast=True, + expiration=120, + num_retries=10) + account = Account("dpaycli", dpay_instance=stm) + account.follow("dpaycli1") diff --git a/tests/dpaycli/test_txbuffers.py b/tests/dpaycli/test_txbuffers.py new file mode 100755 index 0000000..81583f6 --- /dev/null +++ b/tests/dpaycli/test_txbuffers.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from dpaycli import DPay +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.transactionbuilder import TransactionBuilder +from dpayclibase.signedtransactions import Signed_Transaction +from dpayclibase.operations import Transfer +from dpaycli.account import Account +from dpaycli.block import Block +from dpaycligraphenebase.base58 import Base58 +from dpaycli.amount import Amount +from dpaycli.exceptions import ( + InsufficientAuthorityError, + MissingKeyError, + InvalidWifError, + WalletLocked +) +from dpaycliapi import exceptions +from dpaycli.wallet import Wallet +from dpaycli.utils import formatTimeFromNow +from dpaycli.nodelist import NodeList +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.stm = DPay( + node=nodelist.get_nodes(), + keys={"active": wif, "owner": wif, "memo": wif}, + nobroadcast=True, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10 + ) + set_shared_dpay_instance(cls.stm) + cls.stm.set_default_account("test") + + def test_emptyTransaction(self): + stm = self.stm + tx = TransactionBuilder(dpay_instance=stm) + self.assertTrue(tx.is_empty()) + self.assertTrue(tx["ref_block_num"] is not None) + + def test_verify_transaction(self): + stm = self.stm + block = Block(22005665, dpay_instance=stm) + trx = block.transactions[28] + signed_tx = Signed_Transaction(trx) + key = signed_tx.verify(chain=stm.chain_params, recover_parameter=False) + public_key = format(Base58(key[0]), stm.prefix) + self.assertEqual(public_key, "DWB4tzr1wjmuov9ftXR6QNv7qDWsbShMBPQpuwatZsfSc5pKjRDfq") diff --git a/tests/dpaycli/test_utils.py b/tests/dpaycli/test_utils.py new file mode 100755 index 0000000..ad09c51 --- /dev/null +++ b/tests/dpaycli/test_utils.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import unittest +from datetime import datetime, date, timedelta +from dpaycli.utils import ( + formatTimedelta, + assets_from_string, + resolve_authorperm, + resolve_authorpermvoter, + construct_authorperm, + construct_authorpermvoter, + sanitize_permlink, + derive_permlink, + resolve_root_identifier, + make_patch, + remove_from_dict, + formatToTimeStamp, + formatTimeString, + addTzInfo +) + + +class Testcases(unittest.TestCase): + def test_constructAuthorperm(self): + self.assertEqual(construct_authorperm("A", "B"), "@A/B") + self.assertEqual(construct_authorperm({'author': "A", 'permlink': "B"}), "@A/B") + + def test_resolve_root_identifier(self): + self.assertEqual(resolve_root_identifier("/a/@b/c"), ("@b/c", "a")) + + def test_constructAuthorpermvoter(self): + self.assertEqual(construct_authorpermvoter("A", "B", "C"), "@A/B|C") + self.assertEqual(construct_authorpermvoter({'author': "A", 'permlink': "B", 'voter': 'C'}), "@A/B|C") + self.assertEqual(construct_authorpermvoter({'authorperm': "A/B", 'voter': 'C'}), "@A/B|C") + + def test_assets_from_string(self): + self.assertEqual(assets_from_string('USD:BTS'), ['USD', 'BTS']) + self.assertEqual(assets_from_string('BTSBOTS.S1:BTS'), ['BTSBOTS.S1', 'BTS']) + + def test_authorperm_resolve(self): + self.assertEqual(resolve_authorperm('https://dvideo.io/#!/v/pottlund/m5cqkd1a'), + ('pottlund', 'm5cqkd1a')) + self.assertEqual(resolve_authorperm("https://dsite.io/witness-category/@gtg/24lfrm-gtg-witness-log"), + ('gtg', '24lfrm-gtg-witness-log')) + self.assertEqual(resolve_authorperm("@gtg/24lfrm-gtg-witness-log"), + ('gtg', '24lfrm-gtg-witness-log')) + self.assertEqual(resolve_authorperm("https://busy.org/@gtg/24lfrm-gtg-witness-log"), + ('gtg', '24lfrm-gtg-witness-log')) + self.assertEqual(resolve_authorperm('https://dlive.io/livestream/atnazo/61dd94c1-8ff3-11e8-976f-0242ac110003'), + ('atnazo', '61dd94c1-8ff3-11e8-976f-0242ac110003')) + + def test_authorpermvoter_resolve(self): + self.assertEqual(resolve_authorpermvoter('theaussiegame/cryptokittie-giveaway-number-2|test'), + ('theaussiegame', 'cryptokittie-giveaway-number-2', 'test')) + self.assertEqual(resolve_authorpermvoter('holger80/virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt|holger80'), + ('holger80', 'virtuelle-cloud-mining-ponzi-schemen-auch-bekannt-als-hypt', 'holger80')) + + def test_sanitizePermlink(self): + self.assertEqual(sanitize_permlink("aAf_0.12"), "aaf-0-12") + self.assertEqual(sanitize_permlink("[](){}|"), "") + + def test_derivePermlink(self): + self.assertEqual(derive_permlink("Hello World"), "hello-world") + self.assertEqual(derive_permlink("aAf_0.12"), "aaf-0-12") + self.assertEqual(derive_permlink("[](){}"), "") + + def test_patch(self): + self.assertEqual(make_patch("aa", "ab"), '@@ -1 +1 @@\n-aa\n+ab\n') + self.assertEqual(make_patch("Hello!\n Das ist ein Test!\nEnd.\n", "Hello!\n This is a Test\nEnd.\n"), + '@@ -1,3 +1,3 @@\n Hello!\n- Das ist ein Test!\n+ This is a Test\n End.\n') + + def test_formatTimedelta(self): + now = datetime.now() + self.assertEqual(formatTimedelta(now - now), '0:00:00') + + def test_remove_from_dict(self): + a = {'a': 1, 'b': 2} + b = {'b': 2} + self.assertEqual(remove_from_dict(a, ['b'], keep_keys=True), {'b': 2}) + self.assertEqual(remove_from_dict(a, ['a'], keep_keys=False), {'b': 2}) + self.assertEqual(remove_from_dict(b, ['b'], keep_keys=True), {'b': 2}) + self.assertEqual(remove_from_dict(b, ['a'], keep_keys=False), {'b': 2}) + self.assertEqual(remove_from_dict(b, [], keep_keys=True), {}) + self.assertEqual(remove_from_dict(a, ['a', 'b'], keep_keys=False), {}) + + def test_formatDateTimetoTimeStamp(self): + t = "1970-01-01T00:00:00" + t = formatTimeString(t) + timestamp = formatToTimeStamp(t) + self.assertEqual(timestamp, 0) + t2 = "2018-07-10T10:08:39" + timestamp = formatToTimeStamp(t2) + self.assertEqual(timestamp, 1531217319) + t3 = datetime(2018, 7, 10, 10, 8, 39) + timestamp = formatToTimeStamp(t3) + self.assertEqual(timestamp, 1531217319) + + def test_formatTimeString(self): + t = "2018-07-10T10:08:39" + t = formatTimeString(t) + t2 = addTzInfo(datetime(2018, 7, 10, 10, 8, 39)) + self.assertEqual(t, t2) diff --git a/tests/dpaycli/test_vote.py b/tests/dpaycli/test_vote.py new file mode 100755 index 0000000..0755b1d --- /dev/null +++ b/tests/dpaycli/test_vote.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +import pytz +from datetime import datetime, timedelta +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.comment import Comment +from dpaycli.account import Account +from dpaycli.vote import Vote, ActiveVotes, AccountVotes +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.utils import construct_authorperm, resolve_authorperm, resolve_authorpermvoter, construct_authorpermvoter +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + cls.testnet = DPay( + node="https://testnet.dpaydev.com", + nobroadcast=True, + keys={"active": wif}, + num_retries=10 + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + acc = Account("holger80", dpay_instance=cls.bts) + n_votes = 0 + index = 0 + while n_votes == 0: + comment = acc.get_feed(limit=30)[::-1][index] + votes = comment.get_votes() + n_votes = len(votes) + index += 1 + + last_vote = votes[0] + + cls.authorpermvoter = construct_authorpermvoter(last_vote['author'], last_vote['permlink'], last_vote["voter"]) + [author, permlink, voter] = resolve_authorpermvoter(cls.authorpermvoter) + cls.author = author + cls.permlink = permlink + cls.voter = voter + cls.authorperm = construct_authorperm(author, permlink) + + def test_vote(self): + bts = self.bts + vote = Vote(self.authorpermvoter, dpay_instance=bts) + self.assertEqual(self.voter, vote["voter"]) + self.assertEqual(self.author, vote["author"]) + self.assertEqual(self.permlink, vote["permlink"]) + + vote = Vote(self.voter, authorperm=self.authorperm, dpay_instance=bts) + self.assertEqual(self.voter, vote["voter"]) + self.assertEqual(self.author, vote["author"]) + self.assertEqual(self.permlink, vote["permlink"]) + vote_json = vote.json() + self.assertEqual(self.voter, vote_json["voter"]) + self.assertEqual(self.voter, vote.voter) + self.assertTrue(vote.weight >= 0) + self.assertTrue(vote.bbd >= 0) + self.assertTrue(vote.rshares >= 0) + self.assertTrue(vote.percent >= 0) + self.assertTrue(vote.reputation is not None) + self.assertTrue(vote.rep is not None) + self.assertTrue(vote.time is not None) + vote.refresh() + self.assertEqual(self.voter, vote["voter"]) + self.assertEqual(self.author, vote["author"]) + self.assertEqual(self.permlink, vote["permlink"]) + vote_json = vote.json() + self.assertEqual(self.voter, vote_json["voter"]) + self.assertEqual(self.voter, vote.voter) + self.assertTrue(vote.weight >= 0) + self.assertTrue(vote.bbd >= 0) + self.assertTrue(vote.rshares >= 0) + self.assertTrue(vote.percent >= 0) + self.assertTrue(vote.reputation is not None) + self.assertTrue(vote.rep is not None) + self.assertTrue(vote.time is not None) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_keyerror(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + with self.assertRaises( + exceptions.VoteDoesNotExistsException + ): + Vote(construct_authorpermvoter(self.author, self.permlink, "asdfsldfjlasd"), dpay_instance=bts) + + with self.assertRaises( + exceptions.VoteDoesNotExistsException + ): + Vote(construct_authorpermvoter(self.author, "sdlfjd", "asdfsldfjlasd"), dpay_instance=bts) + + with self.assertRaises( + exceptions.VoteDoesNotExistsException + ): + Vote(construct_authorpermvoter("sdalfj", "dsfa", "asdfsldfjlasd"), dpay_instance=bts) + + def test_activevotes(self): + bts = self.bts + votes = ActiveVotes(self.authorperm, dpay_instance=bts) + votes.printAsTable() + vote_list = votes.get_list() + self.assertTrue(isinstance(vote_list, list)) + + def test_accountvotes(self): + bts = self.bts + utc = pytz.timezone('UTC') + limit_time = utc.localize(datetime.utcnow()) - timedelta(days=7) + votes = AccountVotes(self.author, start=limit_time, dpay_instance=bts) + self.assertTrue(len(votes) > 0) + self.assertTrue(isinstance(votes[0], Vote)) diff --git a/tests/dpaycli/test_wallet.py b/tests/dpaycli/test_wallet.py new file mode 100755 index 0000000..09ba440 --- /dev/null +++ b/tests/dpaycli/test_wallet.py @@ -0,0 +1,168 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +import mock +from pprint import pprint +from dpaycli import DPay, exceptions +from dpaycli.account import Account +from dpaycli.amount import Amount +from dpaycli.asset import Asset +from dpaycli.wallet import Wallet +from dpaycli.instance import set_shared_dpay_instance, shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + stm = shared_dpay_instance() + stm.config.refreshBackup() + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + + cls.stm = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + # We want to bundle many operations into a single transaction + bundle=True, + num_retries=10 + # Overwrite wallet to use this list of wifs only + ) + cls.stm.set_default_account("test") + set_shared_dpay_instance(cls.stm) + # self.stm.newWallet("TestingOneTwoThree") + + cls.wallet = Wallet(dpay_instance=cls.stm) + cls.wallet.wipe(True) + cls.wallet.newWallet(pwd="TestingOneTwoThree") + cls.wallet.unlock(pwd="TestingOneTwoThree") + cls.wallet.addPrivateKey(wif) + + @classmethod + def tearDownClass(cls): + stm = shared_dpay_instance() + stm.config.recover_with_latest_backup() + + def test_wallet_lock(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + self.assertTrue(self.wallet.unlocked()) + self.assertFalse(self.wallet.locked()) + self.wallet.lock() + self.assertTrue(self.wallet.locked()) + + def test_change_masterpassword(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + self.assertTrue(self.wallet.unlocked()) + self.wallet.changePassphrase("newPass") + self.wallet.lock() + self.assertTrue(self.wallet.locked()) + self.wallet.unlock(pwd="newPass") + self.assertTrue(self.wallet.unlocked()) + self.wallet.changePassphrase("TestingOneTwoThree") + self.wallet.lock() + + def test_Keys(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + keys = self.wallet.getPublicKeys() + self.assertTrue(len(keys) > 0) + pub = self.wallet.getPublicKeys()[0] + private = self.wallet.getPrivateKeyForPublicKey(pub) + self.assertEqual(private, wif) + + def test_account_by_pub(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + acc = Account("gtg") + pub = acc["owner"]["key_auths"][0][0] + acc_by_pub = self.wallet.getAccount(pub) + self.assertEqual("gtg", acc_by_pub["name"]) + gen = self.wallet.getAccountsFromPublicKey(pub) + acc_by_pub_list = [] + for a in gen: + acc_by_pub_list.append(a) + self.assertEqual("gtg", acc_by_pub_list[0]) + gen = self.wallet.getAllAccounts(pub) + acc_by_pub_list = [] + for a in gen: + acc_by_pub_list.append(a) + self.assertEqual("gtg", acc_by_pub_list[0]["name"]) + self.assertEqual(pub, acc_by_pub_list[0]["pubkey"]) + + def test_pub_lookup(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getOwnerKeyForAccount("test") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getMemoKeyForAccount("test") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getActiveKeyForAccount("test") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getPostingKeyForAccount("test") + + def test_pub_lookup_keys(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getOwnerKeysForAccount("test") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getActiveKeysForAccount("test") + with self.assertRaises( + exceptions.MissingKeyError + ): + self.wallet.getPostingKeysForAccount("test") + + def test_encrypt(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + self.wallet.masterpassword = "TestingOneTwoThree" + self.assertEqual([self.wallet.encrypt_wif("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"), + self.wallet.encrypt_wif("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR")], + ["6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi", + "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg"]) + self.wallet.masterpassword = "Satoshi" + self.assertEqual([self.wallet.encrypt_wif("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5")], + ["6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq"]) + self.wallet.masterpassword = "TestingOneTwoThree" + + def test_deencrypt(self): + stm = self.stm + self.wallet.dpay = stm + self.wallet.unlock(pwd="TestingOneTwoThree") + self.wallet.masterpassword = "TestingOneTwoThree" + self.assertEqual([self.wallet.decrypt_wif("6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi"), + self.wallet.decrypt_wif("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg")], + ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd", + "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"]) + self.wallet.masterpassword = "Satoshi" + self.assertEqual([self.wallet.decrypt_wif("6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq")], + ["5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"]) + self.wallet.masterpassword = "TestingOneTwoThree" diff --git a/tests/dpaycli/test_witness.py b/tests/dpaycli/test_witness.py new file mode 100755 index 0000000..a4b82ed --- /dev/null +++ b/tests/dpaycli/test_witness.py @@ -0,0 +1,152 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import super +import unittest +from parameterized import parameterized +from pprint import pprint +from dpaycli import DPay +from dpaycli.witness import Witness, Witnesses, WitnessesVotedByAccount, WitnessesRankedByVote +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + cls.bts = DPay( + node=nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + keys={"active": wif}, + num_retries=10 + ) + cls.testnet = DPay( + # node="https://testnet.timcliff.com", + node=nodelist.get_nodes(), + nobroadcast=True, + unsigned=True, + keys={"active": wif}, + num_retries=10 + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.bts) + cls.bts.set_default_account("test") + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_feed_publish(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + bts.txbuffer.clear() + w = Witness("gtg", dpay_instance=bts) + tx = w.feed_publish("4 BBD", "1 BEX") + self.assertEqual( + (tx["operations"][0][0]), + "feed_publish" + ) + op = tx["operations"][0][1] + self.assertIn( + "gtg", + op["publisher"]) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_update(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + bts.txbuffer.clear() + w = Witness("gtg", dpay_instance=bts) + props = {"account_creation_fee": "0.1 BEX", + "maximum_block_size": 32000, + "bbd_interest_rate": 0} + tx = w.update(wif, "", props) + self.assertEqual((tx["operations"][0][0]), "witness_update") + op = tx["operations"][0][1] + self.assertIn( + "gtg", + op["owner"]) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_witnesses(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + w = Witnesses(dpay_instance=bts) + w.printAsTable() + self.assertTrue(len(w) > 0) + self.assertTrue(isinstance(w[0], Witness)) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_WitnessesVotedByAccount(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + w = WitnessesVotedByAccount("gtg", dpay_instance=bts) + w.printAsTable() + self.assertTrue(len(w) > 0) + self.assertTrue(isinstance(w[0], Witness)) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_WitnessesRankedByVote(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + w = WitnessesRankedByVote(dpay_instance=bts) + w.printAsTable() + self.assertTrue(len(w) > 0) + self.assertTrue(isinstance(w[0], Witness)) + + @parameterized.expand([ + ("normal"), + ("testnet"), + ]) + def test_export(self, node_param): + if node_param == "normal": + bts = self.bts + else: + bts = self.testnet + owner = "gtg" + if bts.rpc.get_use_appbase(): + witness = bts.rpc.find_witnesses({'owners': [owner]}, api="database")['witnesses'] + if len(witness) > 0: + witness = witness[0] + else: + witness = bts.rpc.get_witness_by_account(owner) + + w = Witness(owner, dpay_instance=bts) + keys = list(witness.keys()) + json_witness = w.json() + exclude_list = ['votes', 'virtual_last_update', 'virtual_scheduled_time'] + for k in keys: + if k not in exclude_list: + if isinstance(witness[k], dict) and isinstance(json_witness[k], list): + self.assertEqual(list(witness[k].values()), json_witness[k]) + else: + self.assertEqual(witness[k], json_witness[k]) diff --git a/tests/dpaycliapi/__init__.py b/tests/dpaycliapi/__init__.py new file mode 100755 index 0000000..e0310a0 --- /dev/null +++ b/tests/dpaycliapi/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/dpaycliapi/test_node.py b/tests/dpaycliapi/test_node.py new file mode 100755 index 0000000..1f00a64 --- /dev/null +++ b/tests/dpaycliapi/test_node.py @@ -0,0 +1,56 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import pytest +import unittest +from dpaycliapi.node import Nodes +from dpaycliapi.rpcutils import ( + is_network_appbase_ready, + get_api_name, get_query, UnauthorizedError, + RPCConnection, RPCError, NumRetriesReached +) + + +class Testcases(unittest.TestCase): + def test_sleep_and_check_retries(self): + nodes = Nodes("test", -1, 5) + nodes.sleep_and_check_retries("error") + nodes = Nodes("test", 1, 5) + nodes.increase_error_cnt() + nodes.increase_error_cnt() + with self.assertRaises( + NumRetriesReached + ): + nodes.sleep_and_check_retries() + + def test_next(self): + nodes = Nodes(["a", "b", "c"], -1, -1) + self.assertEqual(nodes.working_nodes_count, len(nodes)) + self.assertEqual(nodes.url, nodes[0].url) + next(nodes) + self.assertEqual(nodes.url, nodes[0].url) + next(nodes) + self.assertEqual(nodes.url, nodes[1].url) + next(nodes) + self.assertEqual(nodes.url, nodes[2].url) + next(nodes) + self.assertEqual(nodes.url, nodes[0].url) + + nodes = Nodes("a,b,c", 5, 5) + self.assertEqual(nodes.working_nodes_count, len(nodes)) + self.assertEqual(nodes.url, nodes[0].url) + next(nodes) + self.assertEqual(nodes.url, nodes[0].url) + next(nodes) + self.assertEqual(nodes.url, nodes[1].url) + next(nodes) + self.assertEqual(nodes.url, nodes[2].url) + next(nodes) + self.assertEqual(nodes.url, nodes[0].url) + + def test_init(self): + nodes = Nodes(["a", "b", "c"], 5, 5) + nodes2 = Nodes(nodes, 5, 5) + self.assertEqual(nodes.url, nodes2.url) diff --git a/tests/dpaycliapi/test_rpcutils.py b/tests/dpaycliapi/test_rpcutils.py new file mode 100755 index 0000000..297b166 --- /dev/null +++ b/tests/dpaycliapi/test_rpcutils.py @@ -0,0 +1,85 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import pytest +import unittest +from dpaycliapi.rpcutils import ( + is_network_appbase_ready, + get_api_name, get_query, UnauthorizedError, + RPCConnection, RPCError, NumRetriesReached +) + + +class Testcases(unittest.TestCase): + def test_is_network_appbase_ready(self): + self.assertTrue(is_network_appbase_ready({'DPAY_BLOCKCHAIN_VERSION': '0.19.10'})) + self.assertTrue(is_network_appbase_ready({'DPAY_BLOCKCHAIN_VERSION': '0.19.10'})) + self.assertFalse(is_network_appbase_ready({'DPAY_BLOCKCHAIN_VERSION': '0.19.2'})) + self.assertFalse(is_network_appbase_ready({'DPAY_BLOCKCHAIN_VERSION': '0.19.2'})) + + def test_get_api_name(self): + self.assertEqual(get_api_name(True, api="test"), "test_api") + self.assertEqual(get_api_name(True, api="test_api"), "test_api") + self.assertEqual(get_api_name(True, api="jsonrpc"), "jsonrpc") + + self.assertEqual(get_api_name(True), "condenser_api") + self.assertEqual(get_api_name(False, api="test"), "test_api") + self.assertEqual(get_api_name(False, api="test_api"), "test_api") + self.assertTrue(get_api_name(False, api="") is None) + self.assertTrue(get_api_name(False) is None) + + def test_get_query(self): + query = get_query(True, 1, "test_api", "test", args="") + self.assertEqual(query["method"], 'test_api.test') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], dict)) + + args = ({"a": "b"},) + query = get_query(True, 1, "test_api", "test", args=args) + self.assertEqual(query["method"], 'test_api.test') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], dict)) + self.assertEqual(query["params"], args[0]) + + args = ([{"a": "b"}, {"a": "c"}],) + query_list = get_query(True, 1, "test_api", "test", args=args) + query = query_list[0] + self.assertEqual(query["method"], 'test_api.test') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], dict)) + self.assertEqual(query["params"], args[0][0]) + query = query_list[1] + self.assertEqual(query["method"], 'test_api.test') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 2) + self.assertTrue(isinstance(query["params"], dict)) + self.assertEqual(query["params"], args[0][1]) + + args = ("b",) + query = get_query(True, 1, "test_api", "test", args=args) + self.assertEqual(query["method"], 'call') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], list)) + self.assertEqual(query["params"], ["test_api", "test", ["b"]]) + + args = ("b",) + query = get_query(True, 1, "condenser_api", "test", args=args) + self.assertEqual(query["method"], 'call') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], list)) + self.assertEqual(query["params"], ["condenser_api", "test", ["b"]]) + + args = ("b",) + query = get_query(False, 1, "test_api", "test", args=args) + self.assertEqual(query["method"], 'call') + self.assertEqual(query["jsonrpc"], '2.0') + self.assertEqual(query["id"], 1) + self.assertTrue(isinstance(query["params"], list)) + self.assertEqual(query["params"], ["test_api", "test", ["b"]]) diff --git a/tests/dpaycliapi/test_steemnoderpc.py b/tests/dpaycliapi/test_steemnoderpc.py new file mode 100755 index 0000000..b1e020d --- /dev/null +++ b/tests/dpaycliapi/test_steemnoderpc.py @@ -0,0 +1,200 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import time +import unittest +from parameterized import parameterized +import random +import itertools +from pprint import pprint +from dpaycli import DPay +from dpaycliapi.dpaynoderpc import DPayNodeRPC +from dpaycliapi.websocket import DPayWebsocket +from dpaycliapi import exceptions +from dpaycliapi.exceptions import NumRetriesReached, CallRetriesReached +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList +# Py3 compatibility +import sys + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +core_unit = "DWB" + + +class Testcases(unittest.TestCase): + + @classmethod + def setUpClass(cls): + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=3)) + cls.nodes = nodelist.get_nodes(https=False, appbase=True) + cls.nodes_https = nodelist.get_nodes(wss=False, appbase=True) + cls.nodes_appbase = nodelist.get_nodes(normal=False) + cls.test_list = nodelist.get_nodes() + + cls.appbase = DPay( + node=cls.nodes_appbase, + nobroadcast=True, + keys={"active": wif, "owner": wif, "memo": wif}, + num_retries=10 + ) + cls.rpc = DPayNodeRPC(urls=cls.test_list) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_dpay_instance(cls.appbase) + cls.appbase.set_default_account("test") + + def get_reply(self, msg): + reply = ' 403 Forbidden

' \ + '%s


nginx
' % (msg) + return reply + + def test_appbase(self): + bts = self.appbase + self.assertTrue(bts.chain_params['min_version'] == '0.19.10') + self.assertTrue(bts.rpc.get_use_appbase()) + self.assertTrue(isinstance(bts.rpc.get_config(api="database"), dict)) + with self.assertRaises( + exceptions.NoApiWithName + ): + bts.rpc.get_config(api="abc") + with self.assertRaises( + exceptions.NoMethodWithName + ): + bts.rpc.get_config_abc() + + def test_connect_test_node(self): + rpc = self.rpc + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + rpc.rpcclose() + rpc.rpcconnect() + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + + def test_connect_test_node2(self): + rpc = self.rpc + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + rpc.next() + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + + def test_connect_test_str_list(self): + str_list = "" + for node in self.nodes: + str_list += node + ";" + str_list = str_list[:-1] + rpc = DPayNodeRPC(urls=str_list) + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + rpc.next() + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + + def test_connect_test_str_list2(self): + str_list = "" + for node in self.nodes: + str_list += node + "," + str_list = str_list[:-1] + rpc = DPayNodeRPC(urls=str_list) + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + rpc.next() + self.assertIn(rpc.url, self.nodes + self.nodes_appbase + self.nodes_https) + + def test_server_error(self): + rpc = self.rpc + with self.assertRaises( + exceptions.RPCErrorDoRetry + ): + rpc._check_for_server_error(self.get_reply("500 Internal Server Error")) + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("501 Not Implemented")) + + with self.assertRaises( + exceptions.RPCErrorDoRetry + ): + rpc._check_for_server_error(self.get_reply("502 Bad Gateway")) + + with self.assertRaises( + exceptions.RPCErrorDoRetry + ): + rpc._check_for_server_error(self.get_reply("503 Service Temporarily Unavailable")) + + with self.assertRaises( + exceptions.RPCErrorDoRetry + ): + rpc._check_for_server_error(self.get_reply("504 Gateway Time-out")) + + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("505 HTTP Version not supported")) + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("506 Variant Also Negotiates")) + + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("507 Insufficient Storage")) + + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("508 Loop Detected")) + + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("509 Bandwidth Limit Exceeded")) + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("510 Not Extended")) + + with self.assertRaises( + exceptions.RPCError + ): + rpc._check_for_server_error(self.get_reply("511 Network Authentication Required")) + + def test_num_retries(self): + with self.assertRaises( + NumRetriesReached + ): + DPayNodeRPC(urls="https://wrong.link.com", num_retries=2, timeout=1) + with self.assertRaises( + NumRetriesReached + ): + DPayNodeRPC(urls="https://wrong.link.com", num_retries=3, num_retries_call=3, timeout=1) + nodes = ["https://httpstat.us/500", "https://httpstat.us/501", "https://httpstat.us/502", "https://httpstat.us/503", + "https://httpstat.us/505", "https://httpstat.us/511", "https://httpstat.us/520", "https://httpstat.us/522", + "https://httpstat.us/524"] + with self.assertRaises( + NumRetriesReached + ): + DPayNodeRPC(urls=nodes, num_retries=0, num_retries_call=0, timeout=1) + + def test_error_handling(self): + rpc = DPayNodeRPC(urls=self.nodes_appbase, num_retries=2, num_retries_call=3) + with self.assertRaises( + exceptions.NoMethodWithName + ): + rpc.get_wrong_command() + with self.assertRaises( + exceptions.UnhandledRPCError + ): + rpc.get_accounts("test") + + def test_error_handling_appbase(self): + rpc = DPayNodeRPC(urls=self.nodes_appbase, num_retries=2, num_retries_call=3) + with self.assertRaises( + exceptions.NoMethodWithName + ): + rpc.get_wrong_command() + with self.assertRaises( + exceptions.NoApiWithName + ): + rpc.get_block({"block_num": 1}, api="wrong_api") diff --git a/tests/dpaycliapi/test_websocket.py b/tests/dpaycliapi/test_websocket.py new file mode 100755 index 0000000..f08a209 --- /dev/null +++ b/tests/dpaycliapi/test_websocket.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import super +import mock +import string +import unittest +import random +import itertools +from pprint import pprint +from dpaycli import DPay +from dpaycliapi.websocket import DPayWebsocket +from dpaycli.instance import set_shared_dpay_instance +from dpaycli.nodelist import NodeList +# Py3 compatibility +import sys + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +core_unit = "DWB" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + nodelist = NodeList() + nodelist.update_nodes(dpay_instance=DPay(node=nodelist.get_nodes(normal=True, appbase=True), num_retries=10)) + stm = DPay(node=nodelist.get_nodes()) + + self.ws = DPayWebsocket( + urls=stm.rpc.nodes, + num_retries=10 + ) + + def test_connect(self): + ws = self.ws + self.assertTrue(len(next(ws.nodes)) > 0) diff --git a/tests/dpayclibase/__init__.py b/tests/dpayclibase/__init__.py new file mode 100755 index 0000000..e0310a0 --- /dev/null +++ b/tests/dpayclibase/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/dpayclibase/test_memo.py b/tests/dpayclibase/test_memo.py new file mode 100755 index 0000000..9cd301a --- /dev/null +++ b/tests/dpayclibase/test_memo.py @@ -0,0 +1,151 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import chr +from builtins import range +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +from pprint import pprint +from itertools import cycle +from dpaycligraphenebase.account import BrainKey, Address, PublicKey, PrivateKey, PasswordKey +from dpayclibase.memo import ( + get_shared_secret, + _pad, + _unpad, + encode_memo, + decode_memo, + encode_memo_bts, + decode_memo_bts +) + +test_cases = [ + {'from': 'GPH7FPzbN7hnRk24T3Nh9MYM1xBaF5xyRYu8WtyTrtLoUG8cUtszM', + 'message_bts': '688fe6c97f78ad2d3c5a82d9aa61bc23', + 'message': '#FYu8pMPJxTv7q2geNLSQC8dm47uqdNtFLCoDY5yZWjAz2R4wNyHEwQ48hPWm9SuAZ6fCFmjQrFCBVQFSP7EkobrWWRGaeqH6msKkPjRsMd6UUaNva1nmtLc55RAzqPLht', + 'nonce': '16332877645293003478', + 'plain': u'I am this!', + 'to': 'GPH6HAMuJRkjGJkj6cZWBbTU13gkUhBep383prqRdExXsZsYTrWT5', + 'wif': '5Jpkeq1jiNE8Pe24GxFWTsyWbcP59Qq4cD7qg3Wgd6JFJqJkoG8'}, + {'from': 'GPH7FPzbN7hnRk24T3Nh9MYM1xBaF5xyRYu8WtyTrtLoUG8cUtszM', + 'message_bts': 'db7ab7dfefee3ffa2394ec438601ceff', + 'message': '#FYu8pMPJxTv7q2geNLSQC8dm47uqdNtFLCoDY5yZWjAz2R4wNyHEwQ48hPWm9SuAZ6fCFmjQrFCBVQFSP7EkobrWWRGaeqH6msKkPjRsMd6pNxowQQGhkWuR9z5W1aLau', + 'nonce': '16332877645293003478', + 'plain': u'Hello World', + 'to': 'GPH6HAMuJRkjGJkj6cZWBbTU13gkUhBep383prqRdExXsZsYTrWT5', + 'wif': '5Jpkeq1jiNE8Pe24GxFWTsyWbcP59Qq4cD7qg3Wgd6JFJqJkoG8'}, + {'from': 'GPH7FPzbN7hnRk24T3Nh9MYM1xBaF5xyRYu8WtyTrtLoUG8cUtszM', + 'message_bts': '01b6616cbd10bdd0743c82c2bd580651f3e852360a739e7d11c45f483871dc45', + 'message': '#FYu8pMPJxTv7q2geNLSQC8dm47uqdNtFLCoDY5yZWjAz2R4wNyHEwQ48hPWm9SuAZ6fCFmjQrFCBVQFSP7EkobrWWRGaeqH6msKkPjRsMd6iKUwipf3H34zh3CAZVHNDy', + 'nonce': '16332877645293003478', + 'plain': u'Daniel Larimer', + 'to': 'GPH6HAMuJRkjGJkj6cZWBbTU13gkUhBep383prqRdExXsZsYTrWT5', + 'wif': '5Jpkeq1jiNE8Pe24GxFWTsyWbcP59Qq4cD7qg3Wgd6JFJqJkoG8'}, + {'from': 'GPH7FPzbN7hnRk24T3Nh9MYM1xBaF5xyRYu8WtyTrtLoUG8cUtszM', + 'message_bts': '24702af49bc82e06eb74a4acd91b18c389b13a6c9850a0fd3f728f486fe6daf4', + 'message': '#FYu8pMPJxTv7q2geNLSQC8dm47uqdNtFLCoDY5yZWjAz2R4wNyHEwQ48hPWm9SuAZ6fCFmjQrFCBVQFSP7EkobrWWRGaeqH6msKkPjRsMd6QyDwh8a4rxrSLnY2H4ztCK', + 'nonce': '16332877645293003478', + 'plain': u'Thanks you, sir!', + 'to': 'GPH6HAMuJRkjGJkj6cZWBbTU13gkUhBep383prqRdExXsZsYTrWT5', + 'wif': '5Jpkeq1jiNE8Pe24GxFWTsyWbcP59Qq4cD7qg3Wgd6JFJqJkoG8'}, + {'from': 'GPH7FPzbN7hnRk24T3Nh9MYM1xBaF5xyRYu8WtyTrtLoUG8cUtszM', + 'message_bts': 'db059f7a0f9053b041cd95c373ed9dff3445491d03ef17c490870ebcfcc6ec61a53718ec6cc8f5d81da6fcaa77b40d19', + 'message': '#5Kh3GamVLQtmU7PRHr6gyvAXqKtcRUaDy7Yp4BWqFuNeRq88ioc6rTGMGc7bRC1PtUV2LAeqsiQtbuRgPFSppVXPccS5BSWfqSxMF7ytAbmafekm2DweU1F2nqYwFgWYVe8wsHQdZVpzCdm8BJUY4xBCEU2xrB8nX4559EKag5BuU', + 'nonce': '16332877645293003478', + 'plain': u'äöü߀@$²³', + 'to': 'GPH6HAMuJRkjGJkj6cZWBbTU13gkUhBep383prqRdExXsZsYTrWT5', + 'wif': '5Jpkeq1jiNE8Pe24GxFWTsyWbcP59Qq4cD7qg3Wgd6JFJqJkoG8'} +] + +test_shared_secrets = [ + ["5JYWCqDpeVrefVaFxJfDc3mzQ67dtsfhU7zcB7AMJYuTH57VsoE", "GPH56EzLTXkis55hBsompVXmSdnayG3afDNFmsCLohPh6rSNzkzhs", "fbb2fef5a3a115887df84c694e8ac5c9bf998c89d0c22438c18fd018f2529460"], + ["5JKhu9ZKydGFz7yGURocDVEepSY9fk2VRGAA8Xnb9wwFWa8yTWy", "GPH818iy2auxecLxhWTtW219w2VAfBYHxeHaeRASoTFLnsZo1DJ63", "4a52093355abeb31cef02ee1cbdf0661d982d52ad8fe39c68957e3ae03f3bda9"], + ["5KKmTkFCNnedj6hbyRYJwcaMnc4TkuwrPsJDqR2Bj9ShHkfdgQ3", "GPH78SdnBpqhEHxxzwZeKoFEXV6PviymWzBF7ev29pZcTCF8ynJAo", "500e67a07f53d49b88db635c64e4b0a2414168c7054118d40001e86f1abce131"], + ["5KLBuZtagfmGqhDTEPSM84TXKxKfzNyGaxRKCgdcocEU7Nusw49", "GPH5nYv9AusGXgHyMBbSBV4HyEAmhzXqLNRPvUpKmNpFo5soho95o", "febfa7ad6c48bb0ab976c6416da24017b93a58e4e699dba76fc590b4b1ac0d26"], + ["5JrVxMdeBZJvWqV4SmyFq9psQ4Dg8cFXtSWDiL7V5gUJC133xC2", "GPH7HxVNixmh33R44Kr2uJERbhvzkaLen8su4juqyFe2FW2U2cCXA", "82ef43913f83dd3ff0b4f06bcd8801a06c9f046b44b054e0a9ad042c28e5bdba"], + ["5JfEonXJ4H2kSP4V9NzC3uTRtTpLx4wVgDvf5AWN1KKTV6CZ4x7", "GPH6ZtaoP6skA433YGNNJcPGnsgx15psKRBwAy83tw7XWsDy8hso3", "b1ad058e9cc48e305fb46f07736409a55692c67d3507aad6a051b35459ec2f93"], + ["5J5UDLdk9XjvcbzNY5AQoUB2pttsvN7FtQFyyFZXUUsHFAp9iQd", "GPH51wPrJXWLcX6iNPAoZ9sGk4fHXk6krQgTX1jfuyxtKuhoEan83", "82fcc73de1331913945f6ce6d0207864bbc7cffee10ed3533ab32629cb759323"], + ["5JNZpagkR8wWsW3n4hHqFUQVkAu3HhJ9kU1criuruFpAwoaesHs", "GPH5SCy1teB91pNYetxEwV8vRyMApsy8aG61wsi8z2B4Zb6kfnqUf", "704097d0c270e93f0ce5fa91049bb0aa2f38ccfc4bdc38840176abbb98337c0c"], + ["5KKBRfgTgATU5SmF2uy7ewi7BbDDJCtmf3x9CeYziF14uj8YHMM", "GPH7pUa1fp4NtGaRDmZF6TeanHw7zELUp1eWxZasRE3zY4xYKdbhV", "114aba4ab84ea225bbf4b60aaf6d467d3b206ff8a94d531a5a6031ad90c874dd"], + ["5Jg7muALcVxncN32LyGMDK8zut2b1Sw3VJA1xjZE5ght7DRM9ac", "GPH5Vj6uR2iKmrB2DcFyqNzperycD3a32BBYkefzKYCHoGnXemwWS", "60928672da8e9a7dc0f783f2bf8aaf1b206b9bbd85f0a61b638e0b99f5f8ea56"], +] + + +class Testcases(unittest.TestCase): + + def test_padding(self): + for l in range(0, 255): + s = bytes(l * chr(l), 'utf-8') + padded = _pad(s, 16).decode('utf-8') + self.assertEqual(s.decode('utf-8'), _unpad(padded, 16)) + + def test_decrypt_bts(self): + for memo in test_cases: + dec = decode_memo_bts(PrivateKey(memo["wif"]), + PublicKey(memo["to"], prefix="GPH"), + memo["nonce"], + memo["message_bts"]) + self.assertEqual(memo["plain"], dec) + + def test_encrypt_bts(self): + for memo in test_cases: + enc = encode_memo_bts(PrivateKey(memo["wif"]), + PublicKey(memo["to"], prefix="GPH"), + memo["nonce"], + memo["plain"]) + self.assertEqual(memo["message_bts"], enc) + + def test_decrypt(self): + for memo in test_cases: + dec = decode_memo(PrivateKey(memo["wif"]), + memo["message"]) + self.assertEqual(memo["plain"], dec) + + def test_encrypt(self): + for memo in test_cases: + enc = encode_memo(PrivateKey(memo["wif"]), + PublicKey(memo["to"], prefix="GPH"), + memo["nonce"], + memo["plain"], prefix="GPH") + self.assertEqual(memo["message"], enc) + + def test_encrypt_decrypt(self): + base58 = u'#HU6pdQ4Hh8cFrDVooekRPVZu4BdrhAe9RxrWrei2CwfAApAPdM4PT5mSV9cV3tTuWKotYQF6suyM4JHFBZz4pcwyezPzuZ2na7uwhRcLqFotsqxWRBpaXkNks2QCnYLS8' + text = u'#爱' + nonce = u'1462976530069648' + wif = str(PasswordKey("", "", role="", prefix="DWB").get_private_key()) + private_key = PrivateKey(wif=wif, prefix="DWB") + public_key = private_key.pubkey + cypertext = encode_memo(private_key, + public_key, + nonce, + text, prefix="DWB") + self.assertEqual(cypertext, base58) + plaintext = decode_memo(private_key, cypertext) + self.assertEqual(plaintext, text) + + def test_shared_secret(self): + for s in test_shared_secrets: + priv = PrivateKey(s[0]) + pub = PublicKey(s[1], prefix="GPH") + shared_secret = get_shared_secret(priv, pub) + self.assertEqual(s[2], shared_secret) + + def test_shared_secrets_equal(self): + + wifs = cycle([x[0] for x in test_shared_secrets]) + + for i in range(len(test_shared_secrets)): + sender_private_key = PrivateKey(next(wifs)) + sender_public_key = sender_private_key.pubkey + receiver_private_key = PrivateKey(next(wifs)) + receiver_public_key = receiver_private_key.pubkey + + self.assertEqual( + get_shared_secret(sender_private_key, receiver_public_key), + get_shared_secret(receiver_private_key, sender_public_key) + ) diff --git a/tests/dpayclibase/test_objects.py b/tests/dpayclibase/test_objects.py new file mode 100755 index 0000000..cc30d6c --- /dev/null +++ b/tests/dpayclibase/test_objects.py @@ -0,0 +1,37 @@ +from builtins import chr +from builtins import range +from builtins import str +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +import json +from pprint import pprint +from dpayclibase.objects import Amount +from dpayclibase.objects import Operation +from dpaycligraphenebase.types import ( + Uint8, Int16, Uint16, Uint32, Uint64, + Varint32, Int64, String, Bytes, Void, + Array, PointInTime, Signature, Bool, + Set, Fixed_array, Optional, Static_variant, + Map, Id +) + + +class Testcases(unittest.TestCase): + def test_Amount(self): + a = "1.000 BEX" + t = Amount(a) + self.assertEqual(a, t.__str__()) + self.assertEqual(a, str(t)) + + a = {"amount": "3000", "precision": 3, "nai": "@@000000037"} + t = Amount(a, prefix="DWB") + # self.assertEqual(str(a), t.__str__()) + self.assertEqual(a, json.loads(str(t))) + + def test_Operation(self): + a = {"amount": '1000', "precision": 3, "nai": '@@000000013'} + j = ["transfer", {'from': 'a', 'to': 'b', 'amount': a, 'memo': 'c'}] + o = Operation(j) + self.assertEqual(o.json()[1], j[1]) diff --git a/tests/dpayclibase/test_operations.py b/tests/dpayclibase/test_operations.py new file mode 100755 index 0000000..e7e69bd --- /dev/null +++ b/tests/dpayclibase/test_operations.py @@ -0,0 +1,45 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import chr +from builtins import range +from builtins import str +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +import json +from pprint import pprint +from dpaycli.amount import Amount +from dpayclibase.operations import Transfer +from dpayclibase.objects import Operation +from dpayclibase.signedtransactions import Signed_Transaction + +wif = "5J4KCbg1G3my9b9hCaQXnHSm6vrwW9xQTJS6ZciW2Kek7cCkCEk" + + +class Testcases(unittest.TestCase): + def test_Transfer(self): + transferJson = {'from': 'test', 'to': 'test1', 'amount': "1.000 BEX", 'memo': 'foobar'} + t = Transfer(transferJson) + self.assertEqual(transferJson, json.loads(str(t))) + self.assertEqual(transferJson, t.json()) + self.assertEqual(transferJson, t.toJson()) + self.assertEqual(transferJson, t.__json__()) + + transferJson = {'from': 'test', 'to': 'test1', 'amount': ['3000', 3, '@@000000037'], 'memo': 'foobar'} + t = Transfer(transferJson) + self.assertEqual(transferJson, json.loads(str(t))) + self.assertEqual(transferJson, t.json()) + self.assertEqual(transferJson, t.toJson()) + self.assertEqual(transferJson, t.__json__()) + + o = Operation(Transfer(transferJson)) + self.assertEqual(o.json()[1], transferJson) + tx = {'ref_block_num': 0, 'ref_block_prefix': 0, 'expiration': '2018-04-07T09:30:53', 'operations': [o], 'extensions': [], 'signatures': []} + s = Signed_Transaction(tx) + s.sign(wifkeys=[wif], chain="DPAYAPPBASE") + self.assertEqual(s.json()["operations"][0][1], transferJson) diff --git a/tests/dpayclibase/test_transactions.py b/tests/dpayclibase/test_transactions.py new file mode 100755 index 0000000..97db31a --- /dev/null +++ b/tests/dpayclibase/test_transactions.py @@ -0,0 +1,1155 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import bytes +from builtins import chr +from builtins import range +from builtins import super +import random +import unittest +from pprint import pprint +from binascii import hexlify +from collections import OrderedDict + +from dpayclibase import ( + transactions, + memo, + operations, + objects +) +from dpayclibase.objects import Operation +from dpayclibase.signedtransactions import Signed_Transaction +from dpaycligraphenebase.account import PrivateKey +from dpaycligraphenebase import account +from dpayclibase.operationids import getOperationNameForId +from dpaycligraphenebase.py23 import py23_bytes, bytes_types +from dpaycli.amount import Amount +from dpaycli.asset import Asset +from dpaycli.dpay import DPay + + +TEST_AGAINST_CLI_WALLET = False + +prefix = u"DPAY" +default_prefix = u"DWB" +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +ref_block_num = 34294 +ref_block_prefix = 3707022213 +expiration = "2016-04-06T08:29:27" + + +class Testcases(unittest.TestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.stm = DPay( + offline=True + ) + + def doit(self, printWire=False, ops=None): + if ops is None: + ops = [Operation(self.op)] + tx = Signed_Transaction(ref_block_num=ref_block_num, + ref_block_prefix=ref_block_prefix, + expiration=expiration, + operations=ops) + tx = tx.sign([wif], chain=prefix) + tx.verify([PrivateKey(wif, prefix=u"DWB").pubkey], prefix) + txWire = hexlify(py23_bytes(tx)).decode("ascii") + if printWire: + print() + print(txWire) + print() + self.assertEqual(self.cm[:-130], txWire[:-130]) + + if TEST_AGAINST_CLI_WALLET: + from grapheneapi.grapheneapi import GrapheneAPI + rpc = GrapheneAPI("localhost", 8092) + self.cm = rpc.serialize_transaction(tx.json()) + # print("soll: %s" % self.cm[:-130]) + # print("ist: %s" % txWire[:-130]) + # print(txWire[:-130] == self.cm[:-130]) + self.assertEqual(self.cm[:-130], txWire[:-130]) + + def test_Empty_Op(self): + self.cm = (u"f68585abf4dce7c8045700000120020c2218cd5bcbaf3bdaba2f192a7" + "a69cb2307fcc6be2c09e45e204d175fc5fb715df86fcccfa1235babe6" + "09461cc9fdfadbae06381d711576fb4265bd832008") + self.doit(ops=[]) + + def test_Transfer(self): + self.op = operations.Transfer(**{ + "from": "foo", + "to": "baar", + "amount": Amount("111.110 BEX", dpay_instance=self.stm), + "memo": "Fooo", + "prefix": default_prefix + }) + self.cm = (u"f68585abf4dce7c80457010203666f6f046261617206b201000000" + "000003535445454d000004466f6f6f00012025416c234dd5ff15d8" + "b45486833443c128002bcafa57269cada3ad213ef88adb5831f63a" + "58d8b81bbdd92d494da01eeb13ee1786d02ce075228b25d7132f8f" + "3e") + self.doit() + + def test_create_account(self): + self.op = operations.Account_create( + **{ + 'creator': + 'xeroc', + 'fee': + '10.000 BEX', + 'json_metadata': + '', + 'memo_key': + 'DWB6zLNtyFVToBsBZDsgMhgjpwysYVbsQD6YhP3kRkQhANUB4w7Qp', + 'new_account_name': + 'fsafaasf', + 'owner': { + 'account_auths': [], + 'key_auths': [[ + 'DWB5jYVokmZHdEpwo5oCG3ES2Ca4VYz' + 'y6tM8pWWkGdgVnwo2mFLFq', 1 + ], [ + 'DWB6zLNtyFVToBsBZDsgMhgjpwysYVb' + 'sQD6YhP3kRkQhANUB4w7Qp', 1 + ]], + 'weight_threshold': + 1 + }, + 'active': { + 'account_auths': [], + 'key_auths': [[ + 'DWB6pbVDAjRFiw6fkiKYCrkz7PFeL7' + 'XNAfefrsREwg8MKpJ9VYV9x', 1 + ], [ + 'DWB6zLNtyFVToBsBZDsgMhgjpwysYV' + 'bsQD6YhP3kRkQhANUB4w7Qp', 1 + ]], + 'weight_threshold': + 1 + }, + 'posting': { + 'account_auths': [], + 'key_auths': [[ + 'DWB8CemMDjdUWSV5wKotEimhK6c4d' + 'Y7p2PdzC2qM1HpAP8aLtZfE7', 1 + ], [ + 'DWB6zLNtyFVToBsBZDsgMhgjpwys' + 'YVbsQD6YhP3kRkQhANUB4w7Qp', 1 + ], [ + 'DWB6pbVDAjRFiw6fkiKYCrkz7PFeL' + '7XNAfefrsREwg8MKpJ9VYV9x', 1 + ]], + 'weight_threshold': + 1 + }, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570109102700000000000003535445454d000" + "0057865726f63086673616661617366010000000002026f6231b8ed" + "1c5e964b42967759757f8bb879d68e7b09d9ea6eedec21de6fa4c40" + "1000314aa202c9158990b3ec51a1aa49b2ab5d300c97b391df3beb3" + "4bb74f3c62699e010001000000000202fe8cc11cc8251de6977636b" + "55c1ab8a9d12b0b26154ac78e56e7c4257d8bcf6901000314aa202c" + "9158990b3ec51a1aa49b2ab5d300c97b391df3beb34bb74f3c62699" + "e010001000000000302fe8cc11cc8251de6977636b55c1ab8a9d12b" + "0b26154ac78e56e7c4257d8bcf6901000314aa202c9158990b3ec51" + "a1aa49b2ab5d300c97b391df3beb34bb74f3c62699e010003b453f4" + "6013fdbccb90b09ba169c388c34d84454a3b9fbec68d5a7819a734f" + "ca001000314aa202c9158990b3ec51a1aa49b2ab5d300c97b391df3" + "beb34bb74f3c62699e0000012031827ea70b06e413d124d14ed8db3" + "99597fa5f94566e031b706533a9090395be1c0ed317c8af01d12ca7" + "9258ac4d800adff92a84630b567e5ff48cd4b5f716d6") + self.doit() + + def test_Transfer_to_vesting(self): + self.op = operations.Transfer_to_vesting(**{ + "from": "foo", + "to": "baar", + "amount": "111.110 BEX", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c80457010303666f6f046261617206b201000000" + "000003535445454d00000001203a34cd45fb4a2585514614be2c1" + "ba2365257ce5470d20c6c6abda39204eeba0b7e057d889ca8b1b1" + "406f1441520a25d32df2ab9fdb532c3377dc66d0fe41bb3d") + self.doit() + + def test_withdraw_vesting(self): + self.op = operations.Withdraw_vesting(**{ + "account": "foo", + "vesting_shares": "100 VESTS", + "prefix": default_prefix + }) + + self.cm = ( + u"f68585abf4dce7c80457010403666f6f00e1f5050000000006564553545300000" + "00120772da57b15b62780ee3d8afedd8d46ffafb8c62788eab5ce01435df99e1d" + "36de549f260444866ff4e228cac445548060e018a872e7ee99ace324af9844f4c" + "50a") + self.doit() + + def test_Comment(self): + self.op = operations.Comment( + **{ + "parent_author": "foobara", + "parent_permlink": "foobarb", + "author": "foobarc", + "permlink": "foobard", + "title": "foobare", + "body": "foobarf", + "json_metadata": { + "foo": "bar" + }, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c80457010107666f6f6261726107666f6f626172620" + "7666f6f6261726307666f6f6261726407666f6f6261726507666f6f62" + "6172660e7b22666f6f223a2022626172227d00011f34a882f3b06894c" + "29f52e06b8a28187b84b817c0e40f124859970b32511a778736d682f2" + "4d3a6e6da124b340668d25bbcf85ffa23ca622b307ffe10cf182bb82") + self.doit() + + def test_Vote(self): + self.op = operations.Vote( + **{ + "voter": "foobara", + "author": "foobarc", + "permlink": "foobard", + "weight": 1000, + "prefix": default_prefix + }) + self.cm = (u"f68585abf4dce7c80457010007666f6f6261726107666f6f62617263" + "07666f6f62617264e8030001202e09123f732a438ef6d6138484d7ad" + "edfdcf4a4f3d171f7fcafe836efa2a3c8877290bd34c67eded824ac0" + "cc39e33d154d0617f64af936a83c442f62aef08fec") + self.doit() + + def test_Transfer_to_savings(self): + self.op = operations.Transfer_to_savings( + **{ + "from": "testuser", + "to": "testuser", + "amount": "1.000 BEX", + "memo": "testmemo", + "prefix": default_prefix + }) + self.cm = ( + u"f68585abf4dce7c804570120087465737475736572087465737475736572e8030" + "0000000000003535445454d000008746573746d656d6f00011f4df74457bf8824" + "c02da6a722a7c604676c97aad1a51ebcfb7086b0b7c1f19f9257388a06b3c24ae" + "51d97c9eee5e0ecb7b6c32a29af6f56697f0c7516e70a75ce") + self.doit() + + def test_Transfer_from_savings(self): + self.op = operations.Transfer_from_savings( + **{ + "from": "testuser", + "request_id": 9001, + "to": "testser", + "amount": "100.000 BBD", + "memo": "memohere", + "prefix": default_prefix + }) + self.cm = ( + u"f68585abf4dce7c804570121087465737475736572292300000774657374736" + "572a0860100000000000353424400000000086d656d6f6865726500012058760" + "45f4869b6459438019d71d25bdea461899e0a96635c05f19caf424fa1453fc1fe" + "103d9ca6470d629b9971adddf757c829bb47cc96b29662f294bebb4fb2") + self.doit() + + def test_Cancel_transfer_from_savings(self): + self.op = operations.Cancel_transfer_from_savings(**{ + "from": "tesuser", + "request_id": 9001, + "prefix": default_prefix + }) + + self.cm = ( + u"f68585abf4dce7c8045701220774657375736572292300000001200942474f672" + "3937b88e19fb8cade26cc97f68cb626362d0764d134fe837df5262200b5e71bec" + "13a0673995a584a47674897e959d8c1f83389505895fb64ceda5") + self.doit() + + def test_order_create(self): + self.op = operations.Limit_order_create( + **{ + "owner": "", + "orderid": 0, + "amount_to_sell": "0.000 BEX", + "min_to_receive": "0.000 BEX", + "fill_or_kill": False, + "expiration": "2016-12-31T23:59:59", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c8045701050000000000000000000000000003535" + "445454d0000000000000000000003535445454d0000007f46685800" + "011f28a2fc52dcfc19378c5977917b158dfab93e7760259aab7ecdb" + "cb82df7b22e1a5527e02fd3aab7d64302ec550c3edcbba29d73226c" + "f088273e4fafda89eb7de8") + self.doit() + + def test_order_create2(self): + self.op = operations.Limit_order_create2( + **{ + "owner": "alice", + "orderid": 492991, + "amount_to_sell": {"amount": "1", "precision": 3, "nai": "@@000000013"}, + "exchange_rate": { + "base": {"amount": "1", "precision": 3, "nai": "@@000000013"}, + "quote": {"amount": "10", "precision": 3, "nai": "@@000000021"} + }, + "fill_or_kill": False, + "expiration": "2017-05-12T23:11:13", + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011505616c696365bf850700010000000000000' + '0035342440000000000010000000000000003534244000000000a000000' + '0000000003535445454d00001141165900011f20173e93afb4b7087c768' + '366e2e1f0e7627304ba3ccab0479d03612e847451012036d20cd2074efb' + 'ceaed6b8bf8336ace08ae7d9949d6e070492bb964123ab77') + self.doit() + + def test_account_update(self): + self.op = operations.Account_update( + **{ + "account": + "streemian", + "posting": { + "weight_threshold": + 1, + "account_auths": [["xeroc", 1], ["fabian", 1]], + "key_auths": [[ + "DWB6KChDK2sns9MwugxkoRvPEnyju" + "TxHN5upGsZ1EtanCffqBVVX3", 1 + ], [ + "DWB7sw22HqsXbz7D2CmJfmMwt9ri" + "mtk518dRzsR1f8Cgw52dQR1pR", 1 + ]] + }, + "owner": { + "weight_threshold": + 1, + "account_auths": [], + "key_auths": [[ + "DWB7sw22HqsXbz7D2CmJfmMwt9r" + "imtk518dRzsR1f8Cgw52dQR1pR", 1 + ], [ + "DWB6KChDK2sns9MwugxkoRvPEn" + "yjuTxHN5upGsZ1EtanCffqBVVX3", 1 + ]] + }, + "active": { + "weight_threshold": + 2, + "account_auths": [], + "key_auths": [[ + "DWB6KChDK2sns9MwugxkoRvPEnyju" + "TxHN5upGsZ1EtanCffqBVVX3", 1 + ], [ + "DWB7sw22HqsXbz7D2CmJfmMwt9ri" + "mtk518dRzsR1f8Cgw52dQR1pR", 1 + ]] + }, + "memo_key": + "DWB728uLvStTeAkYJsQefks3FX8yfmpFHp8wXw3RY3kwey2JGDooR", + "json_metadata": + "", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c80457010a0973747265656d69616e01010000" + "00000202bbcf38855c9ae9d55704ee50ff56552af1242266c105" + "44a75b61005e17fa78a601000389d28937022880a7f0c7deaa6f" + "46b4d87ce08bd5149335cb39b5a8e9b04981c201000102000000" + "000202bbcf38855c9ae9d55704ee50ff56552af1242266c10544" + "a75b61005e17fa78a601000389d28937022880a7f0c7deaa6f46" + "b4d87ce08bd5149335cb39b5a8e9b04981c20100010100000002" + "0666616269616e0100057865726f6301000202bbcf38855c9ae9" + "d55704ee50ff56552af1242266c10544a75b61005e17fa78a601" + "000389d28937022880a7f0c7deaa6f46b4d87ce08bd5149335cb" + "39b5a8e9b04981c201000318c1ae46b3e98b26684c87737a04ec" + "b1a390efdc7671ced448a92b745372deff000001206a8896c0ce" + "0c949d901c44232694252348004cf9a74ec2f391c0e0b7a4108e" + "7f71522c186a92c17e23a07cdb108a745b9760316daf16f20434" + "53fbeccb331067") + self.doit() + + def test_order_cancel(self): + self.op = operations.Limit_order_cancel(**{ + "owner": "", + "orderid": 2141244, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570106003cac20000001206c9888d0c2c3" + "1dba1302566f524dfac01a15760b93a8726241a7ae6ba00edfd" + "e5b83edaf94a4bd35c2957ded6023576dcbe936338fb9d340e2" + "1b5dad6f0028f6") + self.doit() + + def test_set_route(self): + self.op = operations.Set_withdraw_vesting_route( + **{ + "from_account": "xeroc", + "to_account": "xeroc", + "percent": 1000, + "auto_vest": False, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570114057865726f63057865726f63e803" + "0000011f12d2b8f93f9528f31979e0e1f59a6d45346a88c02ab2" + "c4115b10c9e273fc1e99621af0c2188598c84762b7e99ca63f6b" + "6be6fca318dd85b0d7a4f09f95579290") + self.doit() + + def test_convert(self): + self.op = operations.Convert(**{ + "owner": "xeroc", + "requestid": 2342343235, + "amount": "100.000 BBD", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570108057865726f6343529d8ba0860100000" + "00000035342440000000000011f3d22eb66e5cddcc90f5d6ca0bd7a" + "43e0ab811ecd480022af8a847c45eac720b342188d55643d8cb1711" + "f516e9879be2fa7dfa329b518f19df4afaaf4f41f7715") + self.doit() + + def test_utf8tests(self): + self.op = operations.Comment( + **{ + "parent_author": "", + "parent_permlink": "", + "author": "a", + "permlink": "a", + "title": "-", + "body": "".join([chr(i) for i in range(0, 2048)]), + "json_metadata": {}, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570101000001610161012dec1f75303030307" + "5303030317530303032753030303375303030347530303035753030" + "3036753030303762090a7530303062660d753030306575303030667" + "5303031307530303131753030313275303031337530303134753030" + "3135753030313675303031377530303138753030313975303031617" + "5303031627530303163753030316475303031657530303166202122" + "232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3" + "e3f404142434445464748494a4b4c4d4e4f50515253545556575859" + "5a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273747" + "5767778797a7b7c7d7e7fc280c281c282c283c284c285c286c287c2" + "88c289c28ac28bc28cc28dc28ec28fc290c291c292c293c294c295c" + "296c297c298c299c29ac29bc29cc29dc29ec29fc2a0c2a1c2a2c2a3" + "c2a4c2a5c2a6c2a7c2a8c2a9c2aac2abc2acc2adc2aec2afc2b0c2b" + "1c2b2c2b3c2b4c2b5c2b6c2b7c2b8c2b9c2bac2bbc2bcc2bdc2bec2" + "bfc380c381c382c383c384c385c386c387c388c389c38ac38bc38cc" + "38dc38ec38fc390c391c392c393c394c395c396c397c398c399c39a" + "c39bc39cc39dc39ec39fc3a0c3a1c3a2c3a3c3a4c3a5c3a6c3a7c3a" + "8c3a9c3aac3abc3acc3adc3aec3afc3b0c3b1c3b2c3b3c3b4c3b5c3" + "b6c3b7c3b8c3b9c3bac3bbc3bcc3bdc3bec3bfc480c481c482c483c" + "484c485c486c487c488c489c48ac48bc48cc48dc48ec48fc490c491" + "c492c493c494c495c496c497c498c499c49ac49bc49cc49dc49ec49" + "fc4a0c4a1c4a2c4a3c4a4c4a5c4a6c4a7c4a8c4a9c4aac4abc4acc4" + "adc4aec4afc4b0c4b1c4b2c4b3c4b4c4b5c4b6c4b7c4b8c4b9c4bac" + "4bbc4bcc4bdc4bec4bfc580c581c582c583c584c585c586c587c588" + "c589c58ac58bc58cc58dc58ec58fc590c591c592c593c594c595c59" + "6c597c598c599c59ac59bc59cc59dc59ec59fc5a0c5a1c5a2c5a3c5" + "a4c5a5c5a6c5a7c5a8c5a9c5aac5abc5acc5adc5aec5afc5b0c5b1c" + "5b2c5b3c5b4c5b5c5b6c5b7c5b8c5b9c5bac5bbc5bcc5bdc5bec5bf" + "c680c681c682c683c684c685c686c687c688c689c68ac68bc68cc68" + "dc68ec68fc690c691c692c693c694c695c696c697c698c699c69ac6" + "9bc69cc69dc69ec69fc6a0c6a1c6a2c6a3c6a4c6a5c6a6c6a7c6a8c" + "6a9c6aac6abc6acc6adc6aec6afc6b0c6b1c6b2c6b3c6b4c6b5c6b6" + "c6b7c6b8c6b9c6bac6bbc6bcc6bdc6bec6bfc780c781c782c783c78" + "4c785c786c787c788c789c78ac78bc78cc78dc78ec78fc790c791c7" + "92c793c794c795c796c797c798c799c79ac79bc79cc79dc79ec79fc" + "7a0c7a1c7a2c7a3c7a4c7a5c7a6c7a7c7a8c7a9c7aac7abc7acc7ad" + "c7aec7afc7b0c7b1c7b2c7b3c7b4c7b5c7b6c7b7c7b8c7b9c7bac7b" + "bc7bcc7bdc7bec7bfc880c881c882c883c884c885c886c887c888c8" + "89c88ac88bc88cc88dc88ec88fc890c891c892c893c894c895c896c" + "897c898c899c89ac89bc89cc89dc89ec89fc8a0c8a1c8a2c8a3c8a4" + "c8a5c8a6c8a7c8a8c8a9c8aac8abc8acc8adc8aec8afc8b0c8b1c8b" + "2c8b3c8b4c8b5c8b6c8b7c8b8c8b9c8bac8bbc8bcc8bdc8bec8bfc9" + "80c981c982c983c984c985c986c987c988c989c98ac98bc98cc98dc" + "98ec98fc990c991c992c993c994c995c996c997c998c999c99ac99b" + "c99cc99dc99ec99fc9a0c9a1c9a2c9a3c9a4c9a5c9a6c9a7c9a8c9a" + "9c9aac9abc9acc9adc9aec9afc9b0c9b1c9b2c9b3c9b4c9b5c9b6c9" + "b7c9b8c9b9c9bac9bbc9bcc9bdc9bec9bfca80ca81ca82ca83ca84c" + "a85ca86ca87ca88ca89ca8aca8bca8cca8dca8eca8fca90ca91ca92" + "ca93ca94ca95ca96ca97ca98ca99ca9aca9bca9cca9dca9eca9fcaa" + "0caa1caa2caa3caa4caa5caa6caa7caa8caa9caaacaabcaaccaadca" + "aecaafcab0cab1cab2cab3cab4cab5cab6cab7cab8cab9cabacabbc" + "abccabdcabecabfcb80cb81cb82cb83cb84cb85cb86cb87cb88cb89" + "cb8acb8bcb8ccb8dcb8ecb8fcb90cb91cb92cb93cb94cb95cb96cb9" + "7cb98cb99cb9acb9bcb9ccb9dcb9ecb9fcba0cba1cba2cba3cba4cb" + "a5cba6cba7cba8cba9cbaacbabcbaccbadcbaecbafcbb0cbb1cbb2c" + "bb3cbb4cbb5cbb6cbb7cbb8cbb9cbbacbbbcbbccbbdcbbecbbfcc80" + "cc81cc82cc83cc84cc85cc86cc87cc88cc89cc8acc8bcc8ccc8dcc8" + "ecc8fcc90cc91cc92cc93cc94cc95cc96cc97cc98cc99cc9acc9bcc" + "9ccc9dcc9ecc9fcca0cca1cca2cca3cca4cca5cca6cca7cca8cca9c" + "caaccabccacccadccaeccafccb0ccb1ccb2ccb3ccb4ccb5ccb6ccb7" + "ccb8ccb9ccbaccbbccbcccbdccbeccbfcd80cd81cd82cd83cd84cd8" + "5cd86cd87cd88cd89cd8acd8bcd8ccd8dcd8ecd8fcd90cd91cd92cd" + "93cd94cd95cd96cd97cd98cd99cd9acd9bcd9ccd9dcd9ecd9fcda0c" + "da1cda2cda3cda4cda5cda6cda7cda8cda9cdaacdabcdaccdadcdae" + "cdafcdb0cdb1cdb2cdb3cdb4cdb5cdb6cdb7cdb8cdb9cdbacdbbcdb" + "ccdbdcdbecdbfce80ce81ce82ce83ce84ce85ce86ce87ce88ce89ce" + "8ace8bce8cce8dce8ece8fce90ce91ce92ce93ce94ce95ce96ce97c" + "e98ce99ce9ace9bce9cce9dce9ece9fcea0cea1cea2cea3cea4cea5" + "cea6cea7cea8cea9ceaaceabceacceadceaeceafceb0ceb1ceb2ceb" + "3ceb4ceb5ceb6ceb7ceb8ceb9cebacebbcebccebdcebecebfcf80cf" + "81cf82cf83cf84cf85cf86cf87cf88cf89cf8acf8bcf8ccf8dcf8ec" + "f8fcf90cf91cf92cf93cf94cf95cf96cf97cf98cf99cf9acf9bcf9c" + "cf9dcf9ecf9fcfa0cfa1cfa2cfa3cfa4cfa5cfa6cfa7cfa8cfa9cfa" + "acfabcfaccfadcfaecfafcfb0cfb1cfb2cfb3cfb4cfb5cfb6cfb7cf" + "b8cfb9cfbacfbbcfbccfbdcfbecfbfd080d081d082d083d084d085d" + "086d087d088d089d08ad08bd08cd08dd08ed08fd090d091d092d093" + "d094d095d096d097d098d099d09ad09bd09cd09dd09ed09fd0a0d0a" + "1d0a2d0a3d0a4d0a5d0a6d0a7d0a8d0a9d0aad0abd0acd0add0aed0" + "afd0b0d0b1d0b2d0b3d0b4d0b5d0b6d0b7d0b8d0b9d0bad0bbd0bcd" + "0bdd0bed0bfd180d181d182d183d184d185d186d187d188d189d18a" + "d18bd18cd18dd18ed18fd190d191d192d193d194d195d196d197d19" + "8d199d19ad19bd19cd19dd19ed19fd1a0d1a1d1a2d1a3d1a4d1a5d1" + "a6d1a7d1a8d1a9d1aad1abd1acd1add1aed1afd1b0d1b1d1b2d1b3d" + "1b4d1b5d1b6d1b7d1b8d1b9d1bad1bbd1bcd1bdd1bed1bfd280d281" + "d282d283d284d285d286d287d288d289d28ad28bd28cd28dd28ed28" + "fd290d291d292d293d294d295d296d297d298d299d29ad29bd29cd2" + "9dd29ed29fd2a0d2a1d2a2d2a3d2a4d2a5d2a6d2a7d2a8d2a9d2aad" + "2abd2acd2add2aed2afd2b0d2b1d2b2d2b3d2b4d2b5d2b6d2b7d2b8" + "d2b9d2bad2bbd2bcd2bdd2bed2bfd380d381d382d383d384d385d38" + "6d387d388d389d38ad38bd38cd38dd38ed38fd390d391d392d393d3" + "94d395d396d397d398d399d39ad39bd39cd39dd39ed39fd3a0d3a1d" + "3a2d3a3d3a4d3a5d3a6d3a7d3a8d3a9d3aad3abd3acd3add3aed3af" + "d3b0d3b1d3b2d3b3d3b4d3b5d3b6d3b7d3b8d3b9d3bad3bbd3bcd3b" + "dd3bed3bfd480d481d482d483d484d485d486d487d488d489d48ad4" + "8bd48cd48dd48ed48fd490d491d492d493d494d495d496d497d498d" + "499d49ad49bd49cd49dd49ed49fd4a0d4a1d4a2d4a3d4a4d4a5d4a6" + "d4a7d4a8d4a9d4aad4abd4acd4add4aed4afd4b0d4b1d4b2d4b3d4b" + "4d4b5d4b6d4b7d4b8d4b9d4bad4bbd4bcd4bdd4bed4bfd580d581d5" + "82d583d584d585d586d587d588d589d58ad58bd58cd58dd58ed58fd" + "590d591d592d593d594d595d596d597d598d599d59ad59bd59cd59d" + "d59ed59fd5a0d5a1d5a2d5a3d5a4d5a5d5a6d5a7d5a8d5a9d5aad5a" + "bd5acd5add5aed5afd5b0d5b1d5b2d5b3d5b4d5b5d5b6d5b7d5b8d5" + "b9d5bad5bbd5bcd5bdd5bed5bfd680d681d682d683d684d685d686d" + "687d688d689d68ad68bd68cd68dd68ed68fd690d691d692d693d694" + "d695d696d697d698d699d69ad69bd69cd69dd69ed69fd6a0d6a1d6a" + "2d6a3d6a4d6a5d6a6d6a7d6a8d6a9d6aad6abd6acd6add6aed6afd6" + "b0d6b1d6b2d6b3d6b4d6b5d6b6d6b7d6b8d6b9d6bad6bbd6bcd6bdd" + "6bed6bfd780d781d782d783d784d785d786d787d788d789d78ad78b" + "d78cd78dd78ed78fd790d791d792d793d794d795d796d797d798d79" + "9d79ad79bd79cd79dd79ed79fd7a0d7a1d7a2d7a3d7a4d7a5d7a6d7" + "a7d7a8d7a9d7aad7abd7acd7add7aed7afd7b0d7b1d7b2d7b3d7b4d" + "7b5d7b6d7b7d7b8d7b9d7bad7bbd7bcd7bdd7bed7bfd880d881d882" + "d883d884d885d886d887d888d889d88ad88bd88cd88dd88ed88fd89" + "0d891d892d893d894d895d896d897d898d899d89ad89bd89cd89dd8" + "9ed89fd8a0d8a1d8a2d8a3d8a4d8a5d8a6d8a7d8a8d8a9d8aad8abd" + "8acd8add8aed8afd8b0d8b1d8b2d8b3d8b4d8b5d8b6d8b7d8b8d8b9" + "d8bad8bbd8bcd8bdd8bed8bfd980d981d982d983d984d985d986d98" + "7d988d989d98ad98bd98cd98dd98ed98fd990d991d992d993d994d9" + "95d996d997d998d999d99ad99bd99cd99dd99ed99fd9a0d9a1d9a2d" + "9a3d9a4d9a5d9a6d9a7d9a8d9a9d9aad9abd9acd9add9aed9afd9b0" + "d9b1d9b2d9b3d9b4d9b5d9b6d9b7d9b8d9b9d9bad9bbd9bcd9bdd9b" + "ed9bfda80da81da82da83da84da85da86da87da88da89da8ada8bda" + "8cda8dda8eda8fda90da91da92da93da94da95da96da97da98da99d" + "a9ada9bda9cda9dda9eda9fdaa0daa1daa2daa3daa4daa5daa6daa7" + "daa8daa9daaadaabdaacdaaddaaedaafdab0dab1dab2dab3dab4dab" + "5dab6dab7dab8dab9dabadabbdabcdabddabedabfdb80db81db82db" + "83db84db85db86db87db88db89db8adb8bdb8cdb8ddb8edb8fdb90d" + "b91db92db93db94db95db96db97db98db99db9adb9bdb9cdb9ddb9e" + "db9fdba0dba1dba2dba3dba4dba5dba6dba7dba8dba9dbaadbabdba" + "cdbaddbaedbafdbb0dbb1dbb2dbb3dbb4dbb5dbb6dbb7dbb8dbb9db" + "badbbbdbbcdbbddbbedbbfdc80dc81dc82dc83dc84dc85dc86dc87d" + "c88dc89dc8adc8bdc8cdc8ddc8edc8fdc90dc91dc92dc93dc94dc95" + "dc96dc97dc98dc99dc9adc9bdc9cdc9ddc9edc9fdca0dca1dca2dca" + "3dca4dca5dca6dca7dca8dca9dcaadcabdcacdcaddcaedcafdcb0dc" + "b1dcb2dcb3dcb4dcb5dcb6dcb7dcb8dcb9dcbadcbbdcbcdcbddcbed" + "cbfdd80dd81dd82dd83dd84dd85dd86dd87dd88dd89dd8add8bdd8c" + "dd8ddd8edd8fdd90dd91dd92dd93dd94dd95dd96dd97dd98dd99dd9" + "add9bdd9cdd9ddd9edd9fdda0dda1dda2dda3dda4dda5dda6dda7dd" + "a8dda9ddaaddabddacddadddaeddafddb0ddb1ddb2ddb3ddb4ddb5d" + "db6ddb7ddb8ddb9ddbaddbbddbcddbdddbeddbfde80de81de82de83" + "de84de85de86de87de88de89de8ade8bde8cde8dde8ede8fde90de9" + "1de92de93de94de95de96de97de98de99de9ade9bde9cde9dde9ede" + "9fdea0dea1dea2dea3dea4dea5dea6dea7dea8dea9deaadeabdeacd" + "eaddeaedeafdeb0deb1deb2deb3deb4deb5deb6deb7deb8deb9deba" + "debbdebcdebddebedebfdf80df81df82df83df84df85df86df87df8" + "8df89df8adf8bdf8cdf8ddf8edf8fdf90df91df92df93df94df95df" + "96df97df98df99df9adf9bdf9cdf9ddf9edf9fdfa0dfa1dfa2dfa3d" + "fa4dfa5dfa6dfa7dfa8dfa9dfaadfabdfacdfaddfaedfafdfb0dfb1" + "dfb2dfb3dfb4dfb5dfb6dfb7dfb8dfb9dfbadfbbdfbcdfbddfbedfb" + "f0000011f45c8e1ed9289f5ec7d4f6d7ce891a30ede7470e28d4639" + "8e0dc15c41c784b1862f132378382230d68b59e3592e72a32f310f8" + "8ea4baddb361a3709b664ba7375") + self.doit() + + def test_feed_publish(self): + self.op = operations.Feed_publish( + **{ + "publisher": "xeroc", + "exchange_rate": { + "base": "1.000 BBD", + "quote": "4.123 BEX" + }, + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570107057865726f63e803000000000" + "00003534244000000001b1000000000000003535445454d00" + "000001203847a02aa76964cacfb41565c23286cc64b18f6bb" + "9260832823839b3b90dff18738e1b686ad22f79c42fca73e6" + "1bf633505a2a66cac65555b0ac535ca5ee5a61") + self.doit() + + def test_delete_comment(self): + self.op = operations.Delete_comment( + **{ + "author": "turbot", + "permlink": "testpost", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c80457011106747572626f740874657374706f73" + "7400011f0d413176d24455d6d9b5b9416384fcf63f080a70d8b243" + "c579f996ce8c116ce0583b433d4ce9107438b72d39eb6195027880" + "54b97abc20bf86b17a11d3eb8351") + self.doit() + + def test_witness_update(self): + self.op = operations.Witness_update( + **{ + "owner": + "xeroc", + "url": + "foooobar", + "block_signing_key": + "DWB6zLNtyFVToBsBZDsgMhgjpwysYVbsQD6YhP3kRkQhANUB4w7Qp", + "props": { + "account_creation_fee": "10.000 BEX", + "maximum_block_size": 1111111, + "bbd_interest_rate": 1000 + }, + "fee": + "10.000 BEX", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c80457010b057865726f6308666f6f6f6f6261" + "720314aa202c9158990b3ec51a1aa49b2ab5d300c97b391df3be" + "b34bb74f3c62699e102700000000000003535445454d000047f4" + "1000e803102700000000000003535445454d00000001206adca4" + "bebc872e8d792caeb3b729e9a5e8af90c07ab3f744fb4d0f19d5" + "7b3bec32f5a43f5acdfc065f0227e45e599745c46e41c023d69f" + "b9f2405478badadb4c") + self.doit() + + def test_witness_set_properties(self): + self.op = operations.Witness_set_properties( + **{ + "owner": "init-1", + "props": [ + ["key", "032d2a4af3e23294e0a1d9dbc46e0272d8e1977ce2ae3349527cc90fe1cc9c5db9"], + ["account_creation_fee", "d0070000000000000354455354530000"] + ], + "prefix": default_prefix + }) + + self.cm = ('f68585abf4dce7c80457012a06696e69742d3102146163636f756e745f63726561746' + '96f6e5f66656510d0070000000000000354455354530000036b657921032d2a4af3e2' + '3294e0a1d9dbc46e0272d8e1977ce2ae3349527cc90fe1cc9c5db9000001203f74f25' + '4cff2d6d8c0c8798ee0c73fa93e32f97ccdd5cc42fa0f0d24c48a9fe218d5489dd16b' + 'd538409e11bea4ba92230ac8df0e5d2b2f0e3f8bf613b93b7a9c') + self.doit() + + def test_witness_vote(self): + self.op = operations.Account_witness_vote(**{ + "account": "xeroc", + "witness": "chainsquad", + "approve": True, + "prefix": default_prefix, + }) + + self.cm = (u"f68585abf4dce7c80457010c057865726f630a636" + "861696e73717561640100011f16b43411e11f4739" + "4c1624a3c4d3cf4daba700b8690f494e6add7ad9b" + "ac735ce7775d823aa66c160878cb3348e6857c531" + "114d229be0202dc0857f8f03a00369") + self.doit() + + def test_witness_proxy(self): + self.op = operations.Account_witness_proxy(**{ + "account": "alice", + "proxy": "bob", + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457010d05616c69636503626f6' + '20001202b1c0a06e514fd39d52138b08fe7e6736592' + '5891b0ed0839686ed900adf33aa9281e95ee1e50184' + 'a3b41b7acab0456c83243ebdccfd17e10e7ab94e09fb' + '96309') + self.doit() + + def test_custom(self): + self.op = operations.Custom( + **{ + "required_auths": ["bytemaster"], + "id": 777, + "data": '0a627974656d617374657207737465656d697402a3d1389' + '7d82114466ad87a74b73a53292d8331d1bd1d3082da6bfbc' + 'ff19ed097029db013797711c88cccca3692407f9ff9b9ce72' + '21aaa2d797f1692be2215d0a5f6d2a8cab6832050078bc5729' + '201e3ea24ea9f7873e6dbdc65a6bd9899053b9acda876dc69f1' + '1a13df9ca8b26b6', + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457010f010a627974656d6173746572090384023' + '061363237393734363536643631373337343635373230373733373436' + '353635366436393734303261336431333839376438323131343436366' + '164383761373462373361353332393264383333316431626431643330' + '383264613662666263666631396564303937303239646230313337393' + '737313163383863636363613336393234303766396666396239636537' + '323231616161326437393766313639326265323231356430613566366' + '432613863616236383332303530303738626335373239323031653365' + '613234656139663738373365366462646336356136626439383939303' + '533623961636461383736646336396631316131336466396361386232' + '3662360001203f60bbd0d7595c1e5cf7a6314a14117a94898121b28f1' + '5992820e0fae14188ac0d352b7d96aba83ea808329afedddc1e3d7c43' + '85d23fb9331a68795ba9e7ed08') + self.doit() + + def test_custom_json(self): + self.op = operations.Custom_json( + **{ + "json": [ + "reblog", + OrderedDict( + [ # need an ordered dict to keep order for the test + ("account", "xeroc"), ("author", "chainsquad"), ( + "permlink", "streemian-com-to-open-its-doors-" + "and-offer-a-20-discount") + ]) + ], + "required_auths": [], + "required_posting_auths": ["xeroc"], + "id": + "follow", + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c8045701120001057865726f6306666f6c6c" + "6f777f5b227265626c6f67222c207b226163636f756e74223a" + "20227865726f63222c2022617574686f72223a202263686169" + "6e7371756164222c20227065726d6c696e6b223a2022737472" + "65656d69616e2d636f6d2d746f2d6f70656e2d6974732d646f" + "6f72732d616e642d6f666665722d612d32302d646973636f75" + "6e74227d5d00011f0cffad16cfd8ea4b84c06d412e93a9fc10" + "0bf2fac5f9a40d37d5773deef048217db79cabbf15ef29452d" + "e4ed1c5face51d998348188d66eb9fc1ccef79a0c0d4") + self.doit() + + def test_comment_options(self): + self.op = operations.Comment_options( + **{ + "author": + "xeroc", + "permlink": + "piston", + "max_accepted_payout": + "1000000.000 BBD", + "percent_dpay_dollars": + 10000, + "allow_votes": + True, + "allow_curation_rewards": + True, + "beneficiaries": [{ + "weight": 2000, + "account": "good-karma" + }, { + "weight": 5000, + "account": "null" + }], + "prefix": default_prefix + }) + + self.cm = (u"f68585abf4dce7c804570113057865726f6306706973746f6e" + "00ca9a3b000000000353424400000000102701010100020a67" + "6f6f642d6b61726d61d007046e756c6c881300011f59634e65" + "55fec7c01cb7d4921601c37c250c6746022cc35eaefdd90405" + "d7771b2f65b44e97b7f3159a6d52cb20640502d2503437215f" + "0907b2e2213940f34f2c") + self.doit() + + def test_change_recovery_account(self): + self.op = operations.Change_recovery_account( + **{ + "account_to_recover": "barrie", + "extensions": [], + "new_recovery_account": "boombastic", + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011a066261727269650a626f6f6d6261' + '737469630000011f5513c021e89be2c4d8a725dc9b89334ffcde' + '6a9535487f4b6d42f93de1722c251fd9d4ec414335dc41ea081a' + 'a7a4d27b179b1a07d3415f7e0a6190852b86ecde') + self.doit() + + def test_request_account_recovery(self): + self.op = operations.Request_account_recovery( + **{ + "recovery_account": "whitehorse", + "account_to_recover": "alice", + "new_owner_authority": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB6LYxj96zdypHYqgDdD6Nyh2NxerN3P1Mp3ddNm7gci63nfrSuZ", + 1 + ] + ] + }, + "extensions": [], + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011805737465656d05616c69636501000000000102' + 'bedf9f3cf1655dc635d2b7fd43e1d0ab6eafbc443f6fd013de0cb2e8a11fbc' + '8901000000011f7a23a45fe0d5824201cf4fad0bcc5c3564156c0d4217df10' + 'acd544786e3f407d38b149afc41387090ef95b719bc5264954011ddf21e861' + 'be37f29998286e55e5') + self.doit() + + def test_recover_account(self): + self.op = operations.Recover_account( + **{ + "account_to_recover": "alice", + "new_owner_authority": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB7j3nhkhHTpXqLEvdx2yEGhQeeorTcxSV6WDL2DZGxwUxYGrHvh", + 1 + ] + ] + }, + "recent_owner_authority": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB78Xth94gNxp8nmByFV2vNAhg9bsSdviJ6fQXUTFikySLK3uTxC", + 1 + ] + ] + }, + "extensions": [], + "prefix": default_prefix, + }) + self.cm = ('f68585abf4dce7c80457011905616c6963650100000000010375a6dfff76' + '49647cc074d86386862087a154088d952da64af9dac429559a8d0c010001' + '0000000001032747c05d4aa44704a196493e1fa0c26137abfca64991a9b9' + 'faeb9204ee9897ef01000000011f31e1794992a1573c3d2909cd10405d0f' + 'ea3c5dd77aac04391af76bd51becee7360775fa79f23e898ceebaae74e8c' + 'a079ba7a6bef2c359bb349033dff9a0ccc26') + + self.doit() + + def test_escrow_transfer(self): + self.op = operations.Escrow_transfer( + **{ + "from": "alice", + "to": "bob", + "bbd_amount": {"amount": "1000", "precision": 3, "nai": "@@000000013"}, + "dpay_amount": {"amount": "0", "precision": 3, "nai": "@@000000021"}, + "escrow_id": 23456789, + "agent": "charlie", + "fee": {"amount": "100", "precision": 3, "nai": "@@000000013"}, + "json_meta": "{}", + "ratification_deadline": "2017-02-26T11:22:39", + "escrow_expiration": "2017-02-28T11:22:39", + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011b05616c69636503626f6207636861726c696' + '515ec6501e8030000000000000353424400000000000000000000000003' + '535445454d0000640000000000000003534244000000007fbab2587f5db' + '558027b7d00011f2b07b6e67afda26064932fa5266f5e7f39ce4ac77662' + 'ba30e64c2b930d4744313bcc5959d8dd53fc614c4274a72c5df5c9106a7' + '4b65f9c5eeec92789d47996e0') + self.doit() + + def test_escrow_dispute(self): + self.op = operations.Escrow_dispute( + **{ + "from": "alice", + "to": "bob", + "who": "alice", + "escrow_id": 72526562, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011c05616c69636503626f6205616c696365e2' + 'aa520400012009742d35b45cbd9b9612429dda6873ac1d8bab66066e66' + '01858cde0adf55cfb445d6a8be7a400a594e0d4e603af932a6b610087a' + '30129b8682631ca9d6571714') + self.doit() + + def test_escrow_release(self): + self.op = operations.Escrow_release( + **{ + "from": "alice", + "to": "bob", + "who": "charlie", + "escrow_id": 72526562, + "bbd_amount": {"amount": "5000", "precision": 3, "nai": "@@000000013"}, + "dpay_amount": {"amount": "0", "precision": 3, "nai": "@@000000021"}, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011d05616c69636503626f6207636861726c6965' + 'e2aa52048813000000000000035342440000000000000000000000000353' + '5445454d000000011f12b07aefeb0585ba49ab267c310befe30aeab65c53' + '86057277bcca71901eec367081b35d919e23ec54e311e266722299185ec6' + '2dd10571874c935cc9bec6d350') + self.doit() + + def test_escrow_approve(self): + self.op = operations.Escrow_approve( + **{ + "from": "alice", + "to": "bob", + "agent": "charlie", + "who": "charlie", + "escrow_id": 59102208, + "approve": True, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457011f05616c69636503626f6207636861726c69' + '6507636861726c696500d485030100012054baed32863a307ed68c3af8' + '6f42608ef35c24c3a82c12d9baaa854345f4812a048fdc819ec7e01b0f' + '28f6eb8357de1cb8127df29b255fa78fa1bdd267e7d5fc') + self.doit() + + def test_decline_voting_rights(self): + self.op = operations.Decline_voting_rights( + **{ + "account": "judy", + "decline": True, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c804570124046a7564790100011f5bedbccb7042c5ab' + '8d6debf579bca485a4cc8f95c64515d1885339c416a64d9f26fac5f01c' + 'c3f248297cb3af921103777638bb727f5ef9bd3c3f66953167ee42') + self.doit() + + def test_claim_reward_balance(self): + self.op = operations.Claim_reward_balance( + **{ + "account": "alice", + "reward_dpay": {"amount": "17", "precision": 3, "nai": "@@000000021"}, + "reward_bbd": {"amount": "11", "precision": 3, "nai": "@@000000013"}, + "reward_vests": {"amount": "185025103", "precision": 6, "nai": "@@000000037"}, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457012705616c696365110000000000000003535445454d00000' + 'b0000000000000003534244000000004f42070b00000000065645535453000000011f' + '3eedc4af6e853ff473ef90c4ca8fc039415e6f35fbc7246d0a3b6a584dcc20ac33185' + '3ddd5e3610a4ac02bba668b5ccc8ee7a3977081627ca4fbff6d43c87f3c') + self.doit() + + def test_delegate_vesting_shares(self): + self.op = operations.Delegate_vesting_shares( + **{ + "delegator": "alice", + "delegatee": "bob", + "vesting_shares": {"amount": "94599167138276", "precision": 6, "nai": "@@000000037"}, + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c80457012805616c69636503626f62e4d9c095095600000656455354530000000' + '12044bd813d30bbd4f4e0749d23d275ba12de2a6b6c6c91ef570c5a7f4f1d444c590f17016b02b8' + 'b36f77aa63fa0d8278fc07472c5d20927d2b7fdebcc3820da489') + self.doit() + + def test_account_create_with_delegation(self): + self.op = operations.Account_create_with_delegation( + **{ + "fee": {"amount": "3000", "precision": 3, "nai": "@@000000021"}, + "delegation": {"amount": "0", "precision": 6, "nai": "@@000000037"}, + "creator": "dsite", + "new_account_name": "alice", + "owner": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB5Tki3ecCdCCHCjhhwvQvXuKryL2s34Ma6CXsRzntSUTYVYxCQ9", + 1 + ] + ] + }, + "active": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB6LUoAA8gCL9tHRz7v9xcwR4ZWD3KDRHP5t1U7UAZHdfanLxyBE", + 1 + ] + ] + }, + "posting": { + "weight_threshold": 1, + "account_auths": [], + "key_auths": [ + [ + "DWB8anmpHdfVE4AmwsDpcSXpRsydHysEbv6vGJkRQy1d1CC83zeTA", + 1 + ] + ] + }, + "memo_key": "DWB67RYDyEkP1Ja1jFehJ45BFGA9oHHUnRnYbxKJEtMhVQiHW3S3k", + "json_metadata": "{}", + "extensions": [], + "prefix": default_prefix, + }) + + self.cm = ('f68585abf4dce7c804570129b80b00000000000003535445454d00000000000' + '000000000065645535453000007737465656d697405616c6963650100000000' + '01024b881ad188574738041f9ad6621f5fd784d51ce19e8379285d800aff65e' + 'f72db010001000000000102beb5e1610bde9c7c7f91d181356653981a60de0c' + '5753f88bc31b29e6853866c2010001000000000103e6985b4e8884a4a225030' + 'a8521c688a1c7c4ef2fcfc0398e1a7024b0544e6c7b010002a1109a3fdce014' + 'bb6a720432f74e38731b420cbd2ac6137de140904b142da2d4027b7d0000012' + '00545101c8bcbdb3c4d5029ed2b160ccb33e6b3c3d96949ff528d8fdc30935b' + '065bfc937f01fdb8328bd693fda9687335bd341c8ffce2f7c93bcb98acf1382' + 'd93') + self.doit() + + +""" + def test_limit_order_create(self): + self.op = operations.Limit_order_create(**{ + "fee": {"amount": 100, + "asset_id": "1.3.0" + }, + "seller": "1.2.29", + "amount_to_sell": {"amount": 100000, + "asset_id": "BBD" + }, + "min_to_receive": {"amount": 10000, + "asset_id": "BBD" + }, + "expiration": "2016-05-18T09:22:05", + "fill_or_kill": False, + "extensions": [] + }) + self.cm = ("f68585abf4dce7c8045701016400000000000000001da08601000" + "0000000001027000000000000693d343c57000000011f75cbfd49" + "ae8d9b04af76cc0a7de8b6e30b71167db7fe8e2197ef9d858df18" + "77043493bc24ffdaaffe592357831c978fd8a296b913979f106de" + "be940d60d77b50") + self.doit() + + def test_transfer(self): + pub = format(account.PrivateKey(wif).pubkey, prefix) + from_account_id = "test" + to_account_id = "test1" + amount = 1000000 + asset_id = "BBD" + message = "abcdefgABCDEFG0123456789" + nonce = "5862723643998573708" + + fee = objects.Asset(amount=0, asset_id="BBD") + amount = objects.Asset(amount=int(amount), asset_id=asset_id) + encrypted_memo = memo.encode_memo( + account.PrivateKey(wif), + account.PublicKey(pub, prefix=prefix), + nonce, + message + ) + memoStruct = { + "from": pub, + "to": pub, + "nonce": nonce, + "message": encrypted_memo, + } + memoObj = objects.Memo(**memoStruct) + self.op = operations.Transfer(**{ + "fee": fee, + "from": from_account_id, + "to": to_account_id, + "amount": amount, + "memo": memoObj, + "prefix": prefix + }) + self.cm = ("f68585abf4dce7c804570100000000000000000000000140420" + "f0000000000040102c0ded2bc1f1305fb0faac5e6c03ee3a192" + "4234985427b6167ca569d13df435cf02c0ded2bc1f1305fb0fa" + "ac5e6c03ee3a1924234985427b6167ca569d13df435cf8c94d1" + "9817945c5120fa5b6e83079a878e499e2e52a76a7739e9de409" + "86a8e3bd8a68ce316cee50b210000011f39e3fa7071b795491e" + "3b6851d61e7c959be92cc7deb5d8491cf1c3c8c99a1eb44553c" + "348fb8f5001a78b18233ac66727e32fc776d48e92d9639d64f6" + "8e641948") + self.doit() + + + def self.cmConstructedTX(self): + self.maxDiff = None + self.op = operations.Bid_collateral(**{ + 'fee': {'amount': 100, + 'asset_id': '1.3.0'}, + 'additional_collateral': { + 'amount': 10000, + 'asset_id': '1.3.22'}, + 'debt_covered': { + 'amount': 100000000, + 'asset_id': '1.3.0'}, + 'bidder': '1.2.29', + 'extensions': [] + }) + ops = [Operation(self.op)] + tx = Signed_Transaction( + ref_block_num=ref_block_num, + ref_block_prefix=ref_block_prefix, + expiration=expiration, + operations=ops + ) + tx = tx.sign([wif], chain=prefix) + tx.verify([PrivateKey(wif).pubkey], prefix) + txWire = hexlify(bytes(tx)).decode("ascii") + print("=" * 80) + pprint(tx.json()) + print("=" * 80) + + from grapheneapi.grapheneapi import GrapheneAPI + rpc = GrapheneAPI("localhost", 8092) + self.cm = rpc.serialize_transaction(tx.json()) + print("soll: %s" % self.cm[:-130]) + print("ist: %s" % txWire[:-130]) + print(txWire[:-130] == self.cm[:-130]) + self.assertEqual(self.cm[:-130], txWire[:-130]) + + +if __name__ == '__main__': + t = Testcases() + t.self.cmConstructedTX() +""" diff --git a/tests/dpaycligraphene/__init__.py b/tests/dpaycligraphene/__init__.py new file mode 100755 index 0000000..e0310a0 --- /dev/null +++ b/tests/dpaycligraphene/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/dpaycligraphene/test_account.py b/tests/dpaycligraphene/test_account.py new file mode 100755 index 0000000..44a01fd --- /dev/null +++ b/tests/dpaycligraphene/test_account.py @@ -0,0 +1,223 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import unittest +from dpaycligraphenebase.base58 import Base58 +from dpaycligraphenebase.account import BrainKey, Address, PublicKey, PrivateKey, PasswordKey + + +class Testcases(unittest.TestCase): + def test_B85hexgetb58_btc(self): + self.assertEqual(["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd", + "5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S", + "5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq", + "5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R", + "5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7", + "02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49", + "5b921f7051be5e13e177a0253229903c40493df410ae04f4a450c85568f19131", + "0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e", + "6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e", + ], + [format(Base58("02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49"), "WIF"), + format(Base58("5b921f7051be5e13e177a0253229903c40493df410ae04f4a450c85568f19131"), "WIF"), + format(Base58("0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e"), "WIF"), + format(Base58("6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e"), "WIF"), + format(Base58("b84abd64d66ee1dd614230ebbe9d9c6d66d78d93927c395196666762e9ad69d8"), "WIF"), + repr(Base58("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd")), + repr(Base58("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S")), + repr(Base58("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq")), + repr(Base58("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R")), + ]) + + def test_B85hexgetb58(self): + self.assertEqual(['BTS2CAbTi1ZcgMJ5otBFZSGZJKJenwGa9NvkLxsrS49Kr8JsiSGc', + 'BTShL45FEyUVSVV1LXABQnh4joS9FsUaffRtsdarB5uZjPsrwMZF', + 'BTS7DQR5GsfVaw4wJXzA3TogDhuQ8tUR2Ggj8pwyNCJXheHehL4Q', + 'BTSqc4QMAJHAkna65i8U4b7nkbWk4VYSWpZebW7JBbD7MN8FB5sc', + 'BTS2QAVTJnJQvLUY4RDrtxzX9jS39gEq8gbqYMWjgMxvsvZTJxDSu' + ], + [format(Base58("02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49"), "BTS"), + format(Base58("5b921f7051be5e13e177a0253229903c40493df410ae04f4a450c85568f19131"), "BTS"), + format(Base58("0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e"), "BTS"), + format(Base58("6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e"), "BTS"), + format(Base58("b84abd64d66ee1dd614230ebbe9d9c6d66d78d93927c395196666762e9ad69d8"), "BTS")]) + + def test_Address(self): + self.assertEqual([format(Address("BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi", prefix="BTS"), "BTS"), + format(Address("BTSdXrrTXimLb6TEt3nHnePwFmBT6Cck112", prefix="BTS"), "BTS"), + format(Address("BTSJQUAt4gz4civ8gSs5srTK4r82F7HvpChk", prefix="BTS"), "BTS"), + format(Address("BTSFPXXHXXGbyTBwdKoJaAPXRnhFNtTRS4EL", prefix="BTS"), "BTS"), + format(Address("BTS3qXyZnjJneeAddgNDYNYXbF7ARZrRv5dr", prefix="BTS"), "BTS"), + ], + ["BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi", + "BTSdXrrTXimLb6TEt3nHnePwFmBT6Cck112", + "BTSJQUAt4gz4civ8gSs5srTK4r82F7HvpChk", + "BTSFPXXHXXGbyTBwdKoJaAPXRnhFNtTRS4EL", + "BTS3qXyZnjJneeAddgNDYNYXbF7ARZrRv5dr", + ]) + + def test_PubKey(self): + self.assertEqual([format(PublicKey("BTS6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL", prefix="BTS").address, "BTS"), + format(PublicKey("BTS8YAMLtNcnqGNd3fx28NP3WoyuqNtzxXpwXTkZjbfe9scBmSyGT", prefix="BTS").address, "BTS"), + format(PublicKey("BTS7HUo6bm7Gfoi3RqAtzwZ83BFCwiCZ4tp37oZjtWxGEBJVzVVGw", prefix="BTS").address, "BTS"), + format(PublicKey("BTS6676cZ9qmqPnWMrm4McjCuHcnt6QW5d8oRJ4t8EDH8DdCjvh4V", prefix="BTS").address, "BTS"), + format(PublicKey("BTS7u8m6zUNuzPNK1tPPLtnipxgqV9mVmTzrFNJ9GvovvSTCkVUra", prefix="BTS").address, "BTS") + ], + ["BTS66FCjYKzMwLbE3a59YpmFqA9bwporT4L3", + "BTSKNpRuPX8KhTBsJoFp1JXd7eQEsnCpRw3k", + "BTS838ENJargbUrxXWuE2xD9HKjQaS17GdCd", + "BTSNsrLFWTziSZASnNJjWafFtGBfSu8VG8KU", + "BTSDjAGuXzk3WXabBEgKKc8NsuQM412boBdR" + ]) + + def test_btsprivkey(self): + self.assertEqual([format(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd").address, "BTS"), + format(PrivateKey("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S").address, "BTS"), + format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").address, "BTS"), + format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").address, "BTS"), + format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").address, "BTS") + ], + ["BTSFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi", + "BTSdXrrTXimLb6TEt3nHnePwFmBT6Cck112", + "BTSJQUAt4gz4civ8gSs5srTK4r82F7HvpChk", + "BTSFPXXHXXGbyTBwdKoJaAPXRnhFNtTRS4EL", + "BTS3qXyZnjJneeAddgNDYNYXbF7ARZrRv5dr", + ]) + + def test_btcprivkey(self): + self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").uncompressed.address, "BTC"), + format(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R").uncompressed.address, "BTC"), + format(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7").uncompressed.address, "BTC"), + ], + ["1G7qw8FiVfHEFrSt3tDi6YgfAdrDrEM44Z", + "12c7KAAZfpREaQZuvjC5EhpoN6si9vekqK", + "1Gu5191CVHmaoU3Zz3prept87jjnpFDrXL", + ]) + + def test_PublicKey(self): + self.assertEqual([str(PublicKey("BTS6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL", prefix="BTS")), + str(PublicKey("BTS8YAMLtNcnqGNd3fx28NP3WoyuqNtzxXpwXTkZjbfe9scBmSyGT", prefix="BTS")), + str(PublicKey("BTS7HUo6bm7Gfoi3RqAtzwZ83BFCwiCZ4tp37oZjtWxGEBJVzVVGw", prefix="BTS")), + str(PublicKey("BTS6676cZ9qmqPnWMrm4McjCuHcnt6QW5d8oRJ4t8EDH8DdCjvh4V", prefix="BTS")), + str(PublicKey("BTS7u8m6zUNuzPNK1tPPLtnipxgqV9mVmTzrFNJ9GvovvSTCkVUra", prefix="BTS")) + ], + ["BTS6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL", + "BTS8YAMLtNcnqGNd3fx28NP3WoyuqNtzxXpwXTkZjbfe9scBmSyGT", + "BTS7HUo6bm7Gfoi3RqAtzwZ83BFCwiCZ4tp37oZjtWxGEBJVzVVGw", + "BTS6676cZ9qmqPnWMrm4McjCuHcnt6QW5d8oRJ4t8EDH8DdCjvh4V", + "BTS7u8m6zUNuzPNK1tPPLtnipxgqV9mVmTzrFNJ9GvovvSTCkVUra" + ]) + + def test_Privatekey(self): + self.assertEqual([str(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq")), + str(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R")), + str(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7")), + repr(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq")), + repr(PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R")), + repr(PrivateKey("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7")), + ], + ["5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq", + "5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R", + "5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7", + '0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e', + '6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e', + 'b84abd64d66ee1dd614230ebbe9d9c6d66d78d93927c395196666762e9ad69d8' + ]) + + def test_BrainKey(self): + self.assertEqual([str(BrainKey("COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO").get_private()), + str(BrainKey("NAK TILTING MOOTING TAVERT SCREENY MAGIC BARDIE UPBORNE CONOID MAUVE CARBON NOTAEUM BITUMEN HOOEY KURUMA COWFISH").get_private()), + str(BrainKey("CORKITE CORDAGE FONDISH UNDER FORGET BEFLEA OUTBUD ZOOGAMY BERLINE ACANTHA STYLO YINCE TROPISM TUNKET FALCULA TOMENT").get_private()), + str(BrainKey("MURZA PREDRAW FIT LARIGOT CRYOGEN SEVENTH LISP UNTAWED AMBER CRETIN KOVIL TEATED OUTGRIN POTTAGY KLAFTER DABB").get_private()), + str(BrainKey("VERDICT REPOUR SUNRAY WAMBLY UNFILM UNCOUS COWMAN REBUOY MIURUS KEACORN BENZOLE BEMAUL SAXTIE DOLENT CHABUK BOUGHED").get_private()), + str(BrainKey("HOUGH TRUMPH SUCKEN EXODY MAMMATE PIGGIN CRIME TEPEE URETHAN TOLUATE BLINDLY CACOEPY SPINOSE COMMIE GRIECE FUNDAL").get_private()), + str(BrainKey("OERSTED ETHERIN TESTIS PEGGLE ONCOST POMME SUBAH FLOODER OLIGIST ACCUSE UNPLAT OATLIKE DEWTRY CYCLIZE PIMLICO CHICOT").get_private()), + ], + ["5JfwDztjHYDDdKnCpjY6cwUQfM4hbtYmSJLjGd9KTpk9J4H2jDZ", + "5JcdQEQjBS92rKqwzQnpBndqieKAMQSiXLhU7SFZoCja5c1JyKM", + "5JsmdqfNXegnM1eA8HyL6uimHp6pS9ba4kwoiWjjvqFC1fY5AeV", + "5J2KeFptc73WTZPoT1Sd59prFep6SobGobCYm7T5ZnBKtuW9RL9", + "5HryThsy6ySbkaiGK12r8kQ21vNdH81T5iifFEZNTe59wfPFvU9", + "5Ji4N7LSSv3MAVkM3Gw2kq8GT5uxZYNaZ3d3y2C4Ex1m7vshjBN", + "5HqSHfckRKmZLqqWW7p2iU18BYvyjxQs2sksRWhXMWXsNEtxPZU", + ]) + + def test_BrainKey_normalize(self): + b = "COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO" + self.assertEqual([BrainKey(b + "").get_brainkey(), + BrainKey(b + " ").get_brainkey(), + BrainKey(b + " ").get_brainkey(), + BrainKey(b + "\t").get_brainkey(), + BrainKey(b + "\t\t").get_brainkey(), + BrainKey(b.replace(" ", "\t")).get_brainkey(), + BrainKey(b.replace(" ", " ")).get_brainkey(), + ], + [b, b, b, b, b, b, b]) + + def test_BrainKey_suggest(self): + b = BrainKey() + self.assertTrue(len(b.suggest()) > 0) + + def test_BrainKey_sequences(self): + b = BrainKey("COLORER BICORN KASBEKE FAERIE LOCHIA GOMUTI SOVKHOZ Y GERMAL AUNTIE PERFUMY TIME FEATURE GANGAN CELEMIN MATZO") + keys = ["5Hsbn6kXio4bb7eW5bX7kTp2sdkmbzP8kGWoau46Cf7en7T1RRE", + "5K9MHEyiSye5iFL2srZu3ZVjzAZjcQxUgUvuttcVrymovFbU4cc", + "5JBXhzDWQdYPAzRxxuGtzqM7ULLKPK7GZmktHTyF9foGGfbtDLT", + "5Kbbfbs6DmJFNddWiP1XZfDKwhm5dkn9KX5AENQfQke2RYBBDcz", + "5JUqLwgxn8f7myNz4gDwo5e77HZgopHMDHv4icNVww9Rxu1GDG5", + "5JNBVj5QVh86N8MUUwY3EVUmsZwChZftxnuJx22DzEtHWC4rmvK", + "5JdvczYtxPPjQdXMki1tpNvuSbvPMxJG5y4ndEAuQsC5RYMQXuC", + "5HsUSesU2YB4EA3dmpGtHh8aPAwEdkdhidG8hcU2Nd2tETKk85t", + "5JpveiQd1mt91APyQwvsCdAXWJ7uag3JmhtSxpGienic8vv1k2W", + "5KDGhQUqQmwcGQ9tegimSyyT4vmH8h2fMzoNe1MT9bEGvRvR6kD"] + for i in keys: + p = b.next_sequence().get_private() + self.assertEqual(str(p), i) + + def test_PasswordKey(self): + a = ["Aang7foN3oz1Ungai2qua5toh3map8ladei1eem2ohsh2shuo8aeji9Thoseo7ah", + "iep1Mees9eghiifahwei5iidi0Sazae9aigaeT7itho3quoo2dah5zuvobaelau5", + "ohBeuyoothae5aer9odaegh5Eeloh1fi7obei9ahSh0haeYuas1sheehaiv5LaiX", + "geiQuoo9NeeLoaZee0ain3Ku1biedohsesien4uHo1eib1ahzaesh5shae3iena7", + "jahzeice6Ix8ohBo3eik9pohjahgeegoh9sahthai1aeMahs8ki7Iub1oojeeSuo", + "eiVahHoh2hi4fazah9Tha8loxeeNgequaquuYee6Shoopo3EiWoosheeX6yohg2o", + "PheeCh3ar8xoofoiphoo4aisahjiiPah4vah0eeceiJ2iyeem9wahyupeithah9T", + "IuyiibahNgieshei2eeFu8aic1IeMae9ooXi9jaiwaht4Wiengieghahnguang0U", + "Ipee1quee7sheughemae4eir8pheix3quac3ei0Aquo9ohieLaeseeh8AhGeM2ew", + "Tech5iir0aP6waiMeiHoph3iwoch4iijoogh0zoh9aSh6Ueb2Dee5dang1aa8IiP" + ] + b = ["DWB5NyCrrXHmdikC6QPRAPoDjSHVQJe3WC5bMZuF6YhqhSsfYfjhN", + "DWB8gyvJtYyv5ZbT2ZxbAtgufQ5ovV2bq6EQp4YDTzQuSwyg7Ckry", + "DWB7yE71iVPSpaq8Ae2AmsKfyFxA8pwYv5zgQtCnX7xMwRUQMVoGf", + "DWB5jRgWA2kswPaXsQNtD2MMjs92XfJ1TYob6tjHtsECg2AusF5Wo", + "DWB6XHwVxcP6zP5NV1jUbG6Kso9m8ZG9g2CjDiPcZpAxHngx6ATPB", + "DWB59X1S4ofTAeHd1iNHDGxim5GkLo2AdcznksUsSYGU687ywB5WV", + "DWB6BPPL4iSRbFVVN8v3BEEEyDsC1STRK7Ba9ewQ4Lqvszn5J8VAe", + "DWB7cdK927wj95ptUrCk6HKWVeF74LG5cTjDTV22Z3yJ4Xw8xc9qp", + "DWB7VNFRjrE1hs1CKpEAP9NAabdFpwvzYXRKvkrVBBv2kTQCbNHz7", + "DWB7ZZFhEBjujcKjkmY31i1spPMx6xDSRhkursZLigi2HKLuALe5t", + ] + for i, pwd in enumerate(a): + p = format(PasswordKey("xeroc", pwd, "posting").get_public(), "DWB") + self.assertEqual(p, b[i]) + + def test_Privatekey_pubkey(self): + self.assertEqual([format(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq").pubkey, "STX"), + str(PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq", prefix="STX").pubkey)], + ["STX7W5qsanXHgRAZPijbrLMDwX6VmHqUdL2s8PZiYKD5h1R7JaqRJ", + "STX7W5qsanXHgRAZPijbrLMDwX6VmHqUdL2s8PZiYKD5h1R7JaqRJ"]) + + def test_Privatekey_derive(self): + p1 = PrivateKey("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq") + p2 = PrivateKey("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R") + self.assertEqual([format(p1.child(p2.get_secret()), "DWB"), + format(p2.child(p1.get_secret()), "DWB"), + format(p1.derive_private_key(0), "DWB"), + format(p2.derive_private_key(56), "DWB")], + ["DWBZiwJpC7MUmc9gn3vii3XS36nUceYEfKvFC1NLSrjB7ZRQJ7gt", + "DWB24hzNSDZYgm9C85yxJqyk32DwjXg8pCgkGVzB77hvP2XxGDdvr", + "DWB2e99iqVQUFij7Dk2nWVNC1dL8M86q37Nj4KwPHKBu1Yy49HkwA", + "DWBgqaH9RdvUtVk7NFnx4BZJRrNS7Lj35qaueAeYJ3tKEqPaLwa4"]) diff --git a/tests/dpaycligraphene/test_base58.py b/tests/dpaycligraphene/test_base58.py new file mode 100755 index 0000000..1eb9f42 --- /dev/null +++ b/tests/dpaycligraphene/test_base58.py @@ -0,0 +1,113 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import unittest +from dpaycligraphenebase.base58 import ( + Base58, + base58decode, + base58encode, + ripemd160, + base58CheckEncode, + base58CheckDecode, + gphBase58CheckEncode, + gphBase58CheckDecode) + + +class Testcases(unittest.TestCase): + def test_base58decode(self): + self.assertEqual([base58decode('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ'), + base58decode('5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss'), + base58decode('5KfazyjBBtR2YeHjNqX5D6MXvqTUd2iZmWusrdDSUqoykTyWQZB')], + ['800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d507a5b8d', + '80e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8555c5bbb26', + '80f3a375e00cc5147f30bee97bb5d54b31a12eee148a1ac31ac9edc4ecd13bc1f80cc8148e']) + + def test_base58encode(self): + self.assertEqual(['5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ', + '5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss', + '5KfazyjBBtR2YeHjNqX5D6MXvqTUd2iZmWusrdDSUqoykTyWQZB'], + [base58encode('800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d507a5b8d'), + base58encode('80e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8555c5bbb26'), + base58encode('80f3a375e00cc5147f30bee97bb5d54b31a12eee148a1ac31ac9edc4ecd13bc1f80cc8148e')]) + + def test_gphBase58CheckEncode(self): + self.assertEqual([gphBase58CheckEncode("02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680"), + gphBase58CheckEncode("021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16"), + gphBase58CheckEncode("02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a"), + gphBase58CheckEncode("03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3")], + ["6dumtt9swxCqwdPZBGXh9YmHoEjFFnNfwHaTqRbQTghGAY2gRz", + "5725vivYpuFWbeyTifZ5KevnHyqXCi5hwHbNU9cYz1FHbFXCxX", + "6kZKHSuxqAwdCYsMvwTcipoTsNE2jmEUNBQufGYywpniBKXWZK", + "8b82mpnH8YX1E9RHnU2a2YgLTZ8ooevEGP9N15c1yFqhoBvJur"]) + + def test_gphBase58CheckDecode(self): + self.assertEqual(["02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680", + "021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16", + "02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a", + "03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3"], + [gphBase58CheckDecode("6dumtt9swxCqwdPZBGXh9YmHoEjFFnNfwHaTqRbQTghGAY2gRz"), + gphBase58CheckDecode("5725vivYpuFWbeyTifZ5KevnHyqXCi5hwHbNU9cYz1FHbFXCxX"), + gphBase58CheckDecode("6kZKHSuxqAwdCYsMvwTcipoTsNE2jmEUNBQufGYywpniBKXWZK"), + gphBase58CheckDecode("8b82mpnH8YX1E9RHnU2a2YgLTZ8ooevEGP9N15c1yFqhoBvJur")]) + + def test_btsb58(self): + self.assertEqual(["02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680", + "03457298c4b2c56a8d572c051ca3109dabfe360beb144738180d6c964068ea3e58", + "021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16", + "02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a", + "03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3"], + [gphBase58CheckDecode(gphBase58CheckEncode("02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680")), + gphBase58CheckDecode(gphBase58CheckEncode("03457298c4b2c56a8d572c051ca3109dabfe360beb144738180d6c964068ea3e58")), + gphBase58CheckDecode(gphBase58CheckEncode("021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16")), + gphBase58CheckDecode(gphBase58CheckEncode("02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a")), + gphBase58CheckDecode(gphBase58CheckEncode("03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3"))]) + + def test_Base58CheckDecode(self): + self.assertEqual(["02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680", + "021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16", + "02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a", + "03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3", + "02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49", + "5b921f7051be5e13e177a0253229903c40493df410ae04f4a450c85568f19131", + "0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e", + "6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e", + "b84abd64d66ee1dd614230ebbe9d9c6d66d78d93927c395196666762e9ad69d8"], + [base58CheckDecode("KwKM6S22ZZDYw5dxBFhaRyFtcuWjaoxqDDfyCcBYSevnjdfm9Cjo"), + base58CheckDecode("KwHpCk3sLE6VykHymAEyTMRznQ1Uh5ukvFfyDWpGToT7Hf5jzrie"), + base58CheckDecode("KwKTjyQbKe6mfrtsf4TFMtqAf5as5bSp526s341PQEQvq5ZzEo5W"), + base58CheckDecode("KwMJJgtyBxQ9FEvUCzJmvr8tXxB3zNWhkn14mWMCTGSMt5GwGLgz"), + base58CheckDecode("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"), + base58CheckDecode("5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S"), + base58CheckDecode("5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq"), + base58CheckDecode("5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R"), + base58CheckDecode("5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7")]) + + def test_base58CheckEncodeDecopde(self): + self.assertEqual(["02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680", + "03457298c4b2c56a8d572c051ca3109dabfe360beb144738180d6c964068ea3e58", + "021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16", + "02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a", + "03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3"], + [base58CheckDecode(base58CheckEncode(0x80, "02e649f63f8e8121345fd7f47d0d185a3ccaa843115cd2e9392dcd9b82263bc680")), + base58CheckDecode(base58CheckEncode(0x80, "03457298c4b2c56a8d572c051ca3109dabfe360beb144738180d6c964068ea3e58")), + base58CheckDecode(base58CheckEncode(0x80, "021c7359cd885c0e319924d97e3980206ad64387aff54908241125b3a88b55ca16")), + base58CheckDecode(base58CheckEncode(0x80, "02f561e0b57a552df3fa1df2d87a906b7a9fc33a83d5d15fa68a644ecb0806b49a")), + base58CheckDecode(base58CheckEncode(0x80, "03e7595c3e6b58f907bee951dc29796f3757307e700ecf3d09307a0cc4a564eba3"))]) + + def test_Base58(self): + self.assertEqual([format(Base58("02b52e04a0acfe611a4b6963462aca94b6ae02b24e321eda86507661901adb49"), "wif"), + format(Base58("5b921f7051be5e13e177a0253229903c40493df410ae04f4a450c85568f19131"), "wif"), + format(Base58("0e1bfc9024d1f55a7855dc690e45b2e089d2d825a4671a3c3c7e4ea4e74ec00e"), "wif"), + format(Base58("6e5cc4653d46e690c709ed9e0570a2c75a286ad7c1bc69a648aae6855d919d3e"), "wif"), + format(Base58("b84abd64d66ee1dd614230ebbe9d9c6d66d78d93927c395196666762e9ad69d8"), "wif")], + ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd", + "5JWcdkhL3w4RkVPcZMdJsjos22yB5cSkPExerktvKnRNZR5gx1S", + "5HvVz6XMx84aC5KaaBbwYrRLvWE46cH6zVnv4827SBPLorg76oq", + "5Jete5oFNjjk3aUMkKuxgAXsp7ZyhgJbYNiNjHLvq5xzXkiqw7R", + "5KDT58ksNsVKjYShG4Ls5ZtredybSxzmKec8juj7CojZj6LPRF7"]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/dpaycligraphene/test_bip38.py b/tests/dpaycligraphene/test_bip38.py new file mode 100755 index 0000000..2d5d5a0 --- /dev/null +++ b/tests/dpaycligraphene/test_bip38.py @@ -0,0 +1,30 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import unittest +from dpaycligraphenebase.account import PrivateKey +from dpaycligraphenebase.bip38 import encrypt, decrypt + + +class Testcases(unittest.TestCase): + def test_encrypt(self): + self.assertEqual([format(encrypt(PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"), "TestingOneTwoThree"), "encwif"), + format(encrypt(PrivateKey("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"), "TestingOneTwoThree"), "encwif"), + format(encrypt(PrivateKey("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"), "Satoshi"), "encwif")], + ["6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi", + "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", + "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq"]) + + def test_deencrypt(self): + self.assertEqual([format(decrypt("6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi", "TestingOneTwoThree"), "wif"), + format(decrypt("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "TestingOneTwoThree"), "wif"), + format(decrypt("6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq", "Satoshi"), "wif")], + ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd", + "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR", + "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/dpaycligraphene/test_ecdsa.py b/tests/dpaycligraphene/test_ecdsa.py new file mode 100755 index 0000000..88da997 --- /dev/null +++ b/tests/dpaycligraphene/test_ecdsa.py @@ -0,0 +1,94 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import pytest +from parameterized import parameterized +import unittest +import hashlib +import ecdsa +from binascii import hexlify, unhexlify +from dpaycligraphenebase.account import PrivateKey, PublicKey, Address +import dpaycligraphenebase.ecdsasig as ecda +from dpaycligraphenebase.py23 import py23_bytes + + +wif = "5J4KCbg1G3my9b9hCaQXnHSm6vrwW9xQTJS6ZciW2Kek7cCkCEk" + + +class Testcases(unittest.TestCase): + + # Ignore warning: + # https://www.reddit.com/r/joinmarket/comments/5crhfh/userwarning_implicit_cast_from_char_to_a/ + # @pytest.mark.filterwarnings() + @parameterized.expand([ + ("cryptography"), + ("secp256k1"), + ("ecdsa") + ]) + def test_sign_message(self, module): + if module == "cryptography": + if not ecda.CRYPTOGRAPHY_AVAILABLE: + return + ecda.SECP256K1_MODULE = "cryptography" + elif module == "secp256k1": + if not ecda.SECP256K1_AVAILABLE: + return + ecda.SECP256K1_MODULE = "secp256k1" + else: + ecda.SECP256K1_MODULE = module + pub_key = py23_bytes(repr(PrivateKey(wif).pubkey), "latin") + signature = ecda.sign_message("Foobar", wif) + pub_key_sig = ecda.verify_message("Foobar", signature) + self.assertEqual(hexlify(pub_key_sig), pub_key) + + @parameterized.expand([ + ("cryptography"), + ("secp256k1"), + ]) + def test_sign_message_cross(self, module): + if module == "cryptography": + if not ecda.CRYPTOGRAPHY_AVAILABLE: + return + ecda.SECP256K1_MODULE = "cryptography" + elif module == "secp256k1": + if not ecda.SECP256K1_AVAILABLE: + return + ecda.SECP256K1_MODULE = "secp256k1" + + pub_key = py23_bytes(repr(PrivateKey(wif).pubkey), "latin") + signature = ecda.sign_message("Foobar", wif) + ecda.SECP256K1_MODULE = "ecdsa" + pub_key_sig = ecda.verify_message("Foobar", signature) + self.assertEqual(hexlify(pub_key_sig), pub_key) + signature = ecda.sign_message("Foobar", wif) + ecda.SECP256K1_MODULE = module + pub_key_sig = ecda.verify_message("Foobar", signature) + self.assertEqual(hexlify(pub_key_sig), pub_key) + + @parameterized.expand([ + ("cryptography"), + ("secp256k1"), + ("ecdsa"), + ]) + def test_wrong_signature(self, module): + if module == "cryptography": + if not ecda.CRYPTOGRAPHY_AVAILABLE: + return + ecda.SECP256K1_MODULE = "cryptography" + elif module == "secp256k1": + if not ecda.SECP256K1_AVAILABLE: + return + ecda.SECP256K1_MODULE = "secp256k1" + pub_key = py23_bytes(repr(PrivateKey(wif).pubkey), "latin") + ecda.SECP256K1_MODULE = module + signature = ecda.sign_message("Foobar", wif) + pub_key_sig = ecda.verify_message("Foobar", signature) + self.assertEqual(hexlify(pub_key_sig), pub_key) + pub_key_sig2 = ecda.verify_message("Foobar2", signature) + self.assertTrue(hexlify(pub_key_sig2) != pub_key) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/dpaycligraphene/test_key_format.py b/tests/dpaycligraphene/test_key_format.py new file mode 100755 index 0000000..ea7900c --- /dev/null +++ b/tests/dpaycligraphene/test_key_format.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import unittest +from dpaycligraphenebase.account import PrivateKey, PublicKey, Address +from dpaycligraphenebase.bip38 import encrypt, decrypt + + +key = { + "public_key": u"DWB7jDPoMwyjVH5obFmqzFNp4Ffp7G2nvC7FKFkrMBpo7Sy4uq5Mj", + "private_key": u"20991828d456b389d0768ed7fb69bf26b9bb87208dd699ef49f10481c20d3e18", + "private_key_WIF_format": u"5J4eFhjREJA7hKG6KcvHofHMXyGQZCDpQE463PAaKo9xXY6UDPq", + "bts_address": u"DWB8DvGQqzbgCR5FHiNsFf8kotEXr8VKD3mR", + "pts_address": u"Po3mqkgMzBL4F1VXJArwQxeWf3fWEpxUf3", + "encrypted_private_key": u"5e1ae410919c450dce1c476ae3ed3e5fe779ad248081d85b3dcf2888e698744d0a4b60efb7e854453bec3f6883bcbd1d", + "blockchain_address": u"4f3a560442a05e4fbb257e8dc5859b736306bace", + "Uncompressed_BTC": u"DWBLAFmEtM8as1mbmjVcj5dphLdPguXquimn", + "Compressed_BTC": u"DWBANNTSEaUviJgWLzJBersPmyFZBY4jJETY", + "Uncompressed_PTS": u"DWBEgj7RM6FBwSoccGaESJLC3Mi18785bM3T", + "Compressed_PTS": u"DWBD5rYtofD6D4UHJH6mo953P5wpBfMhdMEi", +} + + +class Testcases(unittest.TestCase): + def test_public_from_private(self): + private_key = PrivateKey(key["private_key"]) + public_key = private_key.get_public_key() + self.assertEqual(key["public_key"], str(public_key)) + + def test_short_address(self): + public_key = PublicKey(key["public_key"]) + self.assertEqual(key["bts_address"], str(public_key.address)) + + def test_blockchain_address(self): + public_key = PublicKey(key["public_key"]) + self.assertEqual(key["blockchain_address"], repr(public_key.address)) + + def test_import_export(self): + public_key = PublicKey(key["public_key"]) + self.assertEqual(key["public_key"], str(public_key)) + + def test_to_wif(self): + private_key = PrivateKey(key["private_key"]) + self.assertEqual(key["private_key_WIF_format"], str(private_key)) + + def test_calc_pub_key(self): + private_key = PrivateKey(key["private_key"]) + public_key = private_key.pubkey + self.assertEqual(key["bts_address"], str(public_key.address)) + + def test_btc_uncompressed(self): + public_key = PublicKey(key["public_key"]) + address = Address(address=None, pubkey=public_key.unCompressed()) + self.assertEqual(str(key["Uncompressed_BTC"]), (format(address.derive256address_with_version(0), "DWB"))) + + def test_btc_compressed(self): + public_key = PublicKey(key["public_key"]) + address = Address(address=None, pubkey=repr(public_key)) + self.assertEqual(str(key["Compressed_BTC"]), (format(address.derive256address_with_version(0), "DWB"))) + + def test_pts_uncompressed(self): + public_key = PublicKey(key["public_key"]) + address = Address(address=None, pubkey=public_key.unCompressed()) + self.assertEqual(str(key["Uncompressed_PTS"]), (format(address.derive256address_with_version(56), "DWB"))) + + def test_pts_compressed(self): + public_key = PublicKey(key["public_key"]) + address = Address(address=None, pubkey=repr(public_key)) + self.assertEqual(str(key["Compressed_PTS"]), (format(address.derive256address_with_version(56), "DWB"))) diff --git a/tests/dpaycligraphene/test_objects.py b/tests/dpaycligraphene/test_objects.py new file mode 100755 index 0000000..321be8e --- /dev/null +++ b/tests/dpaycligraphene/test_objects.py @@ -0,0 +1,31 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import unittest +import json +from dpaycligraphenebase import objects +from dpaycligraphenebase import types +from dpaycli.amount import Amount +from dpaycli import DPay + + +class Testcases(unittest.TestCase): + def test_GrapheneObject(self): + j = {"a": 2, "b": "abcde", "c": ["a", "b"]} + j2 = objects.GrapheneObject(j) + self.assertEqual(j, j2.data) + self.assertEqual(json.loads(j2.__str__()), j2.json()) + + a = objects.Array(['1000', 3, '@@000000013']) + j = {"a": a} + j2 = objects.GrapheneObject(j) + self.assertEqual(j, j2.data) + self.assertEqual(json.loads(j2.__str__()), j2.json()) + + a = types.Array(['1000', 3, '@@000000013']) + j = {"a": a} + j2 = objects.GrapheneObject(j) + self.assertEqual(j, j2.data) + self.assertEqual(json.loads(j2.__str__()), j2.json()) diff --git a/tests/dpaycligraphene/test_py23.py b/tests/dpaycligraphene/test_py23.py new file mode 100755 index 0000000..ccce7fd --- /dev/null +++ b/tests/dpaycligraphene/test_py23.py @@ -0,0 +1,124 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import pytest +import unittest +from dpaycligraphenebase.py23 import ( + py23_bytes, + py23_chr, + bytes_types, + integer_types, + string_types, + text_type, + PY2, + PY3 +) + + +TEST_UNICODE_STR = u'ℝεα∂@ßʟ℮ ☂ℯṧт υηḯ¢☺ḓ℮' +# Tk icon as a .gif: +TEST_BYTE_STR = b'GIF89a\x0e\x00\x0b\x00\x80\xff\x00\xff\x00\x00\xc0\xc0\xc0!\xf9\x04\x01\x00\x00\x01\x00,\x00\x00\x00\x00\x0e\x00\x0b\x00@\x02\x1f\x0c\x8e\x10\xbb\xcan\x90\x99\xaf&\xd8\x1a\xce\x9ar\x06F\xd7\xf1\x90\xa1c\x9e\xe8\x84\x99\x89\x97\xa2J\x01\x00;\x1a\x14\x00;;\xba\nD\x14\x00\x00;;' + + +class Testcases(unittest.TestCase): + def test_bytes_encoding_arg(self): + """ + The bytes class has changed in Python 3 to accept an + additional argument in the constructor: encoding. + It would be nice to support this without breaking the + isinstance(..., bytes) test below. + """ + u = u'Unicode string: \u5b54\u5b50' + b = py23_bytes(u, encoding='utf-8') + self.assertEqual(b, u.encode('utf-8')) + + def test_bytes_encoding_arg_non_kwarg(self): + """ + As above, but with a positional argument + """ + u = u'Unicode string: \u5b54\u5b50' + b = py23_bytes(u, 'utf-8') + self.assertEqual(b, u.encode('utf-8')) + + def test_bytes_int(self): + """ + In Py3, bytes(int) -> bytes object of size given by the parameter initialized with null + """ + self.assertEqual(py23_bytes(5), b'\x00\x00\x00\x00\x00') + # Test using newint: + self.assertEqual(py23_bytes(int(5)), b'\x00\x00\x00\x00\x00') + self.assertTrue(isinstance(py23_bytes(int(5)), bytes_types)) + + def test_bytes_iterable_of_ints(self): + self.assertEqual(py23_bytes([65, 66, 67]), b'ABC') + self.assertEqual(py23_bytes([int(120), int(121), int(122)]), b'xyz') + + def test_bytes_bytes(self): + self.assertEqual(py23_bytes(b'ABC'), b'ABC') + + def test_bytes_is_bytes(self): + b = py23_bytes(b'ABC') + self.assertTrue(py23_bytes(b) is b) + self.assertEqual(repr(py23_bytes(b)), "b'ABC'") + + def test_empty_bytes(self): + b = py23_bytes() + self.assertEqual(b, b'') + + def test_isinstance_bytes(self): + self.assertTrue(isinstance(py23_bytes(b'blah'), bytes_types)) + + def test_isinstance_bytes_subclass(self): + """ + Issue #89 + """ + value = py23_bytes(b'abc') + + class Magic(): + def __bytes__(self): + return py23_bytes(b'abc') + + self.assertEqual(value, py23_bytes(Magic())) + + def test_isinstance_oldbytestrings_bytes(self): + """ + Watch out for this. Byte-strings produced in various places in Py2 + are of type 'str'. With 'from future.builtins import bytes', 'bytes' + is redefined to be a subclass of 'str', not just an alias for 'str'. + """ + self.assertTrue(isinstance(b'blah', bytes_types)) # not with the redefined bytes obj + self.assertTrue(isinstance(u'blah'.encode('utf-8'), bytes_types)) # not with the redefined bytes obj + + def test_bytes_getitem(self): + b = py23_bytes(b'ABCD') + self.assertEqual(b[0], 65) + self.assertEqual(b[-1], 68) + self.assertEqual(b[0:1], b'A') + self.assertEqual(b[:], b'ABCD') + + def test_integer_types(self): + a = int(5) + if PY2: + b = long(10) # noqa: F821 + else: + b = int(10) + self.assertTrue(isinstance(a, integer_types)) + self.assertTrue(isinstance(b, integer_types)) + + def test_string_types(self): + a = 'abc' + b = u'abc' + self.assertTrue(isinstance(a, string_types)) + self.assertTrue(isinstance(b, string_types)) + + def test_chr(self): + BASE58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + self.assertEqual(BASE58_ALPHABET.find(py23_chr(4)), -1) + self.assertEqual(BASE58_ALPHABET.find(b"Z"), 32) + self.assertEqual(BASE58_ALPHABET.find(py23_bytes("Z", "ascii")), 32) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/dpaycligraphene/test_types.py b/tests/dpaycligraphene/test_types.py new file mode 100755 index 0000000..8345fa1 --- /dev/null +++ b/tests/dpaycligraphene/test_types.py @@ -0,0 +1,181 @@ +# This Python file uses the following encoding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +import unittest +import json +from dpaycligraphenebase import types +from dpaycli.amount import Amount +from dpaycli import DPay +from dpaycligraphenebase.py23 import ( + py23_bytes, + py23_chr, + bytes_types, + integer_types, + string_types, + text_type, + PY2, + PY3 +) + + +class Testcases(unittest.TestCase): + + def test_varint(self): + expected = [ + None, + b'\x01', b'\x02', b'\x03', b'\x04', b'\x05', b'\x06', b'\x07', + b'\x08', b'\t', b'\n', b'\x0b', b'\x0c', b'\r', b'\x0e', b'\x0f', + b'\x10', b'\x11', b'\x12', b'\x13', b'\x14', b'\x15', b'\x16', + b'\x17', b'\x18', b'\x19', b'\x1a', b'\x1b', b'\x1c', b'\x1d', + b'\x1e', b'\x1f', b' ', b'!', b'"', b'#', b'$', b'%', b'&', b"'", + b'(', b')', b'*', b'+', b',', b'-', b'.', b'/', b'0', b'1', b'2', + b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', b';', b'<', b'=', + b'>', b'?', b'@', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', + b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', + b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'[', b'\\', b']', b'^', + b'_', b'`', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', + b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', + b'u', b'v', b'w', b'x', b'y', b'z', b'{', b'|', b'}', b'~', + b'\x7f', b'\x80\x01', b'\x81\x01', b'\x82\x01', b'\x83\x01', + b'\x84\x01', b'\x85\x01', b'\x86\x01', b'\x87\x01', b'\x88\x01', + b'\x89\x01', b'\x8a\x01', b'\x8b\x01', b'\x8c\x01', b'\x8d\x01', + b'\x8e\x01', b'\x8f\x01', b'\x90\x01', b'\x91\x01', b'\x92\x01', + b'\x93\x01', b'\x94\x01', b'\x95\x01', b'\x96\x01', b'\x97\x01', + b'\x98\x01', b'\x99\x01', b'\x9a\x01', b'\x9b\x01', b'\x9c\x01', + b'\x9d\x01', b'\x9e\x01', b'\x9f\x01', b'\xa0\x01', b'\xa1\x01', + b'\xa2\x01', b'\xa3\x01', b'\xa4\x01', b'\xa5\x01', b'\xa6\x01', + b'\xa7\x01', b'\xa8\x01', b'\xa9\x01', b'\xaa\x01', b'\xab\x01', + b'\xac\x01', b'\xad\x01', b'\xae\x01', b'\xaf\x01', b'\xb0\x01', + b'\xb1\x01', b'\xb2\x01', b'\xb3\x01', b'\xb4\x01', b'\xb5\x01', + b'\xb6\x01', b'\xb7\x01', b'\xb8\x01', b'\xb9\x01', b'\xba\x01', + b'\xbb\x01', b'\xbc\x01', b'\xbd\x01', b'\xbe\x01', b'\xbf\x01', + b'\xc0\x01', b'\xc1\x01', b'\xc2\x01', b'\xc3\x01', b'\xc4\x01', + b'\xc5\x01', b'\xc6\x01', b'\xc7\x01'] + for i in range(1, 200): + self.assertEqual(types.varint(i), expected[i]) + self.assertEqual(types.varintdecode(expected[i]), i) + + def test_variable_buffer(self): + self.assertEqual( + types.variable_buffer(b"Hello"), + b"\x05Hello" + ) + + def test_JsonObj(self): + j = types.JsonObj(json.dumps(dict(foo="bar"))) + self.assertIn("foo", j) + self.assertEqual(j["foo"], "bar") + + def test_uint8(self): + u = types.Uint8(10) + self.assertEqual(py23_bytes(u), b"\n") + self.assertEqual(str(u), "10") + + def test_uint16(self): + u = types.Uint16(2**16 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff") + self.assertEqual(str(u), str(2**16 - 1)) + + def test_uint32(self): + u = types.Uint32(2**32 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff\xff\xff") + self.assertEqual(str(u), str(2**32 - 1)) + + def test_uint64(self): + u = types.Uint64(2**64 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff\xff\xff\xff\xff\xff\xff") + self.assertEqual(str(u), str(2**64 - 1)) + + def test_int64(self): + u = types.Int64(2**63 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff\xff\xff\xff\xff\xff\x7f") + self.assertEqual(str(u), str(9223372036854775807)) + + def test_int16(self): + u = types.Int16(2**15 - 1) + self.assertEqual(py23_bytes(u), b"\xff\x7f") + self.assertEqual(str(u), str(2**15 - 1)) + + def test_varint32(self): + u = types.Varint32(2**32 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff\xff\xff\x0f") + self.assertEqual(str(u), str(4294967295)) + u = types.Id(2**32 - 1) + self.assertEqual(py23_bytes(u), b"\xff\xff\xff\xff\x0f") + self.assertEqual(str(u), str(4294967295)) + + def test_string(self): + u = types.String("HelloFoobar") + self.assertEqual(py23_bytes(u), b"\x0bHelloFoobar") + self.assertEqual(str(u), "HelloFoobar") + + u = types.String("\x07\x08\x09\x0a\x0b\x0c\x0d\x0e") + self.assertEqual(py23_bytes(u), b"\x14u0007b\t\nu000bf\ru000e") + self.assertEqual(str(u), "\x07\x08\x09\x0a\x0b\x0c\x0d\x0e") + + def test_void(self): + u = types.Void() + self.assertEqual(py23_bytes(u), b"") + self.assertEqual(str(u), "") + + def test_array(self): + u = types.Array([types.Uint8(10) for x in range(2)] + [11]) + self.assertEqual(py23_bytes(u), b'\x03\n\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + self.assertEqual(str(u), "[10, 10, 11]") + u = types.Set([types.Uint16(10) for x in range(10)]) + self.assertEqual(py23_bytes(u), b"\n\n\x00\n\x00\n\x00\n\x00\n\x00\n\x00\n\x00\n\x00\n\x00\n\x00") + self.assertEqual(str(u), "[10, 10, 10, 10, 10, 10, 10, 10, 10, 10]") + u = types.Array(["Foobar"]) + # We do not support py23_bytes of Array containing String only! + # self.assertEqual(py23_bytes(u), b'') + self.assertEqual(str(u), '["Foobar"]') + + def test_PointInTime(self): + u = types.PointInTime("2018-07-06T22:10:00") + self.assertEqual(py23_bytes(u), b"\xb8\xe8?[") + self.assertEqual(str(u), "2018-07-06T22:10:00") + + def test_Signature(self): + u = types.Signature(b"\x00" * 33) + self.assertEqual(py23_bytes(u), b"\x00" * 33) + self.assertEqual(str(u), '"000000000000000000000000000000000000000000000000000000000000000000"') + + def test_Bytes(self): + u = types.Bytes("00" * 5) + self.assertEqual(py23_bytes(u), b'\x05\x00\x00\x00\x00\x00') + self.assertEqual(str(u), "00" * 5) + + def test_Bool(self): + u = types.Bool(True) + self.assertEqual(py23_bytes(u), b"\x01") + self.assertEqual(str(u), 'true') + u = types.Bool(False) + self.assertEqual(py23_bytes(u), b"\x00") + self.assertEqual(str(u), 'false') + + def test_Optional(self): + u = types.Optional(types.Uint16(10)) + self.assertEqual(py23_bytes(u), b"\x01\n\x00") + self.assertEqual(str(u), '10') + self.assertFalse(u.isempty()) + u = types.Optional(None) + self.assertEqual(py23_bytes(u), b"\x00") + self.assertEqual(str(u), 'None') + self.assertTrue(u.isempty()) + + def test_Static_variant(self): + class Tmp(types.Uint16): + def json(self): + return "Foobar" + + u = types.Static_variant(Tmp(10), 10) + self.assertEqual(py23_bytes(u), b"\n\n\x00") + self.assertEqual(str(u), '[10, "Foobar"]') + + def test_Map(self): + u = types.Map([[types.Uint16(10), types.Uint16(11)]]) + self.assertEqual(py23_bytes(u), b"\x01\n\x00\x0b\x00") + self.assertEqual(str(u), '[["10", "11"]]') diff --git a/tox.ini b/tox.ini new file mode 100755 index 0000000..31a9850 --- /dev/null +++ b/tox.ini @@ -0,0 +1,153 @@ +[tox] +envlist = py{27,34,35,36,37} +skip_missing_interpreters = true + +[testenv] +deps = + -rrequirements-test.txt +commands = + pytest + +[testenv:short] +deps = + mock>=2.0.0 + pytest + pytest-mock + parameterized + cryptography + secp256k1 + scrypt +commands = + pytest tests/dpaycliapi tests/dpayclibase tests/dpaycligraphene + +[testenv:py36] +deps = + -rrequirements-test.txt +commands = + coverage run --parallel-mode -m pytest {posargs} + coverage combine + coverage report -m + coverage xml + +[testenv:py36short] +deps = + mock>=2.0.0 + pytest + pytest-mock + parameterized + coverage + cryptography + secp256k1 + scrypt +commands = + coverage run --parallel-mode -m pytest tests/dpaycliapi tests/dpayclibase tests/dpaycligraphene {posargs} + coverage combine + coverage report -m + coverage xml + + +[testenv:flake8] +deps= + flake8 + # flake8-docstrings>=0.2.7 + # flake8-import-order>=0.9 + # pep8-naming + # flake8-colors +commands= + flake8 dpaycli dpaycliapi dpayclibase dpaycligraphenebase setup.py examples tests + +[testenv:pylint] +deps= + pyflakes + pylint +commands= + pylint dpaycli dpaycliapi dpayclibase dpaycligraphenebase tests + +[testenv:doc8] +skip_install = true +deps = + sphinx + doc8 +commands = + doc8 docs/ + +[testenv:mypy] +skip_install = true +deps = + mypy-lang +commands = + mypy dpaycli dpaycliapi dpayclibase dpaycligraphenebase + + +[testenv:bandit] +skip_install = true +deps = + bandit +commands = + bandit -r dpaycli dpaycliapi dpayclibase dpaycligraphenebase -c .bandit.yml + +[testenv:linters] +skip_install = true +deps = + {[testenv:flake8]deps} + {[testenv:pylint]deps} + {[testenv:doc8]deps} + {[testenv:readme]deps} + {[testenv:bandit]deps} +commands = + {[testenv:flake8]commands} + {[testenv:pylint]commands} + {[testenv:doc8]commands} + {[testenv:readme]commands} + {[testenv:bandit]commands} + + +[testenv:readme] +deps = + readme_renderer +commands = + python setup.py check -r -s + +[testenv:docs] +basepython= + python +changedir= + docs +deps=-rdocs/requirements.txt + sphinx + sphinx-click +commands= + sphinx-build -b html ./ ./html + +[testenv:upload_coverage] +deps = + coverage + codacy-coverage +passenv = CODACY_PROJECT_TOKEN +commands = + python-codacy-coverage -r coverage.xml + +# Flake8 Configuration +[flake8] +# Ignore some flake8-docstrings errors +# NOTE(sigmavirus24): While we're still using flake8 2.x, this ignore line +# defaults to selecting all other errors so we do not need select=E,F,W,I,D +# Once Flake8 3.0 is released and in a good state, we can use both and it will +# work well \o/ +ignore = D203,E129,E501,F401,E722,E122,E111,E114,D102,D100,D103,D107 +exclude = + .tox, + .git, + __pycache__, + docs/source/conf.py, + build, + dist, + tests/fixtures/*, + *.pyc, + *.egg-info, + .cache, + .eggs +max-complexity = 10 +import-order-style = google +application-import-names = flake8 +# format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s diff --git a/util/appveyor/build.cmd b/util/appveyor/build.cmd new file mode 100755 index 0000000..e1c8c03 --- /dev/null +++ b/util/appveyor/build.cmd @@ -0,0 +1,21 @@ +@echo off +:: To build extensions for 64 bit Python 3, we need to configure environment +:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 4 +:: +:: More details at: +:: https://packaging.python.org/appveyor/ + +IF "%DISTUTILS_USE_SDK%"=="1" ( + ECHO Configuring environment to build with MSVC on a 64bit architecture + ECHO Using Windows SDK 7.1 + "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 + CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release + SET MSSdk=1 + REM Need the following to allow tox to see the SDK compiler + SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB +) ELSE ( + ECHO Using default MSVC build environment +) + +CALL %* \ No newline at end of file diff --git a/util/dpay.ico b/util/dpay.ico new file mode 100644 index 0000000000000000000000000000000000000000..9f200aec82aacc5d4b6973adf7481f7aedcad93e GIT binary patch literal 4286 zcmeI$>rY#C9LMqFf8Z0B?AhEC^&c>CiDI%SYDPhJZ%T16xrFw#+qlT)L>!2_$lS#! zW7*;oBRGakon*;|OafB}juu9_6e#7k(iNm@>GhnqztSg$r-fn+r};KHrw?AA-}n6b zJHH%BdPaP{H$Qp2;7v`Lbr7e$Fn((9sP*Ow3R+sY#?Esc%Z=)r$=4jjnNV*j3< z>o!NtCIk0dE}>bc|A}lUKy`Oh|A}#3dLSF}!E0g3@(sm>x};}V_Wk`tXKyiay9({z z%%o;m_Jf0{o>u&izNX{5GBldxtru~>A9e8#5mzG%52~p@ou9NG#Qi>0U%SZAgx7W! zcTXiHCT&tO#C-?ZK+_Ss~ny7-8Ih;_bm|a5`F`U?;4)rYFz!tX#dGX zdb(y!GsuQK2nj#ZDeUV+eOK{6InV4sIWx8osQ)r234X8eJ5U47MEzHY1ir#GdYtK@ zQYMFsDJ$8xygr)sDV(i-lbXsm&}Y4~D!Jn0e<=LlP(v-kzbyQVcs(^tjuhjxA7;QR z(^&ixeU+OTy7@ArohjJvZ{zct?Fnay`+q_79%}SQqDz;E1TPQ@ond;Sgo!aT4u=VA z=Pv#{x(Un0=b60w7UTD}WAA#6Lx#5#${-uX{SWsDza13e2V(v?!r@cQFMNn<21) zWNfmCKbl{q`^0mMf1kqi?HxGVQn^~2x=Q}9sEX)+=rZBp7lgu}5eR>bV@hUpstEgZ zAtN&dbk@B&tFBvq{?pOuCTa3R=_&=aTgdeIE{-*?^AK_j+jD1Gfv-u1^ z&SSu#r=xZ=Q`b_NzpReKW}e{5go9J?j_JHl@gCH(Mdf{_yhqQ_WLjxh7IkiofJ zY)^8qc@8izpUv-WuP|4a!Os<03Hzm`g@l$`g?$~Bdcu)Y!mkv5x$qC+jS7F+eUC>A z+4TRNMc<;1-W%I!J-RyUli8eu)p{O9ts|nG6uqw?pp@ZPP59IToc@@tkK0}y)~8x} zJ-cYAPVD}#+!t%LnHY`xFv@#G?H08YgPci`oI#%9Uu8@uPn0fuZJ!S-^KG0bZ*TU0 M==I9?0m(l92TEM4eE