Dash Core  0.12.2.1
P2P Digital Currency
spendfrom.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # Use the raw transactions API to spend dashs received on particular addresses,
4 # and send any change back to that same address.
5 #
6 # Example usage:
7 # spendfrom.py # Lists available funds
8 # spendfrom.py --from=ADDRESS --to=ADDRESS --amount=11.00
9 #
10 # Assumes it will talk to a dashd or Dash-Qt running
11 # on localhost.
12 #
13 # Depends on jsonrpc
14 #
15 
16 from decimal import *
17 import getpass
18 import math
19 import os
20 import os.path
21 import platform
22 import sys
23 import time
24 from jsonrpc import ServiceProxy, json
25 
26 BASE_FEE=Decimal("0.001")
27 
29  """Make sure json library being used does not lose precision converting BTC values"""
30  n = Decimal("20000000.00000003")
31  satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
32  if satoshis != 2000000000000003:
33  raise RuntimeError("JSON encode/decode loses precision")
34 
36  """Return the default location of the Dash Core data directory"""
37  if platform.system() == "Darwin":
38  return os.path.expanduser("~/Library/Application Support/DashCore/")
39  elif platform.system() == "Windows":
40  return os.path.join(os.environ['APPDATA'], "DashCore")
41  return os.path.expanduser("~/.dashcore")
42 
44  """Read the dash.conf file from dbdir, returns dictionary of settings"""
45  from ConfigParser import SafeConfigParser
46 
47  class FakeSecHead(object):
48  def __init__(self, fp):
49  self.fp = fp
50  self.sechead = '[all]\n'
51  def readline(self):
52  if self.sechead:
53  try: return self.sechead
54  finally: self.sechead = None
55  else:
56  s = self.fp.readline()
57  if s.find('#') != -1:
58  s = s[0:s.find('#')].strip() +"\n"
59  return s
60 
61  config_parser = SafeConfigParser()
62  config_parser.readfp(FakeSecHead(open(os.path.join(dbdir, "dash.conf"))))
63  return dict(config_parser.items("all"))
64 
65 def connect_JSON(config):
66  """Connect to a Dash Core JSON-RPC server"""
67  testnet = config.get('testnet', '0')
68  testnet = (int(testnet) > 0) # 0/1 in config file, convert to True/False
69  if not 'rpcport' in config:
70  config['rpcport'] = 19998 if testnet else 9998
71  connect = "http://%s:%s@127.0.0.1:%s"%(config['rpcuser'], config['rpcpassword'], config['rpcport'])
72  try:
73  result = ServiceProxy(connect)
74  # ServiceProxy is lazy-connect, so send an RPC command mostly to catch connection errors,
75  # but also make sure the dashd we're talking to is/isn't testnet:
76  if result.getmininginfo()['testnet'] != testnet:
77  sys.stderr.write("RPC server at "+connect+" testnet setting mismatch\n")
78  sys.exit(1)
79  return result
80  except:
81  sys.stderr.write("Error connecting to RPC server at "+connect+"\n")
82  sys.exit(1)
83 
84 def unlock_wallet(dashd):
85  info = dashd.getinfo()
86  if 'unlocked_until' not in info:
87  return True # wallet is not encrypted
88  t = int(info['unlocked_until'])
89  if t <= time.time():
90  try:
91  passphrase = getpass.getpass("Wallet is locked; enter passphrase: ")
92  dashd.walletpassphrase(passphrase, 5)
93  except:
94  sys.stderr.write("Wrong passphrase\n")
95 
96  info = dashd.getinfo()
97  return int(info['unlocked_until']) > time.time()
98 
99 def list_available(dashd):
100  address_summary = dict()
101 
102  address_to_account = dict()
103  for info in dashd.listreceivedbyaddress(0):
104  address_to_account[info["address"]] = info["account"]
105 
106  unspent = dashd.listunspent(0)
107  for output in unspent:
108  # listunspent doesn't give addresses, so:
109  rawtx = dashd.getrawtransaction(output['txid'], 1)
110  vout = rawtx["vout"][output['vout']]
111  pk = vout["scriptPubKey"]
112 
113  # This code only deals with ordinary pay-to-dash-address
114  # or pay-to-script-hash outputs right now; anything exotic is ignored.
115  if pk["type"] != "pubkeyhash" and pk["type"] != "scripthash":
116  continue
117 
118  address = pk["addresses"][0]
119  if address in address_summary:
120  address_summary[address]["total"] += vout["value"]
121  address_summary[address]["outputs"].append(output)
122  else:
123  address_summary[address] = {
124  "total" : vout["value"],
125  "outputs" : [output],
126  "account" : address_to_account.get(address, "")
127  }
128 
129  return address_summary
130 
131 def select_coins(needed, inputs):
132  # Feel free to improve this, this is good enough for my simple needs:
133  outputs = []
134  have = Decimal("0.0")
135  n = 0
136  while have < needed and n < len(inputs):
137  outputs.append({ "txid":inputs[n]["txid"], "vout":inputs[n]["vout"]})
138  have += inputs[n]["amount"]
139  n += 1
140  return (outputs, have-needed)
141 
142 def create_tx(dashd, fromaddresses, toaddress, amount, fee):
143  all_coins = list_available(dashd)
144 
145  total_available = Decimal("0.0")
146  needed = amount+fee
147  potential_inputs = []
148  for addr in fromaddresses:
149  if addr not in all_coins:
150  continue
151  potential_inputs.extend(all_coins[addr]["outputs"])
152  total_available += all_coins[addr]["total"]
153 
154  if total_available < needed:
155  sys.stderr.write("Error, only %f BTC available, need %f\n"%(total_available, needed));
156  sys.exit(1)
157 
158  #
159  # Note:
160  # Python's json/jsonrpc modules have inconsistent support for Decimal numbers.
161  # Instead of wrestling with getting json.dumps() (used by jsonrpc) to encode
162  # Decimals, I'm casting amounts to float before sending them to dashd.
163  #
164  outputs = { toaddress : float(amount) }
165  (inputs, change_amount) = select_coins(needed, potential_inputs)
166  if change_amount > BASE_FEE: # don't bother with zero or tiny change
167  change_address = fromaddresses[-1]
168  if change_address in outputs:
169  outputs[change_address] += float(change_amount)
170  else:
171  outputs[change_address] = float(change_amount)
172 
173  rawtx = dashd.createrawtransaction(inputs, outputs)
174  signed_rawtx = dashd.signrawtransaction(rawtx)
175  if not signed_rawtx["complete"]:
176  sys.stderr.write("signrawtransaction failed\n")
177  sys.exit(1)
178  txdata = signed_rawtx["hex"]
179 
180  return txdata
181 
182 def compute_amount_in(dashd, txinfo):
183  result = Decimal("0.0")
184  for vin in txinfo['vin']:
185  in_info = dashd.getrawtransaction(vin['txid'], 1)
186  vout = in_info['vout'][vin['vout']]
187  result = result + vout['value']
188  return result
189 
190 def compute_amount_out(txinfo):
191  result = Decimal("0.0")
192  for vout in txinfo['vout']:
193  result = result + vout['value']
194  return result
195 
196 def sanity_test_fee(dashd, txdata_hex, max_fee):
197  class FeeError(RuntimeError):
198  pass
199  try:
200  txinfo = dashd.decoderawtransaction(txdata_hex)
201  total_in = compute_amount_in(dashd, txinfo)
202  total_out = compute_amount_out(txinfo)
203  if total_in-total_out > max_fee:
204  raise FeeError("Rejecting transaction, unreasonable fee of "+str(total_in-total_out))
205 
206  tx_size = len(txdata_hex)/2
207  kb = tx_size/1000 # integer division rounds down
208  if kb > 1 and fee < BASE_FEE:
209  raise FeeError("Rejecting no-fee transaction, larger than 1000 bytes")
210  if total_in < 0.01 and fee < BASE_FEE:
211  raise FeeError("Rejecting no-fee, tiny-amount transaction")
212  # Exercise for the reader: compute transaction priority, and
213  # warn if this is a very-low-priority transaction
214 
215  except FeeError as err:
216  sys.stderr.write((str(err)+"\n"))
217  sys.exit(1)
218 
219 def main():
220  import optparse
221 
222  parser = optparse.OptionParser(usage="%prog [options]")
223  parser.add_option("--from", dest="fromaddresses", default=None,
224  help="addresses to get dashs from")
225  parser.add_option("--to", dest="to", default=None,
226  help="address to get send dashs to")
227  parser.add_option("--amount", dest="amount", default=None,
228  help="amount to send")
229  parser.add_option("--fee", dest="fee", default="0.0",
230  help="fee to include")
231  parser.add_option("--datadir", dest="datadir", default=determine_db_dir(),
232  help="location of dash.conf file with RPC username/password (default: %default)")
233  parser.add_option("--testnet", dest="testnet", default=False, action="store_true",
234  help="Use the test network")
235  parser.add_option("--dry_run", dest="dry_run", default=False, action="store_true",
236  help="Don't broadcast the transaction, just create and print the transaction data")
237 
238  (options, args) = parser.parse_args()
239 
241  config = read_bitcoin_config(options.datadir)
242  if options.testnet: config['testnet'] = True
243  dashd = connect_JSON(config)
244 
245  if options.amount is None:
246  address_summary = list_available(dashd)
247  for address,info in address_summary.iteritems():
248  n_transactions = len(info['outputs'])
249  if n_transactions > 1:
250  print("%s %.8f %s (%d transactions)"%(address, info['total'], info['account'], n_transactions))
251  else:
252  print("%s %.8f %s"%(address, info['total'], info['account']))
253  else:
254  fee = Decimal(options.fee)
255  amount = Decimal(options.amount)
256  while unlock_wallet(dashd) == False:
257  pass # Keep asking for passphrase until they get it right
258  txdata = create_tx(dashd, options.fromaddresses.split(","), options.to, amount, fee)
259  sanity_test_fee(dashd, txdata, amount*Decimal("0.01"))
260  if options.dry_run:
261  print(txdata)
262  else:
263  txid = dashd.sendrawtransaction(txdata)
264  print(txid)
265 
266 if __name__ == '__main__':
267  main()
def select_coins(needed, inputs)
Definition: spendfrom.py:131
def main()
Definition: spendfrom.py:219
def check_json_precision()
Definition: spendfrom.py:28
def compute_amount_in(dashd, txinfo)
Definition: spendfrom.py:182
def create_tx(dashd, fromaddresses, toaddress, amount, fee)
Definition: spendfrom.py:142
def unlock_wallet(dashd)
Definition: spendfrom.py:84
def list_available(dashd)
Definition: spendfrom.py:99
def sanity_test_fee(dashd, txdata_hex, max_fee)
Definition: spendfrom.py:196
def determine_db_dir()
Definition: spendfrom.py:35
def connect_JSON(config)
Definition: spendfrom.py:65
def read_bitcoin_config(dbdir)
Definition: spendfrom.py:43
def compute_amount_out(txinfo)
Definition: spendfrom.py:190