diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index 5dbc1c0..5ed0544 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -10,7 +10,7 @@ from .. import exceptions from ..account import Account from ..address import address, Address, SubAddress from ..numbers import from_atomic, to_atomic, PaymentID -from ..transaction import Transaction, Payment, Transfer +from ..transaction import Transaction, IncomingPayment, OutgoingPayment _log = logging.getLogger(__name__) @@ -173,11 +173,10 @@ class JSONRPCWallet(object): 'account_index': account, 'payment_id': str(payment_id)}) pmts = [] - for tx in _payments['payments']: - data = self._tx2dict(tx) + for data in _payments['payments']: # Monero <= 0.11 : no address is passed because there's only one - data['local_address'] = data['local_address'] or self._master_address - pmts.append(Payment(**data)) + data['address'] = data['address'] or self._master_address + pmts.append(self._inpayment(data)) return pmts def get_transactions_in(self, account=0, confirmed=True, unconfirmed=False): @@ -186,8 +185,7 @@ class JSONRPCWallet(object): txns = _txns.get('in', []) if unconfirmed: txns.extend(_txns.get('pool', [])) - return [Payment(**self._tx2dict(tx)) for tx in - sorted(txns, key=operator.itemgetter('timestamp'))] + return [self._inpayment(tx) for tx in sorted(txns, key=operator.itemgetter('timestamp'))] def get_transactions_out(self, account=0, confirmed=True, unconfirmed=True): _txns = self.raw_request('get_transfers', @@ -195,33 +193,47 @@ class JSONRPCWallet(object): txns = _txns.get('out', []) if unconfirmed: txns.extend(_txns.get('pool', [])) - return [Transfer(**self._tx2dict(tx)) for tx in - sorted(txns, key=operator.itemgetter('timestamp'))] + return [self._outpayment(tx) for tx in sorted(txns, key=operator.itemgetter('timestamp'))] def get_transaction(self, txhash): _tx = self.raw_request('get_transfer_by_txid', {'txid': str(txhash)})['transfer'] - try: - _class = {'in': Payment, 'out': Transfer}[_tx['type']] - except KeyError: - _class = Transaction - return _class(**self._tx2dict(_tx)) + if _tx['type'] == 'in': + return self._inpayment(tx) + elif _tx['type'] == 'out': + return self._outpayment(tx) + return Payment(**self._paymentdict(tx)) - def _tx2dict(self, tx): - pid = tx.get('payment_id', None) + def _paymentdict(self, data): + pid = data.get('payment_id', None) return { - 'hash': tx.get('txid', tx.get('tx_hash')), - 'timestamp': datetime.fromtimestamp(tx['timestamp']) if 'timestamp' in tx else None, - 'amount': from_atomic(tx['amount']), - 'fee': from_atomic(tx['fee']) if 'fee' in tx else None, - 'height': tx.get('height', tx.get('block_height')) or None, + 'txhash': data.get('txid', data.get('tx_hash')), 'payment_id': None if pid is None else PaymentID(pid), - 'note': tx.get('note'), - # NOTE: address will be resolved only after PR#3010 has been merged to Monero - 'local_address': address(tx['address']) if 'address' in tx else None, - 'key': tx.get('key'), - 'blob': tx.get('blob', None), + 'amount': from_atomic(data['amount']), + 'timestamp': datetime.fromtimestamp(data['timestamp']) if 'timestamp' in data else None, + 'note': data.get('note'), + 'transaction': self._tx(data) } + def _inpayment(self, data): + p = self._paymentdict(data) + p.update({'received_by': address(data['address']) if 'address' in data else None}) + return IncomingPayment(**p) + + def _outpayment(self, data): + p = self._paymentdict(data) + p.update({'sent_from': address(data['address']) if 'address' in data else None}) + return OutgoingPayment(**p) + + def _tx(self, data): + return Transaction(**{ + 'hash': data.get('txid', data.get('tx_hash')), + 'fee': from_atomic(data['fee']) if 'fee' in data else None, + 'key': data.get('key'), + 'height': data.get('height', data.get('block_height')) or None, + 'timestamp': datetime.fromtimestamp(data['timestamp']) if 'timestamp' in data else None, + 'blob': data.get('blob', None), + }) + def transfer(self, destinations, priority, ringsize, payment_id=None, unlock_time=0, account=0, relay=True): @@ -247,7 +259,7 @@ class JSONRPCWallet(object): 'tx_hash_list', 'amount_list', 'fee_list', 'tx_key_list', 'tx_blob_list')]))] for d in _pertx: d['payment_id'] = payment_id - return [Transfer(**self._tx2dict(tx)) for tx in _pertx] + return [self._tx(data) for data in _pertx] def raw_request(self, method, params=None): hdr = {'Content-Type': 'application/json'} diff --git a/monero/daemon.py b/monero/daemon.py index a383f7b..7c8401b 100644 --- a/monero/daemon.py +++ b/monero/daemon.py @@ -8,8 +8,8 @@ class Daemon(object): def get_height(self): return self._backend.get_info()['height'] - def send_transaction(self, blob): - return self._backend.send_transaction(blob) + def send_transaction(self, tx): + return self._backend.send_transaction(tx.blob) def get_mempool(self): return self._backend.get_mempool() diff --git a/monero/transaction.py b/monero/transaction.py index 339434d..bd732f8 100644 --- a/monero/transaction.py +++ b/monero/transaction.py @@ -1,45 +1,48 @@ -class Transaction(object): - hash = None - height = None - timestamp = None - fee = None - blob = None - - def __init__(self, hash=None, **kwargs): - self.hash = hash - self.height = kwargs.get('height', self.height) - self.timestamp = kwargs.get('timestamp', self.timestamp) - self.fee = kwargs.get('fee', self.fee) - self.blob = kwargs.get('blob', self.blob) - - def __repr__(self): - return self.hash - - -class LocalTransaction(Transaction): - """A transaction that concerns local wallet, either incoming or outgoing.""" +class Payment(object): + tx_hash = None payment_id = None amount = None - local_address = None + timestamp = None + transaction = None def __init__(self, **kwargs): - super(LocalTransaction, self).__init__(**kwargs) - self.payment_id = kwargs.get('payment_id', self.payment_id) + self.tx_hash = kwargs.get('tx_hash', self.tx_hash) self.amount = kwargs.get('amount', self.amount) - self.local_address = kwargs.get('local_address', self.local_address) + self.timestamp = kwargs.get('timestamp', self.timestamp) + self.payment_id = kwargs.get('payment_id', self.payment_id) + self.transaction = kwargs.get('transaction', self.transaction) -class Payment(LocalTransaction): - """Incoming Transaction""" - pass +class IncomingPayment(Payment): + received_by = None + + def __init__(self, **kwargs): + super(IncomingPayment, self).__init__(**kwargs) + self.received_by = kwargs.get('received_by', self.received_by) -class Transfer(LocalTransaction): - """Outgoing Transaction""" - key = None +class OutgoingPayment(Payment): + sent_from = None note = '' def __init__(self, **kwargs): - super(Transfer, self).__init__(**kwargs) + super(OutgoingPayment, self).__init__(**kwargs) + self.sent_from = kwargs.get('sent_from', self.sent_from) + self.note = kwargs.get('note', self.sent_from) + + +class Transaction(object): + hash = None + fee = None + height = None + timestamp = None + key = None + blob = None + + def __init__(self, **kwargs): + self.hash = kwargs.get('hash', self.hash) + self.fee = kwargs.get('fee', self.fee) + self.height = kwargs.get('height', self.height) + self.timestamp = kwargs.get('timestamp', self.timestamp) self.key = kwargs.get('key', self.key) - self.note = kwargs.get('note', self.note) + self.blob = kwargs.get('blob', self.blob) diff --git a/tests/wallet.py b/tests/wallet.py index a1391f3..f165e34 100644 --- a/tests/wallet.py +++ b/tests/wallet.py @@ -8,7 +8,7 @@ except ImportError: from monero.wallet import Wallet from monero.address import Address -from monero.transaction import Transaction, Payment, Transfer +from monero.transaction import IncomingPayment, OutgoingPayment, Transaction from monero.backends.jsonrpc import JSONRPCWallet class SubaddrWalletTestCase(unittest.TestCase): @@ -162,12 +162,14 @@ class SubaddrWalletTestCase(unittest.TestCase): 'unlock_time': 0}]}} pay_in = self.wallet.get_transactions_in() self.assertEqual(len(list(pay_in)), 3) - for tx in pay_in: - self.assertIsInstance(tx, Payment) + for pmt in pay_in: + self.assertIsInstance(pmt, IncomingPayment) # Once PR#3010 has been merged to Monero, update the JSON and enable the following: -# self.assertIsInstance(tx.local_address, Address) - self.assertIsInstance(tx.amount, Decimal) - self.assertIsInstance(tx.fee, Decimal) +# self.assertIsInstance(pmt.received_by, Address) + self.assertIsInstance(pmt.amount, Decimal) + self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction.fee, Decimal) + self.assertIsInstance(pmt.transaction.height, int) @patch('monero.backends.jsonrpc.requests.post') def test_get_transactions_out(self, mock_post): @@ -257,20 +259,22 @@ class SubaddrWalletTestCase(unittest.TestCase): 'unlock_time': 0}]}} pay_out = self.wallet.get_transactions_out() self.assertEqual(len(list(pay_out)), 6) - for tx in pay_out: - self.assertIsInstance(tx, Transfer) + for pmt in pay_out: + self.assertIsInstance(pmt, OutgoingPayment) # Once PR#3010 has been merged to Monero, update the JSON and enable the following: -# self.assertIsInstance(tx.local_address, Address) - self.assertIsInstance(tx.amount, Decimal) - self.assertIsInstance(tx.fee, Decimal) - self.assertIsInstance(tx.timestamp, datetime) +# self.assertIsInstance(pmt.sent_from, Address) + self.assertIsInstance(pmt.amount, Decimal) + self.assertIsInstance(pmt.timestamp, datetime) + self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction.fee, Decimal) + self.assertIsInstance(pmt.transaction.height, int) @patch('monero.backends.jsonrpc.requests.post') def test_get_payments(self, mock_post): mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = self.get_accounts_result self.wallet = Wallet(JSONRPCWallet()) - mock_post.return_value.status_code = 200 + mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = {'id': 0, 'jsonrpc': '2.0', 'result': {'payments': [{'address': 'BZ9V9tfTDgHYsnAxgeMLaGCUb6yMaGNiZJwiBWQrE23MXcRqSde9DKa9LnPw31o2G8QrdKdUNM7VWhd3dr22ivk54QGqZ6u', @@ -282,8 +286,35 @@ class SubaddrWalletTestCase(unittest.TestCase): 'unlock_time': 0}]}} payments = self.wallet.get_payments(payment_id=0xfeedbadbeef12345) self.assertEqual(len(list(payments)), 1) - for payment in payments: - self.assertIsInstance(payment, Payment) - self.assertIsInstance(payment.local_address, Address) - self.assertIsInstance(payment.amount, Decimal) - self.assertIsInstance(payment.height, int) + for pmt in payments: + self.assertIsInstance(pmt, IncomingPayment) + self.assertIsInstance(pmt.received_by, Address) + self.assertIsInstance(pmt.amount, Decimal) + self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction.height, int) + + @patch('monero.backends.jsonrpc.requests.post') + def test_send_transfer(self, mock_post): + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = self.get_accounts_result + self.wallet = Wallet(JSONRPCWallet()) + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {'id': 0, + 'jsonrpc': '2.0', + 'result': {'amount_list': [3000000000000], + 'fee_list': [3866640000], + 'multisig_txset': '', + 'tx_blob_list': ['020001020005a3d60be09109dfd202e617d904090789e33b02c001885d17d80101a4de34b9e76f8e9b87f50766b33a1f00f49d020002c5fc19a94f82a9cefeaebf5f9f7d8330ff28cc7b1c6dba65d5dd3bfe4693056a00028711ea3e11b97250e7563838e4ea203594c4b33784c2d9777aa738061a31946d2c020901b259965ad1f33d2d014c8cdd4a341a75cf01fef00e6b344b47150e55ae06333b5585b20eb0f4afff160380fde0b30ee3c5ebbb3bbaf11b30f4ef79ee96122175077cec9866b88aa123fdd9f55e2d06cb3815f7e8e88dbcd4f6776aafd13cdf21c86b38671bb4641dd4d696fff29a0546817024cb9f9330fe86a43c1e4088a9019a601aa80025e3f24d0ab5066fdd09254ec751d101960c3422db7657a43b764f015679b3da7221aa349d40ddcce60964af135cbef245c4a2d51bd2b8605b6ce7576f32e9f0b06d91b3840828902cd4c35a1a59fdef21441a02a78229df304f8569f259c3ec218ffe45722020fbfde6a74b0d36753d6afae377e771d454839f8e76762e23dbf35ee354716f7d15f42e7e03048d917984a814fdef3ea4556d0946b30a790e8e0739e8e9f933da61e3ddd2699df0bcf55ac1c149cfbea618830933830bd3937b1f27613cd0959b6020dda7ced635d3c41d31694a526788a7c783e8476594006e2dee7d9fd606d793618fc062ed8cc178c4e1376524a6029a618b882cbfc86f40cc0a036b11d30500290abc052823a5f3b180cb7e05e0c9316a7cbfc9e94c370e0817b3ad5c90c0d9070606883d72d6706f1b5069652a8a98c20d60774ff6dd3ba2fa09d39af8c5f7cca2e592cdd0df79482d44baba5531f24961967c43c720aa8f3d93b376c67064960baacf4c347dc243cee2b44e8194fe14e1efe785924671cf8ff3bf545ab8a05213bcce3adef4dae1988e9d33926722be5927c952f6b8f36e62a8fafdb699c8ffdc61720a6d869bfc2719a05fe9f13c72e295869955bfb88eb79d95d19285340049906d497d9d847c09b7e4009d8b0c09a535e993583270227e00c883822a32fbf1c206e5de38438c1d637fd87600f6d56b673e608625acfebbfb8c32632da3282346588078d4af03c84ef78a141af8ca92d236b7f464aad3f4e66e34689d0bb2fbed1a0b9fc342e5e8a51505b69c9cb7d401778576e36c68475ad738e35f867ad6498428d59fdf3e92d42cb91515675a0a8412a85e359c4aa9f25a684b2e5fa0d3faee9afef115113579b423f2386edf9b8961c1a37967a7718b20594421fdb537069c643f03475f2396ad61faa904c1f35ce2436f6260c51c429a58358c1f19961bb8a9b3516e2eef13551315ab586e68caae69c4c09b40ff6fff4f67fdc2f700600b5a97f280b6a94c11068065d8edf1cbb10203e559ee94ba3cbea4eee1cd36790012196284ba15b3d7427c3fd3647cb65c9230d2fafbd96c978e0545e404317805c549d2bfcb7bc68bdbce5a94f2f090487d58e2be4d011b44532d8543338c64cf91dc869021b94b5c2b7dabf666b2a57c703e4a1cbd6702fceb42cff1108e06b4412abbc5f68cae62fc01bd9ac96e36ce07d1536115d44c752b37d5d90ea44614a76a67ac7da251511a8fed42a012478f29031603da3eef2cde3d7a9e342c92fcc88f1437a50ec0bb18670ece373de05945dd05976433a32bf352715fe5c8960940a9cff86b97bf57786ccfbf7ddac06bb657ed936d6c112bdc6bf6e5f731ef0e066f554b95f530932ac950ebddd01eb04bf36073da30bf1addf137a0f5852fa8480dfc7e2962424c5d3d6f420f85217d30b481dae95db7d87d8da86cc259675b33a546c0eff502768ea78a296ca55b608dd492c2c213415c45b3167c051f45fc04c12c67a06d984f8deef6079dcd0f6d07513c735630d53ff281d9a2024e84f749164db0fe0826690c460342b5c48a4fb2f7cae80efef65cea5b733bf8624314e74efd5212c23925ab08adaa5bd78bfb24bc5e4133328e478235a42538c0d01edb065ff17ed0116d1e33864dbedc31304c04d31ae7451048979c9ca4e92d6bcdb5e4ac42caa22665729967504dd1242f95f71dcbc59cd9d62c831f774ac9ebe0619318f31229075535793924dc79874fff17954686c4463b1094d1e3f8a98c115fec2d2b1e37c11d06f7a24c1dbf7871b7510d8f5785b47a5c62203820add67f0264dfe71ac6185e457d3be8944ba5ff3d78847b21349a2a5813b79fadce4fa2f393edaa4904c7564d8036664b0191090e82bf275b552e8cd5f04ee30511a8edad21bb8041588e71296836d34e86e8f7c6edc4327b7d1122377b19fb6e8961c26a0ab1b4e3a5e0f6d2b6b4448e395985981584c78da0b7678d2a338d553d07be430c0e967c86e0859eb69422959d6eacb43ed9acc678d515466ce9bca08c03ad2a09affa136893566df52a553ca67f529fe9fa72f15b7fd60c769a5168d53c02d701937d20d79284f34e1b7094c40491df2619c03efacc3989d340d7b611c348f9098fa80e007874cd7bdb6eb67c23e17f8446ca01b6ea1fe1475b3cb5a031d50e0424f2efb010259a038d693ca1ab3bc1abf5c56880c2d14355790934617c2b260a74883c88310fa0bb0d2aa0a5778532b3bff7a80586a2fd761e2620c3bcad1006cc328cdd3a9c790d30d260e473a85ffc8200dae9bc2992cd446d55103668db0e2f85976ec87bf7bda26c4c6492f76e493beef5daf29f0b723fa8d9cb4eba4f03396c65b052fbf4521dfd7ed611fe3bbbb577ab46ff87002bad166e6a61873b0e2a8b5be203a6a0c4bd12c7a5cc82284d12b089b4cf245d38d93749383a85b202deba9fbcab44b9f343117669ccf91cf16df5af4309941c718e937228ec2ace05c3fbf708be2d079c92b1045c0167116dc2c52323b23f8514eddbe1e3d30cb005'], + 'tx_hash_list': ['401d8021975a0fee16fe84acbfc4d8ba6312e563fa245baba2aac382e787fb60'], + 'tx_key_list': ['7061d4d939b563a11e344c60938410e2e63ea72c43741fae81b8805cebe5570a']}} + txns = self.wallet.transfer( + '9wFuzNoQDck1pnS9ZhG47kDdLD1BUszSbWpGfWcSRy9m6Npq9NoHWd141KvGag8hu2gajEwzRXJ4iJwmxruv9ofc2CwnYCE', + 3) + self.assertEqual(len(txns), 1) + txn = txns[0] + self.assertIsInstance(txn, Transaction) + self.assertIsInstance(txn.fee, Decimal) + self.assertEqual(txn.hash, + '401d8021975a0fee16fe84acbfc4d8ba6312e563fa245baba2aac382e787fb60') + self.assertEqual(txn.key, + '7061d4d939b563a11e344c60938410e2e63ea72c43741fae81b8805cebe5570a') diff --git a/utils/pushtx.py b/utils/pushtx.py index bb6c3bf..281690d 100644 --- a/utils/pushtx.py +++ b/utils/pushtx.py @@ -6,6 +6,7 @@ import sys from monero.backends.jsonrpc import JSONRPCDaemon from monero.daemon import Daemon +from monero.transaction import Transaction from monero import exceptions def url_data(url): @@ -35,8 +36,9 @@ else: d = Daemon(JSONRPCDaemon(**args.daemon_rpc_url)) for name, blob in blobs: logging.debug("Sending {}".format(name)) + tx = Transaction(blob=blob) try: - res = d.send_transaction(blob) + res = d.send_transaction(tx) except exceptions.TransactionBroadcastError as e: print("{} not sent, reason: {}".format(name, e.details['reason'])) logging.debug(e.details) diff --git a/utils/transfer.py b/utils/transfer.py index 9529d90..d301c1e 100644 --- a/utils/transfer.py +++ b/utils/transfer.py @@ -51,14 +51,14 @@ elif args.verbosity > 1: logging.basicConfig(level=level, format="%(asctime)-15s %(message)s") w = Wallet(JSONRPCWallet(**args.wallet_rpc_url)) -txfrs = w.accounts[args.account].transfer_multiple( +txns = w.accounts[args.account].transfer_multiple( args.destinations, priority=prio, ringsize=args.ring_size, payment_id=args.payment_id, relay=args.outdir is None) -for tx in txfrs: - print(u"Transaction {hash}:\nXMR: {amount:21.12f} @ {fee:13.12f} fee\n" - u"Payment ID: {payment_id}\nTx key: {key}\nSize: {size} B".format( - hash=tx.hash, amount=tx.amount, fee=tx.fee, - payment_id=tx.payment_id, key=tx.key, size=len(tx.blob) >> 1)) +for tx in txns: + print(u"Transaction {hash}:\nfee: {fee:21.12f}\n" + u"Tx key: {key}\nSize: {size} B".format( + hash=tx.hash, fee=tx.fee, + key=tx.key, size=len(tx.blob) >> 1)) if args.outdir: outname = os.path.join(args.outdir, tx.hash + '.tx') outfile = open(outname, 'wb') diff --git a/utils/walletdump.py b/utils/walletdump.py index 04ac632..d977921 100644 --- a/utils/walletdump.py +++ b/utils/walletdump.py @@ -35,15 +35,15 @@ def get_wallet(): _TXHDR = "timestamp height id/hash " \ " amount fee {dir:95s} payment_id" -def tx2str(tx): +def pmt2str(pmt): return "{time} {height:7d} {hash} {amount:17.12f} {fee:13.12f} {addr} {payment_id}".format( - time=tx.timestamp.strftime("%d-%m-%y %H:%M:%S") if getattr(tx, 'timestamp', None) else None, - height=tx.height or 0, - hash=tx.hash, - amount=tx.amount, - fee=tx.fee or 0, - payment_id=tx.payment_id, - addr=getattr(tx, 'local_address', None) or '') + time=pmt.timestamp.strftime("%d-%m-%y %H:%M:%S") if getattr(pmt, 'timestamp', None) else None, + height=pmt.transaction.height or 0, + hash=pmt.transaction.hash, + amount=pmt.amount, + fee=pmt.transaction.fee or 0, + payment_id=pmt.payment_id, + addr=getattr(pmt, 'local_address', None) or '') def a2str(a): return "{addr} {label}".format( @@ -85,23 +85,23 @@ if len(w.accounts) > 1: print("\nIncoming transactions:") print(_TXHDR.format(dir='received by')) for tx in ins: - print(tx2str(tx)) + print(pmt2str(tx)) outs = acc.get_transactions_out(unconfirmed=True) if outs: print("\nOutgoing transactions:") print(_TXHDR.format(dir='sent from')) for tx in outs: - print(tx2str(tx)) + print(pmt2str(tx)) else: ins = w.get_transactions_in(unconfirmed=True) if ins: print("\nIncoming transactions:") print(_TXHDR.format(dir='received by')) for tx in ins: - print(tx2str(tx)) + print(pmt2str(tx)) outs = w.get_transactions_out(unconfirmed=True) if outs: print("\nOutgoing transactions:") print(_TXHDR.format(dir='sent from')) for tx in outs: - print(tx2str(tx)) + print(pmt2str(tx))