mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-17 13:07:51 +02:00
215 lines
8.4 KiB
Python
215 lines
8.4 KiB
Python
#! /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')
|