Skip to content

Commit c32098b

Browse files
committed
Add package setup/doc
1 parent e4dc9a6 commit c32098b

7 files changed

Lines changed: 364 additions & 1 deletion

File tree

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
language: python
2+
cache: pip
3+
python:
4+
- "3.6"
5+
- "3.5"
6+
script: python setup.py build

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,46 @@
11
# electrum-scripting
2-
Scripting utils for electrum wallet
2+
Scripting interface wrapper for electrum wallet
3+
4+
[![PYPI Version][pypi-image]][pypi-url]
5+
[![Build Status][travis-image]][travis-url]
6+
7+
8+
## Setup
9+
10+
You would need to setup electrum wallet first and start the daemon if required.
11+
12+
Sample setup
13+
```
14+
# Download from https://electrum.org/#download
15+
sudo apt-get install python3-pyqt5
16+
wget https://download.electrum.org/3.3.8/Electrum-3.3.8.tar.gz
17+
tar -xvf Electrum-3.3.8.tar.gz
18+
cd Electrum-3.3.8
19+
python3 -m pip install .[fast]
20+
21+
# Start deamon
22+
./run_electrum daemon start
23+
./run_electrum daemon load_wallet
24+
```
25+
26+
Install electrum scripting
27+
```
28+
python3 -m pip install electrum-scripting
29+
```
30+
31+
## Get Started
32+
33+
```python
34+
from electrum_scripting.wallet import WalletScripting as ws
35+
36+
# Call electrum command
37+
ws.call('listunspent')
38+
39+
```
40+
41+
42+
[pypi-image]: https://img.shields.io/pypi/v/electrum-scripting.svg
43+
[pypi-url]: https://pypi.org/project/electrum-scripting/
44+
[travis-image]: https://img.shields.io/travis/devfans/electrum-scripting/master.svg
45+
[travis-url]: https://travis-ci.org/devfans/electrum-scripting
46+

__init__.py

Whitespace-only changes.

electrum_scripting/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__VERSION__ = '0.0.1'
2+
version_info = (0, 0, 1)
3+

