You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

215 lines
8.4 KiB

#! /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.
For notes, see scripts/README.md; in particular, note the use
of "schedules" with the -S flag.
"""
import random
import sys
import threading
from optparse import OptionParser
from twisted.internet import reactor
import time
import os
import pprint
from jmclient import (Taker, load_program_config, get_schedule,
JMClientProtocolFactory, start_reactor,
validate_address, jm_single, WalletError,
choose_orders, choose_sweep_orders,
cheapest_order_choose, weighted_order_choose,
Wallet, BitcoinCoreWallet, sync_wallet,
RegtestBitcoinCoreInterface, estimate_tx_fee,
direct_send, SegwitWallet)
from jmbase.support import get_log, debug_dump_object, get_password
from cli_options import get_sendpayment_parser
log = get_log()
#CLI specific, so relocated here (not used by tumbler)
def pick_order(orders, n): #pragma: no cover
print("Considered orders:")
for i, o in enumerate(orders):
print(" %2d. %20s, CJ fee: %6s, tx fee: %6d" %
(i, o[0]['counterparty'], str(o[0]['cjfee']), o[0]['txfee']))
pickedOrderIndex = -1
if i == 0:
print("Only one possible pick, picking it.")
return orders[0]
while pickedOrderIndex == -1:
try:
pickedOrderIndex = int(raw_input('Pick an order between 0 and ' +
str(i) + ': '))
except ValueError:
pickedOrderIndex = -1
continue
if 0 <= pickedOrderIndex < len(orders):
return orders[pickedOrderIndex]
pickedOrderIndex = -1
def main():
parser = get_sendpayment_parser()
(options, args) = parser.parse_args()
load_program_config()
walletclass = SegwitWallet if jm_single().config.get(
"POLICY", "segwit") == "true" else Wallet
if options.schedule == '' and len(args) < 3:
parser.error('Needs a wallet, amount and destination address')
sys.exit(0)
#without schedule file option, use the arguments to create a schedule
#of a single transaction
sweeping = False
if options.schedule == '':
#note that sendpayment doesn't support fractional amounts, fractions throw
#here.
amount = int(args[1])
if amount == 0:
sweeping = True
destaddr = args[2]
mixdepth = options.mixdepth
addr_valid, errormsg = validate_address(destaddr)
if not addr_valid:
print('ERROR: Address invalid. ' + errormsg)
return
schedule = [[options.mixdepth, amount, options.makercount,
destaddr, 0.0, 0]]
else:
result, schedule = get_schedule(options.schedule)
if not result:
log.info("Failed to load schedule file, quitting. Check the syntax.")
log.info("Error was: " + str(schedule))
sys.exit(0)
mixdepth = 0
for s in schedule:
if s[1] == 0:
sweeping = True
#only used for checking the maximum mixdepth required
mixdepth = max([mixdepth, s[0]])
wallet_name = args[0]
#to allow testing of confirm/unconfirm callback for multiple txs
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
jm_single().bc_interface.tick_forward_chain_interval = 10
jm_single().maker_timeout_sec = 5
chooseOrdersFunc = None
if options.pickorders:
chooseOrdersFunc = pick_order
if sweeping:
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')
if not options.userpcwallet:
max_mix_depth = max([mixdepth, options.amtmixdepths])
if not os.path.exists(os.path.join('wallets', wallet_name)):
wallet = walletclass(wallet_name, None, max_mix_depth, options.gaplimit)
else:
while True:
try:
pwd = get_password("Enter wallet decryption passphrase: ")
wallet = walletclass(wallet_name, pwd, max_mix_depth, options.gaplimit)
except WalletError:
print("Wrong password, try again.")
continue
except Exception as e:
print("Failed to load wallet, error message: " + repr(e))
sys.exit(0)
break
else:
wallet = BitcoinCoreWallet(fromaccount=wallet_name)
sync_wallet(wallet, fast=options.fastsync)
if options.makercount == 0:
if isinstance(wallet, BitcoinCoreWallet):
raise NotImplementedError("Direct send only supported for JM wallets")
direct_send(wallet, amount, mixdepth, destaddr, options.answeryes)
return
if walletclass == Wallet:
print("Only direct sends (use -N 0) are supported for "
"legacy (non-segwit) wallets.")
return
def filter_orders_callback(orders_fees, cjamount):
orders, total_cj_fee = orders_fees
log.info("Chose these orders: " +pprint.pformat(orders))
log.info('total cj fee = ' + str(total_cj_fee))
total_fee_pc = 1.0 * total_cj_fee / cjamount
log.info('total coinjoin fee = ' + str(float('%.3g' % (
100.0 * total_fee_pc))) + '%')
WARNING_THRESHOLD = 0.02 # 2%
if total_fee_pc > WARNING_THRESHOLD:
log.info('\n'.join(['=' * 60] * 3))
log.info('WARNING ' * 6)
log.info('\n'.join(['=' * 60] * 1))
log.info('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.')
log.info('\n'.join(['=' * 60] * 1))
log.info('WARNING ' * 6)
log.info('\n'.join(['=' * 60] * 3))
if not options.answeryes:
if raw_input('send with these orders? (y/n):')[0] != 'y':
return False
return True
def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
if fromtx == "unconfirmed":
#If final entry, stop *here*, don't wait for confirmation
if taker.schedule_index + 1 == len(taker.schedule):
reactor.stop()
return
if fromtx:
if res:
txd, txid = txdetails
taker.wallet.remove_old_utxos(txd)
taker.wallet.add_new_utxos(txd, txid)
reactor.callLater(waittime*60,
clientfactory.getClient().clientStart)
else:
#a transaction failed; just stop
reactor.stop()
else:
if not res:
log.info("Did not complete successfully, shutting down")
#Should usually be unreachable, unless conf received out of order;
#because we should stop on 'unconfirmed' for last (see above)
else:
log.info("All transactions completed correctly")
reactor.stop()
taker = Taker(wallet,
schedule,
order_chooser=chooseOrdersFunc,
callbacks=(filter_orders_callback, None, taker_finished))
clientfactory = JMClientProtocolFactory(taker)
nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
daemon = True if nodaemon == 1 else False
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
jm_single().config.getint("DAEMON", "daemon_port"),
clientfactory, daemon=daemon)
if __name__ == "__main__":
main()
print('done')