diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index 3410c30..c4a9a81 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -17,7 +17,7 @@ _log = logging.getLogger(__name__) class JSONRPCDaemon(object): def __init__(self, protocol='http', host='127.0.0.1', port=18081, path='/json_rpc'): - self.url = '{protocol}://{host}:{port}/json_rpc'.format( + self.url = '{protocol}://{host}:{port}'.format( protocol=protocol, host=host, port=port) @@ -27,13 +27,37 @@ class JSONRPCDaemon(object): info = self.raw_jsonrpc_request('get_info') return info + def send_transaction(self, blob): + res = self.raw_request('/sendrawtransaction', {'tx_as_hex': blob}) + if res['status'] == 'OK': + return res + raise exceptions.TransactionBroadcastError( + "{status}: {reason}".format(**res), + details=res) + + def raw_request(self, path, data): + hdr = {'Content-Type': 'application/json'} + _log.debug(u"Request: {path}\nData: {data}".format( + path=path, + data=pprint.pformat(data))) + rsp = requests.post(self.url + path, headers=hdr, data=json.dumps(data)) + if rsp.status_code != 200: + raise RPCError("Invalid HTTP status {code} for path {path}.".format( + code=rsp.status_code, + path=path)) + result = rsp.json() + _ppresult = pprint.pformat(result) + _log.debug(u"Result:\n{result}".format(result=_ppresult)) + return result + + def raw_jsonrpc_request(self, method, params=None): hdr = {'Content-Type': 'application/json'} data = {'jsonrpc': '2.0', 'id': 0, 'method': method, 'params': params or {}} _log.debug(u"Method: {method}\nParams:\n{params}".format( method=method, params=pprint.pformat(params))) - rsp = requests.post(self.url, headers=hdr, data=json.dumps(data)) + rsp = requests.post(self.url + '/json_rpc', headers=hdr, data=json.dumps(data)) if rsp.status_code != 200: raise RPCError("Invalid HTTP status {code} for method {method}.".format( code=rsp.status_code, diff --git a/monero/daemon.py b/monero/daemon.py index 36ad4e7..81a9216 100644 --- a/monero/daemon.py +++ b/monero/daemon.py @@ -4,3 +4,6 @@ class Daemon(object): def get_info(self): return self._backend.get_info() + + def send_transaction(self, blob): + return self._backend.send_transaction(blob) diff --git a/monero/exceptions.py b/monero/exceptions.py index d26cdde..99be44a 100644 --- a/monero/exceptions.py +++ b/monero/exceptions.py @@ -24,3 +24,9 @@ class AmountIsZero(AccountException): class TransactionNotPossible(AccountException): pass + +class TransactionBroadcastError(BackendException): + def __init__(self, message, details=None): + self.details = details + super(TransactionBroadcastError, self).__init__(message) + diff --git a/utils/pushtx.py b/utils/pushtx.py new file mode 100644 index 0000000..f6b41ae --- /dev/null +++ b/utils/pushtx.py @@ -0,0 +1,47 @@ +import argparse +import logging +import operator +import re +import sys + +from monero.backends.jsonrpc import JSONRPCDaemon +from monero import Daemon +from monero import exceptions + +def url_data(url): + gs = re.compile( + r'^(?P[^:\s]+)(?::(?P[0-9]+))?$' + ).match(url).groupdict() + return dict(filter(operator.itemgetter(1), gs.items())) + +argsparser = argparse.ArgumentParser(description="Push transaction to network") +argsparser.add_argument('daemon_rpc_url', nargs='?', type=url_data, default='127.0.0.1:18081', + help="Daemon RPC URL [host[:port]]") +argsparser.add_argument('-v', dest='verbosity', action='count', default=0, + help="Verbosity (repeat to increase; -v for INFO, -vv for DEBUG") +argsparser.add_argument('-i', dest='tx_filenames', nargs='+', default=None, + help="Files with transaction data. Will read from stdin if not given.") +args = argsparser.parse_args() +level = logging.WARNING +if args.verbosity == 1: + level = logging.INFO +elif args.verbosity > 1: + level = logging.DEBUG +logging.basicConfig(level=level, format="%(asctime)-15s %(message)s") +if args.tx_filenames: + blobs = [(f, open(f, 'r').read()) for f in args.tx_filenames] +else: + blobs = [('transaction', sys.stdin.read())] +d = Daemon(JSONRPCDaemon(**args.daemon_rpc_url)) +for name, blob in blobs: + logging.debug("Sending {}".format(name)) + try: + res = d.send_transaction(blob) + except exceptions.TransactionBroadcastError as e: + print("{} not sent, reason: {}".format(name, e.details['reason'])) + logging.debug(e.details) + continue + if res['not_relayed']: + print("{} not relayed".format(name)) + else: + print("{} successfully sent".format(name)) diff --git a/utils/transfer.py b/utils/transfer.py index 5bc5191..9529d90 100644 --- a/utils/transfer.py +++ b/utils/transfer.py @@ -62,6 +62,6 @@ for tx in txfrs: if args.outdir: outname = os.path.join(args.outdir, tx.hash + '.tx') outfile = open(outname, 'wb') - outfile.write(tx.blob) + outfile.write(tx.blob.encode()) outfile.close() print(u"Transaction saved to {}".format(outname))