From b9b793fb5af8f95dcefec81c08d943aa2fc8360d Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 13 Feb 2023 19:16:45 -0600 Subject: [PATCH] the future is now: moving counterparty-cli into counterparty-lib Last commit from github.com/CounterpartyXCP/counterparty-cli repo: """ Merge pull request #128 from pataegrillo/master - Adding checkdb as a new parameter that will be used by counterparty to perform a database integrity check Commit: 0f7a0a24ae627f909bbbbfa6f9328e76bc1a2eae [0f7a0a2] """ --- Dockerfile | 17 +- counterparty-cli/.gitignore | 11 + counterparty-cli/.travis.yml | 7 + counterparty-cli/CONTRIBUTING.md | 27 ++ counterparty-cli/ChangeLog.md | 35 +++ counterparty-cli/LICENSE | 19 ++ counterparty-cli/README.md | 8 + counterparty-cli/counterpartycli/__init__.py | 16 + counterparty-cli/counterpartycli/client.py | 279 +++++++++++++++++ counterparty-cli/counterpartycli/clientapi.py | 256 +++++++++++++++ counterparty-cli/counterpartycli/console.py | 121 +++++++ counterparty-cli/counterpartycli/messages.py | 295 ++++++++++++++++++ counterparty-cli/counterpartycli/server.py | 186 +++++++++++ counterparty-cli/counterpartycli/setup.py | 227 ++++++++++++++ counterparty-cli/counterpartycli/util.py | 217 +++++++++++++ .../counterpartycli/wallet/__init__.py | 218 +++++++++++++ .../counterpartycli/wallet/bitcoincore.py | 72 +++++ .../counterpartycli/wallet/btcwallet.py | 74 +++++ counterparty-cli/release_procedure.md | 32 ++ counterparty-cli/requirements.txt | 4 + counterparty-cli/setup.py | 125 ++++++++ 21 files changed, 2242 insertions(+), 4 deletions(-) create mode 100644 counterparty-cli/.gitignore create mode 100644 counterparty-cli/.travis.yml create mode 100644 counterparty-cli/CONTRIBUTING.md create mode 100644 counterparty-cli/ChangeLog.md create mode 100644 counterparty-cli/LICENSE create mode 100644 counterparty-cli/README.md create mode 100644 counterparty-cli/counterpartycli/__init__.py create mode 100755 counterparty-cli/counterpartycli/client.py create mode 100755 counterparty-cli/counterpartycli/clientapi.py create mode 100755 counterparty-cli/counterpartycli/console.py create mode 100755 counterparty-cli/counterpartycli/messages.py create mode 100755 counterparty-cli/counterpartycli/server.py create mode 100644 counterparty-cli/counterpartycli/setup.py create mode 100644 counterparty-cli/counterpartycli/util.py create mode 100644 counterparty-cli/counterpartycli/wallet/__init__.py create mode 100644 counterparty-cli/counterpartycli/wallet/bitcoincore.py create mode 100644 counterparty-cli/counterpartycli/wallet/btcwallet.py create mode 100644 counterparty-cli/release_procedure.md create mode 100644 counterparty-cli/requirements.txt create mode 100644 counterparty-cli/setup.py diff --git a/Dockerfile b/Dockerfile index f04bc928cf..9826138792 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,14 @@ ENV LC_ALL en_US.UTF-8 ENV HOME /root # Install counterparty-lib -COPY . /counterparty-lib +# COPY . /counterparty-lib +RUN mkdir /counterparty-lib +COPY requirements.txt /counterparty-lib/requirements.txt +COPY setup.py /counterparty-lib/setup.py +COPY counterpartylib /counterparty-lib/counterpartylib + +COPY counterparty-cli /counterparty-cli + WORKDIR /counterparty-lib RUN pip3 install -r requirements.txt RUN python3 setup.py develop @@ -31,9 +38,11 @@ RUN python3 setup.py install_apsw # NOTE: By default, check out the counterparty-cli master branch. You can override the BRANCH build arg for a different # branch (as you should check out the same branch as what you have with counterparty-lib, or a compatible one) # NOTE2: In the future, counterparty-lib and counterparty-cli will go back to being one repo... -ARG CLI_BRANCH=master -ENV CLI_BRANCH ${CLI_BRANCH} -RUN git clone -b ${CLI_BRANCH} https://github.com/CounterpartyXCP/counterparty-cli.git /counterparty-cli +# ARG CLI_BRANCH=master +# ENV CLI_BRANCH ${CLI_BRANCH} +# RUN git clone -b ${CLI_BRANCH} https://github.com/CounterpartyXCP/counterparty-cli.git /counterparty-cli + +# the future is now WORKDIR /counterparty-cli RUN pip3 install -r requirements.txt RUN python3 setup.py develop diff --git a/counterparty-cli/.gitignore b/counterparty-cli/.gitignore new file mode 100644 index 0000000000..c1311b6104 --- /dev/null +++ b/counterparty-cli/.gitignore @@ -0,0 +1,11 @@ +# precompiled python +*.pyc + +# Setuptools distribution folder. +/dist/ + +# Setuptools build folder. +/build/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info \ No newline at end of file diff --git a/counterparty-cli/.travis.yml b/counterparty-cli/.travis.yml new file mode 100644 index 0000000000..f2c11ab0f4 --- /dev/null +++ b/counterparty-cli/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: +- "3.4" +install: +- pip install -r requirements.txt +- python setup.py install +script: echo "Correctly installed" diff --git a/counterparty-cli/CONTRIBUTING.md b/counterparty-cli/CONTRIBUTING.md new file mode 100644 index 0000000000..112bb40d6e --- /dev/null +++ b/counterparty-cli/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Security Issues + +* If you’ve identified a potential **security issue**, please contact us + directly at . + + +# Reporting an Issue + +* Check to see if the issue has already been reported. + +* Run with verbose logging and paste the relevant log output. + +* List the exact version/commit being run, as well as the platform the software + is running on. + + +# Making a Pull Request + +* Make (almost) all pull requests against the `develop` branch. + +* All original code should follow [PEP8](https://www.python.org/dev/peps/pep-0008/). + +* Code contributions should be well‐commented. + +* Commit messages should be neatly formatted and descriptive, with a summary line. + +* Commits should be organized into logical units. diff --git a/counterparty-cli/ChangeLog.md b/counterparty-cli/ChangeLog.md new file mode 100644 index 0000000000..f7dfa51e40 --- /dev/null +++ b/counterparty-cli/ChangeLog.md @@ -0,0 +1,35 @@ +## Command Line Interface Versions ## +* master (unreleased) + * Added indexd arguments + * removed backend-name argument +* v1.1.4 (2017/10/26) + * Added enhanced send arguments support. +* v1.1.3 (2017/05/01) + * Added `vacuum` command to server CLI. +* v1.1.2 (2016/07/11) + * Added P2SH support (to match counterparty-lib 9.55.0) + * added `get_tx_info` command + * added `--disable-utxo-locks` to `compose_transaction` to disable the locking of selected UTXOs for when the 'user' doesn't intend to broadcast the TX (straight away) + * Peg dependency versions in `setup.py` + * Added `debug_config` argument to print config to CLI. + * Added `--quiet` flag to `bootstrap` command + * Logging improvements + * Removed `rps` and `rpsresolve` commands + * Updated `README.md` +* v1.1.1 (2015/04/20) + * Fix `broadcast` command + * Cleaner, Commented-out Default Config Files + * Support new configuration parameter: `no-check-asset-conservation`, `rpc-batch-size`, `requests-timeout` +* v1.1.0 (2015/03/31) + * Code reorganisation + * Remove `market` command + * Add `getrows` command + * Add `clientapi` module + * Rename `get_running_info` to `getinfo` + * Rename `backend-ssl-verify` to `backend-ssl-no-verify` + * Rename `rpc-allow-cors` to `rpc-no-allow-cors` + * Change installation procedure +* v1.0.1 (2015/03/18) + * Update minimum `counterparty-lib` version from `v9.49.4` to `v9.50.0` +* v1.0.0 (2015/02/05) + * Initial Release diff --git a/counterparty-cli/LICENSE b/counterparty-cli/LICENSE new file mode 100644 index 0000000000..85f942ac3e --- /dev/null +++ b/counterparty-cli/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-Present Counterparty Developers + +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/counterparty-cli/README.md b/counterparty-cli/README.md new file mode 100644 index 0000000000..70f4f92510 --- /dev/null +++ b/counterparty-cli/README.md @@ -0,0 +1,8 @@ +[![Latest Version](https://pypip.in/version/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) +[![Supported Python versions](https://pypip.in/py_versions/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) +[![License](https://pypip.in/license/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) +[![Slack Status](http://slack.counterparty.io/badge.svg)](http://slack.counterparty.io) + +`counterparty-cli` is a command line interface for [`counterparty-lib`](https://github.com/CounterpartyXCP/counterparty-lib). + +For installation and configuration instructions, see the [`counterparty-lib README`](https://github.com/CounterpartyXCP/counterparty-lib), as well as the [Official Project Documentation](http://counterparty.io/docs/). diff --git a/counterparty-cli/counterpartycli/__init__.py b/counterparty-cli/counterpartycli/__init__.py new file mode 100644 index 0000000000..e9f46841ca --- /dev/null +++ b/counterparty-cli/counterpartycli/__init__.py @@ -0,0 +1,16 @@ +import os, sys + +APP_VERSION = '1.1.5' + +CURR_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser('__file__')))) +WIN_EXE_LIB = os.path.normpath(os.path.join(CURR_DIR, 'library')) +if os.path.isdir(WIN_EXE_LIB): + sys.path.insert(0, WIN_EXE_LIB) + +def client_main(): + from counterpartycli import client + client.main() + +def server_main(): + from counterpartycli import server + server.main() diff --git a/counterparty-cli/counterpartycli/client.py b/counterparty-cli/counterpartycli/client.py new file mode 100755 index 0000000000..4ce471a360 --- /dev/null +++ b/counterparty-cli/counterpartycli/client.py @@ -0,0 +1,279 @@ +#! /usr/bin/env python3 + +import os +import sys +import argparse +import logging +import getpass +from decimal import Decimal as D + +from counterpartylib.lib import log +logger = logging.getLogger(__name__) + +from counterpartylib.lib import config, script +from counterpartylib.lib.util import make_id, BET_TYPE_NAME +from counterpartylib.lib.log import isodt +from counterpartylib.lib.exceptions import TransactionError +from counterpartycli.util import add_config_arguments +from counterpartycli.setup import generate_config_files +from counterpartycli import APP_VERSION, util, messages, wallet, console, clientapi + +APP_NAME = 'counterparty-client' + +CONFIG_ARGS = [ + [('-v', '--verbose'), {'dest': 'verbose', 'action': 'store_true', 'help': 'sets log level to DEBUG instead of WARNING'}], + [('--testnet',), {'action': 'store_true', 'default': False, 'help': 'use {} testnet addresses and block numbers'.format(config.BTC_NAME)}], + [('--testcoin',), {'action': 'store_true', 'default': False, 'help': 'use the test {} network on every blockchain'.format(config.XCP_NAME)}], + [('--regtest',), {'action': 'store_true', 'default': False, 'help': 'use {} regtest addresses and block numbers'.format(config.BTC_NAME)}], + [('--customnet',), {'default': '', 'help': 'use a custom network (specify as UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION with version bytes in HH hex format)'}], + + [('--counterparty-rpc-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the Counterparty JSON-RPC server'}], + [('--counterparty-rpc-port',), {'type': int, 'help': 'the port of the Counterparty JSON-RPC server'}], + [('--counterparty-rpc-user',), {'default': 'rpc', 'help': 'the username for the Counterparty JSON-RPC server'}], + [('--counterparty-rpc-password',), {'help': 'the password for the Counterparty JSON-RPC server'}], + [('--counterparty-rpc-ssl',), {'default': False, 'action': 'store_true', 'help': 'use SSL to connect to the Counterparty server (default: false)'}], + [('--counterparty-rpc-ssl-verify',), {'default': False, 'action': 'store_true', 'help': 'verify SSL certificate of the Counterparty server; disallow use of self‐signed certificates (default: false)'}], + + [('--wallet-name',), {'default': 'bitcoincore', 'help': 'the wallet name to connect to'}], + [('--wallet-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the wallet server'}], + [('--wallet-port',), {'type': int, 'help': 'the wallet port to connect to'}], + [('--wallet-user',), {'default': 'bitcoinrpc', 'help': 'the username used to communicate with wallet'}], + [('--wallet-password',), {'help': 'the password used to communicate with wallet'}], + [('--wallet-ssl',), {'action': 'store_true', 'default': False, 'help': 'use SSL to connect to wallet (default: false)'}], + [('--wallet-ssl-verify',), {'action': 'store_true', 'default': False, 'help': 'verify SSL certificate of wallet; disallow use of self‐signed certificates (default: false)'}], + + [('--json-output',), {'action': 'store_true', 'default': False, 'help': 'display result in json format'}], + [('--unconfirmed',), {'action': 'store_true', 'default': False, 'help': 'allow the spending of unconfirmed transaction outputs'}], + [('--encoding',), {'default': 'auto', 'type': str, 'help': 'data encoding method'}], + [('--fee-per-kb',), {'type': D, 'default': D(config.DEFAULT_FEE_PER_KB / config.UNIT), 'help': 'fee per kilobyte, in {}'.format(config.BTC)}], + [('--regular-dust-size',), {'type': D, 'default': D(config.DEFAULT_REGULAR_DUST_SIZE / config.UNIT), 'help': 'value for dust Pay‐to‐Pubkey‐Hash outputs, in {}'.format(config.BTC)}], + [('--multisig-dust-size',), {'type': D, 'default': D(config.DEFAULT_MULTISIG_DUST_SIZE / config.UNIT), 'help': 'for dust OP_CHECKMULTISIG outputs, in {}'.format(config.BTC)}], + [('--op-return-value',), {'type': D, 'default': D(config.DEFAULT_OP_RETURN_VALUE / config.UNIT), 'help': 'value for OP_RETURN outputs, in {}'.format(config.BTC)}], + [('--unsigned',), {'action': 'store_true', 'default': False, 'help': 'print out unsigned hex of transaction; do not sign or broadcast'}], + [('--disable-utxo-locks',), {'action': 'store_true', 'default': False, 'help': 'disable locking of UTXOs being spend'}], + [('--dust-return-pubkey',), {'help': 'pubkey for dust outputs (required for P2SH)'}], + [('--requests-timeout',), {'type': int, 'default': clientapi.DEFAULT_REQUESTS_TIMEOUT, 'help': 'timeout value (in seconds) used for all HTTP requests (default: 5)'}] +] + +def main(): + if os.name == 'nt': + from counterpartylib.lib import util_windows + #patch up cmd.exe's "challenged" (i.e. broken/non-existent) UTF-8 logging + util_windows.fix_win32_unicode() + + # Post installation tasks + generate_config_files() + + # Parse command-line arguments. + parser = argparse.ArgumentParser(prog=APP_NAME, description='Counterparty CLI for counterparty-server', add_help=False) + parser.add_argument('-h', '--help', dest='help', action='store_true', help='show this help message and exit') + parser.add_argument('-V', '--version', action='version', version="{} v{}; {} v{}".format(APP_NAME, APP_VERSION, 'counterparty-lib', config.VERSION_STRING)) + parser.add_argument('--config-file', help='the location of the configuration file') + + add_config_arguments(parser, CONFIG_ARGS, 'client.conf') + + subparsers = parser.add_subparsers(dest='action', help='the action to be taken') + + parser_send = subparsers.add_parser('send', help='create and broadcast a *send* message') + parser_send.add_argument('--source', required=True, help='the source address') + parser_send.add_argument('--destination', required=True, help='the destination address') + parser_send.add_argument('--quantity', required=True, help='the quantity of ASSET to send') + parser_send.add_argument('--asset', required=True, help='the ASSET of which you would like to send QUANTITY') + parser_send.add_argument('--memo', help='A transaction memo attached to this send') + parser_send.add_argument('--memo-is-hex', action='store_true', default=False, help='Whether to interpret memo as a hexadecimal value') + parser_send.add_argument('--no-use-enhanced-send', action='store_false', dest="use_enhanced_send", default=True, help='If set to false, compose a non-enhanced send with a bitcoin dust output') + parser_send.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_sweep = subparsers.add_parser('sweep', help='create and broadcast a *sweep* message') + parser_sweep.add_argument('--source', required=True, help='the source address') + parser_sweep.add_argument('--destination', required=True, help='the destination address') + parser_sweep.add_argument('--flags', default=1, help='the ORed flags for this sweep. 1 for balance sweep, 2 for ownership sweep, 4 for memo as hex. E.G. flag=7 sends all assets, transfer all ownerships and encodes the memo as hex. default=1') + parser_sweep.add_argument('--memo', help='A transaction memo attached to this send') + parser_sweep.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_dispenser = subparsers.add_parser('dispenser', help='create and broadcast a *dispenser*') + parser_dispenser.add_argument('--source', required=True, help='the source address') + parser_dispenser.add_argument('--asset', required=True, help='the ASSET of which you would like to dispense GIVE_QUANTITY') + parser_dispenser.add_argument('--mainchainrate', required=True, help='the quantity of %s (decimal) this dispenser must receive to send the GIVEN_QUANTITY of the ASSET' % config.BTC) + parser_dispenser.add_argument('--give-quantity', required=True, help='the quantity of ASSET that you are giving for each MAINCHAINRATE of %s received' % config.BTC) + parser_dispenser.add_argument('--escrow-quantity', required=True, help='the quantity of ASSET that you are escrowing for this dispenser') + parser_dispenser.add_argument('--status', default=0, help='the status for the dispenser: 0. to open the dispenser (or replenish a drained one). 10. to close the dispenser. Default 0.') + parser_dispenser.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + parser_dispenser.add_argument('--open-address', help='an empty address to open the dispenser on') + + parser_order = subparsers.add_parser('order', help='create and broadcast an *order* message') + parser_order.add_argument('--source', required=True, help='the source address') + parser_order.add_argument('--get-quantity', required=True, help='the quantity of GET_ASSET that you would like to receive') + parser_order.add_argument('--get-asset', required=True, help='the asset that you would like to buy') + parser_order.add_argument('--give-quantity', required=True, help='the quantity of GIVE_ASSET that you are willing to give') + parser_order.add_argument('--give-asset', required=True, help='the asset that you would like to sell') + parser_order.add_argument('--expiration', type=int, required=True, help='the number of blocks for which the order should be valid') + parser_order.add_argument('--fee-fraction-required', default=config.DEFAULT_FEE_FRACTION_REQUIRED, help='the miners’ fee required for an order to match this one, as a fraction of the {} to be bought'.format(config.BTC)) + parser_order_fees = parser_order.add_mutually_exclusive_group() + parser_order_fees.add_argument('--fee-fraction-provided', default=config.DEFAULT_FEE_FRACTION_PROVIDED, help='the miners’ fee provided, as a fraction of the {} to be sold'.format(config.BTC)) + parser_order_fees.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_btcpay = subparsers.add_parser('{}pay'.format(config.BTC).lower(), help='create and broadcast a *{}pay* message, to settle an Order Match for which you owe {}'.format(config.BTC, config.BTC)) + parser_btcpay.add_argument('--source', required=True, help='the source address') + parser_btcpay.add_argument('--order-match-id', required=True, help='the concatenation of the hashes of the two transactions which compose the order match') + parser_btcpay.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_issuance = subparsers.add_parser('issuance', help='issue a new asset, issue more of an existing asset or transfer the ownership of an asset') + parser_issuance.add_argument('--source', required=True, help='the source address') + parser_issuance.add_argument('--transfer-destination', help='for transfer of ownership of asset issuance rights') + parser_issuance.add_argument('--quantity', default=0, help='the quantity of ASSET to be issued') + parser_issuance.add_argument('--asset', required=True, help='the name of the asset to be issued (if it’s available)') + parser_issuance.add_argument('--divisible', action='store_true', help='whether or not the asset is divisible (must agree with previous issuances)') + parser_issuance.add_argument('--description', type=str, required=True, help='a description of the asset (set to ‘LOCK’ to lock against further issuances with non‐zero quantitys)') + parser_issuance.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_broadcast = subparsers.add_parser('broadcast', help='broadcast textual and numerical information to the network') + parser_broadcast.add_argument('--source', required=True, help='the source address') + parser_broadcast.add_argument('--text', type=str, required=True, help='the textual part of the broadcast (set to ‘LOCK’ to lock feed)') + parser_broadcast.add_argument('--value', type=float, default=-1, help='numerical value of the broadcast') + parser_broadcast.add_argument('--fee-fraction', default=0, help='the fraction of bets on this feed that go to its operator') + parser_broadcast.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_bet = subparsers.add_parser('bet', help='offer to make a bet on the value of a feed') + parser_bet.add_argument('--source', required=True, help='the source address') + parser_bet.add_argument('--feed-address', required=True, help='the address which publishes the feed to bet on') + parser_bet.add_argument('--bet-type', choices=list(BET_TYPE_NAME.values()), required=True, help='choices: {}'.format(list(BET_TYPE_NAME.values()))) + parser_bet.add_argument('--deadline', required=True, help='the date and time at which the bet should be decided/settled') + parser_bet.add_argument('--wager', required=True, help='the quantity of XCP to wager') + parser_bet.add_argument('--counterwager', required=True, help='the minimum quantity of XCP to be wagered by the user to bet against you, if he were to accept the whole thing') + parser_bet.add_argument('--target-value', default=0.0, help='target value for Equal/NotEqual bet') + parser_bet.add_argument('--leverage', type=int, default=5040, help='leverage, as a fraction of 5040') + parser_bet.add_argument('--expiration', type=int, required=True, help='the number of blocks for which the bet should be valid') + parser_bet.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_dividend = subparsers.add_parser('dividend', help='pay dividends to the holders of an asset (in proportion to their stake in it)') + parser_dividend.add_argument('--source', required=True, help='the source address') + parser_dividend.add_argument('--quantity-per-unit', required=True, help='the quantity of XCP to be paid per whole unit held of ASSET') + parser_dividend.add_argument('--asset', required=True, help='the asset to which pay dividends') + parser_dividend.add_argument('--dividend-asset', required=True, help='asset in which to pay the dividends') + parser_dividend.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_burn = subparsers.add_parser('burn', help='destroy {} to earn XCP, during an initial period of time') + parser_burn.add_argument('--source', required=True, help='the source address') + parser_burn.add_argument('--quantity', required=True, help='quantity of {} to be burned'.format(config.BTC)) + parser_burn.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_cancel = subparsers.add_parser('cancel', help='cancel an open order or bet you created') + parser_cancel.add_argument('--source', required=True, help='the source address') + parser_cancel.add_argument('--offer-hash', required=True, help='the transaction hash of the order or bet') + parser_cancel.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_publish = subparsers.add_parser('publish', help='publish contract code in the blockchain') + parser_publish.add_argument('--source', required=True, help='the source address') + parser_publish.add_argument('--gasprice', required=True, type=int, help='the price of gas') + parser_publish.add_argument('--startgas', required=True, type=int, help='the maximum quantity of {} to be used to pay for the execution (satoshis)'.format(config.XCP)) + parser_publish.add_argument('--endowment', required=True, type=int, help='quantity of {} to be transfered to the contract (satoshis)'.format(config.XCP)) + parser_publish.add_argument('--code-hex', required=True, type=str, help='the hex‐encoded contract (returned by `serpent compile`)') + parser_publish.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_execute = subparsers.add_parser('execute', help='execute contract code in the blockchain') + parser_execute.add_argument('--source', required=True, help='the source address') + parser_execute.add_argument('--contract-id', required=True, help='the contract ID of the contract to be executed') + parser_execute.add_argument('--gasprice', required=True, type=int, help='the price of gas') + parser_execute.add_argument('--startgas', required=True, type=int, help='the maximum quantity of {} to be used to pay for the execution (satoshis)'.format(config.XCP)) + parser_execute.add_argument('--value', required=True, type=int, help='quantity of {} to be transfered to the contract (satoshis)'.format(config.XCP)) + parser_execute.add_argument('--payload-hex', required=True, type=str, help='data to be provided to the contract (returned by `serpent encode_datalist`)') + parser_execute.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_destroy = subparsers.add_parser('destroy', help='destroy a quantity of a Counterparty asset') + parser_destroy.add_argument('--source', required=True, help='the source address') + parser_destroy.add_argument('--asset', required=True, help='the ASSET of which you would like to destroy QUANTITY') + parser_destroy.add_argument('--quantity', required=True, help='the quantity of ASSET to destroy') + parser_destroy.add_argument('--tag', default='', help='tag') + parser_destroy.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) + + parser_address = subparsers.add_parser('balances', help='display the balances of a {} address'.format(config.XCP_NAME)) + parser_address.add_argument('address', help='the address you are interested in') + + parser_asset = subparsers.add_parser('asset', help='display the basic properties of a {} asset'.format(config.XCP_NAME)) + parser_asset.add_argument('asset', help='the asset you are interested in') + + parser_wallet = subparsers.add_parser('wallet', help='list the addresses in your backend wallet along with their balances in all {} assets'.format(config.XCP_NAME)) + + parser_pending = subparsers.add_parser('pending', help='list pending order matches awaiting {}payment from you'.format(config.BTC)) + + parser_getrows = subparsers.add_parser('getrows', help='get rows from a Counterparty table') + parser_getrows.add_argument('--table', required=True, help='table name') + parser_getrows.add_argument('--filter', nargs=3, action='append', help='filters to get specific rows') + parser_getrows.add_argument('--filter-op', choices=['AND', 'OR'], help='operator uses to combine filters', default='AND') + parser_getrows.add_argument('--order-by', help='field used to order results') + parser_getrows.add_argument('--order-dir', choices=['ASC', 'DESC'], help='direction used to order results') + parser_getrows.add_argument('--start-block', help='return only rows with block_index greater than start-block') + parser_getrows.add_argument('--end-block', help='return only rows with block_index lower than end-block') + parser_getrows.add_argument('--status', help='return only rows with the specified status') + parser_getrows.add_argument('--limit', help='number of rows to return', default=100) + parser_getrows.add_argument('--offset', help='number of rows to skip', default=0) + + parser_getrunninginfo = subparsers.add_parser('getinfo', help='get the current state of the server') + + parser_get_tx_info = subparsers.add_parser('get_tx_info', help='display info of a raw TX') + parser_get_tx_info.add_argument('tx_hex', help='the raw TX') + + args = parser.parse_args() + + # Logging + log.set_up(logger, verbose=args.verbose) + logger.propagate = False + + logger.info('Running v{} of {}.'.format(APP_VERSION, APP_NAME)) + + # Help message + if args.help: + parser.print_help() + sys.exit() + + # Configuration + clientapi.initialize(testnet=args.testnet, testcoin=args.testcoin, regtest=args.regtest, customnet=args.customnet, + counterparty_rpc_connect=args.counterparty_rpc_connect, counterparty_rpc_port=args.counterparty_rpc_port, + counterparty_rpc_user=args.counterparty_rpc_user, counterparty_rpc_password=args.counterparty_rpc_password, + counterparty_rpc_ssl=args.counterparty_rpc_ssl, counterparty_rpc_ssl_verify=args.counterparty_rpc_ssl_verify, + wallet_name=args.wallet_name, wallet_connect=args.wallet_connect, wallet_port=args.wallet_port, + wallet_user=args.wallet_user, wallet_password=args.wallet_password, + wallet_ssl=args.wallet_ssl, wallet_ssl_verify=args.wallet_ssl_verify, + requests_timeout=args.requests_timeout) + + # MESSAGE CREATION + if args.action in list(messages.MESSAGE_PARAMS.keys()): + unsigned_hex = messages.compose(args.action, args) + logger.info('Transaction (unsigned): {}'.format(unsigned_hex)) + if not args.unsigned: + if script.is_multisig(args.source): + logger.info('Multi‐signature transactions are signed and broadcasted manually.') + + elif input('Sign and broadcast? (y/N) ') == 'y': + + if wallet.is_mine(args.source): + if wallet.is_locked(): + passphrase = getpass.getpass('Enter your wallet passhrase: ') + logger.info('Unlocking wallet for 60 (more) seconds.') + wallet.unlock(passphrase) + signed_tx_hex = wallet.sign_raw_transaction(unsigned_hex) + else: + private_key_wif = input('Source address not in wallet. Please enter the private key in WIF format for {}:'.format(args.source)) + if not private_key_wif: + raise TransactionError('invalid private key') + signed_tx_hex = wallet.sign_raw_transaction(unsigned_hex, private_key_wif=private_key_wif) + + logger.info('Transaction (signed): {}'.format(signed_tx_hex)) + tx_hash = wallet.send_raw_transaction(signed_tx_hex) + logger.info('Hash of transaction (broadcasted): {}'.format(tx_hash)) + + + # VIEWING + elif args.action in ['balances', 'asset', 'wallet', 'pending', 'getinfo', 'getrows', 'get_tx_info']: + view = console.get_view(args.action, args) + print_method = getattr(console, 'print_{}'.format(args.action), None) + if args.json_output or print_method is None: + util.json_print(view) + else: + print_method(view) + + else: + parser.print_help() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/clientapi.py b/counterparty-cli/counterpartycli/clientapi.py new file mode 100755 index 0000000000..577a84813b --- /dev/null +++ b/counterparty-cli/counterpartycli/clientapi.py @@ -0,0 +1,256 @@ +import sys +import logging +import binascii +from urllib.parse import quote_plus as urlencode + +from counterpartylib.lib import config, script +from counterpartycli import util +from counterpartycli import wallet +from counterpartycli import messages +from counterpartycli.messages import get_pubkeys + +logger = logging.getLogger() + +DEFAULT_REQUESTS_TIMEOUT = 5 # seconds + +class ConfigurationError(Exception): + pass + +def initialize(testnet=False, testcoin=False, regtest=True, customnet="", + counterparty_rpc_connect=None, counterparty_rpc_port=None, + counterparty_rpc_user=None, counterparty_rpc_password=None, + counterparty_rpc_ssl=False, counterparty_rpc_ssl_verify=False, + wallet_name=None, wallet_connect=None, wallet_port=None, + wallet_user=None, wallet_password=None, + wallet_ssl=False, wallet_ssl_verify=False, + requests_timeout=DEFAULT_REQUESTS_TIMEOUT): + + def handle_exception(exc_type, exc_value, exc_traceback): + logger.error("Unhandled Exception", exc_info=(exc_type, exc_value, exc_traceback)) + sys.excepthook = handle_exception + + # testnet + config.TESTNET = testnet or False + + config.REGTEST = regtest or False + + if len(customnet) > 0: + config.CUSTOMNET = True + config.REGTEST = True + else: + config.CUSTOMNET = False + + # testcoin + config.TESTCOIN = testcoin or False + + ############## + # THINGS WE CONNECT TO + + # Server host (Bitcoin Core) + config.COUNTERPARTY_RPC_CONNECT = counterparty_rpc_connect or 'localhost' + + # Server RPC port (Bitcoin Core) + if counterparty_rpc_port: + config.COUNTERPARTY_RPC_PORT = counterparty_rpc_port + else: + if config.TESTNET: + config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_TESTNET + elif config.CUSTOMNET: + config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_REGTEST + elif config.REGTEST: + config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_REGTEST + else: + config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT + try: + config.COUNTERPARTY_RPC_PORT = int(config.COUNTERPARTY_RPC_PORT) + if not (int(config.COUNTERPARTY_RPC_PORT) > 1 and int(config.COUNTERPARTY_RPC_PORT) < 65535): + raise ConfigurationError('invalid RPC port number') + except: + raise Exception("Please specific a valid port number counterparty-rpc-port configuration parameter") + + # Server RPC user (Bitcoin Core) + config.COUNTERPARTY_RPC_USER = counterparty_rpc_user or 'rpc' + + # Server RPC password (Bitcoin Core) + if counterparty_rpc_password: + config.COUNTERPARTY_RPC_PASSWORD = counterparty_rpc_password + else: + config.COUNTERPARTY_RPC_PASSWORD = None + + # Server RPC SSL + config.COUNTERPARTY_RPC_SSL = counterparty_rpc_ssl or False # Default to off. + + # Server RPC SSL Verify + config.COUNTERPARTY_RPC_SSL_VERIFY = counterparty_rpc_ssl_verify or False # Default to off (support self‐signed certificates) + + # Construct server URL. + config.COUNTERPARTY_RPC = config.COUNTERPARTY_RPC_CONNECT + ':' + str(config.COUNTERPARTY_RPC_PORT) + if config.COUNTERPARTY_RPC_PASSWORD: + config.COUNTERPARTY_RPC = urlencode(config.COUNTERPARTY_RPC_USER) + ':' + urlencode(config.COUNTERPARTY_RPC_PASSWORD) + '@' + config.COUNTERPARTY_RPC + if config.COUNTERPARTY_RPC_SSL: + config.COUNTERPARTY_RPC = 'https://' + config.COUNTERPARTY_RPC + else: + config.COUNTERPARTY_RPC = 'http://' + config.COUNTERPARTY_RPC + config.COUNTERPARTY_RPC += '/rpc/' + + # BTC Wallet name + config.WALLET_NAME = wallet_name or 'bitcoincore' + + # BTC Wallet host + config.WALLET_CONNECT = wallet_connect or 'localhost' + + # BTC Wallet port + if wallet_port: + config.WALLET_PORT = wallet_port + else: + if config.TESTNET: + config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_TESTNET + elif config.CUSTOMNET: + config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_REGTEST + elif config.REGTEST: + config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_REGTEST + else: + config.WALLET_PORT = config.DEFAULT_BACKEND_PORT + try: + config.WALLET_PORT = int(config.WALLET_PORT) + if not (int(config.WALLET_PORT) > 1 and int(config.WALLET_PORT) < 65535): + raise ConfigurationError('invalid wallet API port number') + except: + raise ConfigurationError("Please specific a valid port number wallet-port configuration parameter") + + # BTC Wallet user + config.WALLET_USER = wallet_user or 'bitcoinrpc' + + # BTC Wallet password + if wallet_password: + config.WALLET_PASSWORD = wallet_password + else: + raise ConfigurationError('wallet RPC password not set. (Use configuration file or --wallet-password=PASSWORD)') + + # BTC Wallet SSL + config.WALLET_SSL = wallet_ssl or False # Default to off. + + # BTC Wallet SSL Verify + config.WALLET_SSL_VERIFY = wallet_ssl_verify or False # Default to off (support self‐signed certificates) + + # Construct BTC wallet URL. + config.WALLET_URL = urlencode(config.WALLET_USER) + ':' + urlencode(config.WALLET_PASSWORD) + '@' + config.WALLET_CONNECT + ':' + str(config.WALLET_PORT) + if config.WALLET_SSL: + config.WALLET_URL = 'https://' + config.WALLET_URL + else: + config.WALLET_URL = 'http://' + config.WALLET_URL + + config.REQUESTS_TIMEOUT = requests_timeout + + # Encoding + if config.TESTCOIN: + config.PREFIX = b'XX' # 2 bytes (possibly accidentally created) + else: + config.PREFIX = b'CNTRPRTY' # 8 bytes + + # (more) Testnet + if config.TESTNET: + config.MAGIC_BYTES = config.MAGIC_BYTES_TESTNET + if config.TESTCOIN: + config.ADDRESSVERSION = config.ADDRESSVERSION_TESTNET + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_TESTNET + config.BLOCK_FIRST = config.BLOCK_FIRST_TESTNET_TESTCOIN + config.BURN_START = config.BURN_START_TESTNET_TESTCOIN + config.BURN_END = config.BURN_END_TESTNET_TESTCOIN + config.UNSPENDABLE = config.UNSPENDABLE_TESTNET + else: + config.ADDRESSVERSION = config.ADDRESSVERSION_TESTNET + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_TESTNET + config.BLOCK_FIRST = config.BLOCK_FIRST_TESTNET + config.BURN_START = config.BURN_START_TESTNET + config.BURN_END = config.BURN_END_TESTNET + config.UNSPENDABLE = config.UNSPENDABLE_TESTNET + elif config.CUSTOMNET: + custom_args = customnet.split('|') + + if len(custom_args) == 3: + config.MAGIC_BYTES = config.MAGIC_BYTES_REGTEST + config.ADDRESSVERSION = binascii.unhexlify(custom_args[1]) + config.P2SH_ADDRESSVERSION = binascii.unhexlify(custom_args[2]) + config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST + config.BURN_START = config.BURN_START_REGTEST + config.BURN_END = config.BURN_END_REGTEST + config.UNSPENDABLE = custom_args[0] + else: + raise "Custom net parameter needs to be like UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION (version bytes in HH format)" + elif config.REGTEST: + config.MAGIC_BYTES = config.MAGIC_BYTES_REGTEST + if config.TESTCOIN: + config.ADDRESSVERSION = config.ADDRESSVERSION_REGTEST + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_REGTEST + config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST_TESTCOIN + config.BURN_START = config.BURN_START_REGTEST_TESTCOIN + config.BURN_END = config.BURN_END_REGTEST_TESTCOIN + config.UNSPENDABLE = config.UNSPENDABLE_REGTEST + else: + config.ADDRESSVERSION = config.ADDRESSVERSION_REGTEST + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_REGTEST + config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST + config.BURN_START = config.BURN_START_REGTEST + config.BURN_END = config.BURN_END_REGTEST + config.UNSPENDABLE = config.UNSPENDABLE_REGTEST + else: + config.MAGIC_BYTES = config.MAGIC_BYTES_MAINNET + if config.TESTCOIN: + config.ADDRESSVERSION = config.ADDRESSVERSION_MAINNET + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_MAINNET + config.BLOCK_FIRST = config.BLOCK_FIRST_MAINNET_TESTCOIN + config.BURN_START = config.BURN_START_MAINNET_TESTCOIN + config.BURN_END = config.BURN_END_MAINNET_TESTCOIN + config.UNSPENDABLE = config.UNSPENDABLE_MAINNET + else: + config.ADDRESSVERSION = config.ADDRESSVERSION_MAINNET + config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_MAINNET + config.BLOCK_FIRST = config.BLOCK_FIRST_MAINNET + config.BURN_START = config.BURN_START_MAINNET + config.BURN_END = config.BURN_END_MAINNET + config.UNSPENDABLE = config.UNSPENDABLE_MAINNET + +WALLET_METHODS = [ + 'get_wallet_addresses', 'get_btc_balances', 'sign_raw_transaction', + 'get_pubkey', 'is_valid', 'is_mine', 'get_btc_balance', 'send_raw_transaction', + 'wallet', 'asset', 'balances', 'pending', 'is_locked', 'unlock', 'wallet_last_block', + 'sweep' +] + +def call(method, args, pubkey_resolver=None): + """ + Unified function to call Wallet and Server API methods + Should be used by applications like `counterparty-gui` + + :Example: + + import counterpartycli.clientapi + clientapi.initialize(...) + unsigned_hex = clientapi.call('create_send', {...}) + signed_hex = clientapi.call('sign_raw_transaction', unsigned_hex) + tx_hash = clientapi.call('send_raw_transaction', signed_hex) + """ + if method in WALLET_METHODS: + func = getattr(wallet, method) + return func(**args) + else: + if method.startswith('create_'): + # Get provided pubkeys from params. + pubkeys = [] + for address_name in ['source', 'destination']: + if address_name in args: + address = args[address_name] + if script.is_multisig(address) or address_name != 'destination': # We don’t need the pubkey for a mono‐sig destination. + pubkeys += get_pubkeys(address, pubkey_resolver=pubkey_resolver) + args['pubkey'] = pubkeys + + result = util.api(method, args) + + if method.startswith('create_'): + messages.check_transaction(method, args, result) + + return result + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/console.py b/counterparty-cli/counterpartycli/console.py new file mode 100755 index 0000000000..8bd95f2c0b --- /dev/null +++ b/counterparty-cli/counterpartycli/console.py @@ -0,0 +1,121 @@ +import os +from prettytable import PrettyTable +from counterpartycli import wallet, util + +# TODO: inelegant +def get_view(view_name, args): + if view_name == 'balances': + return wallet.balances(args.address) + elif view_name == 'asset': + return wallet.asset(args.asset) + elif view_name == 'wallet': + return wallet.wallet() + elif view_name == 'pending': + return wallet.pending() + elif view_name == 'getinfo': + return util.api('get_running_info') + elif view_name == 'get_tx_info': + return util.api('get_tx_info', {'tx_hex': args.tx_hex}) + elif view_name == 'getrows': + method = 'get_{}'.format(args.table) + if args.filter: + filters = [tuple(f) for f in args.filter] + else: + filters = [] + params = { + 'filters': filters, + 'filterop': args.filter_op, + 'order_by': args.order_by, + 'order_dir': args.order_dir, + 'start_block': args.start_block, + 'end_block': args.end_block, + 'status': args.status, + 'limit': args.limit, + 'offset': args.offset + } + return util.api(method, params) + +def print_balances(balances): + lines = [] + lines.append('') + lines.append('Address Balances') + table = PrettyTable(['Asset', 'Amount']) + for asset in balances: + table.add_row([asset, balances[asset]]) + lines.append(table.get_string()) + lines.append('') + print(os.linesep.join(lines)) + +def print_asset(asset): + lines = [] + lines.append('') + lines.append('Asset Details') + table = PrettyTable(header=False, align='l') + table.add_row(['Asset Name:', asset['asset']]) + table.add_row(['Asset ID:', asset['asset_id']]) + table.add_row(['Divisible:', asset['divisible']]) + table.add_row(['Locked:', asset['locked']]) + table.add_row(['Supply:', asset['supply']]) + table.add_row(['Issuer:', asset['issuer']]) + table.add_row(['Description:', '‘' + asset['description'] + '’']) + table.add_row(['Balance:', asset['balance']]) + lines.append(table.get_string()) + + if asset['addresses']: + lines.append('') + lines.append('Wallet Balances') + table = PrettyTable(['Address', 'Balance']) + for address in asset['addresses']: + balance = asset['addresses'][address] + table.add_row([address, balance]) + lines.append(table.get_string()) + + if asset['sends']: + lines.append('') + lines.append('Wallet Sends and Receives') + table = PrettyTable(['Type', 'Quantity', 'Source', 'Destination']) + for send in asset['sends']: + table.add_row([send['type'], send['quantity'], send['source'], send['destination']]) + lines.append(table.get_string()) + + lines.append('') + print(os.linesep.join(lines)) + +def print_wallet(wallet): + lines = [] + for address in wallet['addresses']: + table = PrettyTable(['Asset', 'Balance']) + for asset in wallet['addresses'][address]: + balance = wallet['addresses'][address][asset] + table.add_row([asset, balance]) + lines.append(address) + lines.append(table.get_string()) + lines.append('') + total_table = PrettyTable(['Asset', 'Balance']) + for asset in wallet['assets']: + balance = wallet['assets'][asset] + total_table.add_row([asset, balance]) + lines.append('TOTAL') + lines.append(total_table.get_string()) + lines.append('') + print(os.linesep.join(lines)) + +def print_pending(awaiting_btcs): + table = PrettyTable(['Matched Order ID', 'Time Left']) + for order_match in awaiting_btcs: + order_match = format_order_match(order_match) + table.add_row(order_match) + print(table) + +def print_getrows(rows): + if len(rows) > 0: + headers = list(rows[0].keys()) + table = PrettyTable(headers) + for row in rows: + values = list(row.values()) + table.add_row(values) + print(table) + else: + print("No result.") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/messages.py b/counterparty-cli/counterpartycli/messages.py new file mode 100755 index 0000000000..e8e0dc318b --- /dev/null +++ b/counterparty-cli/counterpartycli/messages.py @@ -0,0 +1,295 @@ +import logging +from decimal import Decimal as D +import binascii +from math import ceil +import time +import calendar +import dateutil.parser + +from counterpartylib.lib import script, config, blocks, exceptions, api, transaction +from counterpartylib.lib.util import make_id, BET_TYPE_NAME, BET_TYPE_ID, dhash, generate_asset_name +from counterpartylib.lib.kickstart.utils import ib2h +from counterpartycli import util +from counterpartycli import wallet + +import bitcoin as bitcoinlib + +MESSAGE_PARAMS = { + 'send': ['source', 'destination', 'asset', 'quantity', 'memo', 'memo_is_hex', 'use_enhanced_send'], + 'sweep': ['source', 'destination', 'flags', 'memo'], + 'dispenser': ['source', 'asset', 'give_quantity', 'mainchainrate', 'escrow_quantity', 'status', 'open_address'], + 'order': ['source', 'give_asset', 'give_quantity', 'get_asset', 'get_quantity', 'expiration', 'fee_required', 'fee_provided'], + 'btcpay': ['source', 'order_match_id'], + 'issuance': ['source', 'asset', 'quantity', 'divisible', 'description', 'transfer_destination'], + 'broadcast': ['source', 'fee_fraction', 'text', 'timestamp', 'value'], + 'bet': ['source', 'feed_address', 'bet_type','deadline', 'wager_quantity', 'counterwager_quantity', 'expiration', 'target_value', 'leverage'], + 'dividend': ['source', 'quantity_per_unit', 'asset', 'dividend_asset'], + 'burn': ['source', 'quantity'], + 'cancel': ['source', 'offer_hash'], + 'rps': ['source', 'possible_moves', 'wager', 'move_random_hash', 'expiration'], + 'rpsresolve': ['source', 'random', 'move', 'rps_match_id'], + 'publish': ['source', 'gasprice', 'startgas', 'endowment','code_hex'], + 'execute': ['source', 'contract_id', 'gasprice', 'startgas', 'value', 'payload_hex'], + 'destroy': ['source', 'asset', 'quantity', 'tag'] +} + +class InputError(Exception): + pass +class ArgumentError(Exception): + pass + +class MessageArgs: + def __init__(self, dict_args): + self.__dict__.update(dict_args) + +def input_pubkey(address): + input_message = 'Public keys (hexadecimal) or Private key (Wallet Import Format) for `{}`: '.format(address) + return input(input_message) + +def get_pubkey_monosig(pubkeyhash, pubkey_resolver=input_pubkey): + if wallet.is_valid(pubkeyhash): + + # If in wallet, get from wallet. + logging.debug('Looking for public key for `{}` in wallet.'.format(pubkeyhash)) + if wallet.is_mine(pubkeyhash): + pubkey = wallet.get_pubkey(pubkeyhash) + if pubkey: + return pubkey + logging.debug('Public key for `{}` not found in wallet.'.format(pubkeyhash)) + + # If in blockchain (and not in wallet), get from blockchain. + logging.debug('Looking for public key for `{}` in blockchain.'.format(pubkeyhash)) + try: + pubkey = util.api('search_pubkey', {'pubkeyhash': pubkeyhash, 'provided_pubkeys': None}) + except util.RPCError as e: + pubkey = None + if pubkey: + return pubkey + logging.debug('Public key for `{}` not found in blockchain.'.format(pubkeyhash)) + + # If not in wallet and not in blockchain, get from user. + answer = pubkey_resolver(pubkeyhash) + if not answer: + return None + + # Public Key or Private Key? + is_fully_valid_pubkey = True + try: + is_fully_valid_pubkey = script.is_fully_valid(binascii.unhexlify(answer)) + except binascii.Error: + is_fully_valid_pubkey = False + if is_fully_valid_pubkey: + logging.debug('Answer was a fully valid public key.') + pubkey = answer + else: + logging.debug('Answer was not a fully valid public key. Assuming answer was a private key.') + private_key = answer + try: + pubkey = script.private_key_to_public_key(private_key) + except script.AltcoinSupportError: + raise InputError('invalid private key') + if pubkeyhash != script.pubkey_to_pubkeyhash(binascii.unhexlify(bytes(pubkey, 'utf-8'))): + raise InputError('provided public or private key does not match the source address') + + return pubkey + + return None + +def get_pubkeys(address, pubkey_resolver=input_pubkey): + pubkeys = [] + if script.is_multisig(address): + _, pubs, _ = script.extract_array(address) + for pub in pubs: + pubkey = get_pubkey_monosig(pub, pubkey_resolver=pubkey_resolver) + if pubkey: + pubkeys.append(pubkey) + else: + pubkey = get_pubkey_monosig(address, pubkey_resolver=pubkey_resolver) + if pubkey: + pubkeys.append(pubkey) + return pubkeys + +def common_args(args): + return { + 'fee': args.fee, + 'allow_unconfirmed_inputs': args.unconfirmed, + 'encoding': args.encoding, + 'fee_per_kb': args.fee_per_kb, + 'regular_dust_size': args.regular_dust_size, + 'multisig_dust_size': args.multisig_dust_size, + 'op_return_value': args.op_return_value, + 'dust_return_pubkey': args.dust_return_pubkey, + 'disable_utxo_locks': args.disable_utxo_locks + } + +def prepare_args(args, action): + # Convert. + args.fee_per_kb = int(args.fee_per_kb * config.UNIT) + args.regular_dust_size = int(args.regular_dust_size * config.UNIT) + args.multisig_dust_size = int(args.multisig_dust_size * config.UNIT) + args.op_return_value = int(args.op_return_value * config.UNIT) + + # common + if args.fee: + args.fee = util.value_in(args.fee, config.BTC) + + # send + if action == 'send': + args.quantity = util.value_in(args.quantity, args.asset) + + # sweep + if action == 'sweep': + args.flags = int(args.flags) + + # dispenser + if action == 'dispenser': + args.status = int(args.status) + args.give_quantity = util.value_in(args.give_quantity, args.asset) + args.escrow_quantity = util.value_in(args.escrow_quantity, args.asset) + args.mainchainrate = util.value_in(args.mainchainrate, config.BTC) + + # order + if action == 'order': + fee_required, fee_fraction_provided = D(args.fee_fraction_required), D(args.fee_fraction_provided) + give_quantity, get_quantity = D(args.give_quantity), D(args.get_quantity) + + # Fee argument is either fee_required or fee_provided, as necessary. + if args.give_asset == config.BTC: + args.fee_required = 0 + fee_fraction_provided = util.value_in(fee_fraction_provided, 'fraction') + args.fee_provided = round(D(fee_fraction_provided) * D(give_quantity) * D(config.UNIT)) + print('Fee provided: {} {}'.format(util.value_out(args.fee_provided, config.BTC), config.BTC)) + elif args.get_asset == config.BTC: + args.fee_provided = 0 + fee_fraction_required = util.value_in(args.fee_fraction_required, 'fraction') + args.fee_required = round(D(fee_fraction_required) * D(get_quantity) * D(config.UNIT)) + print('Fee required: {} {}'.format(util.value_out(args.fee_required, config.BTC), config.BTC)) + else: + args.fee_required = 0 + args.fee_provided = 0 + + args.give_quantity = util.value_in(give_quantity, args.give_asset) + args.get_quantity = util.value_in(get_quantity, args.get_asset) + + # issuance + if action == 'issuance': + args.quantity = util.value_in(args.quantity, None, divisible=args.divisible) + + # broadcast + if action == 'broadcast': + args.value = util.value_in(args.value, 'value') + args.fee_fraction = util.value_in(args.fee_fraction, 'fraction') + args.timestamp = int(time.time()) + + # bet + if action == 'bet': + args.deadline = calendar.timegm(dateutil.parser.parse(args.deadline).utctimetuple()) + args.wager = util.value_in(args.wager, config.XCP) + args.counterwager = util.value_in(args.counterwager, config.XCP) + args.target_value = util.value_in(args.target_value, 'value') + args.leverage = util.value_in(args.leverage, 'leverage') + args.bet_type = BET_TYPE_ID[args.bet_type] + + # dividend + if action == 'dividend': + args.quantity_per_unit = util.value_in(args.quantity_per_unit, config.XCP) + + # burn + if action == 'burn': + args.quantity = util.value_in(args.quantity, config.BTC) + + # execute + if action == 'execute': + args.value = util.value_in(args.value, 'XCP') + args.startgas = util.value_in(args.startgas, 'XCP') + + # destroy + if action == 'destroy': + args.quantity = util.value_in(args.quantity, args.asset) + + # RPS + if action == 'rps': + def generate_move_random_hash(move): + move = int(move).to_bytes(2, byteorder='big') + random_bin = os.urandom(16) + move_random_hash_bin = dhash(random_bin + move) + return binascii.hexlify(random_bin).decode('utf8'), binascii.hexlify(move_random_hash_bin).decode('utf8') + + args.wager = util.value_in(args.wager, 'XCP') + random, move_random_hash = generate_move_random_hash(args.move) + setattr(args, 'move_random_hash', move_random_hash) + print('random: {}'.format(random)) + print('move_random_hash: {}'.format(move_random_hash)) + + return args + +def extract_args(args, keys): + params = {} + dargs = vars(args) + for key in keys: + if key in dargs: + params[key] = dargs[key] + return params + +def get_input_value(tx_hex): + unspents = wallet.list_unspent() + ctx = bitcoinlib.core.CTransaction.deserialize(binascii.unhexlify(tx_hex)) + + inputs_value = 0 + for vin in ctx.vin: + vin_tx_hash = ib2h(vin.prevout.hash) + vout_n = vin.prevout.n + found = False + for output in unspents: + if output['txid'] == vin_tx_hash and output['vout'] == vout_n: + inputs_value += int(output['amount'] * config.UNIT) + found = True + if not found: + raise exceptions.TransactionError('input not found in wallet list unspents') + + return inputs_value + +def check_transaction(method, params, tx_hex): + tx_info = transaction.check_outputs(method, params, tx_hex) + input_value = get_input_value(tx_hex) + fee = input_value - tx_info['total_value'] + fee_per_kb = params['fee_per_kb'] if 'fee_per_kb' in params else config.DEFAULT_FEE_PER_KB + + if 'fee' in params and params['fee']: + necessary_fee = params['fee'] + else: + necessary_fee = ceil(((len(tx_hex) / 2) / 1024)) * fee_per_kb # TODO + + if fee > necessary_fee: + raise exceptions.TransactionError('Incorrect fee ({} > {})'.format(fee, necessary_fee)) + +def compose_transaction(args, message_name, param_names): + args = prepare_args(args, message_name) + common_params = common_args(args) + params = extract_args(args, param_names) + params.update(common_params) + + # Get provided pubkeys from params. + pubkeys = [] + for address_name in ['source', 'destination']: + if address_name in params: + address = params[address_name] + if not script.is_p2sh(address) and (script.is_multisig(address) or address_name != 'destination'): # We don’t need the pubkey for a mono‐sig destination. + pubkeys += get_pubkeys(address) + params['pubkey'] = pubkeys + + method = 'create_{}'.format(message_name) + unsigned_tx_hex = util.api(method, params) + + # check_transaction(method, params, unsigned_tx_hex) + + return unsigned_tx_hex + +def compose(message, args): + if message in MESSAGE_PARAMS: + param_names = MESSAGE_PARAMS[message] + return compose_transaction(args, message, param_names) + else: + raise ArgumentError('Invalid message name') + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/server.py b/counterparty-cli/counterpartycli/server.py new file mode 100755 index 0000000000..6ea6dc285c --- /dev/null +++ b/counterparty-cli/counterpartycli/server.py @@ -0,0 +1,186 @@ +#! /usr/bin/env python3 + +import os +import sys +import argparse +import logging +logger = logging.getLogger() + +from counterpartylib.lib import log +log.set_logger(logger) + +from counterpartylib import server +from counterpartylib.lib import config +from counterpartycli.util import add_config_arguments, bootstrap +from counterpartycli.setup import generate_config_files +from counterpartycli import APP_VERSION + +APP_NAME = 'counterparty-server' + +CONFIG_ARGS = [ + [('-v', '--verbose'), {'dest': 'verbose', 'action': 'store_true', 'default': False, 'help': 'sets log level to DEBUG instead of WARNING'}], + [('--testnet',), {'action': 'store_true', 'default': False, 'help': 'use {} testnet addresses and block numbers'.format(config.BTC_NAME)}], + [('--testcoin',), {'action': 'store_true', 'default': False, 'help': 'use the test {} network on every blockchain'.format(config.XCP_NAME)}], + [('--regtest',), {'action': 'store_true', 'default': False, 'help': 'use {} regtest addresses and block numbers'.format(config.BTC_NAME)}], + [('--customnet',), {'default': '', 'help': 'use a custom network (specify as UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION with version bytes in HH hex format)'}], + [('--api-limit-rows',), {'type': int, 'default': 1000, 'help': 'limit api calls to the set results (defaults to 1000). Setting to 0 removes the limit.'}], + [('--backend-name',), {'default': 'addrindex', 'help': 'the backend name to connect to'}], + [('--backend-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the backend server'}], + [('--backend-port',), {'type': int, 'help': 'the backend port to connect to'}], + [('--backend-user',), {'default': 'bitcoinrpc', 'help': 'the username used to communicate with backend'}], + [('--backend-password',), {'help': 'the password used to communicate with backend'}], + [('--backend-ssl',), {'action': 'store_true', 'default': False, 'help': 'use SSL to connect to backend (default: false)'}], + [('--backend-ssl-no-verify',), {'action': 'store_true', 'default': False, 'help': 'verify SSL certificate of backend; disallow use of self‐signed certificates (default: true)'}], + [('--backend-poll-interval',), {'type': float, 'default': 0.5, 'help': 'poll interval, in seconds (default: 0.5)'}], + [('--no-check-asset-conservation',), {'action': 'store_true', 'default': False, 'help': 'Skip asset conservation checking (default: false)'}], + [('--p2sh-dust-return-pubkey',), {'help': 'pubkey to receive dust when multisig encoding is used for P2SH source (default: none)'}], + + [('--indexd-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the indexd server'}], + [('--indexd-port',), {'type': int, 'help': 'the indexd server port to connect to'}], + + [('--rpc-host',), {'default': 'localhost', 'help': 'the IP of the interface to bind to for providing JSON-RPC API access (0.0.0.0 for all interfaces)'}], + [('--rpc-port',), {'type': int, 'help': 'port on which to provide the {} JSON-RPC API'.format(config.APP_NAME)}], + [('--rpc-user',), {'default': 'rpc', 'help': 'required username to use the {} JSON-RPC API (via HTTP basic auth)'.format(config.APP_NAME)}], + [('--rpc-password',), {'help': 'required password (for rpc-user) to use the {} JSON-RPC API (via HTTP basic auth)'.format(config.APP_NAME)}], + [('--rpc-no-allow-cors',), {'action': 'store_true', 'default': False, 'help': 'allow ajax cross domain request'}], + [('--rpc-batch-size',), {'type': int, 'default': config.DEFAULT_RPC_BATCH_SIZE, 'help': 'number of RPC queries by batch (default: {})'.format(config.DEFAULT_RPC_BATCH_SIZE)}], + [('--requests-timeout',), {'type': int, 'default': config.DEFAULT_REQUESTS_TIMEOUT, 'help': 'timeout value (in seconds) used for all HTTP requests (default: 5)'}], + + [('--force',), {'action': 'store_true', 'default': False, 'help': 'skip backend check, version check, process lock (NOT FOR USE ON PRODUCTION SYSTEMS)'}], + [('--database-file',), {'default': None, 'help': 'the path to the SQLite3 database file'}], + [('--log-file',), {'nargs': '?', 'const': None, 'default': False, 'help': 'log to the specified file (specify option without filename to use the default location)'}], + [('--api-log-file',), {'nargs': '?', 'const': None, 'default': False, 'help': 'log API requests to the specified file (specify option without filename to use the default location)'}], + + [('--utxo-locks-max-addresses',), {'type': int, 'default': config.DEFAULT_UTXO_LOCKS_MAX_ADDRESSES, 'help': 'max number of addresses for which to track UTXO locks'}], + [('--utxo-locks-max-age',), {'type': int, 'default': config.DEFAULT_UTXO_LOCKS_MAX_AGE, 'help': 'how long to keep a lock on a UTXO being tracked'}], + [('--checkdb',), {'action': 'store_true', 'default': False, 'help': 'check the database for integrity (default: false)'}] +] + +class VersionError(Exception): + pass +def main(): + if os.name == 'nt': + from counterpartylib.lib import util_windows + #patch up cmd.exe's "challenged" (i.e. broken/non-existent) UTF-8 logging + util_windows.fix_win32_unicode() + + # Post installation tasks + generate_config_files() + + # Parse command-line arguments. + parser = argparse.ArgumentParser(prog=APP_NAME, description='Server for the {} protocol'.format(config.XCP_NAME), add_help=False) + parser.add_argument('-h', '--help', dest='help', action='store_true', help='show this help message and exit') + parser.add_argument('-V', '--version', action='version', version="{} v{}; {} v{}".format(APP_NAME, APP_VERSION, 'counterparty-lib', config.VERSION_STRING)) + parser.add_argument('--config-file', help='the path to the configuration file') + + add_config_arguments(parser, CONFIG_ARGS, 'server.conf') + + subparsers = parser.add_subparsers(dest='action', help='the action to be taken') + + parser_server = subparsers.add_parser('start', help='run the server') + + parser_reparse = subparsers.add_parser('reparse', help='reparse all transactions in the database') + + parser_vacuum = subparsers.add_parser('vacuum', help='VACUUM the database (to improve performance)') + + parser_rollback = subparsers.add_parser('rollback', help='rollback database') + parser_rollback.add_argument('block_index', type=int, help='the index of the last known good block') + + parser_kickstart = subparsers.add_parser('kickstart', help='rapidly build database by reading from Bitcoin Core blockchain') + parser_kickstart.add_argument('--bitcoind-dir', help='Bitcoin Core data directory') + + parser_bootstrap = subparsers.add_parser('bootstrap', help='bootstrap database with hosted snapshot') + parser_bootstrap.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='suppress progress bar') + #parser_bootstrap.add_argument('--branch', help='use a different branch for bootstrap db pulling') + + parser_checkdb = subparsers.add_parser('checkdb', help='do an integrity check on the database') + + + + args = parser.parse_args() + + log.set_up(log.ROOT_LOGGER, verbose=args.verbose, console_logfilter=os.environ.get('COUNTERPARTY_LOGGING', None)) + + logger.info('Running v{} of {}.'.format(APP_VERSION, APP_NAME)) + + # Help message + if args.help: + parser.print_help() + sys.exit() + + # Bootstrapping + if args.action == 'bootstrap': + bootstrap(testnet=args.testnet, quiet=args.quiet) + sys.exit() + + def init_with_catch(fn, init_args): + try: + return fn(**init_args) + except TypeError as e: + if 'unexpected keyword argument' in str(e): + raise VersionError('Unsupported Server Parameter. CLI/Library Version Incompatibility.') + else: + raise e + + # Configuration + COMMANDS_WITH_DB = ['reparse', 'rollback', 'kickstart', 'start', 'vacuum', 'checkdb'] + COMMANDS_WITH_CONFIG = ['debug_config'] + if args.action in COMMANDS_WITH_DB or args.action in COMMANDS_WITH_CONFIG: + init_args = dict(database_file=args.database_file, + log_file=args.log_file, api_log_file=args.api_log_file, + testnet=args.testnet, testcoin=args.testcoin, regtest=args.regtest, + customnet=args.customnet, + api_limit_rows=args.api_limit_rows, + backend_name=args.backend_name, + backend_connect=args.backend_connect, + backend_port=args.backend_port, + backend_user=args.backend_user, + backend_password=args.backend_password, + backend_ssl=args.backend_ssl, + backend_ssl_no_verify=args.backend_ssl_no_verify, + backend_poll_interval=args.backend_poll_interval, + indexd_connect=args.indexd_connect, indexd_port=args.indexd_port, + rpc_host=args.rpc_host, rpc_port=args.rpc_port, rpc_user=args.rpc_user, + rpc_password=args.rpc_password, rpc_no_allow_cors=args.rpc_no_allow_cors, + requests_timeout=args.requests_timeout, + rpc_batch_size=args.rpc_batch_size, + check_asset_conservation=not args.no_check_asset_conservation, + force=args.force, verbose=args.verbose, console_logfilter=os.environ.get('COUNTERPARTY_LOGGING', None), + p2sh_dust_return_pubkey=args.p2sh_dust_return_pubkey, + utxo_locks_max_addresses=args.utxo_locks_max_addresses, + utxo_locks_max_age=args.utxo_locks_max_age, + checkdb=(args.action == 'checkdb') or (args.checkdb)) + #,broadcast_tx_mainnet=args.broadcast_tx_mainnet) + + if args.action in COMMANDS_WITH_DB: + db = init_with_catch(server.initialise, init_args) + + elif args.action in COMMANDS_WITH_CONFIG: + init_with_catch(server.initialise_config, init_args) + + # PARSING + if args.action == 'reparse': + server.reparse(db) + + elif args.action == 'rollback': + server.reparse(db, block_index=args.block_index) + + elif args.action == 'kickstart': + server.kickstart(db, bitcoind_dir=args.bitcoind_dir) + + elif args.action == 'start': + server.start_all(db) + + elif args.action == 'debug_config': + server.debug_config() + + elif args.action == 'vacuum': + server.vacuum(db) + + elif args.action == 'checkdb': + print("Database integrity check done!") + + else: + parser.print_help() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/setup.py b/counterparty-cli/counterpartycli/setup.py new file mode 100644 index 0000000000..d3512c9362 --- /dev/null +++ b/counterparty-cli/counterpartycli/setup.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python + +import os, sys +import shutil +import ctypes.util +import configparser, platform +import urllib.request +import tarfile, zipfile +import appdirs +import hashlib +from decimal import Decimal as D + +# generate commented config file from arguments list (client.CONFIG_ARGS and server.CONFIG_ARGS) and known values +def generate_config_file(filename, config_args, known_config={}, overwrite=False): + if not overwrite and os.path.exists(filename): + return + + config_dir = os.path.dirname(os.path.abspath(filename)) + if not os.path.exists(config_dir): + os.makedirs(config_dir, mode=0o755) + + config_lines = [] + config_lines.append('[Default]') + config_lines.append('') + + for arg in config_args: + key = arg[0][-1].replace('--', '') + value = None + + if key in known_config: + value = known_config[key] + elif 'default' in arg[1]: + value = arg[1]['default'] + + if value is None: + value = '' + elif isinstance(value, bool): + value = '1' if value else '0' + elif isinstance(value, (float, D)): + value = format(value, '.8f') + + if 'default' in arg[1] or value == '': + key = '# {}'.format(key) + + config_lines.append('{} = {}\t\t\t\t# {}'.format(key, value, arg[1]['help'])) + + with open(filename, 'w', encoding='utf8') as config_file: + config_file.writelines("\n".join(config_lines)) + os.chmod(filename, 0o660) + +def extract_old_config(): + old_config = {} + + old_appdir = appdirs.user_config_dir(appauthor='Counterparty', appname='counterpartyd', roaming=True) + old_configfile = os.path.join(old_appdir, 'counterpartyd.conf') + + if os.path.exists(old_configfile): + configfile = configparser.SafeConfigParser(allow_no_value=True, inline_comment_prefixes=('#', ';')) + configfile.read(old_configfile) + if 'Default' in configfile: + for key in configfile['Default']: + new_key = key.replace('backend-rpc-', 'backend-') + new_key = new_key.replace('blockchain-service-name', 'backend-name') + new_value = configfile['Default'][key].replace('jmcorgan', 'addrindex') + old_config[new_key] = new_value + + return old_config + +def extract_bitcoincore_config(): + bitcoincore_config = {} + + # Figure out the path to the bitcoin.conf file + if platform.system() == 'Darwin': + btc_conf_file = os.path.expanduser('~/Library/Application Support/Bitcoin/') + elif platform.system() == 'Windows': + btc_conf_file = os.path.join(os.environ['APPDATA'], 'Bitcoin') + else: + btc_conf_file = os.path.expanduser('~/.bitcoin') + btc_conf_file = os.path.join(btc_conf_file, 'bitcoin.conf') + + # Extract contents of bitcoin.conf to build service_url + if os.path.exists(btc_conf_file): + conf = {} + with open(btc_conf_file, 'r') as fd: + # Bitcoin Core accepts empty rpcuser, not specified in btc_conf_file + for line in fd.readlines(): + if '#' in line or '=' not in line: + continue + k, v = line.split('=', 1) + conf[k.strip()] = v.strip() + + config_keys = { + 'rpcport': 'backend-port', + 'rpcuser': 'backend-user', + 'rpcpassword': 'backend-password', + 'rpcssl': 'backend-ssl' + } + + for bitcoind_key in config_keys: + if bitcoind_key in conf: + counterparty_key = config_keys[bitcoind_key] + bitcoincore_config[counterparty_key] = conf[bitcoind_key] + + return bitcoincore_config + +def get_server_known_config(): + server_known_config = {} + + bitcoincore_config = extract_bitcoincore_config() + server_known_config.update(bitcoincore_config) + + old_config = extract_old_config() + server_known_config.update(old_config) + + return server_known_config + +# generate client config from server config +def server_to_client_config(server_config): + client_config = {} + + config_keys = { + 'backend-connect': 'wallet-connect', + 'backend-port': 'wallet-port', + 'backend-user': 'wallet-user', + 'backend-password': 'wallet-password', + 'backend-ssl': 'wallet-ssl', + 'backend-ssl-verify': 'wallet-ssl-verify', + 'rpc-host': 'counterparty-rpc-connect', + 'rpc-port': 'counterparty-rpc-port', + 'rpc-user': 'counterparty-rpc-user', + 'rpc-password': 'counterparty-rpc-password' + } + + for server_key in config_keys: + if server_key in server_config: + client_key = config_keys[server_key] + client_config[client_key] = server_config[server_key] + + return client_config + +def generate_config_files(): + from counterpartycli.server import CONFIG_ARGS as SERVER_CONFIG_ARGS + from counterpartycli.client import CONFIG_ARGS as CLIENT_CONFIG_ARGS + from counterpartylib.lib import config, util + + configdir = appdirs.user_config_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) + + server_configfile = os.path.join(configdir, 'server.conf') + if not os.path.exists(server_configfile): + # extract known configuration + server_known_config = get_server_known_config() + generate_config_file(server_configfile, SERVER_CONFIG_ARGS, server_known_config) + + client_configfile = os.path.join(configdir, 'client.conf') + if not os.path.exists(client_configfile): + client_known_config = server_to_client_config(server_known_config) + generate_config_file(client_configfile, CLIENT_CONFIG_ARGS, client_known_config) + +def zip_folder(folder_path, zip_path): + zip_file = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) + for root, dirs, files in os.walk(folder_path): + for a_file in files: + zip_file.write(os.path.join(root, a_file)) + zip_file.close() + +def before_py2exe_build(win_dist_dir): + # Clean previous build + if os.path.exists(win_dist_dir): + shutil.rmtree(win_dist_dir) + # py2exe don't manages entry_points + for exe_name in ['client', 'server']: + shutil.copy('counterpartycli/__init__.py', 'counterparty-{}.py'.format(exe_name)) + with open('counterparty-{}.py'.format(exe_name), 'a') as fp: + fp.write('{}_main()'.format(exe_name)) + # Hack + src = 'C:\\Python34\\Lib\\site-packages\\flask_httpauth.py' + dst = 'C:\\Python34\\Lib\\site-packages\\flask\\ext\\httpauth.py' + shutil.copy(src, dst) + +def after_py2exe_build(win_dist_dir): + # clean temporaries scripts + for exe_name in ['client', 'server']: + os.remove('counterparty-{}.py'.format(exe_name)) + # py2exe copies only pyc files in site-packages.zip + # modules with no pyc files must be copied in 'dist/library/' + import counterpartylib, certifi + additionals_modules = [counterpartylib, certifi] + for module in additionals_modules: + moudle_file = os.path.dirname(module.__file__) + dest_file = os.path.join(win_dist_dir, 'library', module.__name__) + shutil.copytree(moudle_file, dest_file) + # additionals DLLs + dlls = ['ssleay32.dll', 'libssl32.dll', 'libeay32.dll'] + dlls.append(ctypes.util.find_msvcrt()) + dlls_path = dlls + for dll in dlls: + dll_path = ctypes.util.find_library(dll) + shutil.copy(dll_path, win_dist_dir) + + # compress distribution folder + zip_path = '{}.zip'.format(win_dist_dir) + zip_folder(win_dist_dir, zip_path) + + # Open,close, read file and calculate MD5 on its contents + with open(zip_path, 'rb') as zip_file: + data = zip_file.read() + md5 = hashlib.md5(data).hexdigest() + + # include MD5 in the zip name + new_zip_path = '{}-{}.zip'.format(win_dist_dir, md5) + os.rename(zip_path, new_zip_path) + + # clean build folder + shutil.rmtree(win_dist_dir) + + # Clean Hack + os.remove('C:\\Python34\\Lib\\site-packages\\flask\\ext\\httpauth.py') + + +# Download bootstrap database +def bootstrap(overwrite=True, ask_confirmation=False): + if ask_confirmation: + question = 'Would you like to bootstrap your local Counterparty database from `https://s3.amazonaws.com/counterparty-bootstrap/`? (y/N): ' + if input(question).lower() != 'y': + return + util.bootstrap(testnet=False) + util.bootstrap(testnet=True) diff --git a/counterparty-cli/counterpartycli/util.py b/counterparty-cli/counterpartycli/util.py new file mode 100644 index 0000000000..2d78687dfe --- /dev/null +++ b/counterparty-cli/counterpartycli/util.py @@ -0,0 +1,217 @@ +#! /usr/bin/python3 + +import sys +import os +import threading +import decimal +import time +import json +import re +import requests +import collections +import logging +import binascii +from datetime import datetime +from dateutil.tz import tzlocal +import argparse +import configparser +import appdirs +import tarfile +import urllib.request +import shutil +import codecs +import tempfile + +logger = logging.getLogger(__name__) + +D = decimal.Decimal + +from counterpartylib import server +from counterpartylib.lib import config, check +from counterpartylib.lib.util import value_input, value_output + +rpc_sessions = {} + +class JsonDecimalEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, D): + return str(o) + return super(JsonDecimalEncoder, self).default(o) + +json_dump = lambda x: json.dumps(x, sort_keys=True, indent=4, cls=JsonDecimalEncoder) +json_print = lambda x: print(json_dump(x)) + +class RPCError(Exception): + pass +class AssetError(Exception): + pass + +def rpc(url, method, params=None, ssl_verify=False, tries=1): + headers = {'content-type': 'application/json'} + payload = { + "method": method, + "params": params, + "jsonrpc": "2.0", + "id": 0, + } + + if url not in rpc_sessions: + rpc_session = requests.Session() + rpc_sessions[url] = rpc_session + else: + rpc_session = rpc_sessions[url] + + response = None + for i in range(tries): + try: + response = rpc_session.post(url, data=json.dumps(payload), headers=headers, verify=ssl_verify, timeout=config.REQUESTS_TIMEOUT) + if i > 0: + logger.debug('Successfully connected.') + break + except requests.exceptions.SSLError as e: + raise e + except requests.exceptions.Timeout as e: + raise e + except requests.exceptions.ConnectionError: + logger.debug('Could not connect to {}. (Try {}/{})'.format(url, i+1, tries)) + time.sleep(5) + + if response == None: + raise RPCError('Cannot communicate with {}.'.format(url)) + elif response.status_code not in (200, 500): + raise RPCError(str(response.status_code) + ' ' + response.reason + ' ' + response.text) + + # Return result, with error handling. + response_json = response.json() + if 'error' not in response_json.keys() or response_json['error'] == None: + return response_json['result'] + else: + raise RPCError('{}'.format(response_json['error'])) + +def api(method, params=None): + return rpc(config.COUNTERPARTY_RPC, method, params=params, ssl_verify=config.COUNTERPARTY_RPC_SSL_VERIFY) + +def wallet_api(method, params=None): + return rpc(config.WALLET_URL, method, params=params, ssl_verify=config.WALLET_SSL_VERIFY) + +def is_divisible(asset): + if asset in (config.BTC, config.XCP, 'leverage', 'value', 'fraction', 'price', 'odds'): + return True + else: + sql = '''SELECT * FROM issuances WHERE (status = ? AND asset = ?)''' + bindings = ['valid', asset] + issuances = api('sql', {'query': sql, 'bindings': bindings}) + + if not issuances: raise AssetError('No such asset: {}'.format(asset)) + return issuances[0]['divisible'] + +def value_in(quantity, asset, divisible=None): + if divisible is None: + divisible = is_divisible(asset) + return value_input(quantity, asset, divisible) + +def value_out(quantity, asset, divisible=None): + if divisible is None: + divisible = is_divisible(asset) + return value_output(quantity, asset, divisible) + +def bootstrap(testnet=False, overwrite=True, ask_confirmation=False, quiet=False): + data_dir = appdirs.user_data_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) + + # Set Constants. + if testnet: + if check.CONSENSUS_HASH_VERSION_TESTNET < 7: + BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-testnet.latest.tar.gz' + else: + BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-testnet-{}.latest.tar.gz'.format(check.CONSENSUS_HASH_VERSION_TESTNET) + TARBALL_PATH = os.path.join(tempfile.gettempdir(), 'counterpartyd-testnet-db.latest.tar.gz') + DATABASE_PATH = os.path.join(data_dir, '{}.testnet.db'.format(config.APP_NAME)) + else: + if check.CONSENSUS_HASH_VERSION_MAINNET < 3: + BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db.latest.tar.gz' + else: + BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-{}.latest.tar.gz'.format(check.CONSENSUS_HASH_VERSION_MAINNET) + TARBALL_PATH = os.path.join(tempfile.gettempdir(), 'counterpartyd-db.latest.tar.gz') + DATABASE_PATH = os.path.join(data_dir, '{}.db'.format(config.APP_NAME)) + + # Prepare Directory. + if not os.path.exists(data_dir): + os.makedirs(data_dir, mode=0o755) + if not overwrite and os.path.exists(DATABASE_PATH): + return + + # Define Progress Bar. + def reporthook(blocknum, blocksize, totalsize): + readsofar = blocknum * blocksize + if totalsize > 0: + percent = readsofar * 1e2 / totalsize + s = "\r%5.1f%% %*d / %d" % ( + percent, len(str(totalsize)), readsofar, totalsize) + sys.stderr.write(s) + if readsofar >= totalsize: # near the end + sys.stderr.write("\n") + else: # total size is unknown + sys.stderr.write("read %d\n" % (readsofar,)) + + print('Downloading database from {}...'.format(BOOTSTRAP_URL)) + urllib.request.urlretrieve(BOOTSTRAP_URL, TARBALL_PATH, reporthook if not quiet else None) + + print('Extracting to "%s"...' % data_dir) + with tarfile.open(TARBALL_PATH, 'r:gz') as tar_file: + tar_file.extractall(path=data_dir) + + assert os.path.exists(DATABASE_PATH) + os.chmod(DATABASE_PATH, 0o660) + + print('Cleaning up...') + os.remove(TARBALL_PATH) + os.remove(os.path.join(data_dir, 'checksums.txt')) + +# Set default values of command line arguments with config file +def add_config_arguments(arg_parser, config_args, default_config_file, config_file_arg_name='config_file'): + cmd_args = arg_parser.parse_known_args()[0] + + config_file = getattr(cmd_args, config_file_arg_name, None) + if not config_file: + config_dir = appdirs.user_config_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) + if not os.path.isdir(config_dir): + os.makedirs(config_dir, mode=0o755) + config_file = os.path.join(config_dir, default_config_file) + + # clean BOM + BUFSIZE = 4096 + BOMLEN = len(codecs.BOM_UTF8) + with codecs.open(config_file, 'r+b') as fp: + chunk = fp.read(BUFSIZE) + if chunk.startswith(codecs.BOM_UTF8): + i = 0 + chunk = chunk[BOMLEN:] + while chunk: + fp.seek(i) + fp.write(chunk) + i += len(chunk) + fp.seek(BOMLEN, os.SEEK_CUR) + chunk = fp.read(BUFSIZE) + fp.seek(-BOMLEN, os.SEEK_CUR) + fp.truncate() + + logger.debug('Loading configuration file: `{}`'.format(config_file)) + configfile = configparser.SafeConfigParser(allow_no_value=True, inline_comment_prefixes=('#', ';')) + with codecs.open(config_file, 'r', encoding='utf8') as fp: + configfile.readfp(fp) + + if not 'Default' in configfile: + configfile['Default'] = {} + + # Initialize default values with the config file. + for arg in config_args: + key = arg[0][-1].replace('--', '') + if 'action' in arg[1] and arg[1]['action'] == 'store_true' and key in configfile['Default']: + arg[1]['default'] = configfile['Default'].getboolean(key) + elif key in configfile['Default'] and configfile['Default'][key]: + arg[1]['default'] = configfile['Default'][key] + elif key in configfile['Default'] and arg[1].get('nargs', '') == '?' and 'const' in arg[1]: + arg[1]['default'] = arg[1]['const'] # bit of a hack + arg_parser.add_argument(*arg[0], **arg[1]) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/wallet/__init__.py b/counterparty-cli/counterpartycli/wallet/__init__.py new file mode 100644 index 0000000000..8e288bda76 --- /dev/null +++ b/counterparty-cli/counterpartycli/wallet/__init__.py @@ -0,0 +1,218 @@ +import os +import getpass +import binascii +import logging +logger = logging.getLogger(__name__) +import sys +import json +import time +from decimal import Decimal as D + +from counterpartycli.wallet import bitcoincore, btcwallet +from counterpartylib.lib import config, util, exceptions, script +from counterpartycli.util import api, value_out + +from pycoin.tx import Tx, SIGHASH_ALL +from pycoin.encoding import wif_to_tuple_of_secret_exponent_compressed, public_pair_to_hash160_sec +from pycoin.ecdsa import generator_secp256k1, public_pair_for_secret_exponent + +class WalletError(Exception): + pass + +class LockedWalletError(WalletError): + pass + +def WALLET(): + return sys.modules['counterpartycli.wallet.{}'.format(config.WALLET_NAME)] + +def get_wallet_addresses(): + return WALLET().get_wallet_addresses() + +def get_btc_balances(): + for address, btc_balance in WALLET().get_btc_balances(): + yield [address, btc_balance] + +def pycoin_sign_raw_transaction(tx_hex, private_key_wif): + for char in private_key_wif: + if char not in script.b58_digits: + raise exceptions.TransactionError('invalid private key') + + if config.TESTNET: + allowable_wif_prefixes = [config.PRIVATEKEY_VERSION_TESTNET] + else: + allowable_wif_prefixes = [config.PRIVATEKEY_VERSION_MAINNET] + + secret_exponent, compressed = wif_to_tuple_of_secret_exponent_compressed( + private_key_wif, allowable_wif_prefixes=allowable_wif_prefixes) + public_pair = public_pair_for_secret_exponent(generator_secp256k1, secret_exponent) + hash160 = public_pair_to_hash160_sec(public_pair, compressed) + hash160_lookup = {hash160: (secret_exponent, public_pair, compressed)} + + tx = Tx.from_hex(tx_hex) + for idx, tx_in in enumerate(tx.txs_in): + tx.sign_tx_in(hash160_lookup, idx, tx_in.script, hash_type=SIGHASH_ALL) + + return tx.as_hex() + +def sign_raw_transaction(tx_hex, private_key_wif=None): + if private_key_wif is None: + if WALLET().is_locked(): + raise LockedWalletError('Wallet is locked.') + return WALLET().sign_raw_transaction(tx_hex) + else: + return pycoin_sign_raw_transaction(tx_hex, private_key_wif) + +def get_pubkey(address): + return WALLET().get_pubkey(address) + +def is_valid(address): + return WALLET().is_valid(address) + +def is_mine(address): + return WALLET().is_mine(address) + +def get_btc_balance(address): + return WALLET().get_btc_balance(address) + +def list_unspent(): + return WALLET().list_unspent() + +def send_raw_transaction(tx_hex): + return WALLET().send_raw_transaction(tx_hex) + +def is_locked(): + return WALLET().is_locked() + +def unlock(passphrase): + return WALLET().unlock(passphrase) + +def wallet_last_block(): + return WALLET().wallet_last_block() + +def wallet(): + wallet = { + 'addresses': {}, + 'assets': {} + } + + def add_total(address, asset, quantity): + if quantity: + if address not in wallet['addresses']: + wallet['addresses'][address] = {} + if asset not in wallet['assets']: + wallet['assets'][asset] = 0 + if asset not in wallet['addresses'][address]: + wallet['addresses'][address][asset] = 0 + wallet['addresses'][address][asset] += quantity + wallet['assets'][asset] += quantity + + for bunch in get_btc_balances(): + address, btc_balance = bunch + add_total(address, 'BTC', btc_balance) + balances = api('get_balances', {'filters': [('address', '==', address),]}) + for balance in balances: + asset = balance['asset'] + balance = D(value_out(balance['quantity'], asset)) + add_total(address, asset, balance) + + return wallet + +def asset(asset_name): + supply = api('get_supply', {'asset': asset_name}) + asset_id = api('get_assets', {'filters': [('asset_name', '==', asset_name),]})[0]['asset_id'] + asset_info = { + 'asset': asset_name, + 'supply': D(value_out(supply, asset_name)), + 'asset_id': asset_id + } + if asset_name in ['XCP', 'BTC']: + asset_info.update({ + 'owner': None, + 'divisible': True, + 'locked': False, + 'description': '', + 'issuer': None + }) + else: + issuances = api('get_issuances', { + 'filters': [('asset', '==', asset_name),], + 'status': 'valid', + 'order_by': 'tx_index', + 'order_dir': 'DESC', + }) + if not issuances: + raise WalletError('Asset not found') + locked = False + for issuance in issuances: + if issuance['locked']: + locked = True + issuance = issuances[0] + asset_info.update({ + 'owner': issuance['issuer'], + 'divisible': bool(issuance['divisible']), + 'locked': locked, + 'description': issuance['description'], + 'issuer': issuance['issuer'] + }) + + asset_info['balance'] = 0 + asset_info['addresses'] = {} + + for bunch in get_btc_balances(): + address, btc_balance = bunch + if asset_name == 'BTC': + balance = btc_balance + else: + balances = api('get_balances', {'filters': [('address', '==', address), ('asset', '==', asset_name)]}) + if balances: + balance = balances[0] + balance = D(value_out(balance['quantity'], asset_name)) + else: + balance = 0 + if balance: + asset_info['balance'] += balance + asset_info['addresses'][address] = balance + + addresses = list(asset_info['addresses'].keys()) + + if asset_name != 'BTC': + all_sends = api('get_sends', {'filters': [('source', 'IN', addresses), ('destination', 'IN', addresses)], 'filterop': 'OR', 'status': 'valid'}) + sends = [] + for send in all_sends: + if send['asset'] == asset_name: + if send['source'] in addresses and send['destination'] in addresses: + tx_type = 'in-wallet' + elif send['source'] in addresses: + tx_type = 'send' + elif send['destination'] in addresses: + tx_type = 'receive' + send['type'] = tx_type + send['quantity'] = D(value_out(send['quantity'], asset_name)) + sends.append(send) + asset_info['sends'] = sends + + return asset_info + +def balances(address): + result = { + 'BTC': get_btc_balance(address) + } + balances = api('get_balances', {'filters': [('address', '==', address),]}) + for balance in balances: + asset = balance['asset'] + balance = D(value_out(balance['quantity'], asset)) + result[asset] = balance + return result + +def pending(): + addresses = [] + for bunch in get_btc_balances(): + addresses.append(bunch[0]) + filters = [ + ('tx0_address', 'IN', addresses), + ('tx1_address', 'IN', addresses) + ] + awaiting_btcs = api('get_order_matches', {'filters': filters, 'filterop': 'OR', 'status': 'pending'}) + return awaiting_btcs + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/wallet/bitcoincore.py b/counterparty-cli/counterpartycli/wallet/bitcoincore.py new file mode 100644 index 0000000000..8eb223aa3f --- /dev/null +++ b/counterparty-cli/counterpartycli/wallet/bitcoincore.py @@ -0,0 +1,72 @@ +import binascii +import logging +logger = logging.getLogger(__name__) +import sys +import json +import time +import requests + +from counterpartylib.lib import config +from counterpartycli.util import wallet_api as rpc + +def get_wallet_addresses(): + addresses = [] + for group in rpc('listaddressgroupings', []): + for bunch in group: + address, btc_balance = bunch[:2] + addresses.append(address) + return addresses + +def get_btc_balances(): + for group in rpc('listaddressgroupings', []): + for bunch in group: + yield bunch[:2] + +def list_unspent(): + return rpc('listunspent', [0, 99999]) + +def sign_raw_transaction(tx_hex): + return rpc('signrawtransactionwithwallet', [tx_hex])['hex'] + +def is_valid(address): + return rpc('validateaddress', [address])['isvalid'] + +def is_mine(address): + return rpc('getaddressinfo', [address])['ismine'] + +def get_pubkey(address): + address_valid = rpc('validateaddress', [address]) + address_infos = rpc('getaddressinfo', [address]) + if address_valid['isvalid'] and address_infos['ismine']: + return address_infos['pubkey'] + return None + +def get_btc_balance(address): + for group in rpc('listaddressgroupings', []): + for bunch in group: + btc_address, btc_balance = bunch[:2] + if btc_address == address: + return btc_balance + return 0 + +def is_locked(): + getinfo = rpc('getwalletinfo', []) + if 'unlocked_until' in getinfo: + if getinfo['unlocked_until'] >= 10: + return False # Wallet is unlocked for at least the next 10 seconds. + else: + return True # Wallet is locked + else: + False + +def unlock(passphrase): + return rpc('walletpassphrase', [passphrase, 60]) + +def send_raw_transaction(tx_hex): + return rpc('sendrawtransaction', [tx_hex]) + +def wallet_last_block(): + getinfo = rpc('getinfo', []) + return getinfo['blocks'] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/counterpartycli/wallet/btcwallet.py b/counterparty-cli/counterpartycli/wallet/btcwallet.py new file mode 100644 index 0000000000..68690b0f5d --- /dev/null +++ b/counterparty-cli/counterpartycli/wallet/btcwallet.py @@ -0,0 +1,74 @@ +import binascii +import logging +logger = logging.getLogger(__name__) +import sys +import json +import time +import requests + +from counterpartylib.lib import config +from counterpartycli.util import wallet_api as rpc + +def get_wallet_addresses(): + addresses = [] + for output in rpc('listunspent', [0, 99999]): + if output['address'] not in addresses: + addresses.append(output['address']) + return addresses + +def get_btc_balances(): + addresses = {} + for output in rpc('listunspent', [0, 99999]): + if output['address'] not in addresses: + addresses[output['address']] = 0 + addresses[output['address']] += output['amount'] + + for address in addresses: + yield [address, addresses[address]] + +def list_unspent(): + return rpc('listunspent', [0, 99999]) + +def sign_raw_transaction(tx_hex): + return rpc('signrawtransaction', [tx_hex])['hex'] + +def is_valid(address): + address_info = rpc('validateaddress', [address]) + # btcwallet return valid for pubkey + if address_info['isvalid'] and address_info['address'] == address: + return True + return False + +def is_mine(address): + address_info = rpc('validateaddress', [address]) + if 'ismine' not in address_info: + return False + return address_info['ismine'] + +def get_pubkey(address): + address_infos = rpc('validateaddress', [address]) + if address_infos['isvalid'] and address_infos['ismine']: + return address_infos['pubkey'] + return None + +def get_btc_balance(address): + balance = 0 + for output in rpc('listunspent', [0, 99999]): + if output['address'] == address: + balance += output['amount'] + return balance + +def is_locked(): + return rpc('walletislocked', []) + +def unlock(passphrase): + return rpc('walletpassphrase', [passphrase, 60]) + +def send_raw_transaction(tx_hex): + return rpc('sendrawtransaction', [tx_hex]) + +def wallet_last_block(): + getinfo = rpc('getinfo', []) + return getinfo['blocks'] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/counterparty-cli/release_procedure.md b/counterparty-cli/release_procedure.md new file mode 100644 index 0000000000..2397c992b0 --- /dev/null +++ b/counterparty-cli/release_procedure.md @@ -0,0 +1,32 @@ +**@ouziel-slama:** + +- Quality Assurance +- Update `CHANGELOG.md` +- Update `APP_VERSION` in `counterpartycli/__init__.py` +- Update `counterpartylib` version in `setup.py` (if necessary) +- Merge develop into Master +- Build binaries: + * In a new VM install Windows dependencies (http://counterparty.io/docs/windows/) + * `git clone https://github.com/CounterpartyXCP/counterparty-cli.git` + * `cd counterparty-cli` + * `python setup.py install` + * `python setup.py py2exe` +- Send @adamkrellenstein the MD5 of the generated ZIP file + +**@adamkrellenstein:** + +- Tag and Sign Release (include MD5 hash in message) +- Write [Release Notes](https://github.com/CounterpartyXCP/counterpartyd/releases) +- Upload (signed) package to PyPi + * `sudo python3 setup.py sdist build` + * `twine upload -s dist/$NEW_FILES` + +**@ouziel-slama:** + +- Upload ZIP file in [Github Release](https://github.com/CounterpartyXCP/counterparty-cli/releases) + +**@ivanazuber:**: + +- Post to [Official Forums](https://forums.counterparty.io/discussion/445/new-version-announcements-counterparty-and-counterpartyd), Skype, [Gitter](https://gitter.im/CounterpartyXCP) +- Post to social media +- SMS and mailing list notifications diff --git a/counterparty-cli/requirements.txt b/counterparty-cli/requirements.txt new file mode 100644 index 0000000000..4887f9f544 --- /dev/null +++ b/counterparty-cli/requirements.txt @@ -0,0 +1,4 @@ +--index-url https://pypi.python.org/simple/ + +requests>=2.20.0 +-e . diff --git a/counterparty-cli/setup.py b/counterparty-cli/setup.py new file mode 100644 index 0000000000..cb677b3331 --- /dev/null +++ b/counterparty-cli/setup.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +from setuptools.command.install import install as _install +from setuptools import setup, find_packages, Command +import os, sys +import shutil +import ctypes.util +from counterpartycli import APP_VERSION + +class generate_configuration_files(Command): + description = "Generate configfiles from old files or bitcoind config file" + user_options = [] + + def initialize_options(self): + pass + def finalize_options(self): + pass + + def run(self): + from counterpartycli.setup import generate_config_files + generate_config_files() + +class install(_install): + description = "Install counterparty-cli and dependencies" + + def run(self): + caller = sys._getframe(2) + caller_module = caller.f_globals.get('__name__','') + caller_name = caller.f_code.co_name + if caller_module == 'distutils.dist' or caller_name == 'run_commands': + _install.run(self) + else: + self.do_egg_install() + self.run_command('generate_configuration_files') + +required_packages = [ + 'appdirs==1.4.0', + 'setuptools-markdown==0.2', + 'prettytable==0.7.2', + 'colorlog==2.7.0', + 'python-dateutil==2.5.3', + 'requests>=2.20.0', + 'counterparty-lib' +] + +setup_options = { + 'name': 'counterparty-cli', + 'version': APP_VERSION, + 'author': 'Counterparty Developers', + 'author_email': 'dev@counterparty.io', + 'maintainer': 'Counterparty Developers', + 'maintainer_email': 'dev@counterparty.io', + 'url': 'http://counterparty.io', + 'license': 'MIT', + 'description': 'Counterparty Protocol Command-Line Interface', + 'long_description': '', + 'keywords': 'counterparty,bitcoin', + 'classifiers': [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Financial and Insurance Industry", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Office/Business :: Financial", + "Topic :: System :: Distributed Computing" + ], + 'download_url': 'https://github.com/CounterpartyXCP/counterparty-cli/releases/tag/' + APP_VERSION, + 'provides': ['counterpartycli'], + 'packages': find_packages(), + 'zip_safe': False, + 'setup_requires': ['setuptools-markdown',], + 'install_requires': required_packages, + 'entry_points': { + 'console_scripts': [ + 'counterparty-client = counterpartycli:client_main', + 'counterparty-server = counterpartycli:server_main', + ] + }, + 'cmdclass': { + 'install': install, + 'generate_configuration_files': generate_configuration_files + } +} +# prepare Windows binaries +if sys.argv[1] == 'py2exe': + import py2exe + from py2exe.distutils_buildexe import py2exe as _py2exe + + WIN_DIST_DIR = 'counterparty-cli-win32-{}'.format(APP_VERSION) + + class py2exe(_py2exe): + def run(self): + from counterpartycli.setup import before_py2exe_build, after_py2exe_build + # prepare build + before_py2exe_build(WIN_DIST_DIR) + # build exe's + _py2exe.run(self) + # tweak build + after_py2exe_build(WIN_DIST_DIR) + + # Update setup_options with py2exe specifics options + setup_options.update({ + 'console': [ + 'counterparty-client.py', + 'counterparty-server.py' + ], + 'zipfile': 'library/site-packages.zip', + 'options': { + 'py2exe': { + 'dist_dir': WIN_DIST_DIR + } + }, + 'cmdclass': { + 'py2exe': py2exe + } + }) +# prepare PyPi package +elif sys.argv[1] == 'sdist': + setup_options['long_description_markdown_filename'] = 'README.md' + +setup(**setup_options)