diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yield-generator.py b/yield-generator.py index caf65843..a58bcff4 100644 --- a/yield-generator.py +++ b/yield-generator.py @@ -1,158 +1,159 @@ #! /usr/bin/env python -import time, os, binascii, sys, datetime -import pprint -data_dir = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, os.path.join(data_dir, 'lib')) +import os +import sys +from datetime import datetime, time -from maker import * -from irc import IRCMessageChannel, random_nick -import bitcoin as btc -import common, blockchaininterface - -from socket import gethostname +from lib import common +from lib.common import debug, calc_cj_fee, Wallet, debug_dump_object +from lib.blockchaininterface import BlockrInterface +from lib.maker import Maker +from lib.irc import IRCMessageChannel, random_nick txfee = 1000 -cjfee = '0.002' # 0.2% fee +cjfee = '0.002' # 0.2% fee nickname = random_nick() nickserv_password = '' -minsize = int(1.2 * txfee / float(cjfee)) #minimum size is such that you always net profit at least 20% of the miner fee +minsize = int(1.2 * txfee / float(cjfee)) # minimum size is such that you always net profit at least 20% of the miner fee mix_levels = 5 +''' +is a maker for the purposes of generating a yield from held +bitcoins without ruining privacy for the taker, the taker could easily check +the history of the utxos this bot sends, so theres not much incentive +to ruin the privacy for barely any more yield +sell-side algorithm: +add up the value of each utxo for each mixing depth, +announce a relative-fee order of the highest balance +spent from utxos that try to make the highest balance even higher +so try to keep coins concentrated in one mixing depth +''' -#is a maker for the purposes of generating a yield from held -# bitcoins without ruining privacy for the taker, the taker could easily check -# the history of the utxos this bot sends, so theres not much incentive -# to ruin the privacy for barely any more yield -#sell-side algorithm: -#add up the value of each utxo for each mixing depth, -# announce a relative-fee order of the highest balance -#spent from utxos that try to make the highest balance even higher -# so try to keep coins concentrated in one mixing depth class YieldGenerator(Maker): - statement_file = os.path.join('logs', 'yigen-statement.csv') - - def __init__(self, msgchan, wallet): - Maker.__init__(self, msgchan, wallet) - self.msgchan.register_channel_callbacks(self.on_welcome, self.on_set_topic, - None, None, self.on_nick_leave, None) - self.tx_unconfirm_timestamp = {} - - def log_statement(self, data): - if common.get_network() == 'testnet': - return - - data = [str(d) for d in data] - self.income_statement = open(self.statement_file, 'a') - self.income_statement.write(','.join(data) + '\n') - self.income_statement.close() - - def on_welcome(self): - Maker.on_welcome(self) - if not os.path.isfile(self.statement_file): - self.log_statement(['timestamp', 'cj amount/satoshi', 'my input count', - 'my input value/satoshi', 'cjfee/satoshi', 'earned/satoshi', - 'confirm time/min', 'notes']) - - timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") - self.log_statement([timestamp, '', '', '', '', '', '', 'Connected']) - - def create_my_orders(self): - mix_balance = self.wallet.get_balance_by_mixdepth() - if len([b for m, b in mix_balance.iteritems() if b > 0]) == 0: - debug('do not have any coins left') - return [] - - #print mix_balance - max_mix = max(mix_balance, key=mix_balance.get) - order = {'oid': 0, 'ordertype': 'relorder', 'minsize': minsize, - 'maxsize': mix_balance[max_mix] - common.DUST_THRESHOLD, 'txfee': txfee, 'cjfee': cjfee} - return [order] - - def oid_to_order(self, cjorder, oid, amount): - mix_balance = self.wallet.get_balance_by_mixdepth() - max_mix = max(mix_balance, key=mix_balance.get) - - #algo attempts to make the largest-balance mixing depth get an even larger balance - debug('finding suitable mixdepth') - mixdepth = (max_mix - 1) % self.wallet.max_mix_depth - while True: - if mixdepth in mix_balance and mix_balance[mixdepth] >= amount: - break - mixdepth = (mixdepth - 1) % self.wallet.max_mix_depth - #mixdepth is the chosen depth we'll be spending from - cj_addr = self.wallet.get_receive_addr((mixdepth + 1) % self.wallet.max_mix_depth) - change_addr = self.wallet.get_change_addr(mixdepth) - - utxos = self.wallet.select_utxos(mixdepth, amount) - my_total_in = sum([va['value'] for va in utxos.values()]) - real_cjfee = calc_cj_fee(cjorder.ordertype, cjorder.cjfee, amount) - change_value = my_total_in - amount - cjorder.txfee + real_cjfee - if change_value <= common.DUST_THRESHOLD: - debug('change value=%d below dust threshold, finding new utxos' % (change_value)) - try: - utxos = self.wallet.select_utxos(mixdepth, amount + common.DUST_THRESHOLD) - except Exception: - debug('dont have the required UTXOs to make a output above the dust threshold, quitting') - return None, None, None - - return utxos, cj_addr, change_addr - - def on_tx_unconfirmed(self, cjorder, txid, removed_utxos): - self.tx_unconfirm_timestamp[cjorder.cj_addr] = int(time.time()) - #if the balance of the highest-balance mixing depth change then reannounce it - oldorder = self.orderlist[0] if len(self.orderlist) > 0 else None - neworders = self.create_my_orders() - if len(neworders) == 0: - return ([0], []) #cancel old order - if oldorder: #oldorder may not exist when this is called from on_tx_confirmed - if oldorder['maxsize'] == neworders[0]['maxsize']: - return ([], []) #change nothing - #announce new order, replacing the old order - return ([], [neworders[0]]) - - def on_tx_confirmed(self, cjorder, confirmations, txid): - confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] - timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") - self.log_statement([timestamp, cjorder.cj_amount, len(cjorder.utxos), - sum([av['value'] for av in cjorder.utxos.values()]), cjorder.real_cjfee, - cjorder.real_cjfee - cjorder.txfee, round(confirm_time / 60.0, 2), '']) - return self.on_tx_unconfirmed(cjorder, txid, None) + statement_file = os.path.join('logs', 'yigen-statement.csv') + + def __init__(self, msgchan, wallet): + super(YieldGenerator, self).__init__(self, msgchan, wallet) + self.msgchan.register_channel_callbacks(self.on_welcome, self.on_set_topic, + None, None, self.on_nick_leave, None) + self.tx_unconfirm_timestamp = {} + + def log_statement(self, data): + if common.get_network() == 'testnet': + return + + data = [str(d) for d in data] + with open(self.statement_file, 'a') as f: + f.write(','.join(data) + '\n') + + def on_welcome(self): + super(YieldGenerator, self).on_welcome(self) + if not os.path.isfile(self.statement_file): + self.log_statement(['timestamp', 'cj amount/satoshi', 'my input count', + 'my input value/satoshi', 'cjfee/satoshi', 'earned/satoshi', + 'confirm time/min', 'notes']) + + timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S") + self.log_statement([timestamp, '', '', '', '', '', '', 'Connected']) + + def create_my_orders(self): + mix_balance = self.wallet.get_balance_by_mixdepth() + if len([b for m, b in mix_balance.iteritems() if b > 0]) == 0: + debug('do not have any coins left') + return [] + + # print mix_balance + max_mix = max(mix_balance, key=mix_balance.get) + order = {'oid': 0, 'ordertype': 'relorder', 'minsize': minsize, + 'maxsize': mix_balance[max_mix] - common.DUST_THRESHOLD, 'txfee': txfee, 'cjfee': cjfee} + return [order] + + def oid_to_order(self, cjorder, oid, amount): + mix_balance = self.wallet.get_balance_by_mixdepth() + max_mix = max(mix_balance, key=mix_balance.get) + + # algo attempts to make the largest-balance mixing depth get an even larger balance + debug('finding suitable mixdepth') + mixdepth = (max_mix - 1) % self.wallet.max_mix_depth + while True: + if mixdepth in mix_balance and mix_balance[mixdepth] >= amount: + break + mixdepth = (mixdepth - 1) % self.wallet.max_mix_depth + # mixdepth is the chosen depth we'll be spending from + cj_addr = self.wallet.get_receive_addr((mixdepth + 1) % self.wallet.max_mix_depth) + change_addr = self.wallet.get_change_addr(mixdepth) + + utxos = self.wallet.select_utxos(mixdepth, amount) + my_total_in = sum([va['value'] for va in utxos.values()]) + real_cjfee = calc_cj_fee(cjorder.ordertype, cjorder.cjfee, amount) + change_value = my_total_in - amount - cjorder.txfee + real_cjfee + if change_value <= common.DUST_THRESHOLD: + debug('change value=%d below dust threshold, finding new utxos' % (change_value)) + try: + utxos = self.wallet.select_utxos(mixdepth, amount + common.DUST_THRESHOLD) + except Exception: + debug('dont have the required UTXOs to make a output above the dust threshold, quitting') + return None, None, None + + return utxos, cj_addr, change_addr + + def on_tx_unconfirmed(self, cjorder, txid, removed_utxos): + self.tx_unconfirm_timestamp[cjorder.cj_addr] = int(time.time()) + # if the balance of the highest-balance mixing depth change then reannounce it + oldorder = self.orderlist[0] if len(self.orderlist) > 0 else None + neworders = self.create_my_orders() + if len(neworders) == 0: + return [0], [] # cancel old order + if oldorder: # oldorder may not exist when this is called from on_tx_confirmed + if oldorder['maxsize'] == neworders[0]['maxsize']: + return [], [] # change nothing + # announce new order, replacing the old order + return [], [neworders[0]] + + def on_tx_confirmed(self, cjorder, confirmations, txid): + confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[cjorder.cj_addr] + timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S") + self.log_statement([timestamp, cjorder.cj_amount, len(cjorder.utxos), + sum([av['value'] for av in cjorder.utxos.values()]), cjorder.real_cjfee, + cjorder.real_cjfee - cjorder.txfee, round(confirm_time / 60.0, 2), '']) + return self.on_tx_unconfirmed(cjorder, txid, None) + def main(): - common.load_program_config() - import sys - seed = sys.argv[1] - if isinstance(common.bc_interface, blockchaininterface.BlockrInterface): - print '\nYou are running a yield generator by polling the blockr.io website' - print 'This is quite bad for privacy. That site is owned by coinbase.com' - print 'Also your bot will run faster and more efficently, you can be immediately notified of new bitcoin network' - print ' information so your money will be working for you as hard as possible' - print 'Learn how to setup JoinMarket with Bitcoin Core: https://github.com/chris-belcher/joinmarket/wiki/Running-JoinMarket-with-Bitcoin-Core-full-node' - ret = raw_input('\nContinue? (y/n):') - if ret[0] != 'y': - return - - wallet = Wallet(seed, max_mix_depth = mix_levels) - common.bc_interface.sync_wallet(wallet) - - common.nickname = nickname - debug('starting yield generator') - irc = IRCMessageChannel(common.nickname, realname='btcint=' + common.config.get("BLOCKCHAIN", "blockchain_source"), - password=nickserv_password) - maker = YieldGenerator(irc, wallet) - try: - debug('connecting to irc') - irc.run() - except: - debug('CRASHING, DUMPING EVERYTHING') - debug_dump_object(wallet, ['addr_cache', 'keys', 'seed']) - debug_dump_object(maker) - debug_dump_object(irc) - import traceback - debug(traceback.format_exc()) + common.load_program_config() + + seed = sys.argv[1] + if isinstance(common.bc_interface, BlockrInterface): + print '\nYou are running a yield generator by polling the blockr.io website' + print 'This is quite bad for privacy. That site is owned by coinbase.com' + print 'Also your bot will run faster and more efficently, you can be immediately notified of new bitcoin network' + print ' information so your money will be working for you as hard as possible' + print 'Learn how to setup JoinMarket with Bitcoin Core: https://github.com/chris-belcher/joinmarket/wiki/Running-JoinMarket-with-Bitcoin-Core-full-node' + ret = raw_input('\nContinue? (y/n):') + if ret[0] != 'y': + return + + wallet = Wallet(seed, max_mix_depth=mix_levels) + common.bc_interface.sync_wallet(wallet) + + common.nickname = nickname + debug('starting yield generator') + irc = IRCMessageChannel(common.nickname, realname='btcint=' + common.config.get("BLOCKCHAIN", "blockchain_source"), + password=nickserv_password) + maker = YieldGenerator(irc, wallet) + try: + debug('connecting to irc') + irc.run() + except: + debug('CRASHING, DUMPING EVERYTHING') + debug_dump_object(wallet, ['addr_cache', 'keys', 'seed']) + debug_dump_object(maker) + debug_dump_object(irc) + import traceback + debug(traceback.format_exc()) + if __name__ == "__main__": - main() - print('done') + main() + print('done')