electrum_scripting/wallet.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import os
2+
import sys
3+
import warnings
4+
5+
6+
MIN_PYTHON_VERSION = "3.6.1" # FIXME duplicated from setup.py
7+
_min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split("."))))
8+
9+
10+
if sys.version_info[:3] < _min_python_version_tuple:
11+
sys.exit("Error: Electrum requires Python version >= %s..." % MIN_PYTHON_VERSION)
12+
13+
14+
def check_imports():
15+
# pure-python dependencies need to be imported here for pyinstaller
16+
try:
17+
import dns
18+
import pyaes
19+
import ecdsa
20+
import certifi
21+
import qrcode
22+
import google.protobuf
23+
import jsonrpclib
24+
import aiorpcx
25+
except ImportError as e:
26+
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
27+
# the following imports are for pyinstaller
28+
from google.protobuf import descriptor
29+
from google.protobuf import message
30+
from google.protobuf import reflection
31+
from google.protobuf import descriptor_pb2
32+
from jsonrpclib import SimpleJSONRPCServer
33+
# make sure that certificates are here
34+
assert os.path.exists(certifi.where())
35+
36+
check_imports()
37+
38+
from electrum.logging import get_logger, configure_logging
39+
from electrum import util
40+
from electrum import constants
41+
from electrum import SimpleConfig
42+
from electrum.wallet import Wallet
43+
from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption
44+
from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled
45+
from electrum.util import InvalidPassword
46+
from electrum.commands import get_parser, known_commands, Commands, config_variables
47+
from electrum import daemon
48+
from electrum import keystore
49+
50+
_logger = get_logger(__name__)
51+
52+
53+
# get password routine
54+
def prompt_password(prompt, confirm=True):
55+
import getpass
56+
password = getpass.getpass(prompt, stream=None)
57+
if password and confirm:
58+
password2 = getpass.getpass("Confirm: ")
59+
if password != password2:
60+
sys.exit("Error: Passwords do not match.")
61+
if not password:
62+
password = None
63+
return password
64+
65+
66+
def init_daemon(config_options):
67+
config = SimpleConfig(config_options)
68+
storage = WalletStorage(config.get_wallet_path())
69+
if not storage.file_exists():
70+
print_msg("Error: Wallet file not found.")
71+
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
72+
sys.exit(0)
73+
if storage.is_encrypted():
74+
if storage.is_encrypted_with_hw_device():
75+
plugins = init_plugins(config, 'cmdline')
76+
password = get_password_for_hw_device_encrypted_storage(plugins)
77+
elif config.get('password'):
78+
password = config.get('password')
79+
else:
80+
password = prompt_password('Password:', False)
81+
if not password:
82+
print_msg("Error: Password required")
83+
sys.exit(1)
84+
else:
85+
password = None
86+
config_options['password'] = password
87+
88+
89+
def init_cmdline(config_options, server):
90+
config = SimpleConfig(config_options)
91+
cmdname = config.get('cmd')
92+
cmd = known_commands[cmdname]
93+
94+
if cmdname == 'signtransaction' and config.get('privkey'):
95+
cmd.requires_wallet = False
96+
cmd.requires_password = False
97+
98+
if cmdname in ['payto', 'paytomany'] and config.get('unsigned'):
99+
cmd.requires_password = False
100+
101+
if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
102+
cmd.requires_network = True
103+
104+
# instantiate wallet for command-line
105+
storage = WalletStorage(config.get_wallet_path())
106+
107+
if cmd.requires_wallet and not storage.file_exists():
108+
print_msg("Error: Wallet file not found.")
109+
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
110+
sys.exit(0)
111+
112+
# important warning
113+
if cmd.name in ['getprivatekeys']:
114+
print_stderr("WARNING: ALL your private keys are secret.")
115+
print_stderr("Exposing a single private key can compromise your entire wallet!")
116+
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
117+
118+
# commands needing password
119+
if (cmd.requires_wallet and storage.is_encrypted() and server is None)\
120+
or (cmd.requires_password and (storage.is_encrypted() or storage.get('use_encryption'))):
121+
if storage.is_encrypted_with_hw_device():
122+
# this case is handled later in the control flow
123+
password = None
124+
elif config.get('password'):
125+
password = config.get('password')
126+
else:
127+
password = prompt_password('Password:', False)
128+
if not password:
129+
print_msg("Error: Password required")
130+
sys.exit(1)
131+
else:
132+
password = None
133+
134+
config_options['password'] = config_options.get('password') or password
135+
136+
if cmd.name == 'password':
137+
new_password = prompt_password('New password:')
138+
config_options['new_password'] = new_password
139+
140+
141+
def get_connected_hw_devices(plugins):
142+
supported_plugins = plugins.get_hardware_support()
143+
# scan devices
144+
devices = []
145+
devmgr = plugins.device_manager
146+
for splugin in supported_plugins:
147+
name, plugin = splugin.name, splugin.plugin
148+
if not plugin:
149+
e = splugin.exception
150+
_logger.error(f"{name}: error during plugin init: {repr(e)}")
151+
continue
152+
try:
153+
u = devmgr.unpaired_device_infos(None, plugin)
154+
except Exception as e:
155+
_logger.error(f'error getting device infos for {name}: {repr(e)}')
156+
continue
157+
devices += list(map(lambda x: (name, x), u))
158+
return devices
159+
160+
161+
def get_password_for_hw_device_encrypted_storage(plugins):
162+
devices = get_connected_hw_devices(plugins)
163+
if len(devices) == 0:
164+
print_msg("Error: No connected hw device found. Cannot decrypt this wallet.")
165+
sys.exit(1)
166+
elif len(devices) > 1:
167+
print_msg("Warning: multiple hardware devices detected. "
168+
"The first one will be used to decrypt the wallet.")
169+
# FIXME we use the "first" device, in case of multiple ones
170+
name, device_info = devices[0]
171+
plugin = plugins.get_plugin(name)
172+
derivation = get_derivation_used_for_hw_device_encryption()
173+
try:
174+
xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler)
175+
except UserCancelled:
176+
sys.exit(0)
177+
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
178+
return password
179+
180+
181+
def run_offline_command(config, config_options, plugins):
182+
print(config)
183+
print(config_options)
184+
print(plugins)
185+
cmdname = config.get('cmd')
186+
cmd = known_commands[cmdname]
187+
print(cmd.__dict__)
188+
password = config_options.get('password')
189+
if cmd.requires_wallet:
190+
storage = WalletStorage(config.get_wallet_path())
191+
if storage.is_encrypted():
192+
if storage.is_encrypted_with_hw_device():
193+
password = get_password_for_hw_device_encrypted_storage(plugins)
194+
config_options['password'] = password
195+
storage.decrypt(password)
196+
wallet = Wallet(storage)
197+
else:
198+
wallet = None
199+
# check password
200+
if cmd.requires_password and wallet.has_password():
201+
try:
202+
wallet.check_password(password)
203+
except InvalidPassword:
204+
print_msg("Error: This password does not decode this wallet.")
205+
sys.exit(1)
206+
if cmd.requires_network:
207+
print_msg("Warning: running command offline")
208+
# arguments passed to function
209+
args = [config.get(x) for x in cmd.params]
210+
# decode json arguments
211+
if cmdname not in ('setconfig',):
212+
args = list(map(json_decode, args))
213+
# options
214+
kwargs = {}
215+
for x in cmd.options:
216+
kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
217+
cmd_runner = Commands(config, wallet, None)
218+
func = getattr(cmd_runner, cmd.name)
219+
result = func(*args, **kwargs)
220+
# save wallet
221+
if wallet:
222+
wallet.storage.write()
223+
return result
224+
225+
226+
def init_plugins(config, gui_name):
227+
from electrum.plugin import Plugins
228+
return Plugins(config, gui_name)
229+
230+
231+
class WalletScripting(object):
232+
@classmethod
233+
def setup(cls):
234+
pass
235+
236+
@classmethod
237+
def call(cls, command, *args, **kwargs):
238+
# command line
239+
config_options = {
240+
'verbosity': '',
241+
'verbosity_shortcuts': '',
242+
'portable': False,
243+
'testnet': False,
244+
'regtest': False,
245+
'simnet': False,
246+
'cmd': command
247+
}
248+
config_options['cwd'] = os.getcwd()
249+
config = SimpleConfig(config_options)
250+
cmdname = config.get('cmd')
251+
252+
server = daemon.get_server(config)
253+
init_cmdline(config_options, server)
254+
if server is not None:
255+
print("goes online")
256+
result = server.run_cmdline(config_options)
257+
print(result)
258+
else:
259+
print("goes offline")
260+
cmd = known_commands[cmdname]
261+
if cmd.requires_network:
262+
print_msg("Daemon not running; try 'electrum daemon start'")
263+
sys.exit(1)
264+
else:
265+
plugins = init_plugins(config, 'cmdline')
266+
result = run_offline_command(config, config_options, plugins)
267+
print(result)
268+
269+

