SHOW:
|
|
- or go back to the newest paste.
1 | #!/usr/bin/env python | |
2 | ||
3 | """ | |
4 | bitpaint.py | |
5 | ~~~~~~~~~~~ | |
6 | Simple command-line utility to use the Bitcoin blockchain to track and manage so-called "colored coins", or "smart contracts/securities". | |
7 | ||
8 | Caution: | |
9 | - Private keys are stored in plaintext in your configuration file | |
10 | - | - Do not allow the "colored coin" to be used in a transaction with |
10 | + | - Colored coin tracking is implemented following the "output |
11 | - | multiple inputs. That would mess up the tracking. I suggest avoid |
11 | + | ordering" method, but with very few test cases available, |
12 | - | using holding-addresses in your normal wallet, and just use this |
12 | + | it is currently difficult to say whether or not the approach is |
13 | - | client to manage your colored coins. |
13 | + | compatible with approaches implemented elsewhere. |
14 | - | - Since there is no transaction fee included during asset transfer, |
14 | + | - Remember to include a transaction fee when transferring assets, |
15 | - | you cannot be sure that the transaction will be confirmed when transferring |
15 | + | so that the transaction will confirm. |
16 | - | "assets" |
16 | + | |
17 | Donations welcome: 1GsnaaAWYMb7yPwBQ19bSLLMTtsfSyjqg3 | |
18 | ||
19 | """ | |
20 | ||
21 | # Import libraries | |
22 | import jsonrpc | |
23 | - | import urllib2, ConfigParser, ctypes, ctypes.util, hashlib, simplejson as json, os |
23 | + | |
24 | - | from jsonrpc import ServiceProxy |
24 | + | import urllib2, ConfigParser, ctypes, ctypes.util, hashlib, simplejson as json, os, sys, binascii, collections |
25 | jsonrpc.dumps = json.dumps | |
26 | ||
27 | ### Start: Generic helpers | |
28 | def JSONtoAmount(value): | |
29 | return long(round(value * 1e8)) | |
30 | def AmountToJSON(amount): | |
31 | return float(amount / 1e8) | |
32 | ### End: Generic helpers | |
33 | ||
34 | ||
35 | ### Start: Create/Read Config | |
36 | # Here we create a configuration file if one does not exist already. | |
37 | # User is asked for details about his bitcoind so the script can connect. | |
38 | config_file = "bitpaint.conf" | |
39 | basic_bitpaint_conf = """[bitcoind] | |
40 | rpchost = %s | |
41 | rpcport = %s | |
42 | rpcuser = %s | |
43 | rpcpwd = %s | |
44 | ||
45 | [HoldingAddresses] | |
46 | addresses = | |
47 | private_keys = | |
48 | """ | |
49 | # If the config file does not exists, ask user for details and write one | |
50 | if not os.path.exists(config_file): | |
51 | print "Configuration file bitpaint.conf not found. Creating one..." | |
52 | host = raw_input("bitcoind rpc host (default: 127.0.0.1): ") | |
53 | if len(host) == 0: host = "127.0.0.1" | |
54 | port = raw_input("bitcoind rpc port (default: 8332): ") | |
55 | if len(port) == 0: port = "8332" | |
56 | user = raw_input("bitcoind rpc username (default: <blank>): ") | |
57 | pwd = raw_input("bitcoind rpc password (default: <blank>): ") | |
58 | f = open(config_file, 'w') | |
59 | f.write(basic_bitpaint_conf % (host,port,user,pwd)) | |
60 | f.close() | |
61 | ||
62 | # Parse the config file | |
63 | reserved_sections = ['bitcoind', 'HoldingAddresses'] | |
64 | config = ConfigParser.ConfigParser() | |
65 | config.read(config_file) | |
66 | rpchost = config.get('bitcoind', 'rpchost') | |
67 | rpcport = config.get('bitcoind', 'rpcport') | |
68 | rpcuser = config.get('bitcoind', 'rpcuser') | |
69 | rpcpwd = config.get('bitcoind', 'rpcpwd') | |
70 | ||
71 | # Connect to bitcoind | |
72 | if len(rpcuser) == 0 and len(rpcpwd) == 0: | |
73 | bitcoind_connection_string = "http://s:%s" % (rpchost,rpcport) | |
74 | else: | |
75 | bitcoind_connection_string = "http://%s:%s@%s:%s" % (rpcuser,rpcpwd,rpchost,rpcport) | |
76 | - | sp = ServiceProxy(bitcoind_connection_string) |
76 | + | sp = jsonrpc.ServiceProxy(bitcoind_connection_string) |
77 | ||
78 | ### End: Create/Read Config | |
79 | ||
80 | ||
81 | ### Start: Address Generation code | |
82 | # The following code was yoinked from addrgen.py | |
83 | # Thanks to: Joric/bitcoin-dev, june 2012, public domain | |
84 | ssl = ctypes.cdll.LoadLibrary (ctypes.util.find_library ('ssl') or 'libeay32') | |
85 | ||
86 | def check_result (val, func, args): | |
87 | if val == 0: raise ValueError | |
88 | else: return ctypes.c_void_p (val) | |
89 | ||
90 | ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p | |
91 | ssl.EC_KEY_new_by_curve_name.errcheck = check_result | |
92 | ||
93 | class KEY: | |
94 | def __init__(self): | |
95 | NID_secp256k1 = 714 | |
96 | self.k = ssl.EC_KEY_new_by_curve_name(NID_secp256k1) | |
97 | self.compressed = False | |
98 | self.POINT_CONVERSION_COMPRESSED = 2 | |
99 | self.POINT_CONVERSION_UNCOMPRESSED = 4 | |
100 | ||
101 | def __del__(self): | |
102 | if ssl: | |
103 | ssl.EC_KEY_free(self.k) | |
104 | self.k = None | |
105 | ||
106 | def generate(self, secret=None): | |
107 | if secret: | |
108 | self.prikey = secret | |
109 | priv_key = ssl.BN_bin2bn(secret, 32, ssl.BN_new()) | |
110 | group = ssl.EC_KEY_get0_group(self.k) | |
111 | pub_key = ssl.EC_POINT_new(group) | |
112 | ctx = ssl.BN_CTX_new() | |
113 | ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx) | |
114 | ssl.EC_KEY_set_private_key(self.k, priv_key) | |
115 | ssl.EC_KEY_set_public_key(self.k, pub_key) | |
116 | ssl.EC_POINT_free(pub_key) | |
117 | ssl.BN_CTX_free(ctx) | |
118 | return self.k | |
119 | else: | |
120 | return ssl.EC_KEY_generate_key(self.k) | |
121 | ||
122 | def get_pubkey(self): | |
123 | size = ssl.i2o_ECPublicKey(self.k, 0) | |
124 | mb = ctypes.create_string_buffer(size) | |
125 | ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb))) | |
126 | return mb.raw | |
127 | ||
128 | def get_secret(self): | |
129 | bn = ssl.EC_KEY_get0_private_key(self.k); | |
130 | bytes = (ssl.BN_num_bits(bn) + 7) / 8 | |
131 | mb = ctypes.create_string_buffer(bytes) | |
132 | n = ssl.BN_bn2bin(bn, mb); | |
133 | return mb.raw.rjust(32, chr(0)) | |
134 | ||
135 | def set_compressed(self, compressed): | |
136 | self.compressed = compressed | |
137 | if compressed: | |
138 | form = self.POINT_CONVERSION_COMPRESSED | |
139 | else: | |
140 | form = self.POINT_CONVERSION_UNCOMPRESSED | |
141 | ssl.EC_KEY_set_conv_form(self.k, form) | |
142 | ||
143 | def dhash(s): | |
144 | return hashlib.sha256(hashlib.sha256(s).digest()).digest() | |
145 | ||
146 | def rhash(s): | |
147 | h1 = hashlib.new('ripemd160') | |
148 | h1.update(hashlib.sha256(s).digest()) | |
149 | return h1.digest() | |
150 | ||
151 | b58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' | |
152 | ||
153 | def base58_encode(n): | |
154 | l = [] | |
155 | while n > 0: | |
156 | n, r = divmod(n, 58) | |
157 | l.insert(0,(b58_digits[r])) | |
158 | return ''.join(l) | |
159 | ||
160 | def base58_decode(s): | |
161 | n = 0 | |
162 | for ch in s: | |
163 | n *= 58 | |
164 | digit = b58_digits.index(ch) | |
165 | n += digit | |
166 | return n | |
167 | ||
168 | def base58_encode_padded(s): | |
169 | res = base58_encode(int('0x' + s.encode('hex'), 16)) | |
170 | pad = 0 | |
171 | for c in s: | |
172 | if c == chr(0): | |
173 | pad += 1 | |
174 | else: | |
175 | break | |
176 | return b58_digits[0] * pad + res | |
177 | ||
178 | def base58_decode_padded(s): | |
179 | pad = 0 | |
180 | for c in s: | |
181 | if c == b58_digits[0]: | |
182 | pad += 1 | |
183 | else: | |
184 | break | |
185 | h = '%x' % base58_decode(s) | |
186 | if len(h) % 2: | |
187 | h = '0' + h | |
188 | res = h.decode('hex') | |
189 | return chr(0) * pad + res | |
190 | ||
191 | def base58_check_encode(s, version=0): | |
192 | vs = chr(version) + s | |
193 | check = dhash(vs)[:4] | |
194 | return base58_encode_padded(vs + check) | |
195 | ||
196 | def base58_check_decode(s, version=0): | |
197 | k = base58_decode_padded(s) | |
198 | v0, data, check0 = k[0], k[1:-4], k[-4:] | |
199 | check1 = dhash(v0 + data)[:4] | |
200 | if check0 != check1: | |
201 | raise BaseException('checksum error') | |
202 | if version != ord(v0): | |
203 | raise BaseException('version mismatch') | |
204 | return data | |
205 | ||
206 | def gen_eckey(passphrase=None, secret=None, pkey=None, compressed=False, rounds=1): | |
207 | k = KEY() | |
208 | if passphrase: | |
209 | secret = passphrase.encode('utf8') | |
210 | for i in xrange(rounds): | |
211 | secret = hashlib.sha256(secret).digest() | |
212 | if pkey: | |
213 | secret = base58_check_decode(pkey, 128) | |
214 | compressed = len(secret) == 33 | |
215 | secret = secret[0:32] | |
216 | k.generate(secret) | |
217 | k.set_compressed(compressed) | |
218 | return k | |
219 | ||
220 | def get_addr(k): | |
221 | pubkey = k.get_pubkey() | |
222 | secret = k.get_secret() | |
223 | hash160 = rhash(pubkey) | |
224 | addr = base58_check_encode(hash160) | |
225 | payload = secret | |
226 | if k.compressed: | |
227 | payload = secret + chr(0) | |
228 | pkey = base58_check_encode(payload, 128) | |
229 | return addr, pkey | |
230 | ### End: Address Generation code | |
231 | ||
232 | ||
233 | ### Start: Transaction code | |
234 | def bc_address_to_hash_160(addr): | |
235 | bytes = b58decode(addr, 25) | |
236 | return bytes[1:21] | |
237 | ||
238 | __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' | |
239 | __b58base = len(__b58chars) | |
240 | ||
241 | def b58decode(v, length): | |
242 | """ decode v into a string of len bytes | |
243 | """ | |
244 | long_value = 0L | |
245 | for (i, c) in enumerate(v[::-1]): | |
246 | long_value += __b58chars.find(c) * (__b58base**i) | |
247 | result = '' | |
248 | while long_value >= 256: | |
249 | div, mod = divmod(long_value, 256) | |
250 | result = chr(mod) + result | |
251 | long_value = div | |
252 | result = chr(long_value) + result | |
253 | nPad = 0 | |
254 | for c in v: | |
255 | if c == __b58chars[0]: nPad += 1 | |
256 | else: break | |
257 | result = chr(0)*nPad + result | |
258 | if length is not None and len(result) != length: | |
259 | return None | |
260 | return result | |
261 | ||
262 | def makek(): | |
263 | # Create a dictionary with address as key and private-key | |
264 | # as value | |
265 | a = config.get('HoldingAddresses', 'addresses').split("+") | |
266 | p = config.get('HoldingAddresses', 'private_keys').split("+") | |
267 | k = {} | |
268 | for a in zip(a,p): | |
269 | k[a[0]] = a[1] | |
270 | return k | |
271 | ||
272 | def maketx(inputs, outputs, send=False): | |
273 | # Create a transaction, sign it - possibly send it - but | |
274 | # in either case return the raw hex | |
275 | # inputs: [('a7813e20045b2f2caf612c589adc7e985029167106a300b6a7157084c26967f5', 1, '1PPgZP53BrcG7hyXdWT9fugewmxL1H8LS3'),...] | |
276 | # outputs: [('1KRavVCsvaLi7ZzktHSCE3hPUvhPDhQKhz', 8000000),...] | |
277 | ip = [] | |
278 | for txid,vout,_ in inputs: | |
279 | ip.append({"txid": txid, "vout": vout}) | |
280 | - | op = {} |
280 | + | op = collections.OrderedDict() |
281 | for addr,amnt in outputs: | |
282 | op[addr] = AmountToJSON(amnt) | |
283 | tx = sp.createrawtransaction(ip,op) | |
284 | k = makek() | |
285 | ip = [] | |
286 | pkeys = [] | |
287 | for txid,vout,addr in inputs: | |
288 | ip.append({"txid": txid, "vout": vout, "scriptPubKey": '76a914'+bc_address_to_hash_160(addr).encode('hex')+'88ac'}) | |
289 | - | pkeys.append(k[addr]) |
289 | + | if addr in k: |
290 | pkeys.append(k[addr]) | |
291 | else: | |
292 | pkeys.append(sp.dumpprivkey(addr)) | |
293 | final_t = sp.signrawtransaction(tx,ip,pkeys) | |
294 | if send: | |
295 | sp.sendrawtransaction(tx) | |
296 | else: | |
297 | print final_t['hex'] | |
298 | return final_t['hex'] | |
299 | ||
300 | ### End: Transaction code | |
301 | ||
302 | ||
303 | ### Start: Blockchain Inspection/Traversion code | |
304 | def translate_bctx_to_bitcoindtx(tx_bc): | |
305 | tx = {} | |
306 | tx['locktime'] = 0 | |
307 | tx['txid'] = tx_bc['hash'] | |
308 | tx['version'] = tx_bc['ver'] | |
309 | tx['vin'] = [] | |
310 | tx['vout'] = [] | |
311 | for i in tx_bc['inputs']: | |
312 | v = {} | |
313 | v['scriptSig'] = {'asm': '', 'hex': ''} | |
314 | v['sequence'] = 4294967295 | |
315 | v['txid'] = json.loads(urllib2.urlopen("http://blockchain.info/rawtx/%s" % (i['prev_out']['tx_index'],)).read())['hash'] | |
316 | v['vout'] = i['prev_out']['n'] | |
317 | tx['vin'].append(v) | |
318 | for i in range(len(tx_bc['out'])): | |
319 | o = tx_bc['out'][i] | |
320 | v = {} | |
321 | v['n'] = i | |
322 | v['scriptPubKey'] = {} | |
323 | v['scriptPubKey']['addresses'] = [o['addr']] | |
324 | v['scriptPubKey']['asm'] = [] | |
325 | v['scriptPubKey']['hex'] = [] | |
326 | v['scriptPubKey']['reqSigs'] = 1, | |
327 | v['scriptPubKey']['type'] = 'pubkeyhash' | |
328 | v['value'] = float(o['value'])/1e8 | |
329 | tx['vout'].append(v) | |
330 | return tx | |
331 | ||
332 | def gettx(txid): | |
333 | # Get the information of a single transaction, using | |
334 | # the bitcoind API | |
335 | try: | |
336 | tx_raw = sp.getrawtransaction(txid) | |
337 | tx = sp.decoderawtransaction(tx_raw) | |
338 | except: | |
339 | print "Error getting transaction "+txid+" details from bitcoind, trying blockchain.info" | |
340 | print "http://blockchain.info/rawtx/%s" % (txid,) | |
341 | tx_bc = json.loads(urllib2.urlopen("http://blockchain.info/rawtx/%s" % (txid,)).read()) | |
342 | tx = translate_bctx_to_bitcoindtx(tx_bc) | |
343 | return tx | |
344 | ||
345 | def getaddresstxs(address): | |
346 | # Get all transactions associated with an address. | |
347 | # Uses blockchain.info to get this, bitcoind API | |
348 | # apparently has no equivalent function. | |
349 | address_info = json.loads(urllib2.urlopen("http://blockchain.info/address/%s?format=json"%(address,) ).read()) | |
350 | tx_list = [] | |
351 | for tx in address_info['txs']: | |
352 | tx_list.append(tx['hash']) | |
353 | return tx_list | |
354 | ||
355 | def getholderschange(txid): | |
356 | # Get a list of the new holders and old holders represented by a | |
357 | # single transaction, given as the tx-id. | |
358 | tid = txid.split(":") | |
359 | tx = gettx(tid[0]) | |
360 | new_holders = [] | |
361 | old_holders = [] | |
362 | for i in tx['vin']: | |
363 | old_holders.append(i['txid']+":"+str(i['vout'])) | |
364 | for o in tx['vout']: | |
365 | - | def spentby(tx_out, single_input = False): |
365 | + | |
366 | return new_holders, old_holders | |
367 | ||
368 | def spentby(tx_out): | |
369 | # Return the id of the transaction which spent the given txid/# | |
370 | # if single_input is true, it only returns if the tx_out was used as a single | |
371 | # input to the transaction. | |
372 | # This is because it is not possible to follow a colored coin across a transaction | |
373 | # with multiple inputs | |
374 | tid = tx_out.split(":") | |
375 | tx = gettx(tid[0]) | |
376 | address = tx['vout'][int(tid[1])]['scriptPubKey']['addresses'][0] | |
377 | - | if single_input and not len(inputs) == 1: |
377 | + | |
378 | - | print t,"used with multiple inputs: tracking lost at address",address |
378 | + | |
379 | - | continue |
379 | + | |
380 | for i in inputs: | |
381 | if i == tx_out: | |
382 | return t | |
383 | return None | |
384 | ||
385 | def match_outputs_to_inputs(input_values, output_values): | |
386 | - | # Recursive function to traverse the "tree of ownership", depth-first, |
386 | + | output_belongs_to_input = [-1]*len(output_values) |
387 | - | # head-recursion. |
387 | + | current_color_number = -1 |
388 | current_color_total = 0.0 | |
389 | current_color_max = -1 | |
390 | - | if prevout_txid == root_tx: |
390 | + | for i in range(len(output_values)): |
391 | - | spent_by = spentby(prevout_txid, single_input = False) |
391 | + | output_value = output_values[i] |
392 | while current_color_total+output_value > current_color_max: | |
393 | - | spent_by = spentby(prevout_txid, single_input = True) |
393 | + | current_color_number += 1 |
394 | current_color_total = 0.0 | |
395 | - | tx_data = gettx(prevout_txid.split(":")[0]) |
395 | + | if current_color_number >= len(input_values): return output_belongs_to_input |
396 | - | o = tx_data['vout'][int(prevout_txid.split(":")[1])] |
396 | + | current_color_max = input_values[current_color_number] |
397 | output_belongs_to_input[i] = current_color_number | |
398 | current_color_total += output_value | |
399 | return output_belongs_to_input | |
400 | ||
401 | def get_relevant_outputs(tx_data,prevout_txid): | |
402 | - | ntxid = spent_by+":"+str(o['n']) |
402 | + | global lost_track |
403 | - | hl = rec(ntxid,root_tx) |
403 | + | relevant_outputs = [] |
404 | input_values = [] | |
405 | output_values = [] | |
406 | input_colors = [-1]*len(tx_data['vin']) | |
407 | for pon in range(len(tx_data['vin'])): | |
408 | po = tx_data['vin'][pon] | |
409 | p_tid = po['txid']+":"+str(po['vout']) | |
410 | if p_tid == prevout_txid: | |
411 | input_colors[pon] = 0 | |
412 | po_data = gettx(po['txid']) | |
413 | input_values.append(po_data['vout'][po['vout']]['value']) | |
414 | for pon in range(len(tx_data['vin'])): | |
415 | p_tid = po['txid']+":"+str(po['vout']) | |
416 | if p_tid in lost_track: | |
417 | po_data = gettx(po['txid']) | |
418 | input_values[input_colors.index(0)] += po_data['vout'][po['vout']]['value'] | |
419 | input_values[pon] = 0 | |
420 | lost_track.remove(p_tid) | |
421 | for o in tx_data['vout']: | |
422 | output_values.append(o['value']) | |
423 | output_colors = match_outputs_to_inputs(input_values, output_values) | |
424 | for o in range(len(output_colors)): | |
425 | if output_colors[o] == 0: | |
426 | relevant_outputs.append(tx_data['txid']+":"+str(o)) | |
427 | return relevant_outputs | |
428 | ||
429 | def rec(prevout_txid, root_tx): | |
430 | holder_list = [] | |
431 | spent_by = spentby(prevout_txid) | |
432 | txid,n = prevout_txid.split(":") | |
433 | if spent_by is None: | |
434 | tx_data = gettx(txid) | |
435 | o = tx_data['vout'][int(n)] | |
436 | holder_list.append((o['scriptPubKey']['addresses'][0],o['value'],prevout_txid)) | |
437 | return holder_list | |
438 | tx_data = gettx(spent_by) | |
439 | relevant_outputs = get_relevant_outputs(tx_data,prevout_txid) | |
440 | if len(relevant_outputs) == 0: | |
441 | lost_track.append(spent_by) | |
442 | for ro in relevant_outputs: | |
443 | hl = rec(ro,root_tx) | |
444 | for h in hl: | |
445 | holder_list.append(h) | |
446 | return holder_list | |
447 | ||
448 | lost_track = [] | |
449 | def get_current_holders(root_tx_out): | |
450 | # Get the current holders of the "colored coin" with | |
451 | # the given root (a string with txid+":"+n_output) | |
452 | global lost_track | |
453 | lost_track = [] | |
454 | return rec(root_tx_out,root_tx_out) | |
455 | ||
456 | def get_unspent(addr): | |
457 | # Get the unspent transactions for an address | |
458 | # * Following section is disabled because blockchain.info has a bug that | |
459 | # returns the wrong transaction IDs. | |
460 | #d = json.loads(urllib2.urlopen('http://blockchain.info/unspent?address=%s' % addr).read()) | |
461 | #return d | |
462 | # * Start of blockchain.info bug workaround: | |
463 | txs = getaddresstxs(addr) | |
464 | received = [] | |
465 | sent = [] | |
466 | for txid in txs: | |
467 | tx = gettx(txid) | |
468 | for i in tx['vin']: | |
469 | prev_out = gettx(i['txid']) | |
470 | for po in prev_out['vout']: | |
471 | n = po['n'] | |
472 | po = po['scriptPubKey'] | |
473 | if po['type'] == 'pubkeyhash': | |
474 | if po['addresses'][0] == addr: | |
475 | sent.append(i['txid']+":"+str(n)) | |
476 | for o in tx['vout']: | |
477 | n = o['n'] | |
478 | o = o['scriptPubKey'] | |
479 | if o['type'] == 'pubkeyhash': | |
480 | if o['addresses'][0] == addr: | |
481 | received.append(txid+":"+str(n)) | |
482 | unspent = [] | |
483 | for r in received: | |
484 | if r not in sent: | |
485 | d = {} | |
486 | txid,n = r.split(":") | |
487 | d['tx_hash'] = txid | |
488 | d['tx_output_n'] = int(n) | |
489 | d['value'] = int(1e8*gettx(txid)['vout'][int(n)]['value']) | |
490 | unspent.append(d) | |
491 | return unspent | |
492 | # * End of blockchain.info bug workaround | |
493 | ||
494 | ||
495 | def get_non_asset_funds(addr): | |
496 | unspent = get_unspent(addr) | |
497 | asset_txids = [] | |
498 | for s in config.sections(): | |
499 | if s in reserved_sections: continue | |
500 | for txid in config.get(s, 'txid').split("+"): | |
501 | asset_txids.append(txid) | |
502 | naf = [] | |
503 | for u in unspent: | |
504 | txid = u['tx_hash']+":"+str(u['tx_output_n']) | |
505 | if not txid in asset_txids: | |
506 | naf.append(u) | |
507 | return naf | |
508 | ||
509 | ### End: Blockchain Inspection/Traversion code | |
510 | ||
511 | ||
512 | ### Start: "User-facing" methods | |
513 | def generate_holding_address(): | |
514 | # Generate an address, add it to the config file | |
515 | addr, pkey = get_addr(gen_eckey()) | |
516 | addresses = config.get('HoldingAddresses', 'addresses') | |
517 | private_keys = config.get('HoldingAddresses', 'private_keys') | |
518 | if len(addresses) > 5: | |
519 | config.set('HoldingAddresses', 'addresses', '+'.join([addresses,addr])) | |
520 | config.set('HoldingAddresses', 'private_keys', '+'.join([private_keys,pkey])) | |
521 | else: | |
522 | config.set('HoldingAddresses', 'addresses', addr) | |
523 | config.set('HoldingAddresses', 'private_keys', pkey) | |
524 | config.write(open(config_file,'w')) | |
525 | return "Address added: "+addr | |
526 | ||
527 | def update_tracked_coins(name): | |
528 | # Update the list of owners of a tracked coin | |
529 | # and write to the config file | |
530 | root_tx = config.get(name, "root_tx") | |
531 | current_holders = get_current_holders(root_tx) | |
532 | holding_addresses = "" | |
533 | holding_amounts = "" | |
534 | holding_txids = "" | |
535 | total = 0.0 | |
536 | for h in current_holders: | |
537 | holding_addresses += "+" + h[0] | |
538 | holding_amounts += "+" + str(h[1]) | |
539 | holding_txids += "+" + h[2] | |
540 | config.set(name, "holders", holding_addresses[1:]) | |
541 | config.set(name, "amounts", holding_amounts[1:]) | |
542 | config.set(name, "txid", holding_txids[1:]) | |
543 | config.write(open(config_file,'w')) | |
544 | ||
545 | def start_tracking_coins(name,txid): | |
546 | # Give a name of a tracked coin, together with a | |
547 | # root output that will be used to track it. | |
548 | # Write this to the config file, and update the | |
549 | - | def transfer_asset(sender, receivers): |
549 | + | |
550 | if name in config.sections(): | |
551 | return name+" already exists." | |
552 | config.add_section(name) | |
553 | config.set(name, "root_tx", txid) | |
554 | config.set(name, "holders", "") | |
555 | config.set(name, "amounts", "") | |
556 | - | print maketx(tx_input, tx_outputs) |
556 | + | |
557 | config.write(open(config_file,'w')) | |
558 | update_tracked_coins(name) | |
559 | ||
560 | def show_holders(name): | |
561 | holders = config.get(name, "holders").split("+") | |
562 | amounts = config.get(name, "amounts").split("+") | |
563 | txids = config.get(name,"txid").split("+") | |
564 | total = 0.0 | |
565 | print "*** %s ***" % (name,) | |
566 | for h in zip(holders,amounts,txids): | |
567 | print h[0],h[1],h[2] | |
568 | total += float(h[1]) | |
569 | print "** Total %s: %f **" % (name,total) | |
570 | ||
571 | def show_my_holdings(): | |
572 | sections = config.sections() | |
573 | my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+") | |
574 | for s in sections: | |
575 | if s in reserved_sections: continue | |
576 | holders = config.get(s, "holders").split("+") | |
577 | amounts = config.get(s, "amounts").split("+") | |
578 | txids = config.get(s, "txid").split("+") | |
579 | for h in holders: | |
580 | if h in my_holding_addresses: | |
581 | total_dividends = 0.0 | |
582 | for naf in get_non_asset_funds(h): | |
583 | total_dividends += float(naf['value'])/1e8 | |
584 | print s,amounts[holders.index(h)],"( div:",total_dividends,")",h,txids[holders.index(h)] | |
585 | ||
586 | def show_my_holding_addresses(): | |
587 | my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+") | |
588 | for a in my_holding_addresses: | |
589 | print a | |
590 | ||
591 | def show_colors(): | |
592 | sections = config.sections() | |
593 | for s in sections: | |
594 | if s in reserved_sections: continue | |
595 | print s,config.get(s, 'root_tx') | |
596 | ||
597 | def transfer_asset(sender, receivers,fee_size=None): | |
598 | address,txid,n = sender.split(":") | |
599 | tx_input = [(txid, int(n), address)] | |
600 | tx_outputs = [] | |
601 | for l in receivers.split(","): | |
602 | address,amount = l.split(":") | |
603 | tx_outputs.append((address,int(float(amount)*1e8))) | |
604 | if fee_size: | |
605 | fee_p_out = sp.listunspent()[0] | |
606 | in_addr = base58_check_encode(binascii.unhexlify(fee_p_out['scriptPubKey'][6:-4])) | |
607 | change_address = config.get("bitcoind","change_address") | |
608 | change_amount = fee_p_out['amount']-fee_size | |
609 | tx_input.append((fee_p_out['txid'],fee_p_out['vout'],in_addr)) | |
610 | tx_outputs.append((change_address, int(1e8*change_amount))) | |
611 | raw_transaction = maketx(tx_input, tx_outputs) | |
612 | ||
613 | def pay_to_shareholders(name, wallet_acct, total_payment_amount): | |
614 | holders = config.get(name, "holders").split("+") | |
615 | amounts = config.get(name, "amounts").split("+") | |
616 | total = 0.0 | |
617 | for a in amounts: | |
618 | total += float(a) | |
619 | payouts = {} | |
620 | for h,a in zip(holders,amounts): | |
621 | d = total_payment_amount*float(a)/total | |
622 | - | transfer_asset(opts.transfer_from, opts.transfer_to) |
622 | + | |
623 | sp.sendmany(wallet_acct,payouts) | |
624 | print "Payouts made:" | |
625 | for k in payouts.keys(): | |
626 | print k,":",payouts[k] | |
627 | ||
628 | def transfer_others(transfer_other_from,transfer_other_to): | |
629 | naf = get_non_asset_funds(transfer_other_from) | |
630 | fee = int(1e8*0.005) | |
631 | total_value = 0 | |
632 | inputs = [] | |
633 | for u in naf: | |
634 | total_value += u['value'] | |
635 | i = (u['tx_hash'], u['tx_output_n'], transfer_other_from) | |
636 | inputs.append(i) | |
637 | outputs = [(transfer_other_to, total_value-fee)] | |
638 | maketx(inputs,outputs,send=False) | |
639 | print "Paid",float(total_value-fee)/1e8,"to",transfer_other_to | |
640 | ||
641 | ||
642 | if __name__ == '__main__': | |
643 | # Process command-line options | |
644 | parser = OptionParser() | |
645 | parser.add_option('-p', '--paint', help='Paint coins for tracking', dest='paint_txid', action='store') | |
646 | parser.add_option('-n', '--new-address', help='Create new holding address for colored coins', dest='gen_address', default=False, action='store_true') | |
647 | parser.add_option('-l', '--list-colors', help='List of names of painted coins being tracked', dest='list_colors', default=False, action='store_true') | |
648 | parser.add_option('-u', '--update-ownership', help='Update ownership info for painted coins', dest='update_name', action='store') | |
649 | parser.add_option('-o', '--owners', help='Show owners of painted coins', dest="holders_name", action="store") | |
650 | parser.add_option('-m', '--my-holdings', help='Show holdings at my addresses', dest="show_holdings", action="store_true") | |
651 | parser.add_option('-a', '--holding-addresses', help='Show my holding addresses', dest="show_addresses", action="store_true") | |
652 | parser.add_option('-f', '--transfer-from', help='Asset to transfer to another address. address:txid:n', dest='transfer_from', action="store") | |
653 | parser.add_option('-t', '--transfer-to', help='Address to transfer asset to. address:amount,...', dest='transfer_to', action="store") | |
654 | parser.add_option('-d', '--pay-holders', help="Pay from your bitcoind wallet to asset holders: <asset_name>:<wallet_acctname>:<payout_amount>", dest="pay_to_holders", action="store") | |
655 | parser.add_option('-w', '--fee', help="Pay a transaction fee from your wallet when transferring an asset: <amount>", dest="fee", action="store") | |
656 | parser.add_option('-x', '--transfer-other-from', help='Transfer bitcoins UNRELATED to the tracked address/coins away from this address', dest="transfer_other_from", action="store") | |
657 | parser.add_option('-y', '--transfer-other-to', help='Transfer bitcoins UNRELATED to the tracked address/coins to this address', dest="transfer_other_to", action="store") | |
658 | opts, args = parser.parse_args() | |
659 | ||
660 | if opts.gen_address: | |
661 | print generate_holding_address() | |
662 | if opts.paint_txid: | |
663 | name,txid,n = opts.paint_txid.split(":") | |
664 | start_tracking_coins(name,txid+":"+n) | |
665 | if opts.holders_name: | |
666 | show_holders(opts.holders_name) | |
667 | if opts.update_name: | |
668 | update_tracked_coins(opts.update_name) | |
669 | if opts.list_colors: | |
670 | show_colors() | |
671 | if opts.show_holdings: | |
672 | show_my_holdings() | |
673 | if opts.show_addresses: | |
674 | show_my_holding_addresses() | |
675 | if opts.pay_to_holders: | |
676 | asset_name, wallet_acct_name, amount = opts.pay_to_holders.split(":") | |
677 | pay_to_shareholders(asset_name, wallet_acct_name, float(amount)) | |
678 | if opts.transfer_from or opts.transfer_to: | |
679 | if opts.transfer_to and opts.transfer_from: | |
680 | if opts.fee: | |
681 | transfer_asset(opts.transfer_from, opts.transfer_to,fee_size=float(opts.fee)) | |
682 | else: | |
683 | transfer_asset(opts.transfer_from, opts.transfer_to) | |
684 | else: | |
685 | print "Make sure you give both a source and destination" | |
686 | if opts.transfer_other_from or opts.transfer_other_to: | |
687 | if opts.transfer_other_to and opts.transfer_other_from: | |
688 | transfer_others(opts.transfer_other_from,opts.transfer_other_to) | |
689 | else: | |
690 | print "Make sure you give both a source and destination" |