diff --git a/joinmarket-qt.py b/joinmarket-qt.py
new file mode 100644
index 00000000..30b79095
--- /dev/null
+++ b/joinmarket-qt.py
@@ -0,0 +1,1426 @@
+
+'''
+Joinmarket GUI using PyQt for doing Sendpayment.
+Some widgets copied and modified from https://github.com/spesmilo/electrum
+The latest version of this code is currently maintained at:
+https://github.com/AdamISZ/joinmarket/tree/gui
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
' for m in mbinfo]), + QMessageBox.Yes,QMessageBox.No) + if reply == QMessageBox.Yes: + log.debug('You agreed, transaction proceeding') + w.statusBar().showMessage("Building transaction...") + thread3 = TaskThread(self) + log.debug("Trigger is: "+str(self.donateLimitBox.value())) + if get_network()=='testnet': + da = donation_address_testnet + else: + da = donation_address + thread3.add(partial(self.pt.do_tx,self.total_cj_fee, self.orders, + self.cjamount, self.utxos, + self.donateCheckBox.isChecked(), + self.donateLimitBox.value(), + da), + on_done=None) + else: + self.giveUp() + return + + def giveUp(self): + self.aborted = True + log.debug("Transaction aborted.") + self.taker.msgchan.shutdown() + self.abortButton.setEnabled(False) + self.startButton.setEnabled(True) + w.statusBar().showMessage("Transaction aborted.") + + def cleanUp(self): + if not self.taker.txid: + if not self.aborted: + if not self.pt.ignored_makers: + w.statusBar().showMessage("Transaction failed.") + QMessageBox.warning(self,"Failed","Transaction was not completed.") + else: + reply = QMessageBox.question(self, "Transaction not completed.", + '\n'.join(["The following counterparties did not respond: ", + ','.join(self.pt.ignored_makers), + "This sometimes happens due to bad network connections.", + "", + "If you would like to try again, ignoring those", + "counterparties, click Yes."]), QMessageBox.Yes, QMessageBox.No) + if reply == QMessageBox.Yes: + self.startSendPayment(ignored_makers=self.pt.ignored_makers) + else: + self.giveUp() + return + + else: + w.statusBar().showMessage("Transaction completed successfully.") + QMessageBox.information(self,"Success", + "Transaction has been broadcast.\n"+ + "Txid: "+str(self.taker.txid)) + #persist the transaction to history + with open(jm_single().config.get("GUI", "history_file"),'ab') as f: + f.write(','.join([self.destaddr, self.btc_amount_str, + self.taker.txid, + datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")])) + f.write('\n') #TODO: Windows + #update the TxHistory tab + txhist = w.centralWidget().widget(3) + txhist.updateTxInfo() + + self.startButton.setEnabled(True) + self.abortButton.setEnabled(False) + + def runIRC(self): + try: + log.debug('starting irc') + self.irc.run() + except: + log.debug('CRASHING, DUMPING EVERYTHING') + debug_dump_object(w.wallet, ['addr_cache', 'keys', 'wallet_name', 'seed']) + debug_dump_object(self.taker) + import traceback + log.debug(traceback.format_exc()) + + def finishPayment(self): + log.debug("Done") + + def validateSettings(self): + valid, errmsg = validate_address(self.widgets[0][1].text()) + if not valid: + QMessageBox.warning(self,"Error", errmsg) + return False + errs = ["Number of counterparties must be provided.", + "Mixdepth must be chosen.", + "Amount, in bitcoins, must be provided." + ] + for i in range(1,4): + if self.widgets[i][1].text().size()==0: + QMessageBox.warning(self, "Error",errs[i-1]) + return False + if not w.wallet: + QMessageBox.warning(self,"Error","There is no wallet loaded.") + return False + return True + + def showBlockrWarning(self): + if jm_single().config.getint("GUI", "privacy_warning") == 0: + return False + qmb = QMessageBox() + qmb.setIcon(QMessageBox.Warning) + qmb.setWindowTitle("Privacy Warning") + qcb = QCheckBox("Don't show this warning again.") + lyt = qmb.layout() + lyt.addWidget(QLabel(warnings['blockr_privacy']), 0, 1) + lyt.addWidget(qcb, 1, 1) + qmb.addButton(QPushButton("Continue"), QMessageBox.YesRole) + qmb.addButton(QPushButton("Cancel"), QMessageBox.NoRole) + + qmb.exec_() + + switch_off_warning = '0' if qcb.isChecked() else '1' + jm_single().config.set("GUI","privacy_warning", switch_off_warning) + + res = qmb.buttonRole(qmb.clickedButton()) + if res == QMessageBox.YesRole: + return False + elif res == QMessageBox.NoRole: + return True + else: + log.debug("GUI error: unrecognized button, canceling.") + return True + + def checkAddress(self, addr): + valid, errmsg = validate_address(str(addr)) + if not valid: + QMessageBox.warning(self, "Error","Bitcoin address not valid.\n"+errmsg) + + def getSettingsWidgets(self): + results = [] + sN = ['Recipient address', 'Number of counterparties', + 'Mixdepth','Amount in bitcoins (BTC)'] + sH = ['The address you want to send the payment to', + 'How many other parties to send to; if you enter 4\n'+ + ', there will be 5 participants, including you', + 'The mixdepth of the wallet to send the payment from', + 'The amount IN BITCOINS to send.\n'+ + 'If you enter 0, a SWEEP transaction\nwill be performed,'+ + ' spending all the coins \nin the given mixdepth.'] + sT = [str, int, int, float] + #todo maxmixdepth + sMM = ['',(2,20),(0,jm_single().config.getint("GUI","max_mix_depth")-1), + (0.00000001,100.0,8)] + sD = ['', '3', '0', ''] + for x in zip(sN, sH, sT, sD, sMM): + ql = QLabel(x[0]) + ql.setToolTip(x[1]) + qle = QLineEdit(x[3]) + if x[2]==int: + qle.setValidator(QIntValidator(*x[4])) + if x[2]==float: + qle.setValidator(QDoubleValidator(*x[4])) + results.append((ql, qle)) + return results + + +class TxHistoryTab(QWidget): + def __init__(self): + super(TxHistoryTab, self).__init__() + self.initUI() + + def initUI(self): + self.tHTW = MyTreeWidget(self, + self.create_menu, self.getHeaders()) + self.tHTW.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tHTW.header().setResizeMode(QHeaderView.Interactive) + self.tHTW.header().setStretchLastSection(False) + self.tHTW.on_update = self.updateTxInfo + vbox = QVBoxLayout() + self.setLayout(vbox) + vbox.setMargin(0) + vbox.setSpacing(0) + vbox.addWidget(self.tHTW) + self.updateTxInfo() + self.show() + + def getHeaders(self): + '''Function included in case dynamic in future''' + return ['Receiving address','Amount in BTC','Transaction id','Date'] + + def updateTxInfo(self, txinfo=None): + self.tHTW.clear() + if not txinfo: + txinfo = self.getTxInfoFromFile() + for t in txinfo: + t_item = QTreeWidgetItem(t) + self.tHTW.addChild(t_item) + for i in range(4): + self.tHTW.resizeColumnToContents(i) + + def getTxInfoFromFile(self): + hf = jm_single().config.get("GUI", "history_file") + if not os.path.isfile(hf): + if w: + w.statusBar().showMessage("No transaction history found.") + return [] + txhist = [] + with open(hf,'rb') as f: + txlines = f.readlines() + for tl in txlines: + txhist.append(tl.strip().split(',')) + if not len(txhist[-1])==4: + QMessageBox.warning(self,"Error", + "Incorrectedly formatted file "+hf) + w.statusBar().showMessage("No transaction history found.") + return [] + return txhist[::-1] #appended to file in date order, window shows reverse + + def create_menu(self, position): + item = self.tHTW.currentItem() + if not item: + return + address_valid = False + if item: + address = str(item.text(0)) + try: + btc.b58check_to_hex(address) + address_valid = True + except AssertionError: + log.debug('no btc address found, not creating menu item') + + menu = QMenu() + if address_valid: + menu.addAction("Copy address to clipboard", + lambda: app.clipboard().setText(address)) + menu.addAction("Copy transaction id to clipboard", + lambda: app.clipboard().setText(str(item.text(2)))) + menu.addAction("Copy full tx info to clipboard", + lambda: app.clipboard().setText( + ','.join([str(item.text(_)) for _ in range(4)]))) + menu.exec_(self.tHTW.viewport().mapToGlobal(position)) + +class JMWalletTab(QWidget): + def __init__(self): + super(JMWalletTab, self).__init__() + self.wallet_name = 'NONE' + self.initUI() + + def initUI(self): + self.label1 = QLabel( + "CURRENT WALLET: "+self.wallet_name + ', total balance: 0.0', + self) + v = MyTreeWidget(self, self.create_menu, self.getHeaders()) + v.setSelectionMode(QAbstractItemView.ExtendedSelection) + v.on_update = self.updateWalletInfo + self.history = v + vbox = QVBoxLayout() + self.setLayout(vbox) + vbox.setMargin(0) + vbox.setSpacing(0) + vbox.addWidget(self.label1) + vbox.addWidget(v) + buttons = QWidget() + vbox.addWidget(buttons) + self.updateWalletInfo() + #vBoxLayout.addWidget(self.label2) + #vBoxLayout.addWidget(self.table) + self.show() + + def getHeaders(self): + '''Function included in case dynamic in future''' + return ['Address','Index','Balance','Used/New'] + + def create_menu(self, position): + item = self.history.currentItem() + address_valid = False + if item: + address = str(item.text(0)) + try: + btc.b58check_to_hex(address) + address_valid = True + except AssertionError: + log.debug('no btc address found, not creating menu item') + + menu = QMenu() + if address_valid: + menu.addAction("Copy address to clipboard", + lambda: app.clipboard().setText(address)) + menu.addAction("Resync wallet from blockchain", lambda: w.resyncWallet()) + #TODO add more items to context menu + menu.exec_(self.history.viewport().mapToGlobal(position)) + + def updateWalletInfo(self, walletinfo=None): + l = self.history + l.clear() + if walletinfo: + self.mainwindow = self.parent().parent().parent() + rows, mbalances, total_bal = walletinfo + if get_network() == 'testnet': + self.wallet_name = self.mainwindow.wallet.seed + else: + self.wallet_name = os.path.basename(self.mainwindow.wallet.path) + self.label1.setText( + "CURRENT WALLET: "+self.wallet_name + ', total balance: '+total_bal) + + for i in range(jm_single().config.getint("GUI","max_mix_depth")): + if walletinfo: + mdbalance = mbalances[i] + else: + mdbalance = "{0:.8f}".format(0) + m_item = QTreeWidgetItem(["Mixdepth " +str(i) + " , balance: "+mdbalance, + '','','','']) + l.addChild(m_item) + for forchange in [0,1]: + heading = 'EXTERNAL' if forchange==0 else 'INTERNAL' + heading_end = ' addresses m/0/%d/%d/' % (i, forchange) + heading += heading_end + seq_item = QTreeWidgetItem([ heading, '', '', '', '']) + m_item.addChild(seq_item) + if not forchange: + seq_item.setExpanded(True) + if not walletinfo: + item = QTreeWidgetItem(['None', '', '', '']) + seq_item.addChild(item) + else: + for j in range(len(rows[i][forchange])): + item = QTreeWidgetItem(rows[i][forchange][j]) + item.setFont(0,QFont(MONOSPACE_FONT)) + if rows[i][forchange][j][3] == 'used': + item.setForeground(3, QBrush(QColor('red'))) + seq_item.addChild(item) + + +class TaskThread(QtCore.QThread): + '''Thread that runs background tasks. Callbacks are guaranteed + to happen in the context of its parent.''' + + Task = namedtuple("Task", "task cb_success cb_done cb_error") + doneSig = QtCore.pyqtSignal(object, object, object) + + def __init__(self, parent, on_error=None): + super(TaskThread, self).__init__(parent) + self.on_error = on_error + self.tasks = Queue.Queue() + self.doneSig.connect(self.on_done) + self.start() + + def add(self, task, on_success=None, on_done=None, on_error=None): + on_error = on_error or self.on_error + self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error)) + + def run(self): + while True: + task = self.tasks.get() + if not task: + break + try: + result = task.task() + self.doneSig.emit(result, task.cb_done, task.cb_success) + except BaseException: + self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) + + def on_done(self, result, cb_done, cb): + # This runs in the parent's thread. + if cb_done: + cb_done() + if cb: + cb(result) + + def stop(self): + self.tasks.put(None) + +class JMMainWindow(QMainWindow): + def __init__(self): + super(JMMainWindow, self).__init__() + self.wallet=None + self.initUI() + + def closeEvent(self, event): + quit_msg = "Are you sure you want to quit?" + reply = QMessageBox.question(self, appWindowTitle, quit_msg, + QMessageBox.Yes, QMessageBox.No) + if reply == QMessageBox.Yes: + persist_config() + event.accept() + else: + event.ignore() + + def initUI(self): + self.statusBar().showMessage("Ready") + self.setGeometry(300,300,250,150) + exitAction = QAction(QIcon('exit.png'), '&Exit', self) + exitAction.setShortcut('Ctrl+Q') + exitAction.setStatusTip('Exit application') + exitAction.triggered.connect(qApp.quit) + generateAction = QAction('&Generate', self) + generateAction.setStatusTip('Generate new wallet') + generateAction.triggered.connect(self.generateWallet) + loadAction = QAction('&Load', self) + loadAction.setStatusTip('Load wallet from file') + loadAction.triggered.connect(self.selectWallet) + recoverAction = QAction('&Recover', self) + recoverAction.setStatusTip('Recover wallet from seedphrase') + recoverAction.triggered.connect(self.recoverWallet) + aboutAction = QAction('About Joinmarket', self) + aboutAction.triggered.connect(self.showAboutDialog) + menubar = QMenuBar() + + walletMenu = menubar.addMenu('&Wallet') + walletMenu.addAction(loadAction) + walletMenu.addAction(generateAction) + walletMenu.addAction(recoverAction) + walletMenu.addAction(exitAction) + aboutMenu = menubar.addMenu('&About') + aboutMenu.addAction(aboutAction) + + self.setMenuBar(menubar) + self.show() + + def showAboutDialog(self): + msgbox = QDialog(self) + lyt = QVBoxLayout(msgbox) + msgbox.setWindowTitle(appWindowTitle) + label1 = QLabel() + label1.setText(""+ + "Read more about Joinmarket
"+ + "
".join(["Joinmarket core software version: "+JM_CORE_VERSION,
+ "JoinmarketQt version: "+JM_GUI_VERSION,
+ "Messaging protocol version:"+" %s" % (
+ str(jm_single().JM_VERSION)),
+ "Help us support Bitcoin fungibility -",
+ "donate here: "]))
+ label2 = QLabel(donation_address)
+ for l in [label1, label2]:
+ l.setTextFormat(QtCore.Qt.RichText)
+ l.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ l.setOpenExternalLinks(True)
+ label2.setText(""+donation_address+"")
+ lyt.addWidget(label1)
+ lyt.addWidget(label2)
+ btnbox = QDialogButtonBox(msgbox)
+ btnbox.setStandardButtons(QDialogButtonBox.Ok)
+ btnbox.accepted.connect(msgbox.accept)
+ lyt.addWidget(btnbox)
+ msgbox.exec_()
+
+ def recoverWallet(self):
+ if get_network()=='testnet':
+ QMessageBox.information(self, 'Error',
+ 'recover from seedphrase not supported for testnet')
+ return
+ d = QDialog(self)
+ d.setModal(1)
+ d.setWindowTitle('Recover from seed')
+ layout = QGridLayout(d)
+ message_e = QTextEdit()
+ layout.addWidget(QLabel('Enter 12 words'), 0, 0)
+ layout.addWidget(message_e, 1, 0)
+ hbox = QHBoxLayout()
+ buttonBox = QDialogButtonBox(self)
+ buttonBox.setStandardButtons(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
+ buttonBox.button(QDialogButtonBox.Ok).clicked.connect(d.accept)
+ buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(d.reject)
+ hbox.addWidget(buttonBox)
+ layout.addLayout(hbox, 3, 0)
+ result = d.exec_()
+ if result != QDialog.Accepted:
+ return
+ msg = str(message_e.toPlainText())
+ words = msg.split() #splits on any number of ws chars
+ if not len(words)==12:
+ QMessageBox.warning(self, "Error","You did not provide 12 words, aborting.")
+ else:
+ try:
+ seed = mn_decode(words)
+ self.initWallet(seed=seed)
+ except ValueError as e:
+ QMessageBox.warning(self, "Error",
+ "Could not decode seedphrase: "+repr(e))
+
+ def selectWallet(self, testnet_seed=None):
+ if get_network() != 'testnet':
+ current_path = os.path.dirname(os.path.realpath(__file__))
+ if os.path.isdir(os.path.join(current_path,'wallets')):
+ current_path = os.path.join(current_path,'wallets')
+ firstarg = QFileDialog.getOpenFileName(self, 'Choose Wallet File',
+ directory=current_path)
+ #TODO validate the file looks vaguely like a wallet file
+ log.debug('Looking for wallet in: '+firstarg)
+ if not firstarg:
+ return
+ decrypted = False
+ while not decrypted:
+ text, ok = QInputDialog.getText(self, 'Decrypt wallet',
+ 'Enter your password:', mode=QLineEdit.Password)
+ if not ok:
+ return
+ pwd = str(text).strip()
+ decrypted = self.loadWalletFromBlockchain(firstarg, pwd)
+ else:
+ if not testnet_seed:
+ testnet_seed, ok = QInputDialog.getText(self, 'Load Testnet wallet',
+ 'Enter a testnet seed:', mode=QLineEdit.Normal)
+ if not ok:
+ return
+ firstarg = str(testnet_seed)
+ pwd = None
+ #ignore return value as there is no decryption failure possible
+ self.loadWalletFromBlockchain(firstarg, pwd)
+
+ def loadWalletFromBlockchain(self, firstarg=None, pwd=None):
+ if (firstarg and pwd) or (firstarg and get_network()=='testnet'):
+ self.wallet = Wallet(str(firstarg),
+ max_mix_depth=jm_single().config.getint("GUI","max_mix_depth"),
+ pwd=pwd)
+ if not self.wallet.decrypted:
+ QMessageBox.warning(self,"Error","Wrong password")
+ return False
+ if 'listunspent_args' not in jm_single().config.options('POLICY'):
+ jm_single().config.set('POLICY','listunspent_args', '[0]')
+ assert self.wallet, "No wallet loaded"
+ thread = TaskThread(self)
+ task = partial(jm_single().bc_interface.sync_wallet, self.wallet)
+ thread.add(task, on_done=self.updateWalletInfo)
+ self.statusBar().showMessage("Reading wallet from blockchain ...")
+ return True
+
+ def updateWalletInfo(self):
+ t = self.centralWidget().widget(0)
+ if not self.wallet: #failure to sync in constructor means object is not created
+ newstmsg = "Unable to sync wallet - see error in console."
+ else:
+ t.updateWalletInfo(get_wallet_printout(self.wallet))
+ newstmsg = "Wallet synced successfully."
+ self.statusBar().showMessage(newstmsg)
+
+ def resyncWallet(self):
+ if not self.wallet:
+ QMessageBox.warning(self, "Error", "No wallet loaded")
+ return
+ self.wallet.init_index() #sync operation assumes index is empty
+ self.loadWalletFromBlockchain()
+
+
+ def generateWallet(self):
+ log.debug('generating wallet')
+ if get_network() == 'testnet':
+ seed = self.getTestnetSeed()
+ self.selectWallet(testnet_seed=seed)
+ else:
+ self.initWallet()
+
+ def getTestnetSeed(self):
+ text, ok = QInputDialog.getText(self, 'Testnet seed',
+ 'Enter a string as seed (can be anything):')
+ if not ok or not text:
+ QMessageBox.warning(self,"Error","No seed entered, aborting")
+ return
+ return str(text).strip()
+
+ def initWallet(self, seed = None):
+ '''Creates a new mainnet
+ wallet
+ '''
+ if not seed:
+ seed = btc.sha256(os.urandom(64))[:32]
+ words = mn_encode(seed)
+ mb = QMessageBox()
+ #TODO: CONSIDERABLY! improve this dialog
+ mb.setText("Write down this wallet recovery seed.")
+ mb.setInformativeText(' '.join(words))
+ mb.setStandardButtons(QMessageBox.Ok)
+ ret = mb.exec_()
+
+ pd = PasswordDialog()
+ while True:
+ pd.exec_()
+ if pd.new_pw.text() != pd.conf_pw.text():
+ QMessageBox.warning(self,"Error","Passwords don't match.")
+ continue
+ break
+
+ walletfile = create_wallet_file(str(pd.new_pw.text()), seed)
+ walletname, ok = QInputDialog.getText(self, 'Choose wallet name',
+ 'Enter wallet file name:', QLineEdit.Normal,"wallet.json")
+ if not ok:
+ QMessageBox.warning(self,"Error","Create wallet aborted")
+ return
+ #create wallets subdir if it doesn't exist
+ if not os.path.exists('wallets'):
+ os.makedirs('wallets')
+ walletpath = os.path.join('wallets', str(walletname))
+ # Does a wallet with the same name exist?
+ if os.path.isfile(walletpath):
+ QMessageBox.warning(self, 'Error',
+ walletpath + ' already exists. Aborting.')
+ return
+ else:
+ fd = open(walletpath, 'w')
+ fd.write(walletfile)
+ fd.close()
+ QMessageBox.information(self, "Wallet created",
+ 'Wallet saved to ' + str(walletname))
+ self.loadWalletFromBlockchain(str(walletname), str(pd.new_pw.text()))
+
+
+def get_wallet_printout(wallet):
+ rows = []
+ mbalances = []
+ total_balance = 0
+ for m in range(wallet.max_mix_depth):
+ rows.append([])
+ balance_depth = 0
+ for forchange in [0,1]:
+ rows[m].append([])
+ for k in range(wallet.index[m][forchange] + jm_single().config.getint(
+ "GUI", "gaplimit")):
+ addr = wallet.get_addr(m, forchange, k)
+ balance = 0.0
+ for addrvalue in wallet.unspent.values():
+ if addr == addrvalue['address']:
+ balance += addrvalue['value']
+ balance_depth += balance
+ used = ('used' if k < wallet.index[m][forchange] else 'new')
+ if balance > 0.0 or (
+ k >= wallet.index[m][forchange] and forchange==0):
+ rows[m][forchange].append([addr, str(k),
+ "{0:.8f}".format(balance/1e8),used])
+ mbalances.append(balance_depth)
+ total_balance += balance_depth
+ #rows is of format [[[addr,index,bal,used],[addr,...]*5],
+ #[[addr, index,..], [addr, index..]*5]]
+ #mbalances is a simple array of 5 mixdepth balances
+ return (rows, ["{0:.8f}".format(x/1e8) for x in mbalances],
+ "{0:.8f}".format(total_balance/1e8))
+
+################################
+load_program_config()
+update_config_for_gui()
+
+#we're not downloading from github, so logs dir
+#might not exist
+if not os.path.exists('logs'):
+ os.makedirs('logs')
+app = QApplication(sys.argv)
+appWindowTitle = 'JoinMarketQt'
+w = JMMainWindow()
+tabWidget = QTabWidget(w)
+tabWidget.addTab(JMWalletTab(), "JM Wallet")
+settingsTab = SettingsTab()
+tabWidget.addTab(settingsTab, "Settings")
+tabWidget.addTab(SpendTab(), "Send Payment")
+tabWidget.addTab(TxHistoryTab(), "Tx History")
+w.resize(600, 500)
+suffix = ' - Testnet' if get_network() == 'testnet' else ''
+w.setWindowTitle(appWindowTitle + suffix)
+tabWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+w.setCentralWidget(tabWidget)
+w.show()
+
+sys.exit(app.exec_())
\ No newline at end of file
diff --git a/joinmarket/__init__.py b/joinmarket/__init__.py
index c5c04f12..c73dae97 100644
--- a/joinmarket/__init__.py
+++ b/joinmarket/__init__.py
@@ -16,9 +16,10 @@
from .slowaes import decryptData, encryptData
from .taker import Taker, OrderbookWatch, CoinJoinTX
from .wallet import AbstractWallet, BitcoinCoreInterface, Wallet, \
- BitcoinCoreWallet
+ BitcoinCoreWallet, create_wallet_file
from .configure import load_program_config, jm_single, get_p2pk_vbyte, \
- get_network, jm_single, get_network, validate_address
+ get_network, jm_single, get_network, validate_address, \
+ get_blockchain_interface_instance
from .blockchaininterface import BlockrInterface, BlockchainInterface
# Set default logging handler to avoid "No handler found" warnings.
diff --git a/joinmarket/blockchaininterface.py b/joinmarket/blockchaininterface.py
index 3de500a3..0f2af662 100644
--- a/joinmarket/blockchaininterface.py
+++ b/joinmarket/blockchaininterface.py
@@ -22,7 +22,7 @@
import subprocess
from joinmarket.jsonrpc import JsonRpcConnectionError, JsonRpcError
-from joinmarket.configure import get_p2pk_vbyte, jm_single
+from joinmarket.configure import get_p2pk_vbyte, jm_single, get_network
from joinmarket.support import get_log, chunks
log = get_log()
@@ -112,13 +112,13 @@ def query_utxo_set(self, txouts):
otherwise returns value in satoshis, address and output script
"""
# address and output script contain the same information btw
-
+
@abc.abstractmethod
def estimate_fee_per_kb(self, N):
'''Use the blockchain interface to
get an estimate of the transaction fee per kb
required for inclusion in the next N blocks.
- '''
+ '''
class BlockrInterface(BlockchainInterface):
@@ -128,10 +128,22 @@ def __init__(self, testnet=False):
super(BlockrInterface, self).__init__()
# see bci.py in bitcoin module
- self.network = 'testnet' if testnet else 'btc'
- self.blockr_domain = 'tbtc' if testnet else 'btc'
+ #self.network = 'testnet' if testnet else 'btc'
+ #self.blockr_domain = 'tbtc' if testnet else 'btc'
self.last_sync_unspent = 0
+ def network_for_blockr_push(self):
+ if get_network() == 'testnet':
+ return 'testnet'
+ else:
+ return 'btc'
+
+ def blockr_domain(self):
+ if get_network() == 'testnet':
+ return 'tbtc'
+ else:
+ return 'btc'
+
def sync_addresses(self, wallet):
log.debug('downloading wallet history')
# sets Wallet internal indexes to be at the next unused address
@@ -140,14 +152,14 @@ def sync_addresses(self, wallet):
unused_addr_count = 0
last_used_addr = ''
while (unused_addr_count < wallet.gaplimit or
- not is_index_ahead_of_cache(
- wallet, mix_depth, forchange)):
+ not is_index_ahead_of_cache(
+ wallet, mix_depth, forchange)):
addrs = [wallet.get_new_addr(mix_depth, forchange)
for _ in range(self.BLOCKR_MAX_ADDR_REQ_COUNT)]
# TODO send a pull request to pybitcointools
# because this surely should be possible with a function from it
- blockr_url = 'https://' + self.blockr_domain
+ blockr_url = 'https://' + self.blockr_domain()
blockr_url += '.blockr.io/api/v1/address/txs/'
res = btc.make_request(blockr_url + ','.join(addrs))
@@ -162,18 +174,18 @@ def sync_addresses(self, wallet):
wallet.index[mix_depth][forchange] = 0
else:
wallet.index[mix_depth][forchange] = wallet.addr_cache[
- last_used_addr][
- 2] + 1
+ last_used_addr][
+ 2] + 1
def sync_unspent(self, wallet):
# finds utxos in the wallet
st = time.time()
# dont refresh unspent dict more often than 10 minutes
- rate_limit_time = 10 * 60
+ rate_limit_time = 5 * 6
if st - self.last_sync_unspent < rate_limit_time:
log.debug(
- 'blockr sync_unspent() happened too recently (%dsec), skipping'
- % (st - self.last_sync_unspent))
+ 'blockr sync_unspent() happened too recently (%dsec), skipping'
+ % (st - self.last_sync_unspent))
return
wallet.unspent = {}
@@ -191,8 +203,8 @@ def sync_unspent(self, wallet):
# unspent() doesnt tell you which address, you get a bunch of utxos
# but dont know which privkey to sign with
- blockr_url = 'https://' + self.blockr_domain + \
- '.blockr.io/api/v1/address/unspent/'
+ blockr_url = 'https://' + self.blockr_domain() + \
+ '.blockr.io/api/v1/address/unspent/'
res = btc.make_request(blockr_url + ','.join(req))
data = json.loads(res)['data']
if 'unspent' in data:
@@ -250,8 +262,8 @@ def run(self):
random.shuffle(self.output_addresses
) # seriously weird bug with blockr.io
data = json.loads(
- btc.make_request(blockr_url + ','.join(
- self.output_addresses
+ btc.make_request(blockr_url + ','.join(
+ self.output_addresses
) + '?unconfirmed=1'))['data']
shared_txid = None
@@ -266,12 +278,12 @@ def run(self):
if len(shared_txid) == 0:
continue
time.sleep(
- 2
- ) # here for some race condition bullshit with blockr.io
+ 2
+ ) # here for some race condition bullshit with blockr.io
blockr_url = 'https://' + self.blockr_domain
blockr_url += '.blockr.io/api/v1/tx/raw/'
data = json.loads(btc.make_request(blockr_url + ','.join(
- shared_txid)))['data']
+ shared_txid)))['data']
if not isinstance(data, list):
data = [data]
for txinfo in data:
@@ -285,7 +297,7 @@ def run(self):
break
self.unconfirmfun(
- btc.deserialize(unconfirmed_txhex), unconfirmed_txid)
+ btc.deserialize(unconfirmed_txhex), unconfirmed_txid)
st = int(time.time())
confirmed_txid = None
@@ -300,7 +312,7 @@ def run(self):
blockr_url = 'https://' + self.blockr_domain
blockr_url += '.blockr.io/api/v1/address/txs/'
data = json.loads(btc.make_request(blockr_url + ','.join(
- self.output_addresses)))['data']
+ self.output_addresses)))['data']
shared_txid = None
for addrtxs in data:
txs = set(str(txdata['tx'])
@@ -315,8 +327,8 @@ def run(self):
blockr_url = 'https://' + self.blockr_domain
blockr_url += '.blockr.io/api/v1/tx/raw/'
data = json.loads(
- btc.make_request(
- blockr_url + ','.join(shared_txid)))['data']
+ btc.make_request(
+ blockr_url + ','.join(shared_txid)))['data']
if not isinstance(data, list):
data = [data]
for txinfo in data:
@@ -329,13 +341,13 @@ def run(self):
confirmed_txhex = str(txinfo['tx']['hex'])
break
self.confirmfun(
- btc.deserialize(confirmed_txhex), confirmed_txid, 1)
+ btc.deserialize(confirmed_txhex), confirmed_txid, 1)
- NotifyThread(self.blockr_domain, txd, unconfirmfun, confirmfun, timeoutfun).start()
+ NotifyThread(self.blockr_domain(), txd, unconfirmfun, confirmfun, timeoutfun).start()
def pushtx(self, txhex):
try:
- json_str = btc.blockr_pushtx(txhex, self.network)
+ json_str = btc.blockr_pushtx(txhex, self.network_for_blockr_push())
data = json.loads(json_str)
if data['status'] != 'success':
log.debug(data)
@@ -358,9 +370,9 @@ def query_utxo_set(self, txout):
txids = [txids]
data = []
for ids in txids:
- blockr_url = 'https://' + self.blockr_domain + '.blockr.io/api/v1/tx/info/'
+ blockr_url = 'https://' + self.blockr_domain() + '.blockr.io/api/v1/tx/info/'
blockr_data = json.loads(
- btc.make_request(blockr_url + ','.join(ids)))['data']
+ btc.make_request(blockr_url + ','.join(ids)))['data']
if not isinstance(blockr_data, list):
blockr_data = [blockr_data]
data += blockr_data
@@ -372,7 +384,7 @@ def query_utxo_set(self, txout):
result.append(None)
else:
result.append({'value': int(Decimal(vout['amount']) * Decimal(
- '1e8')),
+ '1e8')),
'address': vout['address'],
'script': vout['extras']['script']})
return result
@@ -381,14 +393,14 @@ def estimate_fee_per_kb(self, N):
bcypher_fee_estimate_url = 'https://api.blockcypher.com/v1/btc/main'
bcypher_data = json.loads(btc.make_request(bcypher_fee_estimate_url))
log.debug("Got blockcypher result: "+pprint.pformat(bcypher_data))
- if N<=2:
- fee_per_kb = bcypher_data["high_fee_per_kb"]
- elif N <=4:
- fee_per_kb = bcypher_data["medium_fee_per_kb"]
- else:
- fee_per_kb = bcypher_data["low_fee_per_kb"]
-
- return fee_per_kb
+ if N<=2:
+ fee_per_kb = bcypher_data["high_fee_per_kb"]
+ elif N <=4:
+ fee_per_kb = bcypher_data["medium_fee_per_kb"]
+ else:
+ fee_per_kb = bcypher_data["low_fee_per_kb"]
+
+ return fee_per_kb
def bitcoincore_timeout_callback(uc_called, txout_set, txnotify_fun_list,
timeoutfun):
@@ -411,7 +423,7 @@ def __init__(self, request, client_address, base_server):
self.btcinterface = base_server.btcinterface
self.base_server = base_server
BaseHTTPServer.BaseHTTPRequestHandler.__init__(
- self, request, client_address, base_server)
+ self, request, client_address, base_server)
def do_HEAD(self):
pages = ('/walletnotify?', '/alertnotify?')
@@ -562,7 +574,7 @@ def add_watchonly_addresses(self, addr_list, wallet_name):
for addr in addr_list:
self.rpc('importaddress', [addr, wallet_name, False])
if jm_single().config.get(
- "BLOCKCHAIN", "blockchain_source") != 'regtest':
+ "BLOCKCHAIN", "blockchain_source") != 'regtest':
print('restart Bitcoin Core with -rescan if you\'re '
'recovering an existing wallet from backup seed')
print(' otherwise just restart this joinmarket script')
@@ -616,8 +628,8 @@ def sync_addresses(self, wallet):
breakloop = False
while not breakloop:
if unused_addr_count >= wallet.gaplimit and \
- is_index_ahead_of_cache(wallet, mix_depth,
- forchange):
+ is_index_ahead_of_cache(wallet, mix_depth,
+ forchange):
break
mix_change_addrs = [
wallet.get_new_addr(mix_depth, forchange)
@@ -625,7 +637,7 @@ def sync_addresses(self, wallet):
for mc_addr in mix_change_addrs:
if mc_addr not in imported_addr_list:
too_few_addr_mix_change.append(
- (mix_depth, forchange))
+ (mix_depth, forchange))
breakloop = True
break
if mc_addr in used_addr_list:
@@ -733,12 +745,12 @@ def query_utxo_set(self, txout):
def estimate_fee_per_kb(self, N):
estimate = Decimal(1e8)*Decimal(self.rpc('estimatefee', [N]))
- if estimate < 0:
- #This occurs when Core has insufficient data to estimate.
- #TODO anything better than a hardcoded default?
- return 30000
+ if estimate < 0:
+ #This occurs when Core has insufficient data to estimate.
+ #TODO anything better than a hardcoded default?
+ return 30000
else:
- return estimate
+ return estimate
# class for regtest chain access
# running on local daemon. Only
@@ -831,7 +843,7 @@ def get_received_by_addr(self, addresses, query_params):
self.rpc('importaddress', [address, 'watchonly'])
res.append({'address': address,
'balance': int(Decimal(1e8) * Decimal(
- self.rpc('getreceivedbyaddress', [address])))})
+ self.rpc('getreceivedbyaddress', [address])))})
return {'data': res}
# todo: won't run anyways
diff --git a/joinmarket/configure.py b/joinmarket/configure.py
index 0be85b51..333697db 100644
--- a/joinmarket/configure.py
+++ b/joinmarket/configure.py
@@ -118,8 +118,7 @@ def jm_single():
socks5_port = 9050
#for tor
#host = 6dvj6v5imhny3anf.onion
-#onion / i2p have their own ports on CGAN
-#port = 6698
+#port = 6697
#usessl = true
#socks5 = true
diff --git a/joinmarket/wallet.py b/joinmarket/wallet.py
index 37225c72..7a2fb0c6 100644
--- a/joinmarket/wallet.py
+++ b/joinmarket/wallet.py
@@ -3,13 +3,14 @@
import os
import pprint
import sys
+import datetime
from decimal import Decimal
from ConfigParser import NoSectionError
from getpass import getpass
import bitcoin as btc
-from joinmarket.slowaes import decryptData
+from joinmarket.slowaes import decryptData, encryptData
from joinmarket.blockchaininterface import BitcoinCoreInterface
from joinmarket.configure import jm_single, get_network, get_p2pk_vbyte
@@ -35,6 +36,15 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh'):
log.debug("got estimated tx bytes: "+str(tx_estimated_bytes))
return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
+def create_wallet_file(pwd, seed):
+ password_key = btc.bin_dbl_sha256(pwd)
+ encrypted_seed = encryptData(password_key, seed.decode('hex'))
+ timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
+ return json.dumps({'creator': 'joinmarket project',
+ 'creation_time': timestamp,
+ 'encrypted_seed': encrypted_seed.encode('hex'),
+ 'network': get_network()})
+
class AbstractWallet(object):
"""
Abstract wallet for use with JoinMarket
@@ -116,7 +126,8 @@ def __init__(self,
max_mix_depth=2,
gaplimit=6,
extend_mixdepth=False,
- storepassword=False):
+ storepassword=False,
+ pwd = None):
super(Wallet, self).__init__()
self.max_mix_depth = max_mix_depth
self.storepassword = storepassword
@@ -126,24 +137,28 @@ def __init__(self,
self.unspent = {}
self.spent_utxos = []
self.imported_privkeys = {}
- self.seed = self.read_wallet_file_data(seedarg)
+ self.decrypted = False
+ self.seed = self.read_wallet_file_data(seedarg, pwd)
+ if not self.seed:
+ return
if extend_mixdepth and len(self.index_cache) > max_mix_depth:
self.max_mix_depth = len(self.index_cache)
self.gaplimit = gaplimit
- master = btc.bip32_master_key(self.seed, (btc.MAINNET_PRIVATE if
+ self.master_key = btc.bip32_master_key(self.seed, (btc.MAINNET_PRIVATE if
get_network() == 'mainnet' else btc.TESTNET_PRIVATE))
- m_0 = btc.bip32_ckd(master, 0)
- mixing_depth_keys = [btc.bip32_ckd(m_0, c)
+ m_0 = btc.bip32_ckd(self.master_key, 0)
+ self.mixing_depth_keys = [btc.bip32_ckd(m_0, c)
for c in range(self.max_mix_depth)]
self.keys = [(btc.bip32_ckd(m, 0), btc.bip32_ckd(m, 1))
- for m in mixing_depth_keys]
+ for m in self.mixing_depth_keys]
+ self.init_index()
- # self.index = [[0, 0]]*max_mix_depth
+ def init_index(self):
self.index = []
for i in range(self.max_mix_depth):
self.index.append([0, 0])
- def read_wallet_file_data(self, filename):
+ def read_wallet_file_data(self, filename, pwd = None):
self.path = None
self.index_cache = [[0, 0]] * self.max_mix_depth
path = os.path.join('wallets', filename)
@@ -151,6 +166,7 @@ def read_wallet_file_data(self, filename):
if get_network() == 'testnet':
log.debug('filename interpreted as seed, only available in '
'testnet because this probably has lower entropy')
+ self.decrypted = True
return filename
else:
raise IOError('wallet file not found')
@@ -166,9 +182,12 @@ def read_wallet_file_data(self, filename):
sys.exit(0)
if 'index_cache' in walletdata:
self.index_cache = walletdata['index_cache']
- decrypted = False
- while not decrypted:
- password = getpass('Enter wallet decryption passphrase: ')
+ self.decrypted = False
+ while not self.decrypted:
+ if pwd:
+ password = pwd
+ else:
+ password = getpass('Enter wallet decryption passphrase: ')
password_key = btc.bin_dbl_sha256(password)
encrypted_seed = walletdata['encrypted_seed']
try:
@@ -179,12 +198,15 @@ def read_wallet_file_data(self, filename):
# padding by chance from a wrong password; sanity check the
# seed length
if len(decrypted_seed) == 32:
- decrypted = True
+ self.decrypted = True
else:
raise ValueError
except ValueError:
print('Incorrect password')
- decrypted = False
+ self.decrypted = False
+ if pwd:
+ decrypted_seed = None
+ break
if self.storepassword:
self.password_key = password_key
self.walletdata = walletdata
diff --git a/sendpayment.py b/sendpayment.py
index 065ac3d4..5e3941fe 100644
--- a/sendpayment.py
+++ b/sendpayment.py
@@ -176,7 +176,7 @@ def run(self):
class SendPayment(Taker):
def __init__(self, msgchan, wallet, destaddr, amount, makercount, txfee,
- waittime, mixdepth, answeryes, chooseOrdersFunc):
+ waittime, mixdepth, answeryes, chooseOrdersFunc, isolated=False):
Taker.__init__(self, msgchan)
self.wallet = wallet
self.destaddr = destaddr
@@ -187,10 +187,14 @@ def __init__(self, msgchan, wallet, destaddr, amount, makercount, txfee,
self.mixdepth = mixdepth
self.answeryes = answeryes
self.chooseOrdersFunc = chooseOrdersFunc
+ #extra variables for GUI-style
+ self.isolated = isolated
+ self.txid = None
def on_welcome(self):
Taker.on_welcome(self)
- PaymentThread(self).start()
+ if not self.isolated:
+ PaymentThread(self).start()
def main():
@@ -320,6 +324,156 @@ def main():
log.debug(traceback.format_exc())
+#PaymentThread object modified (not a thread, refactored a bit)
+#The reason is that Qt won't work with python threads, and we need
+#separate threads for separate steps (returning chosen orders to gui),
+#so the threading is in the gui code.
+class PT(object):
+
+ def __init__(self, taker):
+ self.taker = taker
+ self.ignored_makers = []
+
+ def create_tx(self):
+ time.sleep(self.taker.waittime)
+ crow = self.taker.db.execute(
+ 'SELECT COUNT(DISTINCT counterparty) FROM orderbook;').fetchone()
+ counterparty_count = crow['COUNT(DISTINCT counterparty)']
+ counterparty_count -= len(self.ignored_makers)
+ if counterparty_count < self.taker.makercount:
+ log.debug('not enough counterparties to fill order, ending')
+ #NB: don't shutdown msgchan here, that is done by the caller
+ #after setting GUI state to reflect the reason for shutdown.
+ return None, None, None, None
+
+ utxos = None
+ orders = None
+ cjamount = None
+ change_addr = None
+ choose_orders_recover = None
+ if self.taker.amount == 0:
+ utxos = self.taker.wallet.get_utxos_by_mixdepth()[
+ self.taker.mixdepth]
+ #do our best to estimate the fee based on the number of
+ #our own utxos; this estimate may be significantly higher
+ #than the default set in option.txfee * makercount, where
+ #we have a large number of utxos to spend. If it is smaller,
+ #we'll be conservative and retain the original estimate.
+ est_ins = len(utxos)+3*self.taker.makercount
+ log.debug("Estimated ins: "+str(est_ins))
+ est_outs = 2*self.taker.makercount + 1
+ log.debug("Estimated outs: "+str(est_outs))
+ estimated_fee = estimate_tx_fee(est_ins, est_outs)
+ log.debug("We have a fee estimate: "+str(estimated_fee))
+ log.debug("And a requested fee of: "+str(
+ self.taker.txfee * self.taker.makercount))
+ if estimated_fee > self.taker.makercount * self.taker.txfee:
+ #both values are integers; we can ignore small rounding errors
+ self.taker.txfee = estimated_fee / self.taker.makercount
+ total_value = sum([va['value'] for va in utxos.values()])
+ orders, cjamount = choose_sweep_orders(
+ self.taker.db, total_value, self.taker.txfee,
+ self.taker.makercount, self.taker.chooseOrdersFunc,
+ self.ignored_makers)
+ if not orders:
+ raise Exception("Could not find orders to complete transaction.")
+ total_cj_fee = total_value - cjamount - \
+ self.taker.txfee*self.taker.makercount
+
+ else:
+ orders, total_cj_fee = self.sendpayment_choose_orders(
+ self.taker.amount, self.taker.makercount)
+ cjamount = self.taker.amount
+ if not orders:
+ log.debug(
+ 'ERROR not enough liquidity in the orderbook, exiting')
+ return None, None, None, None
+ return orders, total_cj_fee, cjamount, utxos
+
+ def do_tx(self, total_cj_fee, orders, cjamount, utxos,
+ donate=False, donate_trigger=1000000, donation_address=None):
+ #for non-sweep, we now have to set amount, change address and utxo selection
+ if self.taker.amount > 0:
+ total_amount = self.taker.amount + total_cj_fee + \
+ self.taker.txfee*self.taker.makercount
+ log.debug('total estimated amount spent = ' + str(total_amount))
+ #adjust the required amount upwards to anticipate a tripling of
+ #transaction fee after re-estimation; this is sufficiently conservative
+ #to make failures unlikely while keeping the occurence of failure to
+ #find sufficient utxos extremely rare. Indeed, a tripling of 'normal'
+ #txfee indicates undesirable behaviour on maker side anyway.
+ try:
+ utxos = self.taker.wallet.select_utxos(self.taker.mixdepth,
+ total_amount+2*self.taker.txfee*self.taker.makercount)
+ except Exception as e:
+ log.debug("Failed to select coins: "+repr(e))
+ return
+ my_total_in = sum([va['value'] for u, va in utxos.iteritems()])
+ log.debug("using coinjoin amount: "+str(cjamount))
+ change_amount = my_total_in-cjamount
+ log.debug("using change amount: "+str(change_amount))
+ if donate and change_amount < donate_trigger*1e8:
+ #sanity check
+ res = validate_address(donation_address)
+ if not res[0]:
+ log.debug("Donation address invalid! Error: "+res[1])
+ return
+ change_addr = donation_address
+ else:
+ change_addr = self.taker.wallet.get_internal_addr(self.taker.mixdepth)
+ log.debug("using change address: "+change_addr)
+
+ #For sweeps, we reset the change address to None, and use the provided
+ #amount and utxos (calculated in the first step)
+ else:
+ change_addr = None
+
+ choose_orders_recover = self.sendpayment_choose_orders
+ log.debug("About to start coinjoin")
+ try:
+ self.taker.start_cj(self.taker.wallet, cjamount, orders, utxos,
+ self.taker.destaddr, change_addr,
+ self.taker.makercount*self.taker.txfee,
+ self.finishcallback, choose_orders_recover)
+ except Exception as e:
+ log.debug("failed to start coinjoin: "+repr(e))
+
+ def finishcallback(self, coinjointx):
+ if coinjointx.all_responded:
+ pushed = coinjointx.self_sign_and_push()
+ if pushed:
+ log.debug('created fully signed tx, ending')
+ self.taker.txid = coinjointx.txid
+ else:
+ #Error should be in log, will not retry.
+ log.debug('failed to push tx, ending.')
+ self.taker.msgchan.shutdown()
+ return
+ self.ignored_makers += coinjointx.nonrespondants
+ log.debug('tx negotation failed, ignored_makers=' + str(
+ self.ignored_makers))
+ #triggers endpoint for GUI
+ self.taker.msgchan.shutdown()
+
+ def sendpayment_choose_orders(self,
+ cj_amount,
+ makercount,
+ nonrespondants=None,
+ active_nicks=None):
+ if nonrespondants is None:
+ nonrespondants = []
+ if active_nicks is None:
+ active_nicks = []
+ self.ignored_makers += nonrespondants
+ orders, total_cj_fee = choose_orders(
+ self.taker.db, cj_amount, makercount, self.taker.chooseOrdersFunc,
+ self.ignored_makers + active_nicks)
+ if not orders:
+ return None, 0
+ log.debug('chosen orders to fill ' + str(orders) + ' totalcjfee=' + str(
+ total_cj_fee))
+ return orders, total_cj_fee
+
if __name__ == "__main__":
main()
print('done')
diff --git a/test/testyg-daemon.py b/test/testyg-daemon.py
new file mode 100644
index 00000000..d07be219
--- /dev/null
+++ b/test/testyg-daemon.py
@@ -0,0 +1,100 @@
+#! /usr/bin/env python
+from __future__ import absolute_import
+'''Run yield generators on regtest.'''
+
+import sys
+import os
+import time
+import binascii
+import pexpect
+import random
+import subprocess
+import unittest
+from commontest import local_command, make_wallets, interact
+
+data_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+sys.path.insert(0, os.path.join(data_dir))
+
+import bitcoin as btc
+
+from joinmarket import load_program_config, jm_single
+from joinmarket import get_p2pk_vbyte, get_log, Wallet
+from joinmarket.support import chunks
+
+python_cmd = 'python2'
+yg_cmd = 'yield-generator-basic.py'
+#yg_cmd = 'yield-generator-mixdepth.py'
+#yg_cmd = 'yield-generator-deluxe.py'
+
+log = get_log()
+
+
+class TumblerTests(unittest.TestCase):
+
+ def setUp(self):
+ #create 7 new random wallets.
+ #put about 10 coins in each, spread over random mixdepths
+ #in units of 0.5
+
+ seeds = chunks(binascii.hexlify(os.urandom(15 * 7)), 7)
+ self.wallets = {}
+ for i in range(7):
+ self.wallets[i] = {'seed': seeds[i],
+ 'wallet': Wallet(seeds[i],
+ max_mix_depth=5)}
+ #adding coins somewhat randomly, spread over all 5 depths
+ for i in range(7):
+ w = self.wallets[i]['wallet']
+ for j in range(5):
+ for k in range(4):
+ base = 0.001 if i == 6 else 2.0
+ amt = base + random.random(
+ ) #average is 0.5 for tumbler, else 1.5
+ jm_single().bc_interface.grab_coins(
+ w.get_external_addr(j), amt)
+
+ def run_tumble(self, amt):
+ yigen_procs = []
+ for i in range(6):
+ ygp = local_command([python_cmd, yg_cmd,\
+ str(self.wallets[i]['seed'])], bg=True)
+ time.sleep(2) #give it a chance
+ yigen_procs.append(ygp)
+
+#A significant delay is needed to wait for the yield generators to sync
+ time.sleep(20)
+
+ #start a tumbler
+ amt = amt * 1e8 #in satoshis
+ #send to any old address
+ dest_address = btc.privkey_to_address(os.urandom(32), get_p2pk_vbyte())
+ try:
+ print 'taker seed: '+self.wallets[6]['seed']
+ while True:
+ print 'hello'
+ time.sleep(80)
+ except subprocess.CalledProcessError, e:
+ for ygp in yigen_procs:
+ ygp.kill()
+ print e.returncode
+ print e.message
+ raise
+
+ if any(yigen_procs):
+ for ygp in yigen_procs:
+ ygp.kill()
+
+ return True
+
+ def test_simple_send(self):
+ self.failUnless(self.run_tumble(1))
+
+
+def main():
+ os.chdir(data_dir)
+ load_program_config()
+ unittest.main()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/texttable.py b/texttable.py
new file mode 100644
index 00000000..995b2b7e
--- /dev/null
+++ b/texttable.py
@@ -0,0 +1,121 @@
+# texttable-python. ( https://github.com/dan-da/texttable-python )
+# author: Dan Libby. https://github.com/dan-da/
+
+from collections import OrderedDict
+
+class ttrow(OrderedDict):
+ pass
+
+#
+# A class to print text in formatted tables.
+#
+class texttable:
+
+
+ # Formats a fixed-width text table, with borders.
+ #
+ # param rows array of rows. each row contains array or ttrow (key/val)
+ # param headertype keys | firstrow | none/None
+ # param footertype keys | lastrow | none/None
+ # param empty_row_string String to use when there is no data, or None.
+ @classmethod
+ def table( cls, rows, headertype = 'keys', footertype = 'none', empty_row_string = 'No Data' ):
+
+ if not len( rows ):
+ if( empty_row_string != None ):
+ rows = [ [ empty_row_string ] ]
+ else:
+ return ''
+
+ header = footer = None
+ if( headertype == 'keys' and isinstance(rows[0], dict)):
+ header = cls.obj_arr( rows[0] ).keys()
+ elif( headertype == 'firstrow' ):
+ header = cls.obj_arr( rows[0] )
+ rows = rows[1:]
+ if( footertype == 'keys' and len( rows ) and isinstance(rows[len(rows)-1],dict) ):
+ footer = cls.obj_arr( rows[len(rows) - 1] ).keys()
+ elif( footertype == 'lastrow' and len( rows ) ):
+ footer = cls.obj_arr( rows[len(rows)-1] )
+ rows = rows[:-1]
+
+ col_widths = {}
+
+ if( header ):
+ cls.calc_row_col_widths( col_widths, header )
+ if( footer ):
+ cls.calc_row_col_widths( col_widths, footer )
+ for row in rows:
+ row = cls.obj_arr( row )
+ cls.calc_row_col_widths( col_widths, row )
+
+ buf = ''
+ if( header ):
+ buf += cls.print_divider_row( col_widths )
+ buf += cls.print_row( col_widths, header )
+ buf += cls.print_divider_row( col_widths )
+ for row in rows:
+ row = cls.obj_arr( row )
+ buf += cls.print_row( col_widths, row )
+ buf += cls.print_divider_row( col_widths )
+ if( footer ):
+ buf += cls.print_row( col_widths, footer )
+ buf += cls.print_divider_row( col_widths )
+
+ return buf
+
+ @classmethod
+ def print_divider_row( cls, col_widths ):
+ buf = '+'
+ for i in range(0, len(col_widths)):
+ width = col_widths[i]
+ buf += '-' + '-'.ljust( width, '-' ) + "-+"
+ buf += "\n"
+ return buf
+
+ @classmethod
+ def print_row( cls, col_widths, row ):
+ buf = '|'
+ idx = 0
+ for val in row:
+
+ if isinstance(row, dict):
+ val = row[val]
+ val = str(val)
+
+ pad_type = 'left' if cls.is_numeric( val ) else 'right'
+ if pad_type == 'left':
+ buf += ' ' + val.rjust( col_widths[idx], ' ' ) + " |"
+ else:
+ buf += ' ' + val.ljust( col_widths[idx], ' ' ) + " |"
+ idx = idx + 1
+ return buf + "\n"
+
+ @classmethod
+ def calc_row_col_widths( cls, col_widths, row ):
+ idx = 0
+
+ for val in row:
+
+ if isinstance(row, dict):
+ val = row[val]
+ val = str(val)
+
+ if idx not in col_widths:
+ col_widths[idx] = 0
+ if( len(val) > col_widths[idx] ):
+ col_widths[idx] = len(val)
+ idx = idx + 1
+
+ @classmethod
+ def obj_arr( cls, t ):
+ return t
+
+ @classmethod
+ def is_numeric(cls, var):
+ try:
+ float(var)
+ return True
+ except ValueError:
+ return False
+
diff --git a/wallet-tool.py b/wallet-tool.py
index f7a0655c..75225113 100644
--- a/wallet-tool.py
+++ b/wallet-tool.py
@@ -8,12 +8,40 @@
import sqlite3
from optparse import OptionParser
+from texttable import texttable
+
from joinmarket import load_program_config, get_network, Wallet, encryptData, \
get_p2pk_vbyte, jm_single, mn_decode, mn_encode, BitcoinCoreInterface, \
- JsonRpcError
+ JsonRpcError, create_wallet_file
import bitcoin as btc
+
+# a function to print a text table.
+# Detects if running in a TTY and if so disables terminal wrapping
+# because the wrapping is too ugly for words.
+# In this mode, terminals truncate text if line is too long.
+# A good way to view the full table if it is too wide for terminal
+# is to pipe output through less -S, which enables horizontal scrolling.
+# eg: python wallet-tool.py