Skip to content

Instantly share code, notes, and snippets.

@kanzure
Created May 29, 2014 04:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kanzure/9ad6a265aac887fa26c1 to your computer and use it in GitHub Desktop.
Save kanzure/9ad6a265aac887fa26c1 to your computer and use it in GitHub Desktop.
Send bitcoin from coinbase.com to a deterministic wallet by "spraying", so as to achieve a better distribution of eggs in baskets or something equally tasty.
"""
Bitcoin spray: distribute BTC from a coinbase.com account into many addresses
generated by a deterministic wallet (an electrum wallet).
1.0 BTC ->
0.01 BTC
0.01 BTC
0.01 BTC
etc..
This is particularly useful because coinbase.com is paying all transaction
fees.
"Coinbase pays fees on transactions of 0.001 BTC or greater."
https://coinbase.com/api/doc/1.0/transactions/send_money.html
I suggest disabling "send BTC" email notifications on coinbase.com for the
duration of this script so that the email doesn't get intercepted.
Make API calls to coinbase.com every time a new block is seen on the Bitcoin
network. These API calls will tell coinbase.com to send bitcoin from
coinbase.com to particular addresses in the hierarchical wallet.
Components:
[x] hierarchical wallet address generator thing
[x] resume
[x] listen/poll for new blocks
[x] coinbase.com api client (for sending bitcoins)
Limitations:
* doesn't consider reorgs at all
"""
COINBASE_API_KEY = "YOUR-API-KEY"
COINBASE_API_SECRET = "YOUR-API-SECRET"
from coinbase_passwords import *
import websocket
import json
import logging
import thread
import time
import datetime
# for coinbase stuff
import urllib2
import hashlib
import hmac
# The index number of the next address to use in the deterministic wallet. So,
# if the program just crashed for whatever reason, you would look at the last
# index used to send some BTC, and write that number plus one here.
address_indexer = 38
# Amount BTC to distribute per address. A better system would use a random
# number within some range.
per_address = 0.01
# Number of transactions per block to create. There is no guarantee that each
# transaction will be included in each block. Some might appear in future
# blocks, so the transaction count might be anywhere between 0 and the the
# total number of transactions that have been attempted but not yet included in
# a block. This number controls the creation of new transactions per block. A
# better system might monitor for unconfirmed transactions, and only create new
# transactions once the previous transactions have been confirmed at least
# once so that the total balance doesn't end up tied up in lousy unconfirmed
# transactions. A better system would use a random number within some range
# (including zero in that range).
transactions_per_block = 4
# Also, there is a minor incentive to keep the number of transactions per block
# low. In particular, and especially if you choose a high-entropy number for
# the value of "per_address", it will be easy for others to guess that the
# other addresses belong to you because the same account balances are being
# transferred. Similarly, a large increase in the number of small-value
# transactions in the blockchain in a given block is another piece of
# information that can be used to correlate the addresses as probably belonging
# to the same owner. For these reasons and others, splitting up the
# transactions into separate blocks helps to obfuscate your presence at least a
# little.
max_transactions = 1000
# sudo apt-get install electrum
# https://bitcointalk.org/index.php?topic=612143.0
import electrum
# load default electrum configuration
config = electrum.SimpleConfig()
storage = electrum.wallet.WalletStorage(config)
wallet = electrum.wallet.Wallet(storage)
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
log = logging.getLogger(__name__)
def get_total_balance(wallet=wallet):
"""
:return: total balance in satoshis (confirmed)
"""
#return wallet.get_account_balance(0)[0]
return wallet.get_balance()[0]
def get_address_balance(address, wallet=wallet):
"""
:param address: some address
:return: confirmed address balance in satoshis
"""
return wallet.get_addr_balance(address)[0]
def get_nth_address(number, wallet=wallet):
"""
Generate the nth address. Doesn't generate change addresses.
:param number: the numberth public address to generate
:return: address
"""
return wallet.accounts[0].get_address(0, number)
def get_first_address_with_zero_balance(wallet=wallet, minimum=0, limit=10000):
"""
Find an address that has a balance of zero. Ideally this would find an
address that hasn't been used before, because it is possible that it will
find an address that has been previously used and emptied back to zero.
A better system would check the blockchain and find the first unused
address, and then rename this function appropriately.
:param limit: Number of search cycles to employ after exhausting
pre-generated list of addresses.
:param minimum: first usable address (skip everything before) (useful for
resuming)
:return: (number, address)
"""
for (number, address) in enumerate(wallet.addresses()):
balance = get_address_balance(address, wallet=wallet)
if balance == 0 and number >= minimum:
return (number, address)
else:
# Exhausted pre-generated addresses. Search for next address that has
# zero balance.
counter = number
while counter <= limit:
address = get_nth_address(counter, wallet=wallet)
balance = get_address_balance(address, wallet=wallet)
if balance == 0 and counter >= minimum:
return (counter, address)
counter += 1
# Really I shouldn't use a limit, but I'm skeptical that 10,000
# addresses are really in use. Human intervention required..
raise Exception("Couldn't find an address with an empty balance.")
def execute_coinbase_http(url, body=None):
"""
https://coinbase.com/docs/api/authentication
"""
# just a precaution..
if "https" not in url:
raise Exception("i don't think so, tim")
opener = urllib2.build_opener()
nonce = int(time.time() * 1e6)
message = str(nonce) + url + ('' if body is None else body)
signature = hmac.new(COINBASE_API_SECRET, message, hashlib.sha256).hexdigest()
opener.addheaders = [('ACCESS_KEY', COINBASE_API_KEY),
('ACCESS_SIGNATURE', signature),
('ACCESS_NONCE', nonce)]
try:
return opener.open(urllib2.Request(url, body, {'Content-Type': 'application/json'}))
except urllib2.HTTPError as e:
print e
return e
def send_btc(amount, address):
"""
Use coinbase.com to send some BTC to an address. The amount is in units of
BTC. When the transaction is successfully created, coinbase.com will return
some json with the "success" key set to json true.
"""
# Don't debug with httpbin while the headers are enabled in
# execute_coinbase_http.
#url = "http://httpbin.org/post"
url = "https://coinbase.com/api/v1/transactions/send_money"
body = json.dumps({
"transaction": {
"to": address,
"amount": amount,
},
})
response = execute_coinbase_http(url, body=body)
content = json.loads(response.read())
return content
class BlockchainInfoWebSocketAPI(object):
"""
http://blockchain.info/api/api_websocket
"""
url = "ws://ws.blockchain.info/inv"
@staticmethod
def on_open(ws):
"""
Spawn a function that pings blockchain.info every 30 seconds so that
the websocket connection doesn't get killed from that end.
"""
def run(*args):
# subscribe to blocks
BlockchainInfoWebSocketAPI.subscribe_to_blocks(ws)
# ping every 25 seconds to prevent remote server from disconnecting
while 1:
log.debug("BlockchainInfoWebSocketAPI: doing heartbeat ping to blockchain.info")
ws.send("")
time.sleep(25)
# run the "run" method in a new thread
thread.start_new_thread(run, ())
@staticmethod
def on_close(ws):
log.info("BlockchainInfoWebSocketAPI: closing websocket connection")
@staticmethod
def on_error(ws, error):
log.exception("BlockchainInfoWebSocketAPI error: " + error)
@staticmethod
def on_message(ws, message):
global transactions_per_block
global per_address
global address_indexer
data = json.loads(message)
if data["op"] == "block":
log.info("BlockchainInfoWebSocketAPI: received new block")
i = 0
while i < transactions_per_block:
amount = per_address
(latest_index, address) = get_first_address_with_zero_balance(minimum=address_indexer)
log.info("BlockchainInfoWebSocketAPI: sending {amount} BTC to address #{num} - {address}".format(
amount=amount,
num=latest_index,
address=address,
))
response = send_btc(str(amount), address)
log.info("BlockchainInfoWebSocketAPI: coinbase.com request successful? " + str(response["success"]))
log.info(response)
# Kinda lying, it's really just an indexer, so point it to the
# next one please.
address_indexer = latest_index + 1
i += 1
@staticmethod
def subscribe_to_blocks(ws):
"""
Communicates with blockchain.info to subscribe to block notifications.
Use blocks_sub for blocks. The ping_block operation is used only for
debugging (it immediately pings the last known block).
"""
ws.send('{"op":"blocks_sub"}')
# ws.send('{"op":"ping_block"}')
@staticmethod
def _run_forever():
log.info("BlockchainInfoWebSocketAPI: begin blockchain.info websocket connection")
ws = websocket.WebSocketApp(
BlockchainInfoWebSocketAPI.url,
on_message=BlockchainInfoWebSocketAPI.on_message,
on_error=BlockchainInfoWebSocketAPI.on_error,
on_close=BlockchainInfoWebSocketAPI.on_close,
on_open=BlockchainInfoWebSocketAPI.on_open,
)
ws.run_forever()
return ws
@staticmethod
def run_forever():
delay = 1
while 1:
# try to use run_forever, wait between with an exponential backoff
try:
BlockchainInfoWebSocketAPI._run_forever()
except websocket.WebSocketException, exc:
log.exception(exc)
log.warning("BlockchainInfoWebSocketAPI: will attempt to restart connection in {0} seconds...".format(delay))
time.sleep(delay)
delay *= 2
except Exception, exc:
raise exc
def quick_time_estimate(per_address, transactions_per_block, max_transactions):
"""
Estimate the total BTC to distribute, how many blocks to use, how many
hours this will probably take (assuming 10 minutes per block), and an
estimated timestamp for when everything will be done.
"""
total_btc = per_address * max_transactions
# Number of required blocks is based on the total number of transactions.
blocks = float(max_transactions) / float(transactions_per_block)
# each block takes 10 minutes (uh, on average, or rather, that's the target)
minutes = blocks * 10
# each hour takes 60 minutes
hours = minutes / 60
timefuture = datetime.datetime.now() + datetime.timedelta(minutes=minutes)
return (total_btc, blocks, hours, timefuture)
def dump_estimates(
per_address=per_address,
transactions_per_block=transactions_per_block,
max_transactions=max_transactions,
):
(total_btc, blocks, hours, timefuture) = quick_time_estimate(per_address, transactions_per_block, max_transactions)
output = "total_btc: {total_btc}\n"
output += "blocks: {blocks}\n"
output += "hours: {hours}\n"
output += "approximately done at: {timefuture}"
output = output.format(
total_btc=total_btc,
blocks=blocks,
hours=hours,
timefuture=timefuture,
)
return output
if __name__ == "__main__":
print dump_estimates(), "\n"
BlockchainInfoWebSocketAPI.run_forever()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment