Created
May 29, 2014 04:26
-
-
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.
This file contains 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
""" | |
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