#! /usr/bin/env python from __future__ import absolute_import, print_function """ A sample implementation of a single coinjoin script, adapted from `sendpayment.py` in Joinmarket-Org/joinmarket. This is primitive and not yet well tested, but it is designed to illustrate the main functionality of the new architecture: this code can be run in a separate environment (but not safely over the internet, better on one machine) to the joinmarketdaemon. Moreover, it can run several transactions using the -b option, e.g.: `python sendpayment.py -b 3 -N 3 -m 1 walletseed amount address`; note here only one destination address for multiple transactions, only one mixdepth and other settings; this is just a proof of concept. The idea is that the "backend" (daemon) will keep its orderbook and stay connected on the message channel between runs, only shutting down after all are complete. It should be very easy to extend this further, of course. More complex applications can extend from Taker and add more features. This will also allow easier coding of non-CLI interfaces. A plugin for Electrum is in process and already working. Other potential customisations of the Taker object instantiation include: external_addr=None implies joining to another mixdepth in the same wallet. order_chooser can be set to a different custom function that selects counterparty offers according to different rules. """ import random import sys import threading from optparse import OptionParser from twisted.internet import reactor import time from jmclient import (Taker, load_program_config, JMTakerClientProtocolFactory, start_reactor, validate_address, jm_single, choose_orders, choose_sweep_orders, pick_order, cheapest_order_choose, weighted_order_choose, Wallet, BitcoinCoreWallet, estimate_tx_fee) from jmbase.support import get_log, debug_dump_object log = get_log() txcount = 1 wallet = None def check_high_fee(total_fee_pc): WARNING_THRESHOLD = 0.02 # 2% if total_fee_pc > WARNING_THRESHOLD: print('\n'.join(['=' * 60] * 3)) print('WARNING ' * 6) print('\n'.join(['=' * 60] * 1)) print('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.') print('\n'.join(['=' * 60] * 1)) print('WARNING ' * 6) print('\n'.join(['=' * 60] * 3)) def main(): parser = OptionParser( usage= 'usage: %prog [options] [wallet file / fromaccount] [amount] [destaddr]', description='Sends a single payment from a given mixing depth of your ' + 'wallet to an given address using coinjoin and then switches off. Also sends from bitcoinqt. ' + 'Setting amount to zero will do a sweep, where the entire mix depth is emptied') parser.add_option( '-f', '--txfee', action='store', type='int', dest='txfee', default=-1, help= 'number of satoshis per participant to use as the initial estimate ' + 'for the total transaction fee, default=dynamically estimated, note that this is adjusted ' + 'based on the estimated fee calculated after tx construction, based on ' + 'policy set in joinmarket.cfg.') parser.add_option( '-w', '--wait-time', action='store', type='float', dest='waittime', help='wait time in seconds to allow orders to arrive, default=15', default=15) parser.add_option( '-N', '--makercount', action='store', type='int', dest='makercount', help='how many makers to coinjoin with, default random from 4 to 6', default=random.randint(4, 6)) parser.add_option('-p', '--port', type='int', dest='daemonport', help='port on which joinmarketd is running', default='12345') parser.add_option('-b', '--txcount', type='int', dest='txcount', help=('optionally do more than 1 transaction to the ' 'same destination, of the same amount'), default=1) parser.add_option( '-C', '--choose-cheapest', action='store_true', dest='choosecheapest', default=False, help= 'override weightened offers picking and choose cheapest. this might reduce anonymity.') parser.add_option( '-P', '--pick-orders', action='store_true', dest='pickorders', default=False, help= 'manually pick which orders to take. doesn\'t work while sweeping.') parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', help='mixing depth to spend from, default=0', default=0) parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option('-g', '--gap-limit', type="int", action='store', dest='gaplimit', help='gap limit for wallet, default=6', default=6) parser.add_option('--yes', action='store_true', dest='answeryes', default=False, help='answer yes to everything') parser.add_option( '--rpcwallet', action='store_true', dest='userpcwallet', default=False, help=('Use the Bitcoin Core wallet through json rpc, instead ' 'of the internal joinmarket wallet. Requires ' 'blockchain_source=json-rpc')) (options, args) = parser.parse_args() if len(args) < 3: parser.error('Needs a wallet, amount and destination address') sys.exit(0) wallet_name = args[0] amount = int(args[1]) destaddr = args[2] load_program_config() jm_single().maker_timeout_sec = 5 addr_valid, errormsg = validate_address(destaddr) if not addr_valid: print('ERROR: Address invalid. ' + errormsg) return chooseOrdersFunc = None if options.pickorders: chooseOrdersFunc = pick_order if amount == 0: print('WARNING: You may have to pick offers multiple times') print('WARNING: due to manual offer picking while sweeping') elif options.choosecheapest: chooseOrdersFunc = cheapest_order_choose else: # choose randomly (weighted) chooseOrdersFunc = weighted_order_choose # Dynamically estimate a realistic fee if it currently is the default value. # At this point we do not know even the number of our own inputs, so # we guess conservatively with 2 inputs and 2 outputs each if options.txfee == -1: options.txfee = max(options.txfee, estimate_tx_fee(2, 2)) log.debug("Estimated miner/tx fee for each cj participant: " + str( options.txfee)) assert (options.txfee >= 0) log.debug('starting sendpayment') global wallet if not options.userpcwallet: wallet = Wallet(wallet_name, options.amtmixdepths, options.gaplimit) else: wallet = BitcoinCoreWallet(fromaccount=wallet_name) jm_single().bc_interface.sync_wallet(wallet) def taker_finished(res): global wallet global txcount txcount += 1 if res: log.debug("Transaction finished OK, result was: ") else: log.info("A transaction failed, quitting") sys.exit(1) if txcount > options.txcount: log.debug("Shutting down") reactor.stop() else: #need to update for new transactions; only working for #regtest at the moment (otherwise too slow) jm_single().bc_interface.sync_wallet(wallet) time.sleep(3) #for blocks to mine #restarts from the entry point of the client-server protocol (JMInit) #with the *same* Taker object. clientfactory.getClient().clientStart() taker = Taker(wallet, options.mixdepth, amount, options.makercount, order_chooser=chooseOrdersFunc, external_addr=destaddr, callbacks=(None, None, taker_finished)) clientfactory = JMTakerClientProtocolFactory(taker) start_reactor("localhost", options.daemonport, clientfactory) if __name__ == "__main__": main() print('done')