forked from JoinMarket-Org/joinmarket
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathyield-generator-mixdepth.py
326 lines (283 loc) · 13.6 KB
/
yield-generator-mixdepth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#! /usr/bin/env python
from __future__ import absolute_import, print_function
import datetime
import os
import time
import binascii
import sys
from joinmarket import Maker, IRCMessageChannel
from joinmarket import blockchaininterface, BlockrInterface
from joinmarket import jm_single, get_network, load_program_config
from joinmarket import random_nick
from joinmarket import get_log, calc_cj_fee, debug_dump_object
from joinmarket import Wallet
#data_dir = os.path.dirname(os.path.realpath(__file__))
#sys.path.insert(0, os.path.join(data_dir, 'lib'))
#import bitcoin as btc
#import blockchaininterface
from socket import gethostname
mix_levels = 5
#CONFIGURATION
#miner fee contribution
txfee = 5000
# fees for available mix levels from max to min amounts.
cjfee = ['0.00015', '0.00014', '0.00013', '0.00012', '0.00011']
#cjfee = ["%0.5f" % (0.00015 - n*0.00001) for n in range(mix_levels)]
nickname = random_nick()
nickserv_password = ''
#END CONFIGURATION
print(cjfee)
log = get_log()
#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 balance in each mixing depth
# amounts made to be non-overlapping
# minsize set by the miner fee contribution, so you never earn less in cjfee than miner fee
# cjfee drops as you go down to the lower-balance mixing depths, provides
# incentive for people to clump coins together for you in one mix depth
#announce an absolute fee order between the dust limit and minimum amount
# so that there is liquidity in the very low amounts too
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 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()
log.debug('mix_balance = ' + str(mix_balance))
nondust_mix_balance = dict([(m, b)
for m, b in mix_balance.iteritems()
if b > jm_single().DUST_THRESHOLD])
if len(nondust_mix_balance) == 0:
log.debug('do not have any coins left')
return []
#sorts the mixdepth_balance map by balance size
sorted_mix_balance = sorted(
list(mix_balance.iteritems()),
key=lambda a: a[1],
reverse=True)
minsize = int(
1.5 * txfee / float(min(cjfee))
) #minimum size is such that you always net profit at least 50% of the miner fee
filtered_mix_balance = [f for f in sorted_mix_balance if f[1] > minsize]
delta = mix_levels - len(filtered_mix_balance)
log.debug('minsize=' + str(minsize) + ' calc\'d with cjfee=' + str(min(
cjfee)))
lower_bound_balances = filtered_mix_balance[1:] + [(-1, minsize)]
mix_balance_min = [
(mxb[0], mxb[1], minb[1])
for mxb, minb in zip(filtered_mix_balance, lower_bound_balances)
]
mix_balance_min = mix_balance_min[::-1] #reverse list order
thecjfee = cjfee[::-1]
log.debug('mixdepth_balance_min = ' + str(mix_balance_min))
orders = []
oid = 0
for mix_bal_min in mix_balance_min:
mixdepth, balance, mins = mix_bal_min
#the maker class reads specific keys from the dict, but others
# are allowed in there and will be ignored
order = {'oid': oid + 1,
'ordertype': 'relorder',
'minsize': max(mins - jm_single().DUST_THRESHOLD,
jm_single().DUST_THRESHOLD) + 1,
'maxsize': max(balance - jm_single().DUST_THRESHOLD,
jm_single().DUST_THRESHOLD),
'txfee': txfee,
'cjfee': thecjfee[oid + delta],
'mixdepth': mixdepth}
oid += 1
orders.append(order)
absorder_size = min(minsize, sorted_mix_balance[0][1])
if absorder_size != 0:
lowest_cjfee = thecjfee[min(oid, len(thecjfee) - 1)]
absorder_fee = calc_cj_fee('relorder', lowest_cjfee, minsize)
log.debug('absorder fee = ' + str(absorder_fee) + ' uses cjfee=' +
str(lowest_cjfee))
#the absorder is always oid=0
order = {'oid': 0,
'ordertype': 'absorder',
'minsize': jm_single().DUST_THRESHOLD + 1,
'maxsize': absorder_size - jm_single().DUST_THRESHOLD,
'txfee': txfee,
'cjfee': absorder_fee}
orders = [order] + orders
log.debug('generated orders = \n' + '\n'.join([str(o) for o in orders]))
# sanity check
for order in orders:
assert order['minsize'] >= 0
assert order['maxsize'] > 0
assert order['minsize'] <= order['maxsize']
return orders
def oid_to_order(self, cjorder, oid, amount):
total_amount = amount + cjorder.txfee
mix_balance = self.wallet.get_balance_by_mixdepth()
filtered_mix_balance = [m
for m in mix_balance.iteritems()
if m[1] >= total_amount]
log.debug('mix depths that have enough, filtered_mix_balance = ' + str(
filtered_mix_balance))
# use mix depth that has the closest amount of coins to what this transaction needs
# keeps coins moving through mix depths more quickly
# and its more likely to use txos of a similiar size to this transaction
filtered_mix_balance = sorted(
filtered_mix_balance,
key=lambda x: x[1]) #sort smallest to largest usable amount
log.debug('sorted order of filtered_mix_balance = ' + str(
filtered_mix_balance))
mixdepth = filtered_mix_balance[0][0]
log.debug('filling offer, mixdepth=' + str(mixdepth))
# mixdepth is the chosen depth we'll be spending from
cj_addr = self.wallet.get_internal_addr((mixdepth + 1) %
self.wallet.max_mix_depth)
change_addr = self.wallet.get_internal_addr(mixdepth)
utxos = self.wallet.select_utxos(mixdepth, total_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 <= jm_single().DUST_THRESHOLD:
log.debug('change value=%d below dust threshold, finding new utxos'
% (change_value))
try:
utxos = self.wallet.select_utxos(
mixdepth, total_amount + jm_single().DUST_THRESHOLD)
except Exception:
log.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())
'''
case 0
the absorder will basically never get changed, unless there are no utxos left, when neworders==[]
case 1
a single coin is split into two coins across levels
must announce a new order, plus modify the old order
case 2
two existing mixdepths get modified
announce the modified new orders
case 3
one existing mixdepth gets emptied into another
cancel it, modify the place it went
algorithm
find all the orders which have changed, the length of that list tells us which case
'''
myorders = self.create_my_orders()
oldorderlist = self.orderlist
if len(myorders) == 0:
return ([o['oid'] for o in oldorderlist], [])
cancel_orders = []
ann_orders = []
neworders = [o for o in myorders if o['ordertype'] == 'relorder']
oldorders = [o for o in oldorderlist if o['ordertype'] == 'relorder']
#new_setdiff_old = The relative complement of `new` in `old` = members in `new` which are not in `old`
new_setdiff_old = [o for o in neworders if o not in oldorders]
old_setdiff_new = [o for o in oldorders if o not in neworders]
log.debug('neworders = \n' + '\n'.join([str(o) for o in neworders]))
log.debug('oldorders = \n' + '\n'.join([str(o) for o in oldorders]))
log.debug('new_setdiff_old = \n' + '\n'.join([str(
o) for o in new_setdiff_old]))
log.debug('old_setdiff_new = \n' + '\n'.join([str(
o) for o in old_setdiff_new]))
if len(neworders) == len(oldorders):
ann_orders = new_setdiff_old
elif len(neworders) > len(oldorders):
ann_orders = new_setdiff_old
elif len(neworders) < len(oldorders):
ann_orders = new_setdiff_old
ann_oids = [o['oid'] for o in ann_orders]
cancel_orders = [o['oid']
for o in old_setdiff_new
if o['oid'] not in ann_oids]
#check if the absorder has changed, or if it needs to be newly announced
new_abs = [o for o in myorders if o['ordertype'] == 'absorder']
old_abs = [o for o in oldorderlist if o['ordertype'] == 'absorder']
if len(new_abs) > len(old_abs):
#announce an absorder where there wasnt one before
ann_orders = [new_abs[0]] + ann_orders
elif len(new_abs) == len(old_abs) and len(old_abs) > 0:
#maxsize is the only thing that changes, except cjfee but that changes at the same time
if new_abs[0]['maxsize'] != old_abs[0]['maxsize']:
ann_orders = [new_abs[0]] + ann_orders
log.debug('can_orders = \n' + '\n'.join([str(o) for o in cancel_orders
]))
log.debug('ann_orders = \n' + '\n'.join([str(o) for o in ann_orders]))
return (cancel_orders, ann_orders)
def on_tx_confirmed(self, cjorder, confirmations, txid):
if cjorder.cj_addr in self.tx_unconfirm_timestamp:
confirm_time = int(time.time()) - self.tx_unconfirm_timestamp[
cjorder.cj_addr]
del self.tx_unconfirm_timestamp[cjorder.cj_addr]
else:
confirm_time = 0
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)
def main():
load_program_config()
import sys
seed = sys.argv[1]
if isinstance(jm_single().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)
jm_single().bc_interface.sync_wallet(wallet)
jm_single().nickname = nickname
log.debug('starting yield generator')
irc = IRCMessageChannel(jm_single().nickname,
realname='btcint=' + jm_single().config.get(
"BLOCKCHAIN", "blockchain_source"),
password=nickserv_password)
maker = YieldGenerator(irc, wallet)
try:
log.debug('connecting to irc')
irc.run()
except:
log.debug('CRASHING, DUMPING EVERYTHING')
debug_dump_object(wallet, ['addr_cache', 'keys', 'seed'])
debug_dump_object(maker)
debug_dump_object(irc)
import traceback
log.debug(traceback.format_exc())
if __name__ == "__main__":
main()
print('done')