setup.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import electrum_scripting, os
2+
3+
try:
4+
from setuptools import setup
5+
except ImportError:
6+
from distutils.core import setup
7+
8+
from os import path
9+
this_directory = path.abspath(path.dirname(__file__))
10+
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
11+
long_description = f.read()
12+
13+
setup(
14+
name='electrum_scripting',
15+
version=electrum_scripting.__VERSION__,
16+
description="Electrum wallet scripting interface wrapper",
17+
long_description=long_description,
18+
classifiers=[
19+
'License :: OSI Approved :: MIT License',
20+
'Topic :: Internet :: Crypto :: Wallet',
21+
'Programming Language :: Python :: 3',
22+
'Environment :: Linux Environment',
23+
'Intended Audience :: Developers',
24+
'Operating System :: OS Independent',
25+
],
26+
keywords='Electrum Scripting Wallet Bitcoin Crypto',
27+
author="Stefan Liu",
28+
author_email="stefanliu@outlook.com",
29+
url="http://github.com/devfans/electrum-scripting",
30+
license="MIT",
31+
packages=["electrum_scripting"],
32+
include_package_data=True,
33+
zip_safe=True
34+
)

test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python3
2+
from electrum_scripting.wallet import WalletScripting as ws
3+
ws.setup()
4+
5+
# cmd: listunspent
6+
print('calling')
7+
ws.call('listunspent')

0 commit comments

Comments
 (0)