Last active
July 7, 2021 12:04
-
-
Save sorce/c60dfaac06d19842edfd5b7e2804ddc5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import sys | |
import json | |
import requests | |
from decimal import Decimal | |
import logging | |
from logging.handlers import RotatingFileHandler | |
worklog = logging.getLogger('sweep_funds') | |
#shandler = logging.handlers.RotatingFileHandler('sweep_funds.log', maxBytes=1024*1024*10, backupCount=1000) | |
shandler = logging.StreamHandler(sys.stdout) | |
worklog.setLevel(logging.DEBUG) | |
worklog.addHandler(shandler) | |
shandler.setFormatter(logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s', '%Y/%m/%d %H:%M:%S')) | |
try: | |
input = raw_input | |
except NameError: | |
pass | |
ADDRESS_TO_SWEEP = '123H4o8DFhKBBi4MEMrVFUcYebHx7MZxLx' | |
PRIVKEYS = ['L41XHuuV9GaxomC1GpgP4HBJTDf41CTeQgpUiWzTfjR5QNKqQ7Qg'] | |
OUTPUT_ADDRESS = '1Loakb53Up5Y9iWbav9L4RpgNdLnVaG2GW' | |
TX_FEE = 9666 # static transaction fee to be paid for the sweep tx, in satoshis | |
GET_UTXOS_FROM_BITWATCH = False # whether to use the bitwatch API (with some filtering) instead of a bitcoin node with addrindex (not recommended) - note a bitcoin node is still required for further script functioning | |
SEARCHTX_MAX_TX_COUNT = 3000 # the max amount of transactions to fetch from the searchrawtransactions bitcoin RPC call | |
ONLY_MULTISIG = True # whether to only sweep multisig UTXOs when using get_utxos (instead of bitwatch) | |
BITCOIN_USER = '' | |
BITCOIN_PW = '' | |
BITCOIN_URL = '127.0.0.1' | |
BITCOIN_PORT = '8332' | |
def formatted(x): | |
return format(float(x), ',.8f').strip('0').rstrip('.') | |
def to_satoshis(x): | |
return int(float(x) * 1e8) | |
def from_satoshis(x): | |
return float(x) / 1e8 | |
def bitcoin_(method, *args): | |
service_url = 'http://{}:{}@{}:{}'.format(BITCOIN_USER, BITCOIN_PW, BITCOIN_URL, BITCOIN_PORT) | |
js = requests.post( | |
service_url, headers={'content-type': 'application/json'}, verify=False, timeout=60, data=json.dumps({ | |
'method': method, 'params': args, 'jsonrpc': '2.0' | |
}) | |
) | |
return js.json() | |
def sign_tx(tx, private_keys): | |
if type(private_keys) is not list: | |
private_keys = [private_keys] | |
decoded = bitcoin_('decoderawtransaction', tx) | |
m = [] | |
for d in decoded['result']['vout']: | |
m.append({'scriptPubKey': d['scriptPubKey']['hex'], 'vout': d['n'], 'txid': decoded['result']['txid']}) | |
signed_tx = bitcoin_('signrawtransaction', tx, m, private_keys) | |
return signed_tx['result']['hex'] | |
def get_utxos_bitwatch(addy): | |
url = 'http://api.bitwatch.co/listunspent/{}?verbose=0&minconf=0&maxconf=999999'.format(addy) | |
worklog.info('grabbing utxos from bitwatch api .. this could take a while ...') | |
try: | |
resp = requests.get(url, timeout=30) # the bitwatch api can take a while to return anything ... | |
utxos = resp.json()['result'] | |
# why does the bitwatch api return spent outputs for a listunspent call if they are of type pubkeyhash? I don't know, just skip them | |
real_utxos = [] | |
for utxo in utxos: | |
if utxo['type'] not in ['pubkeyhash']: | |
real_utxos.append(utxo) | |
return real_utxos | |
except requests.exceptions.ReadTimeout as e: | |
worklog.error('[!] Error fetching bitwatch API, try again in a couple of minutes.') | |
return None | |
def get_utxos(addy, only_multisig=False): | |
inputs = [] # all inputs | |
outputs = [] # all outputs | |
full_outputs = {} | |
utxos = [] | |
worklog.info('Fetching transactions for {} .. this might take a while ...'.format(addy)) | |
raw_transactions = bitcoin_('searchrawtransactions', addy, 1, 0, SEARCHTX_MAX_TX_COUNT)['result'] | |
worklog.info('Found {} transactions'.format(len(raw_transactions))) | |
for i, tx in enumerate(raw_transactions): | |
for vout in [x for x in tx['vout'] if 'scriptPubKey' in x and 'addresses' in x['scriptPubKey'] and addy in x['scriptPubKey']['addresses']]: | |
outputs.append({'txid': tx['txid'], 'vout': vout['n']}) | |
# make synonymous keys so create_tx is compatible with searchrawtransactions and the bitwatch api | |
vout['txid'] = tx['txid'] | |
vout['vout'] = vout['n'] | |
vout['amount'] = vout['value'] | |
full_outputs[(tx['txid'], vout['n'])] = vout | |
for vin in tx['vin']: | |
inputs.append({'txid': vin['txid'], 'vout': vin['vout']}) | |
utxos = [d for d in outputs if d not in inputs] | |
utxos = [full_outputs[(d['txid'], d['vout'])] for d in utxos] | |
if only_multisig: # we just assume we're sweeping 1 of x multisig where addy is 1 of the x | |
utxos = [d for d in utxos if 'scriptPubKey' in d and d['scriptPubKey']['type'] in ['multisig']] | |
return utxos | |
def create_tx(utxos, destination, fee): | |
inputs = [] | |
outputs = {} | |
total = Decimal(0) | |
for utxo in utxos: | |
total += Decimal(utxo['amount']).quantize(Decimal('1.00000000')) | |
inputs.append({'txid': utxo['txid'], 'vout': utxo['vout']}) | |
fee = Decimal(from_satoshis(fee)) | |
if fee >= total: | |
worklog.debug('fee ({}) >= total dust ({}). Aborting'.format(formatted(fee), formatted(total))) | |
return None | |
real_total = total - fee | |
outputs[destination] = str(real_total.quantize(Decimal('1.00000000'))) | |
resp = bitcoin_('createrawtransaction', inputs, outputs) | |
if resp['error'] or 'result' not in resp or not resp['result']: | |
worklog.error('createrawtransaction error:\n{}'.format(resp)) | |
return resp['result'] | |
if __name__ == '__main__': | |
if GET_UTXOS_FROM_BITWATCH: | |
utxos = get_utxos_bitwatch(ADDRESS_TO_SWEEP) # note: only multisig utxos will be returned, regardless of ONLY_MULTISIG | |
else: | |
utxos = get_utxos(ADDRESS_TO_SWEEP, only_multisig=ONLY_MULTISIG) | |
if utxos: | |
total = Decimal(0) | |
for d in utxos: | |
total += Decimal(d['amount']) | |
worklog.info('Found {} {}UTXOs on {} with a value of {}'.format( | |
len(utxos), 'multisig ' if ONLY_MULTISIG else '', | |
ADDRESS_TO_SWEEP, formatted(total.quantize(Decimal('1.00000000'))) | |
)) | |
utx = create_tx(utxos, OUTPUT_ADDRESS, TX_FEE) | |
worklog.info('\nUnsigned TX: {}'.format(utx)) | |
stx = sign_tx(utx, PRIVKEYS) | |
worklog.info('\nSigned TX: {}'.format(stx)) | |
inp = raw_input('\nBroadcast TX?\n-> ') | |
if inp.lower() in ['y', 'yes', 'yeah', 'sure', 'why not', 'idc']: | |
tx = bitcoin_('sendrawtransaction', stx) | |
if not tx['result']: | |
worklog.error('Error sending:\n{}'.format(tx)) | |
else: | |
worklog.info('\n\nSent TX: {}'.format(tx['result'])) | |
else: | |
worklog.info('\n\nnot broadcasting tx') | |
else: | |
worklog.error('Found no UTXOs, aborting') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do u sweep just "dust" UTXOs?& how do u define dust then?
.
Sorry, but when I follow the code I find it accumulate multi-sig UTXOS to a single address without considering their values being dust or not. On the other side, when I fastly read the original discussion thread behind the idea I understood u collect a bunch of dust UTXOS in just one value accepted UTXO with a fee equal to 1/4 their sum???
.
-I'm asking for some research Analysis I perform on the UTXOS set & what affects its patterns, lifespan,.... etc