├── requirements.txt ├── .travis.yml ├── .gitignore ├── counterpartycli ├── __init__.py ├── wallet │ ├── bitcoincore.py │ ├── btcwallet.py │ └── __init__.py ├── console.py ├── util.py ├── setup.py ├── server.py ├── clientapi.py ├── messages.py └── client.py ├── README.md ├── CONTRIBUTING.md ├── LICENSE ├── release_procedure.md ├── ChangeLog.md └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | --index-url https://pypi.python.org/simple/ 2 | 3 | requests>=2.20.0 4 | -e . 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | install: 5 | - pip install -r requirements.txt 6 | - python setup.py install 7 | script: echo "Correctly installed" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # precompiled python 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | 7 | # Setuptools build folder. 8 | /build/ 9 | 10 | # Python egg metadata, regenerated from source files by setuptools. 11 | /*.egg-info -------------------------------------------------------------------------------- /counterpartycli/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | APP_VERSION = '1.1.5' 4 | 5 | CURR_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser('__file__')))) 6 | WIN_EXE_LIB = os.path.normpath(os.path.join(CURR_DIR, 'library')) 7 | if os.path.isdir(WIN_EXE_LIB): 8 | sys.path.insert(0, WIN_EXE_LIB) 9 | 10 | def client_main(): 11 | from counterpartycli import client 12 | client.main() 13 | 14 | def server_main(): 15 | from counterpartycli import server 16 | server.main() 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version](https://pypip.in/version/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) 2 | [![Supported Python versions](https://pypip.in/py_versions/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) 3 | [![License](https://pypip.in/license/counterparty-cli/badge.svg)](https://pypi.python.org/pypi/counterparty-cli/) 4 | [![Slack Status](http://slack.counterparty.io/badge.svg)](http://slack.counterparty.io) 5 | 6 | `counterparty-cli` is a command line interface for [`counterparty-lib`](https://github.com/CounterpartyXCP/counterparty-lib). 7 | 8 | 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/). 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Security Issues 2 | 3 | * If you’ve identified a potential **security issue**, please contact us 4 | directly at . 5 | 6 | 7 | # Reporting an Issue 8 | 9 | * Check to see if the issue has already been reported. 10 | 11 | * Run with verbose logging and paste the relevant log output. 12 | 13 | * List the exact version/commit being run, as well as the platform the software 14 | is running on. 15 | 16 | 17 | # Making a Pull Request 18 | 19 | * Make (almost) all pull requests against the `develop` branch. 20 | 21 | * All original code should follow [PEP8](https://www.python.org/dev/peps/pep-0008/). 22 | 23 | * Code contributions should be well‐commented. 24 | 25 | * Commit messages should be neatly formatted and descriptive, with a summary line. 26 | 27 | * Commits should be organized into logical units. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-Present Counterparty Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /release_procedure.md: -------------------------------------------------------------------------------- 1 | **@ouziel-slama:** 2 | 3 | - Quality Assurance 4 | - Update `CHANGELOG.md` 5 | - Update `APP_VERSION` in `counterpartycli/__init__.py` 6 | - Update `counterpartylib` version in `setup.py` (if necessary) 7 | - Merge develop into Master 8 | - Build binaries: 9 | * In a new VM install Windows dependencies (http://counterparty.io/docs/windows/) 10 | * `git clone https://github.com/CounterpartyXCP/counterparty-cli.git` 11 | * `cd counterparty-cli` 12 | * `python setup.py install` 13 | * `python setup.py py2exe` 14 | - Send @adamkrellenstein the MD5 of the generated ZIP file 15 | 16 | **@adamkrellenstein:** 17 | 18 | - Tag and Sign Release (include MD5 hash in message) 19 | - Write [Release Notes](https://github.com/CounterpartyXCP/counterpartyd/releases) 20 | - Upload (signed) package to PyPi 21 | * `sudo python3 setup.py sdist build` 22 | * `twine upload -s dist/$NEW_FILES` 23 | 24 | **@ouziel-slama:** 25 | 26 | - Upload ZIP file in [Github Release](https://github.com/CounterpartyXCP/counterparty-cli/releases) 27 | 28 | **@ivanazuber:**: 29 | 30 | - Post to [Official Forums](https://forums.counterparty.io/discussion/445/new-version-announcements-counterparty-and-counterpartyd), Skype, [Gitter](https://gitter.im/CounterpartyXCP) 31 | - Post to social media 32 | - SMS and mailing list notifications 33 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## Command Line Interface Versions ## 2 | * master (unreleased) 3 | * Added indexd arguments 4 | * removed backend-name argument 5 | * v1.1.4 (2017/10/26) 6 | * Added enhanced send arguments support. 7 | * v1.1.3 (2017/05/01) 8 | * Added `vacuum` command to server CLI. 9 | * v1.1.2 (2016/07/11) 10 | * Added P2SH support (to match counterparty-lib 9.55.0) 11 | * added `get_tx_info` command 12 | * 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) 13 | * Peg dependency versions in `setup.py` 14 | * Added `debug_config` argument to print config to CLI. 15 | * Added `--quiet` flag to `bootstrap` command 16 | * Logging improvements 17 | * Removed `rps` and `rpsresolve` commands 18 | * Updated `README.md` 19 | * v1.1.1 (2015/04/20) 20 | * Fix `broadcast` command 21 | * Cleaner, Commented-out Default Config Files 22 | * Support new configuration parameter: `no-check-asset-conservation`, `rpc-batch-size`, `requests-timeout` 23 | * v1.1.0 (2015/03/31) 24 | * Code reorganisation 25 | * Remove `market` command 26 | * Add `getrows` command 27 | * Add `clientapi` module 28 | * Rename `get_running_info` to `getinfo` 29 | * Rename `backend-ssl-verify` to `backend-ssl-no-verify` 30 | * Rename `rpc-allow-cors` to `rpc-no-allow-cors` 31 | * Change installation procedure 32 | * v1.0.1 (2015/03/18) 33 | * Update minimum `counterparty-lib` version from `v9.49.4` to `v9.50.0` 34 | * v1.0.0 (2015/02/05) 35 | * Initial Release 36 | -------------------------------------------------------------------------------- /counterpartycli/wallet/bitcoincore.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | import sys 5 | import json 6 | import time 7 | import requests 8 | 9 | from counterpartylib.lib import config 10 | from counterpartycli.util import wallet_api as rpc 11 | 12 | def get_wallet_addresses(): 13 | addresses = [] 14 | for group in rpc('listaddressgroupings', []): 15 | for bunch in group: 16 | address, btc_balance = bunch[:2] 17 | addresses.append(address) 18 | return addresses 19 | 20 | def get_btc_balances(): 21 | for group in rpc('listaddressgroupings', []): 22 | for bunch in group: 23 | yield bunch[:2] 24 | 25 | def list_unspent(): 26 | return rpc('listunspent', [0, 99999]) 27 | 28 | def sign_raw_transaction(tx_hex): 29 | return rpc('signrawtransactionwithwallet', [tx_hex])['hex'] 30 | 31 | def is_valid(address): 32 | return rpc('validateaddress', [address])['isvalid'] 33 | 34 | def is_mine(address): 35 | return rpc('getaddressinfo', [address])['ismine'] 36 | 37 | def get_pubkey(address): 38 | address_valid = rpc('validateaddress', [address]) 39 | address_infos = rpc('getaddressinfo', [address]) 40 | if address_valid['isvalid'] and address_infos['ismine']: 41 | return address_infos['pubkey'] 42 | return None 43 | 44 | def get_btc_balance(address): 45 | for group in rpc('listaddressgroupings', []): 46 | for bunch in group: 47 | btc_address, btc_balance = bunch[:2] 48 | if btc_address == address: 49 | return btc_balance 50 | return 0 51 | 52 | def is_locked(): 53 | getinfo = rpc('getwalletinfo', []) 54 | if 'unlocked_until' in getinfo: 55 | if getinfo['unlocked_until'] >= 10: 56 | return False # Wallet is unlocked for at least the next 10 seconds. 57 | else: 58 | return True # Wallet is locked 59 | else: 60 | False 61 | 62 | def unlock(passphrase): 63 | return rpc('walletpassphrase', [passphrase, 60]) 64 | 65 | def send_raw_transaction(tx_hex): 66 | return rpc('sendrawtransaction', [tx_hex]) 67 | 68 | def wallet_last_block(): 69 | getinfo = rpc('getinfo', []) 70 | return getinfo['blocks'] 71 | 72 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 73 | -------------------------------------------------------------------------------- /counterpartycli/wallet/btcwallet.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | import sys 5 | import json 6 | import time 7 | import requests 8 | 9 | from counterpartylib.lib import config 10 | from counterpartycli.util import wallet_api as rpc 11 | 12 | def get_wallet_addresses(): 13 | addresses = [] 14 | for output in rpc('listunspent', [0, 99999]): 15 | if output['address'] not in addresses: 16 | addresses.append(output['address']) 17 | return addresses 18 | 19 | def get_btc_balances(): 20 | addresses = {} 21 | for output in rpc('listunspent', [0, 99999]): 22 | if output['address'] not in addresses: 23 | addresses[output['address']] = 0 24 | addresses[output['address']] += output['amount'] 25 | 26 | for address in addresses: 27 | yield [address, addresses[address]] 28 | 29 | def list_unspent(): 30 | return rpc('listunspent', [0, 99999]) 31 | 32 | def sign_raw_transaction(tx_hex): 33 | return rpc('signrawtransaction', [tx_hex])['hex'] 34 | 35 | def is_valid(address): 36 | address_info = rpc('validateaddress', [address]) 37 | # btcwallet return valid for pubkey 38 | if address_info['isvalid'] and address_info['address'] == address: 39 | return True 40 | return False 41 | 42 | def is_mine(address): 43 | address_info = rpc('validateaddress', [address]) 44 | if 'ismine' not in address_info: 45 | return False 46 | return address_info['ismine'] 47 | 48 | def get_pubkey(address): 49 | address_infos = rpc('validateaddress', [address]) 50 | if address_infos['isvalid'] and address_infos['ismine']: 51 | return address_infos['pubkey'] 52 | return None 53 | 54 | def get_btc_balance(address): 55 | balance = 0 56 | for output in rpc('listunspent', [0, 99999]): 57 | if output['address'] == address: 58 | balance += output['amount'] 59 | return balance 60 | 61 | def is_locked(): 62 | return rpc('walletislocked', []) 63 | 64 | def unlock(passphrase): 65 | return rpc('walletpassphrase', [passphrase, 60]) 66 | 67 | def send_raw_transaction(tx_hex): 68 | return rpc('sendrawtransaction', [tx_hex]) 69 | 70 | def wallet_last_block(): 71 | getinfo = rpc('getinfo', []) 72 | return getinfo['blocks'] 73 | 74 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools.command.install import install as _install 3 | from setuptools import setup, find_packages, Command 4 | import os, sys 5 | import shutil 6 | import ctypes.util 7 | from counterpartycli import APP_VERSION 8 | 9 | class generate_configuration_files(Command): 10 | description = "Generate configfiles from old files or bitcoind config file" 11 | user_options = [] 12 | 13 | def initialize_options(self): 14 | pass 15 | def finalize_options(self): 16 | pass 17 | 18 | def run(self): 19 | from counterpartycli.setup import generate_config_files 20 | generate_config_files() 21 | 22 | class install(_install): 23 | description = "Install counterparty-cli and dependencies" 24 | 25 | def run(self): 26 | caller = sys._getframe(2) 27 | caller_module = caller.f_globals.get('__name__','') 28 | caller_name = caller.f_code.co_name 29 | if caller_module == 'distutils.dist' or caller_name == 'run_commands': 30 | _install.run(self) 31 | else: 32 | self.do_egg_install() 33 | self.run_command('generate_configuration_files') 34 | 35 | required_packages = [ 36 | 'appdirs==1.4.0', 37 | 'setuptools-markdown==0.2', 38 | 'prettytable==0.7.2', 39 | 'colorlog==2.7.0', 40 | 'python-dateutil==2.5.3', 41 | 'requests>=2.20.0', 42 | 'counterparty-lib' 43 | ] 44 | 45 | setup_options = { 46 | 'name': 'counterparty-cli', 47 | 'version': APP_VERSION, 48 | 'author': 'Counterparty Developers', 49 | 'author_email': 'dev@counterparty.io', 50 | 'maintainer': 'Counterparty Developers', 51 | 'maintainer_email': 'dev@counterparty.io', 52 | 'url': 'http://counterparty.io', 53 | 'license': 'MIT', 54 | 'description': 'Counterparty Protocol Command-Line Interface', 55 | 'long_description': '', 56 | 'keywords': 'counterparty,bitcoin', 57 | 'classifiers': [ 58 | "Development Status :: 5 - Production/Stable", 59 | "Environment :: Console", 60 | "Intended Audience :: Developers", 61 | "Intended Audience :: End Users/Desktop", 62 | "Intended Audience :: Financial and Insurance Industry", 63 | "License :: OSI Approved :: MIT License", 64 | "Natural Language :: English", 65 | "Operating System :: Microsoft :: Windows", 66 | "Operating System :: POSIX", 67 | "Programming Language :: Python :: 3 :: Only", 68 | "Topic :: Office/Business :: Financial", 69 | "Topic :: System :: Distributed Computing" 70 | ], 71 | 'download_url': 'https://github.com/CounterpartyXCP/counterparty-cli/releases/tag/' + APP_VERSION, 72 | 'provides': ['counterpartycli'], 73 | 'packages': find_packages(), 74 | 'zip_safe': False, 75 | 'setup_requires': ['setuptools-markdown',], 76 | 'install_requires': required_packages, 77 | 'entry_points': { 78 | 'console_scripts': [ 79 | 'counterparty-client = counterpartycli:client_main', 80 | 'counterparty-server = counterpartycli:server_main', 81 | ] 82 | }, 83 | 'cmdclass': { 84 | 'install': install, 85 | 'generate_configuration_files': generate_configuration_files 86 | } 87 | } 88 | # prepare Windows binaries 89 | if sys.argv[1] == 'py2exe': 90 | import py2exe 91 | from py2exe.distutils_buildexe import py2exe as _py2exe 92 | 93 | WIN_DIST_DIR = 'counterparty-cli-win32-{}'.format(APP_VERSION) 94 | 95 | class py2exe(_py2exe): 96 | def run(self): 97 | from counterpartycli.setup import before_py2exe_build, after_py2exe_build 98 | # prepare build 99 | before_py2exe_build(WIN_DIST_DIR) 100 | # build exe's 101 | _py2exe.run(self) 102 | # tweak build 103 | after_py2exe_build(WIN_DIST_DIR) 104 | 105 | # Update setup_options with py2exe specifics options 106 | setup_options.update({ 107 | 'console': [ 108 | 'counterparty-client.py', 109 | 'counterparty-server.py' 110 | ], 111 | 'zipfile': 'library/site-packages.zip', 112 | 'options': { 113 | 'py2exe': { 114 | 'dist_dir': WIN_DIST_DIR 115 | } 116 | }, 117 | 'cmdclass': { 118 | 'py2exe': py2exe 119 | } 120 | }) 121 | # prepare PyPi package 122 | elif sys.argv[1] == 'sdist': 123 | setup_options['long_description_markdown_filename'] = 'README.md' 124 | 125 | setup(**setup_options) 126 | -------------------------------------------------------------------------------- /counterpartycli/console.py: -------------------------------------------------------------------------------- 1 | import os 2 | from prettytable import PrettyTable 3 | from counterpartycli import wallet, util 4 | 5 | # TODO: inelegant 6 | def get_view(view_name, args): 7 | if view_name == 'balances': 8 | return wallet.balances(args.address) 9 | elif view_name == 'asset': 10 | return wallet.asset(args.asset) 11 | elif view_name == 'wallet': 12 | return wallet.wallet() 13 | elif view_name == 'pending': 14 | return wallet.pending() 15 | elif view_name == 'getinfo': 16 | return util.api('get_running_info') 17 | elif view_name == 'get_tx_info': 18 | return util.api('get_tx_info', {'tx_hex': args.tx_hex}) 19 | elif view_name == 'getrows': 20 | method = 'get_{}'.format(args.table) 21 | if args.filter: 22 | filters = [tuple(f) for f in args.filter] 23 | else: 24 | filters = [] 25 | params = { 26 | 'filters': filters, 27 | 'filterop': args.filter_op, 28 | 'order_by': args.order_by, 29 | 'order_dir': args.order_dir, 30 | 'start_block': args.start_block, 31 | 'end_block': args.end_block, 32 | 'status': args.status, 33 | 'limit': args.limit, 34 | 'offset': args.offset 35 | } 36 | return util.api(method, params) 37 | 38 | def print_balances(balances): 39 | lines = [] 40 | lines.append('') 41 | lines.append('Address Balances') 42 | table = PrettyTable(['Asset', 'Amount']) 43 | for asset in balances: 44 | table.add_row([asset, balances[asset]]) 45 | lines.append(table.get_string()) 46 | lines.append('') 47 | print(os.linesep.join(lines)) 48 | 49 | def print_asset(asset): 50 | lines = [] 51 | lines.append('') 52 | lines.append('Asset Details') 53 | table = PrettyTable(header=False, align='l') 54 | table.add_row(['Asset Name:', asset['asset']]) 55 | table.add_row(['Asset ID:', asset['asset_id']]) 56 | table.add_row(['Divisible:', asset['divisible']]) 57 | table.add_row(['Locked:', asset['locked']]) 58 | table.add_row(['Supply:', asset['supply']]) 59 | table.add_row(['Issuer:', asset['issuer']]) 60 | table.add_row(['Description:', '‘' + asset['description'] + '’']) 61 | table.add_row(['Balance:', asset['balance']]) 62 | lines.append(table.get_string()) 63 | 64 | if asset['addresses']: 65 | lines.append('') 66 | lines.append('Wallet Balances') 67 | table = PrettyTable(['Address', 'Balance']) 68 | for address in asset['addresses']: 69 | balance = asset['addresses'][address] 70 | table.add_row([address, balance]) 71 | lines.append(table.get_string()) 72 | 73 | if asset['sends']: 74 | lines.append('') 75 | lines.append('Wallet Sends and Receives') 76 | table = PrettyTable(['Type', 'Quantity', 'Source', 'Destination']) 77 | for send in asset['sends']: 78 | table.add_row([send['type'], send['quantity'], send['source'], send['destination']]) 79 | lines.append(table.get_string()) 80 | 81 | lines.append('') 82 | print(os.linesep.join(lines)) 83 | 84 | def print_wallet(wallet): 85 | lines = [] 86 | for address in wallet['addresses']: 87 | table = PrettyTable(['Asset', 'Balance']) 88 | for asset in wallet['addresses'][address]: 89 | balance = wallet['addresses'][address][asset] 90 | table.add_row([asset, balance]) 91 | lines.append(address) 92 | lines.append(table.get_string()) 93 | lines.append('') 94 | total_table = PrettyTable(['Asset', 'Balance']) 95 | for asset in wallet['assets']: 96 | balance = wallet['assets'][asset] 97 | total_table.add_row([asset, balance]) 98 | lines.append('TOTAL') 99 | lines.append(total_table.get_string()) 100 | lines.append('') 101 | print(os.linesep.join(lines)) 102 | 103 | def print_pending(awaiting_btcs): 104 | table = PrettyTable(['Matched Order ID', 'Time Left']) 105 | for order_match in awaiting_btcs: 106 | order_match = format_order_match(order_match) 107 | table.add_row(order_match) 108 | print(table) 109 | 110 | def print_getrows(rows): 111 | if len(rows) > 0: 112 | headers = list(rows[0].keys()) 113 | table = PrettyTable(headers) 114 | for row in rows: 115 | values = list(row.values()) 116 | table.add_row(values) 117 | print(table) 118 | else: 119 | print("No result.") 120 | 121 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 122 | -------------------------------------------------------------------------------- /counterpartycli/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import getpass 3 | import binascii 4 | import logging 5 | logger = logging.getLogger(__name__) 6 | import sys 7 | import json 8 | import time 9 | from decimal import Decimal as D 10 | 11 | from counterpartycli.wallet import bitcoincore, btcwallet 12 | from counterpartylib.lib import config, util, exceptions, script 13 | from counterpartycli.util import api, value_out 14 | 15 | from pycoin.tx import Tx, SIGHASH_ALL 16 | from pycoin.encoding import wif_to_tuple_of_secret_exponent_compressed, public_pair_to_hash160_sec 17 | from pycoin.ecdsa import generator_secp256k1, public_pair_for_secret_exponent 18 | 19 | class WalletError(Exception): 20 | pass 21 | 22 | class LockedWalletError(WalletError): 23 | pass 24 | 25 | def WALLET(): 26 | return sys.modules['counterpartycli.wallet.{}'.format(config.WALLET_NAME)] 27 | 28 | def get_wallet_addresses(): 29 | return WALLET().get_wallet_addresses() 30 | 31 | def get_btc_balances(): 32 | for address, btc_balance in WALLET().get_btc_balances(): 33 | yield [address, btc_balance] 34 | 35 | def pycoin_sign_raw_transaction(tx_hex, private_key_wif): 36 | for char in private_key_wif: 37 | if char not in script.b58_digits: 38 | raise exceptions.TransactionError('invalid private key') 39 | 40 | if config.TESTNET: 41 | allowable_wif_prefixes = [config.PRIVATEKEY_VERSION_TESTNET] 42 | else: 43 | allowable_wif_prefixes = [config.PRIVATEKEY_VERSION_MAINNET] 44 | 45 | secret_exponent, compressed = wif_to_tuple_of_secret_exponent_compressed( 46 | private_key_wif, allowable_wif_prefixes=allowable_wif_prefixes) 47 | public_pair = public_pair_for_secret_exponent(generator_secp256k1, secret_exponent) 48 | hash160 = public_pair_to_hash160_sec(public_pair, compressed) 49 | hash160_lookup = {hash160: (secret_exponent, public_pair, compressed)} 50 | 51 | tx = Tx.from_hex(tx_hex) 52 | for idx, tx_in in enumerate(tx.txs_in): 53 | tx.sign_tx_in(hash160_lookup, idx, tx_in.script, hash_type=SIGHASH_ALL) 54 | 55 | return tx.as_hex() 56 | 57 | def sign_raw_transaction(tx_hex, private_key_wif=None): 58 | if private_key_wif is None: 59 | if WALLET().is_locked(): 60 | raise LockedWalletError('Wallet is locked.') 61 | return WALLET().sign_raw_transaction(tx_hex) 62 | else: 63 | return pycoin_sign_raw_transaction(tx_hex, private_key_wif) 64 | 65 | def get_pubkey(address): 66 | return WALLET().get_pubkey(address) 67 | 68 | def is_valid(address): 69 | return WALLET().is_valid(address) 70 | 71 | def is_mine(address): 72 | return WALLET().is_mine(address) 73 | 74 | def get_btc_balance(address): 75 | return WALLET().get_btc_balance(address) 76 | 77 | def list_unspent(): 78 | return WALLET().list_unspent() 79 | 80 | def send_raw_transaction(tx_hex): 81 | return WALLET().send_raw_transaction(tx_hex) 82 | 83 | def is_locked(): 84 | return WALLET().is_locked() 85 | 86 | def unlock(passphrase): 87 | return WALLET().unlock(passphrase) 88 | 89 | def wallet_last_block(): 90 | return WALLET().wallet_last_block() 91 | 92 | def wallet(): 93 | wallet = { 94 | 'addresses': {}, 95 | 'assets': {} 96 | } 97 | 98 | def add_total(address, asset, quantity): 99 | if quantity: 100 | if address not in wallet['addresses']: 101 | wallet['addresses'][address] = {} 102 | if asset not in wallet['assets']: 103 | wallet['assets'][asset] = 0 104 | if asset not in wallet['addresses'][address]: 105 | wallet['addresses'][address][asset] = 0 106 | wallet['addresses'][address][asset] += quantity 107 | wallet['assets'][asset] += quantity 108 | 109 | for bunch in get_btc_balances(): 110 | address, btc_balance = bunch 111 | add_total(address, 'BTC', btc_balance) 112 | balances = api('get_balances', {'filters': [('address', '==', address),]}) 113 | for balance in balances: 114 | asset = balance['asset'] 115 | balance = D(value_out(balance['quantity'], asset)) 116 | add_total(address, asset, balance) 117 | 118 | return wallet 119 | 120 | def asset(asset_name): 121 | supply = api('get_supply', {'asset': asset_name}) 122 | asset_id = api('get_assets', {'filters': [('asset_name', '==', asset_name),]})[0]['asset_id'] 123 | asset_info = { 124 | 'asset': asset_name, 125 | 'supply': D(value_out(supply, asset_name)), 126 | 'asset_id': asset_id 127 | } 128 | if asset_name in ['XCP', 'BTC']: 129 | asset_info.update({ 130 | 'owner': None, 131 | 'divisible': True, 132 | 'locked': False, 133 | 'description': '', 134 | 'issuer': None 135 | }) 136 | else: 137 | issuances = api('get_issuances', { 138 | 'filters': [('asset', '==', asset_name),], 139 | 'status': 'valid', 140 | 'order_by': 'tx_index', 141 | 'order_dir': 'DESC', 142 | }) 143 | if not issuances: 144 | raise WalletError('Asset not found') 145 | locked = False 146 | for issuance in issuances: 147 | if issuance['locked']: 148 | locked = True 149 | issuance = issuances[0] 150 | asset_info.update({ 151 | 'owner': issuance['issuer'], 152 | 'divisible': bool(issuance['divisible']), 153 | 'locked': locked, 154 | 'description': issuance['description'], 155 | 'issuer': issuance['issuer'] 156 | }) 157 | 158 | asset_info['balance'] = 0 159 | asset_info['addresses'] = {} 160 | 161 | for bunch in get_btc_balances(): 162 | address, btc_balance = bunch 163 | if asset_name == 'BTC': 164 | balance = btc_balance 165 | else: 166 | balances = api('get_balances', {'filters': [('address', '==', address), ('asset', '==', asset_name)]}) 167 | if balances: 168 | balance = balances[0] 169 | balance = D(value_out(balance['quantity'], asset_name)) 170 | else: 171 | balance = 0 172 | if balance: 173 | asset_info['balance'] += balance 174 | asset_info['addresses'][address] = balance 175 | 176 | addresses = list(asset_info['addresses'].keys()) 177 | 178 | if asset_name != 'BTC': 179 | all_sends = api('get_sends', {'filters': [('source', 'IN', addresses), ('destination', 'IN', addresses)], 'filterop': 'OR', 'status': 'valid'}) 180 | sends = [] 181 | for send in all_sends: 182 | if send['asset'] == asset_name: 183 | if send['source'] in addresses and send['destination'] in addresses: 184 | tx_type = 'in-wallet' 185 | elif send['source'] in addresses: 186 | tx_type = 'send' 187 | elif send['destination'] in addresses: 188 | tx_type = 'receive' 189 | send['type'] = tx_type 190 | send['quantity'] = D(value_out(send['quantity'], asset_name)) 191 | sends.append(send) 192 | asset_info['sends'] = sends 193 | 194 | return asset_info 195 | 196 | def balances(address): 197 | result = { 198 | 'BTC': get_btc_balance(address) 199 | } 200 | balances = api('get_balances', {'filters': [('address', '==', address),]}) 201 | for balance in balances: 202 | asset = balance['asset'] 203 | balance = D(value_out(balance['quantity'], asset)) 204 | result[asset] = balance 205 | return result 206 | 207 | def pending(): 208 | addresses = [] 209 | for bunch in get_btc_balances(): 210 | addresses.append(bunch[0]) 211 | filters = [ 212 | ('tx0_address', 'IN', addresses), 213 | ('tx1_address', 'IN', addresses) 214 | ] 215 | awaiting_btcs = api('get_order_matches', {'filters': filters, 'filterop': 'OR', 'status': 'pending'}) 216 | return awaiting_btcs 217 | 218 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 219 | -------------------------------------------------------------------------------- /counterpartycli/util.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import sys 4 | import os 5 | import threading 6 | import decimal 7 | import time 8 | import json 9 | import re 10 | import requests 11 | import collections 12 | import logging 13 | import binascii 14 | from datetime import datetime 15 | from dateutil.tz import tzlocal 16 | import argparse 17 | import configparser 18 | import appdirs 19 | import tarfile 20 | import urllib.request 21 | import shutil 22 | import codecs 23 | import tempfile 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | D = decimal.Decimal 28 | 29 | from counterpartylib import server 30 | from counterpartylib.lib import config, check 31 | from counterpartylib.lib.util import value_input, value_output 32 | 33 | rpc_sessions = {} 34 | 35 | class JsonDecimalEncoder(json.JSONEncoder): 36 | def default(self, o): 37 | if isinstance(o, D): 38 | return str(o) 39 | return super(JsonDecimalEncoder, self).default(o) 40 | 41 | json_dump = lambda x: json.dumps(x, sort_keys=True, indent=4, cls=JsonDecimalEncoder) 42 | json_print = lambda x: print(json_dump(x)) 43 | 44 | class RPCError(Exception): 45 | pass 46 | class AssetError(Exception): 47 | pass 48 | 49 | def rpc(url, method, params=None, ssl_verify=False, tries=1): 50 | headers = {'content-type': 'application/json'} 51 | payload = { 52 | "method": method, 53 | "params": params, 54 | "jsonrpc": "2.0", 55 | "id": 0, 56 | } 57 | 58 | if url not in rpc_sessions: 59 | rpc_session = requests.Session() 60 | rpc_sessions[url] = rpc_session 61 | else: 62 | rpc_session = rpc_sessions[url] 63 | 64 | response = None 65 | for i in range(tries): 66 | try: 67 | response = rpc_session.post(url, data=json.dumps(payload), headers=headers, verify=ssl_verify, timeout=config.REQUESTS_TIMEOUT) 68 | if i > 0: 69 | logger.debug('Successfully connected.') 70 | break 71 | except requests.exceptions.SSLError as e: 72 | raise e 73 | except requests.exceptions.Timeout as e: 74 | raise e 75 | except requests.exceptions.ConnectionError: 76 | logger.debug('Could not connect to {}. (Try {}/{})'.format(url, i+1, tries)) 77 | time.sleep(5) 78 | 79 | if response == None: 80 | raise RPCError('Cannot communicate with {}.'.format(url)) 81 | elif response.status_code not in (200, 500): 82 | raise RPCError(str(response.status_code) + ' ' + response.reason + ' ' + response.text) 83 | 84 | # Return result, with error handling. 85 | response_json = response.json() 86 | if 'error' not in response_json.keys() or response_json['error'] == None: 87 | return response_json['result'] 88 | else: 89 | raise RPCError('{}'.format(response_json['error'])) 90 | 91 | def api(method, params=None): 92 | return rpc(config.COUNTERPARTY_RPC, method, params=params, ssl_verify=config.COUNTERPARTY_RPC_SSL_VERIFY) 93 | 94 | def wallet_api(method, params=None): 95 | return rpc(config.WALLET_URL, method, params=params, ssl_verify=config.WALLET_SSL_VERIFY) 96 | 97 | def is_divisible(asset): 98 | if asset in (config.BTC, config.XCP, 'leverage', 'value', 'fraction', 'price', 'odds'): 99 | return True 100 | else: 101 | sql = '''SELECT * FROM issuances WHERE (status = ? AND asset = ?)''' 102 | bindings = ['valid', asset] 103 | issuances = api('sql', {'query': sql, 'bindings': bindings}) 104 | 105 | if not issuances: raise AssetError('No such asset: {}'.format(asset)) 106 | return issuances[0]['divisible'] 107 | 108 | def value_in(quantity, asset, divisible=None): 109 | if divisible is None: 110 | divisible = is_divisible(asset) 111 | return value_input(quantity, asset, divisible) 112 | 113 | def value_out(quantity, asset, divisible=None): 114 | if divisible is None: 115 | divisible = is_divisible(asset) 116 | return value_output(quantity, asset, divisible) 117 | 118 | def bootstrap(testnet=False, overwrite=True, ask_confirmation=False, quiet=False): 119 | data_dir = appdirs.user_data_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) 120 | 121 | # Set Constants. 122 | if testnet: 123 | if check.CONSENSUS_HASH_VERSION_TESTNET < 7: 124 | BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-testnet.latest.tar.gz' 125 | else: 126 | BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-testnet-{}.latest.tar.gz'.format(check.CONSENSUS_HASH_VERSION_TESTNET) 127 | TARBALL_PATH = os.path.join(tempfile.gettempdir(), 'counterpartyd-testnet-db.latest.tar.gz') 128 | DATABASE_PATH = os.path.join(data_dir, '{}.testnet.db'.format(config.APP_NAME)) 129 | else: 130 | if check.CONSENSUS_HASH_VERSION_MAINNET < 3: 131 | BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db.latest.tar.gz' 132 | else: 133 | BOOTSTRAP_URL = 'https://counterparty.io/bootstrap/counterparty-db-{}.latest.tar.gz'.format(check.CONSENSUS_HASH_VERSION_MAINNET) 134 | TARBALL_PATH = os.path.join(tempfile.gettempdir(), 'counterpartyd-db.latest.tar.gz') 135 | DATABASE_PATH = os.path.join(data_dir, '{}.db'.format(config.APP_NAME)) 136 | 137 | # Prepare Directory. 138 | if not os.path.exists(data_dir): 139 | os.makedirs(data_dir, mode=0o755) 140 | if not overwrite and os.path.exists(DATABASE_PATH): 141 | return 142 | 143 | # Define Progress Bar. 144 | def reporthook(blocknum, blocksize, totalsize): 145 | readsofar = blocknum * blocksize 146 | if totalsize > 0: 147 | percent = readsofar * 1e2 / totalsize 148 | s = "\r%5.1f%% %*d / %d" % ( 149 | percent, len(str(totalsize)), readsofar, totalsize) 150 | sys.stderr.write(s) 151 | if readsofar >= totalsize: # near the end 152 | sys.stderr.write("\n") 153 | else: # total size is unknown 154 | sys.stderr.write("read %d\n" % (readsofar,)) 155 | 156 | print('Downloading database from {}...'.format(BOOTSTRAP_URL)) 157 | urllib.request.urlretrieve(BOOTSTRAP_URL, TARBALL_PATH, reporthook if not quiet else None) 158 | 159 | print('Extracting to "%s"...' % data_dir) 160 | with tarfile.open(TARBALL_PATH, 'r:gz') as tar_file: 161 | tar_file.extractall(path=data_dir) 162 | 163 | assert os.path.exists(DATABASE_PATH) 164 | os.chmod(DATABASE_PATH, 0o660) 165 | 166 | print('Cleaning up...') 167 | os.remove(TARBALL_PATH) 168 | os.remove(os.path.join(data_dir, 'checksums.txt')) 169 | 170 | # Set default values of command line arguments with config file 171 | def add_config_arguments(arg_parser, config_args, default_config_file, config_file_arg_name='config_file'): 172 | cmd_args = arg_parser.parse_known_args()[0] 173 | 174 | config_file = getattr(cmd_args, config_file_arg_name, None) 175 | if not config_file: 176 | config_dir = appdirs.user_config_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) 177 | if not os.path.isdir(config_dir): 178 | os.makedirs(config_dir, mode=0o755) 179 | config_file = os.path.join(config_dir, default_config_file) 180 | 181 | # clean BOM 182 | BUFSIZE = 4096 183 | BOMLEN = len(codecs.BOM_UTF8) 184 | with codecs.open(config_file, 'r+b') as fp: 185 | chunk = fp.read(BUFSIZE) 186 | if chunk.startswith(codecs.BOM_UTF8): 187 | i = 0 188 | chunk = chunk[BOMLEN:] 189 | while chunk: 190 | fp.seek(i) 191 | fp.write(chunk) 192 | i += len(chunk) 193 | fp.seek(BOMLEN, os.SEEK_CUR) 194 | chunk = fp.read(BUFSIZE) 195 | fp.seek(-BOMLEN, os.SEEK_CUR) 196 | fp.truncate() 197 | 198 | logger.debug('Loading configuration file: `{}`'.format(config_file)) 199 | configfile = configparser.SafeConfigParser(allow_no_value=True, inline_comment_prefixes=('#', ';')) 200 | with codecs.open(config_file, 'r', encoding='utf8') as fp: 201 | configfile.readfp(fp) 202 | 203 | if not 'Default' in configfile: 204 | configfile['Default'] = {} 205 | 206 | # Initialize default values with the config file. 207 | for arg in config_args: 208 | key = arg[0][-1].replace('--', '') 209 | if 'action' in arg[1] and arg[1]['action'] == 'store_true' and key in configfile['Default']: 210 | arg[1]['default'] = configfile['Default'].getboolean(key) 211 | elif key in configfile['Default'] and configfile['Default'][key]: 212 | arg[1]['default'] = configfile['Default'][key] 213 | elif key in configfile['Default'] and arg[1].get('nargs', '') == '?' and 'const' in arg[1]: 214 | arg[1]['default'] = arg[1]['const'] # bit of a hack 215 | arg_parser.add_argument(*arg[0], **arg[1]) 216 | 217 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 218 | -------------------------------------------------------------------------------- /counterpartycli/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys 4 | import shutil 5 | import ctypes.util 6 | import configparser, platform 7 | import urllib.request 8 | import tarfile, zipfile 9 | import appdirs 10 | import hashlib 11 | from decimal import Decimal as D 12 | 13 | # generate commented config file from arguments list (client.CONFIG_ARGS and server.CONFIG_ARGS) and known values 14 | def generate_config_file(filename, config_args, known_config={}, overwrite=False): 15 | if not overwrite and os.path.exists(filename): 16 | return 17 | 18 | config_dir = os.path.dirname(os.path.abspath(filename)) 19 | if not os.path.exists(config_dir): 20 | os.makedirs(config_dir, mode=0o755) 21 | 22 | config_lines = [] 23 | config_lines.append('[Default]') 24 | config_lines.append('') 25 | 26 | for arg in config_args: 27 | key = arg[0][-1].replace('--', '') 28 | value = None 29 | 30 | if key in known_config: 31 | value = known_config[key] 32 | elif 'default' in arg[1]: 33 | value = arg[1]['default'] 34 | 35 | if value is None: 36 | value = '' 37 | elif isinstance(value, bool): 38 | value = '1' if value else '0' 39 | elif isinstance(value, (float, D)): 40 | value = format(value, '.8f') 41 | 42 | if 'default' in arg[1] or value == '': 43 | key = '# {}'.format(key) 44 | 45 | config_lines.append('{} = {}\t\t\t\t# {}'.format(key, value, arg[1]['help'])) 46 | 47 | with open(filename, 'w', encoding='utf8') as config_file: 48 | config_file.writelines("\n".join(config_lines)) 49 | os.chmod(filename, 0o660) 50 | 51 | def extract_old_config(): 52 | old_config = {} 53 | 54 | old_appdir = appdirs.user_config_dir(appauthor='Counterparty', appname='counterpartyd', roaming=True) 55 | old_configfile = os.path.join(old_appdir, 'counterpartyd.conf') 56 | 57 | if os.path.exists(old_configfile): 58 | configfile = configparser.SafeConfigParser(allow_no_value=True, inline_comment_prefixes=('#', ';')) 59 | configfile.read(old_configfile) 60 | if 'Default' in configfile: 61 | for key in configfile['Default']: 62 | new_key = key.replace('backend-rpc-', 'backend-') 63 | new_key = new_key.replace('blockchain-service-name', 'backend-name') 64 | new_value = configfile['Default'][key].replace('jmcorgan', 'addrindex') 65 | old_config[new_key] = new_value 66 | 67 | return old_config 68 | 69 | def extract_bitcoincore_config(): 70 | bitcoincore_config = {} 71 | 72 | # Figure out the path to the bitcoin.conf file 73 | if platform.system() == 'Darwin': 74 | btc_conf_file = os.path.expanduser('~/Library/Application Support/Bitcoin/') 75 | elif platform.system() == 'Windows': 76 | btc_conf_file = os.path.join(os.environ['APPDATA'], 'Bitcoin') 77 | else: 78 | btc_conf_file = os.path.expanduser('~/.bitcoin') 79 | btc_conf_file = os.path.join(btc_conf_file, 'bitcoin.conf') 80 | 81 | # Extract contents of bitcoin.conf to build service_url 82 | if os.path.exists(btc_conf_file): 83 | conf = {} 84 | with open(btc_conf_file, 'r') as fd: 85 | # Bitcoin Core accepts empty rpcuser, not specified in btc_conf_file 86 | for line in fd.readlines(): 87 | if '#' in line or '=' not in line: 88 | continue 89 | k, v = line.split('=', 1) 90 | conf[k.strip()] = v.strip() 91 | 92 | config_keys = { 93 | 'rpcport': 'backend-port', 94 | 'rpcuser': 'backend-user', 95 | 'rpcpassword': 'backend-password', 96 | 'rpcssl': 'backend-ssl' 97 | } 98 | 99 | for bitcoind_key in config_keys: 100 | if bitcoind_key in conf: 101 | counterparty_key = config_keys[bitcoind_key] 102 | bitcoincore_config[counterparty_key] = conf[bitcoind_key] 103 | 104 | return bitcoincore_config 105 | 106 | def get_server_known_config(): 107 | server_known_config = {} 108 | 109 | bitcoincore_config = extract_bitcoincore_config() 110 | server_known_config.update(bitcoincore_config) 111 | 112 | old_config = extract_old_config() 113 | server_known_config.update(old_config) 114 | 115 | return server_known_config 116 | 117 | # generate client config from server config 118 | def server_to_client_config(server_config): 119 | client_config = {} 120 | 121 | config_keys = { 122 | 'backend-connect': 'wallet-connect', 123 | 'backend-port': 'wallet-port', 124 | 'backend-user': 'wallet-user', 125 | 'backend-password': 'wallet-password', 126 | 'backend-ssl': 'wallet-ssl', 127 | 'backend-ssl-verify': 'wallet-ssl-verify', 128 | 'rpc-host': 'counterparty-rpc-connect', 129 | 'rpc-port': 'counterparty-rpc-port', 130 | 'rpc-user': 'counterparty-rpc-user', 131 | 'rpc-password': 'counterparty-rpc-password' 132 | } 133 | 134 | for server_key in config_keys: 135 | if server_key in server_config: 136 | client_key = config_keys[server_key] 137 | client_config[client_key] = server_config[server_key] 138 | 139 | return client_config 140 | 141 | def generate_config_files(): 142 | from counterpartycli.server import CONFIG_ARGS as SERVER_CONFIG_ARGS 143 | from counterpartycli.client import CONFIG_ARGS as CLIENT_CONFIG_ARGS 144 | from counterpartylib.lib import config, util 145 | 146 | configdir = appdirs.user_config_dir(appauthor=config.XCP_NAME, appname=config.APP_NAME, roaming=True) 147 | 148 | server_configfile = os.path.join(configdir, 'server.conf') 149 | if not os.path.exists(server_configfile): 150 | # extract known configuration 151 | server_known_config = get_server_known_config() 152 | generate_config_file(server_configfile, SERVER_CONFIG_ARGS, server_known_config) 153 | 154 | client_configfile = os.path.join(configdir, 'client.conf') 155 | if not os.path.exists(client_configfile): 156 | client_known_config = server_to_client_config(server_known_config) 157 | generate_config_file(client_configfile, CLIENT_CONFIG_ARGS, client_known_config) 158 | 159 | def zip_folder(folder_path, zip_path): 160 | zip_file = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) 161 | for root, dirs, files in os.walk(folder_path): 162 | for a_file in files: 163 | zip_file.write(os.path.join(root, a_file)) 164 | zip_file.close() 165 | 166 | def before_py2exe_build(win_dist_dir): 167 | # Clean previous build 168 | if os.path.exists(win_dist_dir): 169 | shutil.rmtree(win_dist_dir) 170 | # py2exe don't manages entry_points 171 | for exe_name in ['client', 'server']: 172 | shutil.copy('counterpartycli/__init__.py', 'counterparty-{}.py'.format(exe_name)) 173 | with open('counterparty-{}.py'.format(exe_name), 'a') as fp: 174 | fp.write('{}_main()'.format(exe_name)) 175 | # Hack 176 | src = 'C:\\Python34\\Lib\\site-packages\\flask_httpauth.py' 177 | dst = 'C:\\Python34\\Lib\\site-packages\\flask\\ext\\httpauth.py' 178 | shutil.copy(src, dst) 179 | 180 | def after_py2exe_build(win_dist_dir): 181 | # clean temporaries scripts 182 | for exe_name in ['client', 'server']: 183 | os.remove('counterparty-{}.py'.format(exe_name)) 184 | # py2exe copies only pyc files in site-packages.zip 185 | # modules with no pyc files must be copied in 'dist/library/' 186 | import counterpartylib, certifi 187 | additionals_modules = [counterpartylib, certifi] 188 | for module in additionals_modules: 189 | moudle_file = os.path.dirname(module.__file__) 190 | dest_file = os.path.join(win_dist_dir, 'library', module.__name__) 191 | shutil.copytree(moudle_file, dest_file) 192 | # additionals DLLs 193 | dlls = ['ssleay32.dll', 'libssl32.dll', 'libeay32.dll'] 194 | dlls.append(ctypes.util.find_msvcrt()) 195 | dlls_path = dlls 196 | for dll in dlls: 197 | dll_path = ctypes.util.find_library(dll) 198 | shutil.copy(dll_path, win_dist_dir) 199 | 200 | # compress distribution folder 201 | zip_path = '{}.zip'.format(win_dist_dir) 202 | zip_folder(win_dist_dir, zip_path) 203 | 204 | # Open,close, read file and calculate MD5 on its contents 205 | with open(zip_path, 'rb') as zip_file: 206 | data = zip_file.read() 207 | md5 = hashlib.md5(data).hexdigest() 208 | 209 | # include MD5 in the zip name 210 | new_zip_path = '{}-{}.zip'.format(win_dist_dir, md5) 211 | os.rename(zip_path, new_zip_path) 212 | 213 | # clean build folder 214 | shutil.rmtree(win_dist_dir) 215 | 216 | # Clean Hack 217 | os.remove('C:\\Python34\\Lib\\site-packages\\flask\\ext\\httpauth.py') 218 | 219 | 220 | # Download bootstrap database 221 | def bootstrap(overwrite=True, ask_confirmation=False): 222 | if ask_confirmation: 223 | question = 'Would you like to bootstrap your local Counterparty database from `https://s3.amazonaws.com/counterparty-bootstrap/`? (y/N): ' 224 | if input(question).lower() != 'y': 225 | return 226 | util.bootstrap(testnet=False) 227 | util.bootstrap(testnet=True) 228 | -------------------------------------------------------------------------------- /counterpartycli/server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import argparse 6 | import logging 7 | logger = logging.getLogger() 8 | 9 | from counterpartylib.lib import log 10 | log.set_logger(logger) 11 | 12 | from counterpartylib import server 13 | from counterpartylib.lib import config 14 | from counterpartycli.util import add_config_arguments, bootstrap 15 | from counterpartycli.setup import generate_config_files 16 | from counterpartycli import APP_VERSION 17 | 18 | APP_NAME = 'counterparty-server' 19 | 20 | CONFIG_ARGS = [ 21 | [('-v', '--verbose'), {'dest': 'verbose', 'action': 'store_true', 'default': False, 'help': 'sets log level to DEBUG instead of WARNING'}], 22 | [('--testnet',), {'action': 'store_true', 'default': False, 'help': 'use {} testnet addresses and block numbers'.format(config.BTC_NAME)}], 23 | [('--testcoin',), {'action': 'store_true', 'default': False, 'help': 'use the test {} network on every blockchain'.format(config.XCP_NAME)}], 24 | [('--regtest',), {'action': 'store_true', 'default': False, 'help': 'use {} regtest addresses and block numbers'.format(config.BTC_NAME)}], 25 | [('--customnet',), {'default': '', 'help': 'use a custom network (specify as UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION with version bytes in HH hex format)'}], 26 | [('--api-limit-rows',), {'type': int, 'default': 1000, 'help': 'limit api calls to the set results (defaults to 1000). Setting to 0 removes the limit.'}], 27 | [('--backend-name',), {'default': 'addrindex', 'help': 'the backend name to connect to'}], 28 | [('--backend-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the backend server'}], 29 | [('--backend-port',), {'type': int, 'help': 'the backend port to connect to'}], 30 | [('--backend-user',), {'default': 'bitcoinrpc', 'help': 'the username used to communicate with backend'}], 31 | [('--backend-password',), {'help': 'the password used to communicate with backend'}], 32 | [('--backend-ssl',), {'action': 'store_true', 'default': False, 'help': 'use SSL to connect to backend (default: false)'}], 33 | [('--backend-ssl-no-verify',), {'action': 'store_true', 'default': False, 'help': 'verify SSL certificate of backend; disallow use of self‐signed certificates (default: true)'}], 34 | [('--backend-poll-interval',), {'type': float, 'default': 0.5, 'help': 'poll interval, in seconds (default: 0.5)'}], 35 | [('--no-check-asset-conservation',), {'action': 'store_true', 'default': False, 'help': 'Skip asset conservation checking (default: false)'}], 36 | [('--p2sh-dust-return-pubkey',), {'help': 'pubkey to receive dust when multisig encoding is used for P2SH source (default: none)'}], 37 | 38 | [('--indexd-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the indexd server'}], 39 | [('--indexd-port',), {'type': int, 'help': 'the indexd server port to connect to'}], 40 | 41 | [('--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)'}], 42 | [('--rpc-port',), {'type': int, 'help': 'port on which to provide the {} JSON-RPC API'.format(config.APP_NAME)}], 43 | [('--rpc-user',), {'default': 'rpc', 'help': 'required username to use the {} JSON-RPC API (via HTTP basic auth)'.format(config.APP_NAME)}], 44 | [('--rpc-password',), {'help': 'required password (for rpc-user) to use the {} JSON-RPC API (via HTTP basic auth)'.format(config.APP_NAME)}], 45 | [('--rpc-no-allow-cors',), {'action': 'store_true', 'default': False, 'help': 'allow ajax cross domain request'}], 46 | [('--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)}], 47 | [('--requests-timeout',), {'type': int, 'default': config.DEFAULT_REQUESTS_TIMEOUT, 'help': 'timeout value (in seconds) used for all HTTP requests (default: 5)'}], 48 | 49 | [('--force',), {'action': 'store_true', 'default': False, 'help': 'skip backend check, version check, process lock (NOT FOR USE ON PRODUCTION SYSTEMS)'}], 50 | [('--database-file',), {'default': None, 'help': 'the path to the SQLite3 database file'}], 51 | [('--log-file',), {'nargs': '?', 'const': None, 'default': False, 'help': 'log to the specified file (specify option without filename to use the default location)'}], 52 | [('--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)'}], 53 | 54 | [('--utxo-locks-max-addresses',), {'type': int, 'default': config.DEFAULT_UTXO_LOCKS_MAX_ADDRESSES, 'help': 'max number of addresses for which to track UTXO locks'}], 55 | [('--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'}], 56 | [('--checkdb',), {'action': 'store_true', 'default': False, 'help': 'check the database for integrity (default: false)'}] 57 | ] 58 | 59 | class VersionError(Exception): 60 | pass 61 | def main(): 62 | if os.name == 'nt': 63 | from counterpartylib.lib import util_windows 64 | #patch up cmd.exe's "challenged" (i.e. broken/non-existent) UTF-8 logging 65 | util_windows.fix_win32_unicode() 66 | 67 | # Post installation tasks 68 | generate_config_files() 69 | 70 | # Parse command-line arguments. 71 | parser = argparse.ArgumentParser(prog=APP_NAME, description='Server for the {} protocol'.format(config.XCP_NAME), add_help=False) 72 | parser.add_argument('-h', '--help', dest='help', action='store_true', help='show this help message and exit') 73 | parser.add_argument('-V', '--version', action='version', version="{} v{}; {} v{}".format(APP_NAME, APP_VERSION, 'counterparty-lib', config.VERSION_STRING)) 74 | parser.add_argument('--config-file', help='the path to the configuration file') 75 | 76 | add_config_arguments(parser, CONFIG_ARGS, 'server.conf') 77 | 78 | subparsers = parser.add_subparsers(dest='action', help='the action to be taken') 79 | 80 | parser_server = subparsers.add_parser('start', help='run the server') 81 | 82 | parser_reparse = subparsers.add_parser('reparse', help='reparse all transactions in the database') 83 | 84 | parser_vacuum = subparsers.add_parser('vacuum', help='VACUUM the database (to improve performance)') 85 | 86 | parser_rollback = subparsers.add_parser('rollback', help='rollback database') 87 | parser_rollback.add_argument('block_index', type=int, help='the index of the last known good block') 88 | 89 | parser_kickstart = subparsers.add_parser('kickstart', help='rapidly build database by reading from Bitcoin Core blockchain') 90 | parser_kickstart.add_argument('--bitcoind-dir', help='Bitcoin Core data directory') 91 | 92 | parser_bootstrap = subparsers.add_parser('bootstrap', help='bootstrap database with hosted snapshot') 93 | parser_bootstrap.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='suppress progress bar') 94 | #parser_bootstrap.add_argument('--branch', help='use a different branch for bootstrap db pulling') 95 | 96 | parser_checkdb = subparsers.add_parser('checkdb', help='do an integrity check on the database') 97 | 98 | 99 | 100 | args = parser.parse_args() 101 | 102 | log.set_up(log.ROOT_LOGGER, verbose=args.verbose, console_logfilter=os.environ.get('COUNTERPARTY_LOGGING', None)) 103 | 104 | logger.info('Running v{} of {}.'.format(APP_VERSION, APP_NAME)) 105 | 106 | # Help message 107 | if args.help: 108 | parser.print_help() 109 | sys.exit() 110 | 111 | # Bootstrapping 112 | if args.action == 'bootstrap': 113 | bootstrap(testnet=args.testnet, quiet=args.quiet) 114 | sys.exit() 115 | 116 | def init_with_catch(fn, init_args): 117 | try: 118 | return fn(**init_args) 119 | except TypeError as e: 120 | if 'unexpected keyword argument' in str(e): 121 | raise VersionError('Unsupported Server Parameter. CLI/Library Version Incompatibility.') 122 | else: 123 | raise e 124 | 125 | # Configuration 126 | COMMANDS_WITH_DB = ['reparse', 'rollback', 'kickstart', 'start', 'vacuum', 'checkdb'] 127 | COMMANDS_WITH_CONFIG = ['debug_config'] 128 | if args.action in COMMANDS_WITH_DB or args.action in COMMANDS_WITH_CONFIG: 129 | init_args = dict(database_file=args.database_file, 130 | log_file=args.log_file, api_log_file=args.api_log_file, 131 | testnet=args.testnet, testcoin=args.testcoin, regtest=args.regtest, 132 | customnet=args.customnet, 133 | api_limit_rows=args.api_limit_rows, 134 | backend_name=args.backend_name, 135 | backend_connect=args.backend_connect, 136 | backend_port=args.backend_port, 137 | backend_user=args.backend_user, 138 | backend_password=args.backend_password, 139 | backend_ssl=args.backend_ssl, 140 | backend_ssl_no_verify=args.backend_ssl_no_verify, 141 | backend_poll_interval=args.backend_poll_interval, 142 | indexd_connect=args.indexd_connect, indexd_port=args.indexd_port, 143 | rpc_host=args.rpc_host, rpc_port=args.rpc_port, rpc_user=args.rpc_user, 144 | rpc_password=args.rpc_password, rpc_no_allow_cors=args.rpc_no_allow_cors, 145 | requests_timeout=args.requests_timeout, 146 | rpc_batch_size=args.rpc_batch_size, 147 | check_asset_conservation=not args.no_check_asset_conservation, 148 | force=args.force, verbose=args.verbose, console_logfilter=os.environ.get('COUNTERPARTY_LOGGING', None), 149 | p2sh_dust_return_pubkey=args.p2sh_dust_return_pubkey, 150 | utxo_locks_max_addresses=args.utxo_locks_max_addresses, 151 | utxo_locks_max_age=args.utxo_locks_max_age, 152 | checkdb=(args.action == 'checkdb') or (args.checkdb)) 153 | #,broadcast_tx_mainnet=args.broadcast_tx_mainnet) 154 | 155 | if args.action in COMMANDS_WITH_DB: 156 | db = init_with_catch(server.initialise, init_args) 157 | 158 | elif args.action in COMMANDS_WITH_CONFIG: 159 | init_with_catch(server.initialise_config, init_args) 160 | 161 | # PARSING 162 | if args.action == 'reparse': 163 | server.reparse(db) 164 | 165 | elif args.action == 'rollback': 166 | server.reparse(db, block_index=args.block_index) 167 | 168 | elif args.action == 'kickstart': 169 | server.kickstart(db, bitcoind_dir=args.bitcoind_dir) 170 | 171 | elif args.action == 'start': 172 | server.start_all(db) 173 | 174 | elif args.action == 'debug_config': 175 | server.debug_config() 176 | 177 | elif args.action == 'vacuum': 178 | server.vacuum(db) 179 | 180 | elif args.action == 'checkdb': 181 | print("Database integrity check done!") 182 | 183 | else: 184 | parser.print_help() 185 | 186 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 187 | -------------------------------------------------------------------------------- /counterpartycli/clientapi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import binascii 4 | from urllib.parse import quote_plus as urlencode 5 | 6 | from counterpartylib.lib import config, script 7 | from counterpartycli import util 8 | from counterpartycli import wallet 9 | from counterpartycli import messages 10 | from counterpartycli.messages import get_pubkeys 11 | 12 | logger = logging.getLogger() 13 | 14 | DEFAULT_REQUESTS_TIMEOUT = 5 # seconds 15 | 16 | class ConfigurationError(Exception): 17 | pass 18 | 19 | def initialize(testnet=False, testcoin=False, regtest=True, customnet="", 20 | counterparty_rpc_connect=None, counterparty_rpc_port=None, 21 | counterparty_rpc_user=None, counterparty_rpc_password=None, 22 | counterparty_rpc_ssl=False, counterparty_rpc_ssl_verify=False, 23 | wallet_name=None, wallet_connect=None, wallet_port=None, 24 | wallet_user=None, wallet_password=None, 25 | wallet_ssl=False, wallet_ssl_verify=False, 26 | requests_timeout=DEFAULT_REQUESTS_TIMEOUT): 27 | 28 | def handle_exception(exc_type, exc_value, exc_traceback): 29 | logger.error("Unhandled Exception", exc_info=(exc_type, exc_value, exc_traceback)) 30 | sys.excepthook = handle_exception 31 | 32 | # testnet 33 | config.TESTNET = testnet or False 34 | 35 | config.REGTEST = regtest or False 36 | 37 | if len(customnet) > 0: 38 | config.CUSTOMNET = True 39 | config.REGTEST = True 40 | else: 41 | config.CUSTOMNET = False 42 | 43 | # testcoin 44 | config.TESTCOIN = testcoin or False 45 | 46 | ############## 47 | # THINGS WE CONNECT TO 48 | 49 | # Server host (Bitcoin Core) 50 | config.COUNTERPARTY_RPC_CONNECT = counterparty_rpc_connect or 'localhost' 51 | 52 | # Server RPC port (Bitcoin Core) 53 | if counterparty_rpc_port: 54 | config.COUNTERPARTY_RPC_PORT = counterparty_rpc_port 55 | else: 56 | if config.TESTNET: 57 | config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_TESTNET 58 | elif config.CUSTOMNET: 59 | config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_REGTEST 60 | elif config.REGTEST: 61 | config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT_REGTEST 62 | else: 63 | config.COUNTERPARTY_RPC_PORT = config.DEFAULT_RPC_PORT 64 | try: 65 | config.COUNTERPARTY_RPC_PORT = int(config.COUNTERPARTY_RPC_PORT) 66 | if not (int(config.COUNTERPARTY_RPC_PORT) > 1 and int(config.COUNTERPARTY_RPC_PORT) < 65535): 67 | raise ConfigurationError('invalid RPC port number') 68 | except: 69 | raise Exception("Please specific a valid port number counterparty-rpc-port configuration parameter") 70 | 71 | # Server RPC user (Bitcoin Core) 72 | config.COUNTERPARTY_RPC_USER = counterparty_rpc_user or 'rpc' 73 | 74 | # Server RPC password (Bitcoin Core) 75 | if counterparty_rpc_password: 76 | config.COUNTERPARTY_RPC_PASSWORD = counterparty_rpc_password 77 | else: 78 | config.COUNTERPARTY_RPC_PASSWORD = None 79 | 80 | # Server RPC SSL 81 | config.COUNTERPARTY_RPC_SSL = counterparty_rpc_ssl or False # Default to off. 82 | 83 | # Server RPC SSL Verify 84 | config.COUNTERPARTY_RPC_SSL_VERIFY = counterparty_rpc_ssl_verify or False # Default to off (support self‐signed certificates) 85 | 86 | # Construct server URL. 87 | config.COUNTERPARTY_RPC = config.COUNTERPARTY_RPC_CONNECT + ':' + str(config.COUNTERPARTY_RPC_PORT) 88 | if config.COUNTERPARTY_RPC_PASSWORD: 89 | config.COUNTERPARTY_RPC = urlencode(config.COUNTERPARTY_RPC_USER) + ':' + urlencode(config.COUNTERPARTY_RPC_PASSWORD) + '@' + config.COUNTERPARTY_RPC 90 | if config.COUNTERPARTY_RPC_SSL: 91 | config.COUNTERPARTY_RPC = 'https://' + config.COUNTERPARTY_RPC 92 | else: 93 | config.COUNTERPARTY_RPC = 'http://' + config.COUNTERPARTY_RPC 94 | config.COUNTERPARTY_RPC += '/rpc/' 95 | 96 | # BTC Wallet name 97 | config.WALLET_NAME = wallet_name or 'bitcoincore' 98 | 99 | # BTC Wallet host 100 | config.WALLET_CONNECT = wallet_connect or 'localhost' 101 | 102 | # BTC Wallet port 103 | if wallet_port: 104 | config.WALLET_PORT = wallet_port 105 | else: 106 | if config.TESTNET: 107 | config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_TESTNET 108 | elif config.CUSTOMNET: 109 | config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_REGTEST 110 | elif config.REGTEST: 111 | config.WALLET_PORT = config.DEFAULT_BACKEND_PORT_REGTEST 112 | else: 113 | config.WALLET_PORT = config.DEFAULT_BACKEND_PORT 114 | try: 115 | config.WALLET_PORT = int(config.WALLET_PORT) 116 | if not (int(config.WALLET_PORT) > 1 and int(config.WALLET_PORT) < 65535): 117 | raise ConfigurationError('invalid wallet API port number') 118 | except: 119 | raise ConfigurationError("Please specific a valid port number wallet-port configuration parameter") 120 | 121 | # BTC Wallet user 122 | config.WALLET_USER = wallet_user or 'bitcoinrpc' 123 | 124 | # BTC Wallet password 125 | if wallet_password: 126 | config.WALLET_PASSWORD = wallet_password 127 | else: 128 | raise ConfigurationError('wallet RPC password not set. (Use configuration file or --wallet-password=PASSWORD)') 129 | 130 | # BTC Wallet SSL 131 | config.WALLET_SSL = wallet_ssl or False # Default to off. 132 | 133 | # BTC Wallet SSL Verify 134 | config.WALLET_SSL_VERIFY = wallet_ssl_verify or False # Default to off (support self‐signed certificates) 135 | 136 | # Construct BTC wallet URL. 137 | config.WALLET_URL = urlencode(config.WALLET_USER) + ':' + urlencode(config.WALLET_PASSWORD) + '@' + config.WALLET_CONNECT + ':' + str(config.WALLET_PORT) 138 | if config.WALLET_SSL: 139 | config.WALLET_URL = 'https://' + config.WALLET_URL 140 | else: 141 | config.WALLET_URL = 'http://' + config.WALLET_URL 142 | 143 | config.REQUESTS_TIMEOUT = requests_timeout 144 | 145 | # Encoding 146 | if config.TESTCOIN: 147 | config.PREFIX = b'XX' # 2 bytes (possibly accidentally created) 148 | else: 149 | config.PREFIX = b'CNTRPRTY' # 8 bytes 150 | 151 | # (more) Testnet 152 | if config.TESTNET: 153 | config.MAGIC_BYTES = config.MAGIC_BYTES_TESTNET 154 | if config.TESTCOIN: 155 | config.ADDRESSVERSION = config.ADDRESSVERSION_TESTNET 156 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_TESTNET 157 | config.BLOCK_FIRST = config.BLOCK_FIRST_TESTNET_TESTCOIN 158 | config.BURN_START = config.BURN_START_TESTNET_TESTCOIN 159 | config.BURN_END = config.BURN_END_TESTNET_TESTCOIN 160 | config.UNSPENDABLE = config.UNSPENDABLE_TESTNET 161 | else: 162 | config.ADDRESSVERSION = config.ADDRESSVERSION_TESTNET 163 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_TESTNET 164 | config.BLOCK_FIRST = config.BLOCK_FIRST_TESTNET 165 | config.BURN_START = config.BURN_START_TESTNET 166 | config.BURN_END = config.BURN_END_TESTNET 167 | config.UNSPENDABLE = config.UNSPENDABLE_TESTNET 168 | elif config.CUSTOMNET: 169 | custom_args = customnet.split('|') 170 | 171 | if len(custom_args) == 3: 172 | config.MAGIC_BYTES = config.MAGIC_BYTES_REGTEST 173 | config.ADDRESSVERSION = binascii.unhexlify(custom_args[1]) 174 | config.P2SH_ADDRESSVERSION = binascii.unhexlify(custom_args[2]) 175 | config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST 176 | config.BURN_START = config.BURN_START_REGTEST 177 | config.BURN_END = config.BURN_END_REGTEST 178 | config.UNSPENDABLE = custom_args[0] 179 | else: 180 | raise "Custom net parameter needs to be like UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION (version bytes in HH format)" 181 | elif config.REGTEST: 182 | config.MAGIC_BYTES = config.MAGIC_BYTES_REGTEST 183 | if config.TESTCOIN: 184 | config.ADDRESSVERSION = config.ADDRESSVERSION_REGTEST 185 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_REGTEST 186 | config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST_TESTCOIN 187 | config.BURN_START = config.BURN_START_REGTEST_TESTCOIN 188 | config.BURN_END = config.BURN_END_REGTEST_TESTCOIN 189 | config.UNSPENDABLE = config.UNSPENDABLE_REGTEST 190 | else: 191 | config.ADDRESSVERSION = config.ADDRESSVERSION_REGTEST 192 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_REGTEST 193 | config.BLOCK_FIRST = config.BLOCK_FIRST_REGTEST 194 | config.BURN_START = config.BURN_START_REGTEST 195 | config.BURN_END = config.BURN_END_REGTEST 196 | config.UNSPENDABLE = config.UNSPENDABLE_REGTEST 197 | else: 198 | config.MAGIC_BYTES = config.MAGIC_BYTES_MAINNET 199 | if config.TESTCOIN: 200 | config.ADDRESSVERSION = config.ADDRESSVERSION_MAINNET 201 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_MAINNET 202 | config.BLOCK_FIRST = config.BLOCK_FIRST_MAINNET_TESTCOIN 203 | config.BURN_START = config.BURN_START_MAINNET_TESTCOIN 204 | config.BURN_END = config.BURN_END_MAINNET_TESTCOIN 205 | config.UNSPENDABLE = config.UNSPENDABLE_MAINNET 206 | else: 207 | config.ADDRESSVERSION = config.ADDRESSVERSION_MAINNET 208 | config.P2SH_ADDRESSVERSION = config.P2SH_ADDRESSVERSION_MAINNET 209 | config.BLOCK_FIRST = config.BLOCK_FIRST_MAINNET 210 | config.BURN_START = config.BURN_START_MAINNET 211 | config.BURN_END = config.BURN_END_MAINNET 212 | config.UNSPENDABLE = config.UNSPENDABLE_MAINNET 213 | 214 | WALLET_METHODS = [ 215 | 'get_wallet_addresses', 'get_btc_balances', 'sign_raw_transaction', 216 | 'get_pubkey', 'is_valid', 'is_mine', 'get_btc_balance', 'send_raw_transaction', 217 | 'wallet', 'asset', 'balances', 'pending', 'is_locked', 'unlock', 'wallet_last_block', 218 | 'sweep' 219 | ] 220 | 221 | def call(method, args, pubkey_resolver=None): 222 | """ 223 | Unified function to call Wallet and Server API methods 224 | Should be used by applications like `counterparty-gui` 225 | 226 | :Example: 227 | 228 | import counterpartycli.clientapi 229 | clientapi.initialize(...) 230 | unsigned_hex = clientapi.call('create_send', {...}) 231 | signed_hex = clientapi.call('sign_raw_transaction', unsigned_hex) 232 | tx_hash = clientapi.call('send_raw_transaction', signed_hex) 233 | """ 234 | if method in WALLET_METHODS: 235 | func = getattr(wallet, method) 236 | return func(**args) 237 | else: 238 | if method.startswith('create_'): 239 | # Get provided pubkeys from params. 240 | pubkeys = [] 241 | for address_name in ['source', 'destination']: 242 | if address_name in args: 243 | address = args[address_name] 244 | if script.is_multisig(address) or address_name != 'destination': # We don’t need the pubkey for a mono‐sig destination. 245 | pubkeys += get_pubkeys(address, pubkey_resolver=pubkey_resolver) 246 | args['pubkey'] = pubkeys 247 | 248 | result = util.api(method, args) 249 | 250 | if method.startswith('create_'): 251 | messages.check_transaction(method, args, result) 252 | 253 | return result 254 | 255 | 256 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 257 | -------------------------------------------------------------------------------- /counterpartycli/messages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from decimal import Decimal as D 3 | import binascii 4 | from math import ceil 5 | import time 6 | import calendar 7 | import dateutil.parser 8 | 9 | from counterpartylib.lib import script, config, blocks, exceptions, api, transaction 10 | from counterpartylib.lib.util import make_id, BET_TYPE_NAME, BET_TYPE_ID, dhash, generate_asset_name 11 | from counterpartylib.lib.kickstart.utils import ib2h 12 | from counterpartycli import util 13 | from counterpartycli import wallet 14 | 15 | import bitcoin as bitcoinlib 16 | 17 | MESSAGE_PARAMS = { 18 | 'send': ['source', 'destination', 'asset', 'quantity', 'memo', 'memo_is_hex', 'use_enhanced_send'], 19 | 'sweep': ['source', 'destination', 'flags', 'memo'], 20 | 'dispenser': ['source', 'asset', 'give_quantity', 'mainchainrate', 'escrow_quantity', 'status', 'open_address'], 21 | 'order': ['source', 'give_asset', 'give_quantity', 'get_asset', 'get_quantity', 'expiration', 'fee_required', 'fee_provided'], 22 | 'btcpay': ['source', 'order_match_id'], 23 | 'issuance': ['source', 'asset', 'quantity', 'divisible', 'description', 'transfer_destination'], 24 | 'broadcast': ['source', 'fee_fraction', 'text', 'timestamp', 'value'], 25 | 'bet': ['source', 'feed_address', 'bet_type','deadline', 'wager_quantity', 'counterwager_quantity', 'expiration', 'target_value', 'leverage'], 26 | 'dividend': ['source', 'quantity_per_unit', 'asset', 'dividend_asset'], 27 | 'burn': ['source', 'quantity'], 28 | 'cancel': ['source', 'offer_hash'], 29 | 'rps': ['source', 'possible_moves', 'wager', 'move_random_hash', 'expiration'], 30 | 'rpsresolve': ['source', 'random', 'move', 'rps_match_id'], 31 | 'publish': ['source', 'gasprice', 'startgas', 'endowment','code_hex'], 32 | 'execute': ['source', 'contract_id', 'gasprice', 'startgas', 'value', 'payload_hex'], 33 | 'destroy': ['source', 'asset', 'quantity', 'tag'] 34 | } 35 | 36 | class InputError(Exception): 37 | pass 38 | class ArgumentError(Exception): 39 | pass 40 | 41 | class MessageArgs: 42 | def __init__(self, dict_args): 43 | self.__dict__.update(dict_args) 44 | 45 | def input_pubkey(address): 46 | input_message = 'Public keys (hexadecimal) or Private key (Wallet Import Format) for `{}`: '.format(address) 47 | return input(input_message) 48 | 49 | def get_pubkey_monosig(pubkeyhash, pubkey_resolver=input_pubkey): 50 | if wallet.is_valid(pubkeyhash): 51 | 52 | # If in wallet, get from wallet. 53 | logging.debug('Looking for public key for `{}` in wallet.'.format(pubkeyhash)) 54 | if wallet.is_mine(pubkeyhash): 55 | pubkey = wallet.get_pubkey(pubkeyhash) 56 | if pubkey: 57 | return pubkey 58 | logging.debug('Public key for `{}` not found in wallet.'.format(pubkeyhash)) 59 | 60 | # If in blockchain (and not in wallet), get from blockchain. 61 | logging.debug('Looking for public key for `{}` in blockchain.'.format(pubkeyhash)) 62 | try: 63 | pubkey = util.api('search_pubkey', {'pubkeyhash': pubkeyhash, 'provided_pubkeys': None}) 64 | except util.RPCError as e: 65 | pubkey = None 66 | if pubkey: 67 | return pubkey 68 | logging.debug('Public key for `{}` not found in blockchain.'.format(pubkeyhash)) 69 | 70 | # If not in wallet and not in blockchain, get from user. 71 | answer = pubkey_resolver(pubkeyhash) 72 | if not answer: 73 | return None 74 | 75 | # Public Key or Private Key? 76 | is_fully_valid_pubkey = True 77 | try: 78 | is_fully_valid_pubkey = script.is_fully_valid(binascii.unhexlify(answer)) 79 | except binascii.Error: 80 | is_fully_valid_pubkey = False 81 | if is_fully_valid_pubkey: 82 | logging.debug('Answer was a fully valid public key.') 83 | pubkey = answer 84 | else: 85 | logging.debug('Answer was not a fully valid public key. Assuming answer was a private key.') 86 | private_key = answer 87 | try: 88 | pubkey = script.private_key_to_public_key(private_key) 89 | except script.AltcoinSupportError: 90 | raise InputError('invalid private key') 91 | if pubkeyhash != script.pubkey_to_pubkeyhash(binascii.unhexlify(bytes(pubkey, 'utf-8'))): 92 | raise InputError('provided public or private key does not match the source address') 93 | 94 | return pubkey 95 | 96 | return None 97 | 98 | def get_pubkeys(address, pubkey_resolver=input_pubkey): 99 | pubkeys = [] 100 | if script.is_multisig(address): 101 | _, pubs, _ = script.extract_array(address) 102 | for pub in pubs: 103 | pubkey = get_pubkey_monosig(pub, pubkey_resolver=pubkey_resolver) 104 | if pubkey: 105 | pubkeys.append(pubkey) 106 | else: 107 | pubkey = get_pubkey_monosig(address, pubkey_resolver=pubkey_resolver) 108 | if pubkey: 109 | pubkeys.append(pubkey) 110 | return pubkeys 111 | 112 | def common_args(args): 113 | return { 114 | 'fee': args.fee, 115 | 'allow_unconfirmed_inputs': args.unconfirmed, 116 | 'encoding': args.encoding, 117 | 'fee_per_kb': args.fee_per_kb, 118 | 'regular_dust_size': args.regular_dust_size, 119 | 'multisig_dust_size': args.multisig_dust_size, 120 | 'op_return_value': args.op_return_value, 121 | 'dust_return_pubkey': args.dust_return_pubkey, 122 | 'disable_utxo_locks': args.disable_utxo_locks 123 | } 124 | 125 | def prepare_args(args, action): 126 | # Convert. 127 | args.fee_per_kb = int(args.fee_per_kb * config.UNIT) 128 | args.regular_dust_size = int(args.regular_dust_size * config.UNIT) 129 | args.multisig_dust_size = int(args.multisig_dust_size * config.UNIT) 130 | args.op_return_value = int(args.op_return_value * config.UNIT) 131 | 132 | # common 133 | if args.fee: 134 | args.fee = util.value_in(args.fee, config.BTC) 135 | 136 | # send 137 | if action == 'send': 138 | args.quantity = util.value_in(args.quantity, args.asset) 139 | 140 | # sweep 141 | if action == 'sweep': 142 | args.flags = int(args.flags) 143 | 144 | # dispenser 145 | if action == 'dispenser': 146 | args.status = int(args.status) 147 | args.give_quantity = util.value_in(args.give_quantity, args.asset) 148 | args.escrow_quantity = util.value_in(args.escrow_quantity, args.asset) 149 | args.mainchainrate = util.value_in(args.mainchainrate, config.BTC) 150 | 151 | # order 152 | if action == 'order': 153 | fee_required, fee_fraction_provided = D(args.fee_fraction_required), D(args.fee_fraction_provided) 154 | give_quantity, get_quantity = D(args.give_quantity), D(args.get_quantity) 155 | 156 | # Fee argument is either fee_required or fee_provided, as necessary. 157 | if args.give_asset == config.BTC: 158 | args.fee_required = 0 159 | fee_fraction_provided = util.value_in(fee_fraction_provided, 'fraction') 160 | args.fee_provided = round(D(fee_fraction_provided) * D(give_quantity) * D(config.UNIT)) 161 | print('Fee provided: {} {}'.format(util.value_out(args.fee_provided, config.BTC), config.BTC)) 162 | elif args.get_asset == config.BTC: 163 | args.fee_provided = 0 164 | fee_fraction_required = util.value_in(args.fee_fraction_required, 'fraction') 165 | args.fee_required = round(D(fee_fraction_required) * D(get_quantity) * D(config.UNIT)) 166 | print('Fee required: {} {}'.format(util.value_out(args.fee_required, config.BTC), config.BTC)) 167 | else: 168 | args.fee_required = 0 169 | args.fee_provided = 0 170 | 171 | args.give_quantity = util.value_in(give_quantity, args.give_asset) 172 | args.get_quantity = util.value_in(get_quantity, args.get_asset) 173 | 174 | # issuance 175 | if action == 'issuance': 176 | args.quantity = util.value_in(args.quantity, None, divisible=args.divisible) 177 | 178 | # broadcast 179 | if action == 'broadcast': 180 | args.value = util.value_in(args.value, 'value') 181 | args.fee_fraction = util.value_in(args.fee_fraction, 'fraction') 182 | args.timestamp = int(time.time()) 183 | 184 | # bet 185 | if action == 'bet': 186 | args.deadline = calendar.timegm(dateutil.parser.parse(args.deadline).utctimetuple()) 187 | args.wager = util.value_in(args.wager, config.XCP) 188 | args.counterwager = util.value_in(args.counterwager, config.XCP) 189 | args.target_value = util.value_in(args.target_value, 'value') 190 | args.leverage = util.value_in(args.leverage, 'leverage') 191 | args.bet_type = BET_TYPE_ID[args.bet_type] 192 | 193 | # dividend 194 | if action == 'dividend': 195 | args.quantity_per_unit = util.value_in(args.quantity_per_unit, config.XCP) 196 | 197 | # burn 198 | if action == 'burn': 199 | args.quantity = util.value_in(args.quantity, config.BTC) 200 | 201 | # execute 202 | if action == 'execute': 203 | args.value = util.value_in(args.value, 'XCP') 204 | args.startgas = util.value_in(args.startgas, 'XCP') 205 | 206 | # destroy 207 | if action == 'destroy': 208 | args.quantity = util.value_in(args.quantity, args.asset) 209 | 210 | # RPS 211 | if action == 'rps': 212 | def generate_move_random_hash(move): 213 | move = int(move).to_bytes(2, byteorder='big') 214 | random_bin = os.urandom(16) 215 | move_random_hash_bin = dhash(random_bin + move) 216 | return binascii.hexlify(random_bin).decode('utf8'), binascii.hexlify(move_random_hash_bin).decode('utf8') 217 | 218 | args.wager = util.value_in(args.wager, 'XCP') 219 | random, move_random_hash = generate_move_random_hash(args.move) 220 | setattr(args, 'move_random_hash', move_random_hash) 221 | print('random: {}'.format(random)) 222 | print('move_random_hash: {}'.format(move_random_hash)) 223 | 224 | return args 225 | 226 | def extract_args(args, keys): 227 | params = {} 228 | dargs = vars(args) 229 | for key in keys: 230 | if key in dargs: 231 | params[key] = dargs[key] 232 | return params 233 | 234 | def get_input_value(tx_hex): 235 | unspents = wallet.list_unspent() 236 | ctx = bitcoinlib.core.CTransaction.deserialize(binascii.unhexlify(tx_hex)) 237 | 238 | inputs_value = 0 239 | for vin in ctx.vin: 240 | vin_tx_hash = ib2h(vin.prevout.hash) 241 | vout_n = vin.prevout.n 242 | found = False 243 | for output in unspents: 244 | if output['txid'] == vin_tx_hash and output['vout'] == vout_n: 245 | inputs_value += int(output['amount'] * config.UNIT) 246 | found = True 247 | if not found: 248 | raise exceptions.TransactionError('input not found in wallet list unspents') 249 | 250 | return inputs_value 251 | 252 | def check_transaction(method, params, tx_hex): 253 | tx_info = transaction.check_outputs(method, params, tx_hex) 254 | input_value = get_input_value(tx_hex) 255 | fee = input_value - tx_info['total_value'] 256 | fee_per_kb = params['fee_per_kb'] if 'fee_per_kb' in params else config.DEFAULT_FEE_PER_KB 257 | 258 | if 'fee' in params and params['fee']: 259 | necessary_fee = params['fee'] 260 | else: 261 | necessary_fee = ceil(((len(tx_hex) / 2) / 1024)) * fee_per_kb # TODO 262 | 263 | if fee > necessary_fee: 264 | raise exceptions.TransactionError('Incorrect fee ({} > {})'.format(fee, necessary_fee)) 265 | 266 | def compose_transaction(args, message_name, param_names): 267 | args = prepare_args(args, message_name) 268 | common_params = common_args(args) 269 | params = extract_args(args, param_names) 270 | params.update(common_params) 271 | 272 | # Get provided pubkeys from params. 273 | pubkeys = [] 274 | for address_name in ['source', 'destination']: 275 | if address_name in params: 276 | address = params[address_name] 277 | 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. 278 | pubkeys += get_pubkeys(address) 279 | params['pubkey'] = pubkeys 280 | 281 | method = 'create_{}'.format(message_name) 282 | unsigned_tx_hex = util.api(method, params) 283 | 284 | # check_transaction(method, params, unsigned_tx_hex) 285 | 286 | return unsigned_tx_hex 287 | 288 | def compose(message, args): 289 | if message in MESSAGE_PARAMS: 290 | param_names = MESSAGE_PARAMS[message] 291 | return compose_transaction(args, message, param_names) 292 | else: 293 | raise ArgumentError('Invalid message name') 294 | 295 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 296 | -------------------------------------------------------------------------------- /counterpartycli/client.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import argparse 6 | import logging 7 | import getpass 8 | from decimal import Decimal as D 9 | 10 | from counterpartylib.lib import log 11 | logger = logging.getLogger(__name__) 12 | 13 | from counterpartylib.lib import config, script 14 | from counterpartylib.lib.util import make_id, BET_TYPE_NAME 15 | from counterpartylib.lib.log import isodt 16 | from counterpartylib.lib.exceptions import TransactionError 17 | from counterpartycli.util import add_config_arguments 18 | from counterpartycli.setup import generate_config_files 19 | from counterpartycli import APP_VERSION, util, messages, wallet, console, clientapi 20 | 21 | APP_NAME = 'counterparty-client' 22 | 23 | CONFIG_ARGS = [ 24 | [('-v', '--verbose'), {'dest': 'verbose', 'action': 'store_true', 'help': 'sets log level to DEBUG instead of WARNING'}], 25 | [('--testnet',), {'action': 'store_true', 'default': False, 'help': 'use {} testnet addresses and block numbers'.format(config.BTC_NAME)}], 26 | [('--testcoin',), {'action': 'store_true', 'default': False, 'help': 'use the test {} network on every blockchain'.format(config.XCP_NAME)}], 27 | [('--regtest',), {'action': 'store_true', 'default': False, 'help': 'use {} regtest addresses and block numbers'.format(config.BTC_NAME)}], 28 | [('--customnet',), {'default': '', 'help': 'use a custom network (specify as UNSPENDABLE_ADDRESS|ADDRESSVERSION|P2SH_ADDRESSVERSION with version bytes in HH hex format)'}], 29 | 30 | [('--counterparty-rpc-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the Counterparty JSON-RPC server'}], 31 | [('--counterparty-rpc-port',), {'type': int, 'help': 'the port of the Counterparty JSON-RPC server'}], 32 | [('--counterparty-rpc-user',), {'default': 'rpc', 'help': 'the username for the Counterparty JSON-RPC server'}], 33 | [('--counterparty-rpc-password',), {'help': 'the password for the Counterparty JSON-RPC server'}], 34 | [('--counterparty-rpc-ssl',), {'default': False, 'action': 'store_true', 'help': 'use SSL to connect to the Counterparty server (default: false)'}], 35 | [('--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)'}], 36 | 37 | [('--wallet-name',), {'default': 'bitcoincore', 'help': 'the wallet name to connect to'}], 38 | [('--wallet-connect',), {'default': 'localhost', 'help': 'the hostname or IP of the wallet server'}], 39 | [('--wallet-port',), {'type': int, 'help': 'the wallet port to connect to'}], 40 | [('--wallet-user',), {'default': 'bitcoinrpc', 'help': 'the username used to communicate with wallet'}], 41 | [('--wallet-password',), {'help': 'the password used to communicate with wallet'}], 42 | [('--wallet-ssl',), {'action': 'store_true', 'default': False, 'help': 'use SSL to connect to wallet (default: false)'}], 43 | [('--wallet-ssl-verify',), {'action': 'store_true', 'default': False, 'help': 'verify SSL certificate of wallet; disallow use of self‐signed certificates (default: false)'}], 44 | 45 | [('--json-output',), {'action': 'store_true', 'default': False, 'help': 'display result in json format'}], 46 | [('--unconfirmed',), {'action': 'store_true', 'default': False, 'help': 'allow the spending of unconfirmed transaction outputs'}], 47 | [('--encoding',), {'default': 'auto', 'type': str, 'help': 'data encoding method'}], 48 | [('--fee-per-kb',), {'type': D, 'default': D(config.DEFAULT_FEE_PER_KB / config.UNIT), 'help': 'fee per kilobyte, in {}'.format(config.BTC)}], 49 | [('--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)}], 50 | [('--multisig-dust-size',), {'type': D, 'default': D(config.DEFAULT_MULTISIG_DUST_SIZE / config.UNIT), 'help': 'for dust OP_CHECKMULTISIG outputs, in {}'.format(config.BTC)}], 51 | [('--op-return-value',), {'type': D, 'default': D(config.DEFAULT_OP_RETURN_VALUE / config.UNIT), 'help': 'value for OP_RETURN outputs, in {}'.format(config.BTC)}], 52 | [('--unsigned',), {'action': 'store_true', 'default': False, 'help': 'print out unsigned hex of transaction; do not sign or broadcast'}], 53 | [('--disable-utxo-locks',), {'action': 'store_true', 'default': False, 'help': 'disable locking of UTXOs being spend'}], 54 | [('--dust-return-pubkey',), {'help': 'pubkey for dust outputs (required for P2SH)'}], 55 | [('--requests-timeout',), {'type': int, 'default': clientapi.DEFAULT_REQUESTS_TIMEOUT, 'help': 'timeout value (in seconds) used for all HTTP requests (default: 5)'}] 56 | ] 57 | 58 | def main(): 59 | if os.name == 'nt': 60 | from counterpartylib.lib import util_windows 61 | #patch up cmd.exe's "challenged" (i.e. broken/non-existent) UTF-8 logging 62 | util_windows.fix_win32_unicode() 63 | 64 | # Post installation tasks 65 | generate_config_files() 66 | 67 | # Parse command-line arguments. 68 | parser = argparse.ArgumentParser(prog=APP_NAME, description='Counterparty CLI for counterparty-server', add_help=False) 69 | parser.add_argument('-h', '--help', dest='help', action='store_true', help='show this help message and exit') 70 | parser.add_argument('-V', '--version', action='version', version="{} v{}; {} v{}".format(APP_NAME, APP_VERSION, 'counterparty-lib', config.VERSION_STRING)) 71 | parser.add_argument('--config-file', help='the location of the configuration file') 72 | 73 | add_config_arguments(parser, CONFIG_ARGS, 'client.conf') 74 | 75 | subparsers = parser.add_subparsers(dest='action', help='the action to be taken') 76 | 77 | parser_send = subparsers.add_parser('send', help='create and broadcast a *send* message') 78 | parser_send.add_argument('--source', required=True, help='the source address') 79 | parser_send.add_argument('--destination', required=True, help='the destination address') 80 | parser_send.add_argument('--quantity', required=True, help='the quantity of ASSET to send') 81 | parser_send.add_argument('--asset', required=True, help='the ASSET of which you would like to send QUANTITY') 82 | parser_send.add_argument('--memo', help='A transaction memo attached to this send') 83 | parser_send.add_argument('--memo-is-hex', action='store_true', default=False, help='Whether to interpret memo as a hexadecimal value') 84 | 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') 85 | parser_send.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 86 | 87 | parser_sweep = subparsers.add_parser('sweep', help='create and broadcast a *sweep* message') 88 | parser_sweep.add_argument('--source', required=True, help='the source address') 89 | parser_sweep.add_argument('--destination', required=True, help='the destination address') 90 | 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') 91 | parser_sweep.add_argument('--memo', help='A transaction memo attached to this send') 92 | parser_sweep.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 93 | 94 | parser_dispenser = subparsers.add_parser('dispenser', help='create and broadcast a *dispenser*') 95 | parser_dispenser.add_argument('--source', required=True, help='the source address') 96 | parser_dispenser.add_argument('--asset', required=True, help='the ASSET of which you would like to dispense GIVE_QUANTITY') 97 | 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) 98 | 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) 99 | parser_dispenser.add_argument('--escrow-quantity', required=True, help='the quantity of ASSET that you are escrowing for this dispenser') 100 | 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.') 101 | parser_dispenser.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 102 | parser_dispenser.add_argument('--open-address', help='an empty address to open the dispenser on') 103 | 104 | parser_order = subparsers.add_parser('order', help='create and broadcast an *order* message') 105 | parser_order.add_argument('--source', required=True, help='the source address') 106 | parser_order.add_argument('--get-quantity', required=True, help='the quantity of GET_ASSET that you would like to receive') 107 | parser_order.add_argument('--get-asset', required=True, help='the asset that you would like to buy') 108 | parser_order.add_argument('--give-quantity', required=True, help='the quantity of GIVE_ASSET that you are willing to give') 109 | parser_order.add_argument('--give-asset', required=True, help='the asset that you would like to sell') 110 | parser_order.add_argument('--expiration', type=int, required=True, help='the number of blocks for which the order should be valid') 111 | 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)) 112 | parser_order_fees = parser_order.add_mutually_exclusive_group() 113 | 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)) 114 | parser_order_fees.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 115 | 116 | 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)) 117 | parser_btcpay.add_argument('--source', required=True, help='the source address') 118 | parser_btcpay.add_argument('--order-match-id', required=True, help='the concatenation of the hashes of the two transactions which compose the order match') 119 | parser_btcpay.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 120 | 121 | parser_issuance = subparsers.add_parser('issuance', help='issue a new asset, issue more of an existing asset or transfer the ownership of an asset') 122 | parser_issuance.add_argument('--source', required=True, help='the source address') 123 | parser_issuance.add_argument('--transfer-destination', help='for transfer of ownership of asset issuance rights') 124 | parser_issuance.add_argument('--quantity', default=0, help='the quantity of ASSET to be issued') 125 | parser_issuance.add_argument('--asset', required=True, help='the name of the asset to be issued (if it’s available)') 126 | parser_issuance.add_argument('--divisible', action='store_true', help='whether or not the asset is divisible (must agree with previous issuances)') 127 | 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)') 128 | parser_issuance.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 129 | 130 | parser_broadcast = subparsers.add_parser('broadcast', help='broadcast textual and numerical information to the network') 131 | parser_broadcast.add_argument('--source', required=True, help='the source address') 132 | parser_broadcast.add_argument('--text', type=str, required=True, help='the textual part of the broadcast (set to ‘LOCK’ to lock feed)') 133 | parser_broadcast.add_argument('--value', type=float, default=-1, help='numerical value of the broadcast') 134 | parser_broadcast.add_argument('--fee-fraction', default=0, help='the fraction of bets on this feed that go to its operator') 135 | parser_broadcast.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 136 | 137 | parser_bet = subparsers.add_parser('bet', help='offer to make a bet on the value of a feed') 138 | parser_bet.add_argument('--source', required=True, help='the source address') 139 | parser_bet.add_argument('--feed-address', required=True, help='the address which publishes the feed to bet on') 140 | parser_bet.add_argument('--bet-type', choices=list(BET_TYPE_NAME.values()), required=True, help='choices: {}'.format(list(BET_TYPE_NAME.values()))) 141 | parser_bet.add_argument('--deadline', required=True, help='the date and time at which the bet should be decided/settled') 142 | parser_bet.add_argument('--wager', required=True, help='the quantity of XCP to wager') 143 | 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') 144 | parser_bet.add_argument('--target-value', default=0.0, help='target value for Equal/NotEqual bet') 145 | parser_bet.add_argument('--leverage', type=int, default=5040, help='leverage, as a fraction of 5040') 146 | parser_bet.add_argument('--expiration', type=int, required=True, help='the number of blocks for which the bet should be valid') 147 | parser_bet.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 148 | 149 | parser_dividend = subparsers.add_parser('dividend', help='pay dividends to the holders of an asset (in proportion to their stake in it)') 150 | parser_dividend.add_argument('--source', required=True, help='the source address') 151 | parser_dividend.add_argument('--quantity-per-unit', required=True, help='the quantity of XCP to be paid per whole unit held of ASSET') 152 | parser_dividend.add_argument('--asset', required=True, help='the asset to which pay dividends') 153 | parser_dividend.add_argument('--dividend-asset', required=True, help='asset in which to pay the dividends') 154 | parser_dividend.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 155 | 156 | parser_burn = subparsers.add_parser('burn', help='destroy {} to earn XCP, during an initial period of time') 157 | parser_burn.add_argument('--source', required=True, help='the source address') 158 | parser_burn.add_argument('--quantity', required=True, help='quantity of {} to be burned'.format(config.BTC)) 159 | parser_burn.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 160 | 161 | parser_cancel = subparsers.add_parser('cancel', help='cancel an open order or bet you created') 162 | parser_cancel.add_argument('--source', required=True, help='the source address') 163 | parser_cancel.add_argument('--offer-hash', required=True, help='the transaction hash of the order or bet') 164 | parser_cancel.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 165 | 166 | parser_publish = subparsers.add_parser('publish', help='publish contract code in the blockchain') 167 | parser_publish.add_argument('--source', required=True, help='the source address') 168 | parser_publish.add_argument('--gasprice', required=True, type=int, help='the price of gas') 169 | 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)) 170 | parser_publish.add_argument('--endowment', required=True, type=int, help='quantity of {} to be transfered to the contract (satoshis)'.format(config.XCP)) 171 | parser_publish.add_argument('--code-hex', required=True, type=str, help='the hex‐encoded contract (returned by `serpent compile`)') 172 | parser_publish.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 173 | 174 | parser_execute = subparsers.add_parser('execute', help='execute contract code in the blockchain') 175 | parser_execute.add_argument('--source', required=True, help='the source address') 176 | parser_execute.add_argument('--contract-id', required=True, help='the contract ID of the contract to be executed') 177 | parser_execute.add_argument('--gasprice', required=True, type=int, help='the price of gas') 178 | 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)) 179 | parser_execute.add_argument('--value', required=True, type=int, help='quantity of {} to be transfered to the contract (satoshis)'.format(config.XCP)) 180 | parser_execute.add_argument('--payload-hex', required=True, type=str, help='data to be provided to the contract (returned by `serpent encode_datalist`)') 181 | parser_execute.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 182 | 183 | parser_destroy = subparsers.add_parser('destroy', help='destroy a quantity of a Counterparty asset') 184 | parser_destroy.add_argument('--source', required=True, help='the source address') 185 | parser_destroy.add_argument('--asset', required=True, help='the ASSET of which you would like to destroy QUANTITY') 186 | parser_destroy.add_argument('--quantity', required=True, help='the quantity of ASSET to destroy') 187 | parser_destroy.add_argument('--tag', default='', help='tag') 188 | parser_destroy.add_argument('--fee', help='the exact {} fee to be paid to miners'.format(config.BTC)) 189 | 190 | parser_address = subparsers.add_parser('balances', help='display the balances of a {} address'.format(config.XCP_NAME)) 191 | parser_address.add_argument('address', help='the address you are interested in') 192 | 193 | parser_asset = subparsers.add_parser('asset', help='display the basic properties of a {} asset'.format(config.XCP_NAME)) 194 | parser_asset.add_argument('asset', help='the asset you are interested in') 195 | 196 | 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)) 197 | 198 | parser_pending = subparsers.add_parser('pending', help='list pending order matches awaiting {}payment from you'.format(config.BTC)) 199 | 200 | parser_getrows = subparsers.add_parser('getrows', help='get rows from a Counterparty table') 201 | parser_getrows.add_argument('--table', required=True, help='table name') 202 | parser_getrows.add_argument('--filter', nargs=3, action='append', help='filters to get specific rows') 203 | parser_getrows.add_argument('--filter-op', choices=['AND', 'OR'], help='operator uses to combine filters', default='AND') 204 | parser_getrows.add_argument('--order-by', help='field used to order results') 205 | parser_getrows.add_argument('--order-dir', choices=['ASC', 'DESC'], help='direction used to order results') 206 | parser_getrows.add_argument('--start-block', help='return only rows with block_index greater than start-block') 207 | parser_getrows.add_argument('--end-block', help='return only rows with block_index lower than end-block') 208 | parser_getrows.add_argument('--status', help='return only rows with the specified status') 209 | parser_getrows.add_argument('--limit', help='number of rows to return', default=100) 210 | parser_getrows.add_argument('--offset', help='number of rows to skip', default=0) 211 | 212 | parser_getrunninginfo = subparsers.add_parser('getinfo', help='get the current state of the server') 213 | 214 | parser_get_tx_info = subparsers.add_parser('get_tx_info', help='display info of a raw TX') 215 | parser_get_tx_info.add_argument('tx_hex', help='the raw TX') 216 | 217 | args = parser.parse_args() 218 | 219 | # Logging 220 | log.set_up(logger, verbose=args.verbose) 221 | logger.propagate = False 222 | 223 | logger.info('Running v{} of {}.'.format(APP_VERSION, APP_NAME)) 224 | 225 | # Help message 226 | if args.help: 227 | parser.print_help() 228 | sys.exit() 229 | 230 | # Configuration 231 | clientapi.initialize(testnet=args.testnet, testcoin=args.testcoin, regtest=args.regtest, customnet=args.customnet, 232 | counterparty_rpc_connect=args.counterparty_rpc_connect, counterparty_rpc_port=args.counterparty_rpc_port, 233 | counterparty_rpc_user=args.counterparty_rpc_user, counterparty_rpc_password=args.counterparty_rpc_password, 234 | counterparty_rpc_ssl=args.counterparty_rpc_ssl, counterparty_rpc_ssl_verify=args.counterparty_rpc_ssl_verify, 235 | wallet_name=args.wallet_name, wallet_connect=args.wallet_connect, wallet_port=args.wallet_port, 236 | wallet_user=args.wallet_user, wallet_password=args.wallet_password, 237 | wallet_ssl=args.wallet_ssl, wallet_ssl_verify=args.wallet_ssl_verify, 238 | requests_timeout=args.requests_timeout) 239 | 240 | # MESSAGE CREATION 241 | if args.action in list(messages.MESSAGE_PARAMS.keys()): 242 | unsigned_hex = messages.compose(args.action, args) 243 | logger.info('Transaction (unsigned): {}'.format(unsigned_hex)) 244 | if not args.unsigned: 245 | if script.is_multisig(args.source): 246 | logger.info('Multi‐signature transactions are signed and broadcasted manually.') 247 | 248 | elif input('Sign and broadcast? (y/N) ') == 'y': 249 | 250 | if wallet.is_mine(args.source): 251 | if wallet.is_locked(): 252 | passphrase = getpass.getpass('Enter your wallet passhrase: ') 253 | logger.info('Unlocking wallet for 60 (more) seconds.') 254 | wallet.unlock(passphrase) 255 | signed_tx_hex = wallet.sign_raw_transaction(unsigned_hex) 256 | else: 257 | private_key_wif = input('Source address not in wallet. Please enter the private key in WIF format for {}:'.format(args.source)) 258 | if not private_key_wif: 259 | raise TransactionError('invalid private key') 260 | signed_tx_hex = wallet.sign_raw_transaction(unsigned_hex, private_key_wif=private_key_wif) 261 | 262 | logger.info('Transaction (signed): {}'.format(signed_tx_hex)) 263 | tx_hash = wallet.send_raw_transaction(signed_tx_hex) 264 | logger.info('Hash of transaction (broadcasted): {}'.format(tx_hash)) 265 | 266 | 267 | # VIEWING 268 | elif args.action in ['balances', 'asset', 'wallet', 'pending', 'getinfo', 'getrows', 'get_tx_info']: 269 | view = console.get_view(args.action, args) 270 | print_method = getattr(console, 'print_{}'.format(args.action), None) 271 | if args.json_output or print_method is None: 272 | util.json_print(view) 273 | else: 274 | print_method(view) 275 | 276 | else: 277 | parser.print_help() 278 | 279 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 280 | --------------------------------------------------------------------------------