Skip to content

Instantly share code, notes, and snippets.

@sorce
Last active July 7, 2021 12:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sorce/c60dfaac06d19842edfd5b7e2804ddc5 to your computer and use it in GitHub Desktop.
Save sorce/c60dfaac06d19842edfd5b7e2804ddc5 to your computer and use it in GitHub Desktop.
#!/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')
@Shymaa-Arafat
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment