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" |