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 so-called "colored coins", or "smart contracts/securities". |
| 6 | + | Simple command-line utility to use the Bitcoin blockchain to track and manage so-called "colored coins", or "smart contracts/securities". |
| 7 | ||
| 8 | - | Create "colored coin", receive colored coin, pay to owners of colored coins. |
| 8 | + | |
| 9 | - Private keys are stored in plaintext in your configuration file | |
| 10 | - | Features: |
| 10 | + | |
| 11 | - | - Tracking using bitcoind and blockchain.info |
| 11 | + | |
| 12 | - | - Generate address for colored coin (so you can store |
| 12 | + | |
| 13 | - | coins outside your normal wallet, so that the satoshi |
| 13 | + | |
| 14 | - | client doesn't accidentally spend them) |
| 14 | + | - Since there is no transaction fee included during asset transfer, |
| 15 | - | - Start tracking a "colored coin": "paint" a coin |
| 15 | + | you cannot be sure that the transaction will be confirmed when transferring |
| 16 | - | - Generate list of holders of colored coin (btc-address and |
| 16 | + | "assets" |
| 17 | - | amount of coin held) |
| 17 | + | |
| 18 | - | - Transfer colored coin from one address to another |
| 18 | + | |
| 19 | ||
| 20 | - | Upcoming features: |
| 20 | + | |
| 21 | - | - Pay from wallet to colored coin owners |
| 21 | + | |
| 22 | - | - Forward non-colored payments from asset-addresses to |
| 22 | + | |
| 23 | - | wallet-addresses |
| 23 | + | |
| 24 | from jsonrpc import ServiceProxy | |
| 25 | from optparse import OptionParser | |
| 26 | - | Quick guide - issue "security": |
| 26 | + | |
| 27 | - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 27 | + | |
| 28 | - | 1. Create a holding address: |
| 28 | + | |
| 29 | - | $ python bitpaint.py --new-address |
| 29 | + | |
| 30 | - | 2. Transfer the coins you want to color to that address from your |
| 30 | + | |
| 31 | - | regular wallet. |
| 31 | + | |
| 32 | - | 3. Make a note of the transaction id <txid> and the output number |
| 32 | + | |
| 33 | - | that transferred the coins to the holding address <n>. |
| 33 | + | |
| 34 | - | 4. Start tracking from the output of that transaction: |
| 34 | + | |
| 35 | - | $ python bitpaint.py --paint <name>:<txid>:<n> |
| 35 | + | |
| 36 | # Here we create a configuration file if one does not exist already. | |
| 37 | - | After these steps, the address you generated holds the full colored |
| 37 | + | |
| 38 | - | coin. <name> is any name you would like to give this colored coin. |
| 38 | + | |
| 39 | - | <name> only exists locally. |
| 39 | + | |
| 40 | rpchost = %s | |
| 41 | rpcport = %s | |
| 42 | - | Quick guide - transfer ownership |
| 42 | + | |
| 43 | - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 43 | + | |
| 44 | - | 1. Ask receiver to download this script and generate a holding |
| 44 | + | |
| 45 | - | address, <receiver_address> |
| 45 | + | |
| 46 | - | 2. Transfer the colored coin: |
| 46 | + | |
| 47 | - | $ python --transfer-from <addr>:<txid>:<n> --transfer-to <receiver_address1>:<amount1>,<receiver_address2>:<amount2>,... |
| 47 | + | |
| 48 | """ | |
| 49 | # If the config file does not exists, ask user for details and write one | |
| 50 | - | Quick guide - list current owners |
| 50 | + | |
| 51 | - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 51 | + | |
| 52 | - | 1. Update the list of owners of an asset <name>: |
| 52 | + | |
| 53 | - | $ python --update-ownership <name> |
| 53 | + | |
| 54 | - | 2. Show the list of owners: |
| 54 | + | |
| 55 | - | $ python --owners <name> |
| 55 | + | |
| 56 | user = raw_input("bitcoind rpc username (default: <blank>): ")
| |
| 57 | pwd = raw_input("bitcoind rpc password (default: <blank>): ")
| |
| 58 | - | Quick guide - list "colored coins" I own |
| 58 | + | |
| 59 | - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 59 | + | |
| 60 | - | 1. Show the list of colored coins owned by my addresses: |
| 60 | + | |
| 61 | - | $ python --my-holdings |
| 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 | - | - Since there is no transaction fee included, you cannot be sure that |
| 70 | + | |
| 71 | - | the transaction will be confirmed |
| 71 | + | |
| 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) | |
| 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 | - | user = raw_input("bitcoind rpc username (default: <blank>: ")
|
| 111 | + | |
| 112 | - | pwd = raw_input("bitcoind rpc password (default: <blank>: ")
|
| 112 | + | |
| 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 | - | bitcoind_connection_string = "http://%s:%s@%s:%s" % (rpcuser,rpcpwd,rpchost,rpcport) |
| 126 | + | |
| 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 = {}
| |
| 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]) | |
| 290 | final_t = sp.signrawtransaction(tx,ip,pkeys) | |
| 291 | if send: | |
| 292 | sp.sendrawtransaction(tx) | |
| 293 | else: | |
| 294 | print final_t['hex'] | |
| 295 | return final_t['hex'] | |
| 296 | ||
| 297 | ### End: Transaction code | |
| 298 | ||
| 299 | ||
| 300 | ### Start: Blockchain Inspection/Traversion code | |
| 301 | def translate_bctx_to_bitcoindtx(tx_bc): | |
| 302 | tx = {}
| |
| 303 | tx['locktime'] = 0 | |
| 304 | tx['txid'] = tx_bc['hash'] | |
| 305 | tx['version'] = tx_bc['ver'] | |
| 306 | tx['vin'] = [] | |
| 307 | tx['vout'] = [] | |
| 308 | for i in tx_bc['inputs']: | |
| 309 | v = {}
| |
| 310 | v['scriptSig'] = {'asm': '', 'hex': ''}
| |
| 311 | v['sequence'] = 4294967295 | |
| 312 | v['txid'] = json.loads(urllib2.urlopen("http://blockchain.info/rawtx/%s" % (i['prev_out']['tx_index'],)).read())['hash']
| |
| 313 | v['vout'] = i['prev_out']['n'] | |
| 314 | tx['vin'].append(v) | |
| 315 | for i in range(len(tx_bc['out'])): | |
| 316 | o = tx_bc['out'][i] | |
| 317 | v = {}
| |
| 318 | v['n'] = i | |
| 319 | v['scriptPubKey'] = {}
| |
| 320 | v['scriptPubKey']['addresses'] = [o['addr']] | |
| 321 | v['scriptPubKey']['asm'] = [] | |
| 322 | v['scriptPubKey']['hex'] = [] | |
| 323 | v['scriptPubKey']['reqSigs'] = 1, | |
| 324 | v['scriptPubKey']['type'] = 'pubkeyhash' | |
| 325 | v['value'] = float(o['value'])/1e8 | |
| 326 | tx['vout'].append(v) | |
| 327 | return tx | |
| 328 | ||
| 329 | def gettx(txid): | |
| 330 | # Get the information of a single transaction, using | |
| 331 | # the bitcoind API | |
| 332 | try: | |
| 333 | tx_raw = sp.getrawtransaction(txid) | |
| 334 | tx = sp.decoderawtransaction(tx_raw) | |
| 335 | except: | |
| 336 | print "Error getting transaction "+txid+" details from bitcoind, trying blockchain.info" | |
| 337 | print "http://blockchain.info/rawtx/%s" % (txid,) | |
| 338 | tx_bc = json.loads(urllib2.urlopen("http://blockchain.info/rawtx/%s" % (txid,)).read())
| |
| 339 | tx = translate_bctx_to_bitcoindtx(tx_bc) | |
| 340 | return tx | |
| 341 | ||
| 342 | def getaddresstxs(address): | |
| 343 | # Get all transactions associated with an address. | |
| 344 | # Uses blockchain.info to get this, bitcoind API | |
| 345 | # apparently has no equivalent function. | |
| 346 | address_info = json.loads(urllib2.urlopen("http://blockchain.info/address/%s?format=json"%(address,) ).read())
| |
| 347 | tx_list = [] | |
| 348 | for tx in address_info['txs']: | |
| 349 | tx_list.append(tx['hash']) | |
| 350 | return tx_list | |
| 351 | ||
| 352 | def getholderschange(txid): | |
| 353 | - | tx_raw = sp.getrawtransaction(txid) |
| 353 | + | |
| 354 | - | tx = sp.decoderawtransaction(tx_raw) |
| 354 | + | |
| 355 | tid = txid.split(":")
| |
| 356 | tx = gettx(tid[0]) | |
| 357 | new_holders = [] | |
| 358 | old_holders = [] | |
| 359 | for i in tx['vin']: | |
| 360 | old_holders.append(i['txid']+":"+str(i['vout'])) | |
| 361 | for o in tx['vout']: | |
| 362 | new_holders.append((o['scriptPubKey']['addresses'][0], o['value'])) | |
| 363 | return new_holders, old_holders | |
| 364 | ||
| 365 | def spentby(tx_out, single_input = False): | |
| 366 | # Return the id of the transaction which spent the given txid/# | |
| 367 | # if single_input is true, it only returns if the tx_out was used as a single | |
| 368 | # input to the transaction. | |
| 369 | # This is because it is not possible to follow a colored coin across a transaction | |
| 370 | # with multiple inputs | |
| 371 | tid = tx_out.split(":")
| |
| 372 | tx = gettx(tid[0]) | |
| 373 | address = tx['vout'][int(tid[1])]['scriptPubKey']['addresses'][0] | |
| 374 | tx_list = getaddresstxs(address) | |
| 375 | for t in tx_list: | |
| 376 | outputs,inputs = getholderschange(t) | |
| 377 | if single_input and not len(inputs) == 1: | |
| 378 | print t,"used with multiple inputs: tracking lost at address",address | |
| 379 | continue | |
| 380 | for i in inputs: | |
| 381 | if i == tx_out: | |
| 382 | return t | |
| 383 | return None | |
| 384 | ||
| 385 | def rec(prevout_txid, root_tx): | |
| 386 | # Recursive function to traverse the "tree of ownership", depth-first, | |
| 387 | # head-recursion. | |
| 388 | holder_list = [] | |
| 389 | ||
| 390 | if prevout_txid == root_tx: | |
| 391 | spent_by = spentby(prevout_txid, single_input = False) | |
| 392 | else: | |
| 393 | spent_by = spentby(prevout_txid, single_input = True) | |
| 394 | if spent_by is None: | |
| 395 | tx_data = gettx(prevout_txid.split(":")[0])
| |
| 396 | o = tx_data['vout'][int(prevout_txid.split(":")[1])]
| |
| 397 | holder_list.append((o['scriptPubKey']['addresses'][0],o['value'],prevout_txid)) | |
| 398 | return holder_list | |
| 399 | tx_data = gettx(spent_by) | |
| 400 | - | def rec(prevout_txid): |
| 400 | + | |
| 401 | for o in tx_data['vout']: | |
| 402 | ntxid = spent_by+":"+str(o['n']) | |
| 403 | hl = rec(ntxid,root_tx) | |
| 404 | for h in hl: | |
| 405 | - | spent_by = spentby(prevout_txid, single_input = True) |
| 405 | + | holder_list.append(h) |
| 406 | ||
| 407 | return holder_list | |
| 408 | ||
| 409 | def get_current_holders(root_tx_out): | |
| 410 | # Get the current holders of the "colored coin" with | |
| 411 | # the given root (a string with txid+":"+n_output) | |
| 412 | - | hl = rec(ntxid) |
| 412 | + | return rec(root_tx_out,root_tx_out) |
| 413 | - | if hl is None: |
| 413 | + | |
| 414 | - | holder_list.append((o['scriptPubKey']['addresses'][0],o['value'],ntxid)) |
| 414 | + | def get_unspent(addr): |
| 415 | # Get the unspent transactions for an address | |
| 416 | - | for h in hl: |
| 416 | + | # * Following section is disabled because blockchain.info has a bug that |
| 417 | - | holder_list.append(h) |
| 417 | + | # returns the wrong transaction IDs. |
| 418 | #d = json.loads(urllib2.urlopen('http://blockchain.info/unspent?address=%s' % addr).read())
| |
| 419 | #return d | |
| 420 | # * Start of blockchain.info bug workaround: | |
| 421 | txs = getaddresstxs(addr) | |
| 422 | received = [] | |
| 423 | sent = [] | |
| 424 | - | return rec(root_tx_out) |
| 424 | + | for txid in txs: |
| 425 | tx = gettx(txid) | |
| 426 | for i in tx['vin']: | |
| 427 | prev_out = gettx(i['txid']) | |
| 428 | for po in prev_out['vout']: | |
| 429 | n = po['n'] | |
| 430 | po = po['scriptPubKey'] | |
| 431 | if po['type'] == 'pubkeyhash': | |
| 432 | if po['addresses'][0] == addr: | |
| 433 | sent.append(i['txid']+":"+str(n)) | |
| 434 | for o in tx['vout']: | |
| 435 | n = o['n'] | |
| 436 | o = o['scriptPubKey'] | |
| 437 | if o['type'] == 'pubkeyhash': | |
| 438 | if o['addresses'][0] == addr: | |
| 439 | received.append(txid+":"+str(n)) | |
| 440 | unspent = [] | |
| 441 | for r in received: | |
| 442 | if r not in sent: | |
| 443 | d = {}
| |
| 444 | txid,n = r.split(":")
| |
| 445 | d['tx_hash'] = txid | |
| 446 | d['tx_output_n'] = int(n) | |
| 447 | d['value'] = int(1e8*gettx(txid)['vout'][int(n)]['value']) | |
| 448 | unspent.append(d) | |
| 449 | return unspent | |
| 450 | # * End of blockchain.info bug workaround | |
| 451 | ||
| 452 | ||
| 453 | def get_non_asset_funds(addr): | |
| 454 | unspent = get_unspent(addr) | |
| 455 | asset_txids = [] | |
| 456 | for s in config.sections(): | |
| 457 | if s in reserved_sections: continue | |
| 458 | for txid in config.get(s, 'txid').split("+"):
| |
| 459 | asset_txids.append(txid) | |
| 460 | naf = [] | |
| 461 | for u in unspent: | |
| 462 | txid = u['tx_hash']+":"+str(u['tx_output_n']) | |
| 463 | if not txid in asset_txids: | |
| 464 | naf.append(u) | |
| 465 | return naf | |
| 466 | ||
| 467 | ### End: Blockchain Inspection/Traversion code | |
| 468 | ||
| 469 | ||
| 470 | ### Start: "User-facing" methods | |
| 471 | def generate_holding_address(): | |
| 472 | # Generate an address, add it to the config file | |
| 473 | addr, pkey = get_addr(gen_eckey()) | |
| 474 | addresses = config.get('HoldingAddresses', 'addresses')
| |
| 475 | private_keys = config.get('HoldingAddresses', 'private_keys')
| |
| 476 | if len(addresses) > 5: | |
| 477 | config.set('HoldingAddresses', 'addresses', '+'.join([addresses,addr]))
| |
| 478 | config.set('HoldingAddresses', 'private_keys', '+'.join([private_keys,pkey]))
| |
| 479 | else: | |
| 480 | config.set('HoldingAddresses', 'addresses', addr)
| |
| 481 | config.set('HoldingAddresses', 'private_keys', pkey)
| |
| 482 | config.write(open(config_file,'w')) | |
| 483 | return "Address added: "+addr | |
| 484 | ||
| 485 | def update_tracked_coins(name): | |
| 486 | # Update the list of owners of a tracked coin | |
| 487 | # and write to the config file | |
| 488 | root_tx = config.get(name, "root_tx") | |
| 489 | - | reserved_sections = ['bitcoind', 'HoldingAddresses'] |
| 489 | + | |
| 490 | holding_addresses = "" | |
| 491 | holding_amounts = "" | |
| 492 | holding_txids = "" | |
| 493 | total = 0.0 | |
| 494 | for h in current_holders: | |
| 495 | holding_addresses += "+" + h[0] | |
| 496 | holding_amounts += "+" + str(h[1]) | |
| 497 | holding_txids += "+" + h[2] | |
| 498 | - | print h, amounts[holders.index(h)],txids[holders.index(h)] |
| 498 | + | |
| 499 | config.set(name, "amounts", holding_amounts[1:]) | |
| 500 | config.set(name, "txid", holding_txids[1:]) | |
| 501 | config.write(open(config_file,'w')) | |
| 502 | ||
| 503 | def start_tracking_coins(name,txid): | |
| 504 | # Give a name of a tracked coin, together with a | |
| 505 | # root output that will be used to track it. | |
| 506 | # Write this to the config file, and update the | |
| 507 | - | tx_input = (txid, int(n), address) |
| 507 | + | |
| 508 | if name in config.sections(): | |
| 509 | return name+" already exists." | |
| 510 | config.add_section(name) | |
| 511 | config.set(name, "root_tx", txid) | |
| 512 | config.set(name, "holders", "") | |
| 513 | config.set(name, "amounts", "") | |
| 514 | config.set(name, "txid", "") | |
| 515 | - | ###### |
| 515 | + | |
| 516 | - | # Upcoming: |
| 516 | + | |
| 517 | - | # - GUI |
| 517 | + | |
| 518 | - | # - Paint coins |
| 518 | + | |
| 519 | - | # - Create holding address |
| 519 | + | |
| 520 | - | # - Transfer asset |
| 520 | + | |
| 521 | - | # - Forward dividends to wallet |
| 521 | + | |
| 522 | - | # - Proportional payments to asset holders |
| 522 | + | |
| 523 | - | # - Add options for losing track: cutoff, remove, proportional follow. |
| 523 | + | |
| 524 | - | ###### |
| 524 | + | |
| 525 | print h[0],h[1],h[2] | |
| 526 | total += float(h[1]) | |
| 527 | print "** Total %s: %f **" % (name,total) | |
| 528 | ||
| 529 | def show_my_holdings(): | |
| 530 | sections = config.sections() | |
| 531 | my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
| |
| 532 | for s in sections: | |
| 533 | if s in reserved_sections: continue | |
| 534 | holders = config.get(s, "holders").split("+")
| |
| 535 | amounts = config.get(s, "amounts").split("+")
| |
| 536 | txids = config.get(s, "txid").split("+")
| |
| 537 | - | # Not implemented yet - function to safely transfer other unrelated funds (e.g. dividends, coupons, etc.) AWAY from addresses |
| 537 | + | |
| 538 | - | # holding some asset: |
| 538 | + | |
| 539 | - | #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")
|
| 539 | + | total_dividends = 0.0 |
| 540 | - | #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")
|
| 540 | + | for naf in get_non_asset_funds(h): |
| 541 | total_dividends += float(naf['value'])/1e8 | |
| 542 | print s,amounts[holders.index(h)],"( div:",total_dividends,")",h,txids[holders.index(h)] | |
| 543 | ||
| 544 | def show_my_holding_addresses(): | |
| 545 | my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
| |
| 546 | for a in my_holding_addresses: | |
| 547 | - | print start_tracking_coins(name,txid+":"+n) |
| 547 | + | |
| 548 | ||
| 549 | def transfer_asset(sender, receivers): | |
| 550 | address,txid,n = sender.split(":")
| |
| 551 | tx_input = [(txid, int(n), address)] | |
| 552 | tx_outputs = [] | |
| 553 | for l in receivers.split(","):
| |
| 554 | address,amount = l.split(":")
| |
| 555 | tx_outputs.append((address,int(float(amount)*1e8))) | |
| 556 | - | if opts.transfer_from: |
| 556 | + | |
| 557 | - | if opts.transfer_to: |
| 557 | + | |
| 558 | def pay_to_shareholders(name, wallet_acct, total_payment_amount): | |
| 559 | holders = config.get(name, "holders").split("+")
| |
| 560 | - | print "Missing address to transfer to" |
| 560 | + | |
| 561 | - | if opts.transfer_to: |
| 561 | + | |
| 562 | - | if opts.transfer_from: |
| 562 | + | for a in amounts: |
| 563 | total += float(a) | |
| 564 | payouts = {}
| |
| 565 | - | print "Missing address/input-txid to transfer from" |
| 565 | + | for h,a in zip(holders,amounts): |
| 566 | d = total_payment_amount*float(a)/total | |
| 567 | payouts[h] = d | |
| 568 | sp.sendmany(wallet_acct,payouts) | |
| 569 | print "Payouts made:" | |
| 570 | for k in payouts.keys(): | |
| 571 | print k,":",payouts[k] | |
| 572 | ||
| 573 | def transfer_others(transfer_other_from,transfer_other_to): | |
| 574 | naf = get_non_asset_funds(transfer_other_from) | |
| 575 | fee = int(1e8*0.005) | |
| 576 | total_value = 0 | |
| 577 | inputs = [] | |
| 578 | for u in naf: | |
| 579 | total_value += u['value'] | |
| 580 | i = (u['tx_hash'], u['tx_output_n'], transfer_other_from) | |
| 581 | inputs.append(i) | |
| 582 | outputs = [(transfer_other_to, total_value-fee)] | |
| 583 | maketx(inputs,outputs,send=False) | |
| 584 | print "Paid",float(total_value-fee)/1e8,"to",transfer_other_to | |
| 585 | ||
| 586 | ||
| 587 | if __name__ == '__main__': | |
| 588 | # Process command-line options | |
| 589 | parser = OptionParser() | |
| 590 | parser.add_option('-p', '--paint', help='Paint coins for tracking', dest='paint_txid', action='store')
| |
| 591 | parser.add_option('-n', '--new-address', help='Create new holding address for colored coins', dest='gen_address', default=False, action='store_true')
| |
| 592 | parser.add_option('-l', '--list-colors', help='List of names of painted coins being tracked', dest='list_colors', default=False, action='store_true')
| |
| 593 | parser.add_option('-u', '--update-ownership', help='Update ownership info for painted coins', dest='update_name', action='store')
| |
| 594 | parser.add_option('-o', '--owners', help='Show owners of painted coins', dest="holders_name", action="store")
| |
| 595 | parser.add_option('-m', '--my-holdings', help='Show holdings at my addresses', dest="show_holdings", action="store_true")
| |
| 596 | parser.add_option('-a', '--holding-addresses', help='Show my holding addresses', dest="show_addresses", action="store_true")
| |
| 597 | parser.add_option('-f', '--transfer-from', help='Asset to transfer to another address. address:txid:n', dest='transfer_from', action="store")
| |
| 598 | parser.add_option('-t', '--transfer-to', help='Address to transfer asset to. address:amount,...', dest='transfer_to', action="store")
| |
| 599 | 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")
| |
| 600 | 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")
| |
| 601 | 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")
| |
| 602 | opts, args = parser.parse_args() | |
| 603 | ||
| 604 | if opts.gen_address: | |
| 605 | print generate_holding_address() | |
| 606 | if opts.paint_txid: | |
| 607 | name,txid,n = opts.paint_txid.split(":")
| |
| 608 | start_tracking_coins(name,txid+":"+n) | |
| 609 | if opts.holders_name: | |
| 610 | show_holders(opts.holders_name) | |
| 611 | if opts.update_name: | |
| 612 | update_tracked_coins(opts.update_name) | |
| 613 | if opts.show_holdings: | |
| 614 | show_my_holdings() | |
| 615 | if opts.show_addresses: | |
| 616 | show_my_holding_addresses() | |
| 617 | if opts.pay_to_holders: | |
| 618 | asset_name, wallet_acct_name, amount = opts.pay_to_holders.split(":")
| |
| 619 | pay_to_shareholders(asset_name, wallet_acct_name, float(amount)) | |
| 620 | if opts.transfer_from or opts.transfer_to: | |
| 621 | if opts.transfer_to and opts.transfer_from: | |
| 622 | transfer_asset(opts.transfer_from, opts.transfer_to) | |
| 623 | else: | |
| 624 | print "Make sure you give both a source and destination" | |
| 625 | if opts.transfer_other_from or opts.transfer_other_to: | |
| 626 | if opts.transfer_other_to and opts.transfer_other_from: | |
| 627 | transfer_others(opts.transfer_other_from,opts.transfer_other_to) | |
| 628 | else: | |
| 629 | print "Make sure you give both a source and destination" |