mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-13 12:33:40 +02:00
Add support for burning coins with sendpayment
Users burn coins by passing "BURN" as an address to sendpayment
This commit is contained in:
parent
dc715c9481
commit
2271ce05d7
7 changed files with 85 additions and 21 deletions
|
|
@ -670,6 +670,13 @@ def mk_freeze_script(pub, locktime):
|
|||
btc.OP_CHECKSIG]
|
||||
return binascii.hexlify(serialize_script(scr)).decode()
|
||||
|
||||
def mk_burn_script(data):
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError("data must be in bytes")
|
||||
data = binascii.hexlify(data).decode()
|
||||
scr = [btc.OP_RETURN, data]
|
||||
return serialize_script(scr)
|
||||
|
||||
# Signing and verifying
|
||||
|
||||
def verify_tx_input(tx, i, script, sig, pub, scriptCode=None, amount=None):
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
|
|||
from .cryptoengine import BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError
|
||||
from .configure import (load_test_config,
|
||||
load_program_config, get_p2pk_vbyte, jm_single, get_network, update_persist_config,
|
||||
validate_address, get_irc_mchannels, get_blockchain_interface_instance,
|
||||
get_p2sh_vbyte, set_config, is_segwit_mode, is_native_segwit_mode)
|
||||
validate_address, is_burn_destination, get_irc_mchannels,
|
||||
get_blockchain_interface_instance, get_p2sh_vbyte, set_config, is_segwit_mode,
|
||||
is_native_segwit_mode)
|
||||
from .blockchaininterface import (BlockchainInterface,
|
||||
RegtestBitcoinCoreInterface, BitcoinCoreInterface)
|
||||
from .electruminterface import ElectrumInterface
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ def get_tumbler_parser():
|
|||
def get_sendpayment_parser():
|
||||
parser = OptionParser(
|
||||
usage=
|
||||
'usage: %prog [options] wallet_file amount destaddr\n' +
|
||||
'usage: %prog [options] wallet_file amount destination\n' +
|
||||
' %prog [options] wallet_file bitcoin_uri',
|
||||
description='Sends a single payment from a given mixing depth of your '
|
||||
+
|
||||
|
|
|
|||
|
|
@ -405,6 +405,11 @@ def validate_address(addr):
|
|||
return True, 'address validated'
|
||||
|
||||
|
||||
_BURN_DESTINATION = "BURN"
|
||||
|
||||
def is_burn_destination(destination):
|
||||
return destination == _BURN_DESTINATION
|
||||
|
||||
def donation_address(reusable_donation_pubkey=None): #pragma: no cover
|
||||
#Donation code currently disabled, so not tested.
|
||||
if not reusable_donation_pubkey:
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ import os
|
|||
import sys
|
||||
import time
|
||||
import numbers
|
||||
from binascii import hexlify
|
||||
|
||||
from jmbase import get_log, jmprint
|
||||
from .configure import jm_single, validate_address
|
||||
from .configure import jm_single, validate_address, is_burn_destination
|
||||
from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\
|
||||
schedule_to_text
|
||||
from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime
|
||||
from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \
|
||||
FidelityBondMixin
|
||||
from jmbitcoin import deserialize, make_shuffled_tx, serialize, txhash,\
|
||||
amount_to_str
|
||||
amount_to_str, mk_burn_script, bin_hash160
|
||||
from jmbase.support import EXIT_SUCCESS
|
||||
log = get_log()
|
||||
|
||||
|
|
@ -20,7 +23,7 @@ Utility functions for tumbler-style takers;
|
|||
Currently re-used by CLI script tumbler.py and joinmarket-qt
|
||||
"""
|
||||
|
||||
def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
|
||||
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
|
||||
accept_callback=None, info_callback=None):
|
||||
"""Send coins directly from one mixdepth to one destination address;
|
||||
does not need IRC. Sweep as for normal sendpayment (set amount=0).
|
||||
|
|
@ -41,24 +44,65 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
|
|||
The txid if transaction is pushed, False otherwise
|
||||
"""
|
||||
#Sanity checks
|
||||
assert validate_address(destaddr)[0]
|
||||
assert validate_address(destination)[0] or is_burn_destination(destination)
|
||||
assert isinstance(mixdepth, numbers.Integral)
|
||||
assert mixdepth >= 0
|
||||
assert isinstance(amount, numbers.Integral)
|
||||
assert amount >=0
|
||||
assert isinstance(wallet_service.wallet, BaseWallet)
|
||||
|
||||
if is_burn_destination(destination):
|
||||
#Additional checks
|
||||
if not isinstance(wallet_service.wallet, FidelityBondMixin):
|
||||
log.error("Only fidelity bond wallets can burn coins")
|
||||
return
|
||||
if answeryes:
|
||||
log.error("Burning coins not allowed without asking for confirmation")
|
||||
return
|
||||
if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH:
|
||||
log.error("Burning coins only allowed from mixdepth " + str(
|
||||
FidelityBondMixin.FIDELITY_BOND_MIXDEPTH))
|
||||
return
|
||||
if amount != 0:
|
||||
log.error("Only sweeping allowed when burning coins, to keep the tx " +
|
||||
"small. Tip: use the coin control feature to freeze utxos")
|
||||
return
|
||||
|
||||
from pprint import pformat
|
||||
txtype = wallet_service.get_txtype()
|
||||
if amount == 0:
|
||||
utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
|
||||
if utxos == {}:
|
||||
log.error(
|
||||
"There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.")
|
||||
"There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.")
|
||||
return
|
||||
|
||||
total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])
|
||||
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
|
||||
outs = [{"address": destaddr, "value": total_inputs_val - fee_est}]
|
||||
|
||||
if is_burn_destination(destination):
|
||||
if len(utxos) > 1:
|
||||
log.error("Only one input allowed when burning coins, to keep "
|
||||
+ "the tx small. Tip: use the coin control feature to freeze utxos")
|
||||
return
|
||||
address_type = FidelityBondMixin.BIP32_BURN_ID
|
||||
index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type)
|
||||
path = wallet_service.wallet.get_path(mixdepth, address_type, index)
|
||||
privkey, engine = wallet_service.wallet._get_priv_from_path(path)
|
||||
pubkey = engine.privkey_to_pubkey(privkey)
|
||||
pubkeyhash = bin_hash160(pubkey)
|
||||
|
||||
#size of burn output is slightly different from regular outputs
|
||||
burn_script = mk_burn_script(pubkeyhash) #in hex
|
||||
fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2)
|
||||
|
||||
outs = [{"script": burn_script, "value": total_inputs_val - fee_est}]
|
||||
destination = "BURNER OUTPUT embedding pubkey at " \
|
||||
+ wallet_service.wallet.get_path_repr(path) \
|
||||
+ "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
|
||||
else:
|
||||
#regular send (non-burn)
|
||||
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
|
||||
outs = [{"address": destination, "value": total_inputs_val - fee_est}]
|
||||
else:
|
||||
#8 inputs to be conservative
|
||||
initial_fee_est = estimate_tx_fee(8,2, txtype=txtype)
|
||||
|
|
@ -69,7 +113,7 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
|
|||
fee_est = initial_fee_est
|
||||
total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])
|
||||
changeval = total_inputs_val - fee_est - amount
|
||||
outs = [{"value": amount, "address": destaddr}]
|
||||
outs = [{"value": amount, "address": destination}]
|
||||
change_addr = wallet_service.get_internal_addr(mixdepth)
|
||||
outs.append({"value": changeval, "address": change_addr})
|
||||
|
||||
|
|
@ -85,14 +129,14 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False,
|
|||
log.info("In serialized form (for copy-paste):")
|
||||
log.info(tx)
|
||||
actual_amount = amount if amount != 0 else total_inputs_val - fee_est
|
||||
log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr)
|
||||
log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination)
|
||||
if not answeryes:
|
||||
if not accept_callback:
|
||||
if input('Would you like to push to the network? (y/n):')[0] != 'y':
|
||||
log.info("You chose not to broadcast the transaction, quitting.")
|
||||
return False
|
||||
else:
|
||||
accepted = accept_callback(pformat(txsigned), destaddr, actual_amount,
|
||||
accepted = accept_callback(pformat(txsigned), destination, actual_amount,
|
||||
fee_est)
|
||||
if not accepted:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class Mnemonic(MnemonicParent):
|
|||
def detect_language(cls, code):
|
||||
return "english"
|
||||
|
||||
def estimate_tx_fee(ins, outs, txtype='p2pkh'):
|
||||
def estimate_tx_fee(ins, outs, txtype='p2pkh', extra_bytes=0):
|
||||
'''Returns an estimate of the number of satoshis required
|
||||
for a transaction with the given number of inputs and outputs,
|
||||
based on information from the blockchain interface.
|
||||
|
|
@ -81,13 +81,14 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh'):
|
|||
raise ValueError("Estimated fee per kB greater than absurd value: " + \
|
||||
str(absurd_fee) + ", quitting.")
|
||||
if txtype in ['p2pkh', 'p2shMofN']:
|
||||
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype)
|
||||
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype) + extra_bytes
|
||||
return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
|
||||
elif txtype in ['p2wpkh', 'p2sh-p2wpkh']:
|
||||
witness_estimate, non_witness_estimate = btc.estimate_tx_size(
|
||||
ins, outs, txtype)
|
||||
non_witness_estimate += extra_bytes
|
||||
return int(int((
|
||||
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
|
||||
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
|
||||
else:
|
||||
raise NotImplementedError("Txtype: " + txtype + " not implemented.")
|
||||
|
||||
|
|
@ -416,7 +417,8 @@ class BaseWallet(object):
|
|||
"""
|
||||
if self.TYPE == TYPE_P2PKH:
|
||||
return 'p2pkh'
|
||||
elif self.TYPE == TYPE_P2SH_P2WPKH:
|
||||
elif self.TYPE in (TYPE_P2SH_P2WPKH,
|
||||
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS):
|
||||
return 'p2sh-p2wpkh'
|
||||
elif self.TYPE == TYPE_P2WPKH:
|
||||
return 'p2wpkh'
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from twisted.internet import reactor
|
|||
import pprint
|
||||
|
||||
from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\
|
||||
JMClientProtocolFactory, start_reactor, validate_address, jm_single,\
|
||||
estimate_tx_fee, direct_send, WalletService,\
|
||||
JMClientProtocolFactory, start_reactor, validate_address, is_burn_destination, \
|
||||
jm_single, estimate_tx_fee, direct_send, WalletService,\
|
||||
open_test_wallet_maybe, get_wallet_path, NO_ROUNDING, \
|
||||
get_sendpayment_parser, get_max_cj_fee_values, check_regtest
|
||||
from twisted.python.log import startLogging
|
||||
|
|
@ -81,8 +81,13 @@ def main():
|
|||
destaddr = args[2]
|
||||
mixdepth = options.mixdepth
|
||||
addr_valid, errormsg = validate_address(destaddr)
|
||||
if not addr_valid:
|
||||
command_to_burn = (is_burn_destination(destaddr) and sweeping and
|
||||
options.makercount == 0 and not options.p2ep)
|
||||
if not addr_valid and not command_to_burn:
|
||||
jmprint('ERROR: Address invalid. ' + errormsg, "error")
|
||||
if is_burn_destination(destaddr):
|
||||
jmprint("The required options for burning coins are zero makers"
|
||||
+ " (-N 0), sweeping (amount = 0) and not using P2EP", "info")
|
||||
sys.exit(EXIT_ARGERROR)
|
||||
if sweeping == False and amount < DUST_THRESHOLD:
|
||||
jmprint('ERROR: Amount ' + btc.amount_to_str(amount) +
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue