From d0bc224e781b87d4fa0161a71ca0982ec6e2d5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Mon, 22 Jan 2018 03:21:25 +0100 Subject: [PATCH 1/5] Fix: use proper class; add comment on pending PR --- monero/backends/jsonrpc.py | 4 ++-- tests/wallet.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index 9148536..304b6e7 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -170,13 +170,13 @@ class JSONRPCWallet(object): def get_transactions_in(self, account=0): _transfers = self.raw_request('get_transfers', {'account_index': account, 'in': True, 'out': False, 'pool': False}) - return [Transaction(**self._tx2dict(tx)) for tx in + return [Payment(**self._tx2dict(tx)) for tx in sorted(_transfers.get('in', []), key=operator.itemgetter('timestamp'))] def get_transactions_out(self, account=0): _transfers = self.raw_request('get_transfers', {'account_index': account, 'in': False, 'out': True, 'pool': False}) - return [Transaction(**self._tx2dict(tx)) for tx in + return [Transfer(**self._tx2dict(tx)) for tx in sorted(_transfers.get('out', []), key=operator.itemgetter('timestamp'))] def _tx2dict(self, tx): diff --git a/tests/wallet.py b/tests/wallet.py index fc22f2e..a1391f3 100644 --- a/tests/wallet.py +++ b/tests/wallet.py @@ -163,8 +163,9 @@ class SubaddrWalletTestCase(unittest.TestCase): pay_in = self.wallet.get_transactions_in() self.assertEqual(len(list(pay_in)), 3) for tx in pay_in: - self.assertIsInstance(tx, Transaction) -# self.assertIsInstance(tx.address, Address) + self.assertIsInstance(tx, Payment) +# 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) @@ -257,8 +258,9 @@ class SubaddrWalletTestCase(unittest.TestCase): pay_out = self.wallet.get_transactions_out() self.assertEqual(len(list(pay_out)), 6) for tx in pay_out: - self.assertIsInstance(tx, Transaction) -# self.assertIsInstance(tx.address, Address) + self.assertIsInstance(tx, Transfer) +# 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) From 38704ba8ea9d93b202b4d363123139ba27abc0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Mon, 22 Jan 2018 03:55:08 +0100 Subject: [PATCH 2/5] Handle mempool transactions --- monero/account.py | 10 +++++---- monero/backends/jsonrpc.py | 45 ++++++++++++++++++++++++++++++-------- monero/daemon.py | 6 +++++ monero/transaction.py | 27 ++++++++++++++--------- monero/wallet.py | 24 ++++++++++++++++---- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/monero/account.py b/monero/account.py index d8f6d16..292b91b 100644 --- a/monero/account.py +++ b/monero/account.py @@ -30,11 +30,13 @@ class Account(object): def get_payments(self, payment_id=None): return self._backend.get_payments(account=self.index, payment_id=payment_id) - def get_transactions_in(self): - return self._backend.get_transactions_in(account=self.index) + def get_transactions_in(self, confirmed=True, unconfirmed=False): + return self._backend.get_transactions_in( + account=self.index, confirmed=confirmed, unconfirmed=unconfirmed) - def get_transactions_out(self): - return self._backend.get_transactions_out(account=self.index) + def get_transactions_out(self, confirmed=True, unconfirmed=True): + return self._backend.get_transactions_out( + account=self.index, confirmed=confirmed, unconfirmed=unconfirmed) def transfer(self, address, amount, priority=prio.NORMAL, ringsize=5, payment_id=None, unlock_time=0, diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index 304b6e7..b7e2caf 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -37,6 +37,16 @@ class JSONRPCDaemon(object): "{status}: {reason}".format(**res), details=res) + def get_mempool(self): + res = self.raw_request('/get_transaction_pool', {}) + txs = [] + for tx in res['transactions']: + txs.append(Transaction( + hash=tx['id_hash'], + fee=from_atomic(tx['fee']), + timestamp=datetime.fromtimestamp(tx['receive_time']))) + return txs + def raw_request(self, path, data): hdr = {'Content-Type': 'application/json'} _log.debug(u"Request: {path}\nData: {data}".format( @@ -103,6 +113,9 @@ class JSONRPCWallet(object): _log.debug("JSONRPC wallet backend auth: '{user}'/'{stars}'".format( user=user, stars=('*' * len(password)) if password else '')) + def get_height(self): + return self.raw_request('getheight')['height'] + def get_view_key(self): return self.raw_request('query_key', {'key_type': 'view_key'})['key'] @@ -167,17 +180,31 @@ class JSONRPCWallet(object): pmts.append(Payment(**data)) return pmts - def get_transactions_in(self, account=0): - _transfers = self.raw_request('get_transfers', - {'account_index': account, 'in': True, 'out': False, 'pool': False}) + def get_transactions_in(self, account=0, confirmed=True, unconfirmed=False): + _txns = self.raw_request('get_transfers', + {'account_index': account, 'in': confirmed, 'out': False, 'pool': unconfirmed}) + txns = _txns.get('in', []) + if unconfirmed: + txns.extend(_txns.get('pool', [])) return [Payment(**self._tx2dict(tx)) for tx in - sorted(_transfers.get('in', []), key=operator.itemgetter('timestamp'))] + sorted(txns, key=operator.itemgetter('timestamp'))] - def get_transactions_out(self, account=0): - _transfers = self.raw_request('get_transfers', - {'account_index': account, 'in': False, 'out': True, 'pool': False}) + def get_transactions_out(self, account=0, confirmed=True, unconfirmed=True): + _txns = self.raw_request('get_transfers', + {'account_index': account, 'in': False, 'out': confirmed, 'pool': unconfirmed}) + txns = _txns.get('out', []) + if unconfirmed: + txns.extend(_txns.get('pool', [])) return [Transfer(**self._tx2dict(tx)) for tx in - sorted(_transfers.get('out', []), key=operator.itemgetter('timestamp'))] + 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)) def _tx2dict(self, tx): pid = tx.get('payment_id', None) @@ -186,7 +213,7 @@ class JSONRPCWallet(object): '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')), + 'height': tx.get('height', tx.get('block_height')) or None, '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 diff --git a/monero/daemon.py b/monero/daemon.py index 81a9216..a383f7b 100644 --- a/monero/daemon.py +++ b/monero/daemon.py @@ -5,5 +5,11 @@ class Daemon(object): def get_info(self): return self._backend.get_info() + def get_height(self): + return self._backend.get_info()['height'] + def send_transaction(self, blob): return self._backend.send_transaction(blob) + + def get_mempool(self): + return self._backend.get_mempool() diff --git a/monero/transaction.py b/monero/transaction.py index 6616f30..339434d 100644 --- a/monero/transaction.py +++ b/monero/transaction.py @@ -2,37 +2,44 @@ class Transaction(object): hash = None height = None timestamp = None - payment_id = '0000000000000000' - amount = None fee = None - local_address = 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.payment_id = kwargs.get('payment_id', self.payment_id) - self.amount = kwargs.get('amount', self.amount) self.fee = kwargs.get('fee', self.fee) - self.local_address = kwargs.get('local_address', self.local_address) + self.blob = kwargs.get('blob', self.blob) def __repr__(self): return self.hash -class Payment(Transaction): +class LocalTransaction(Transaction): + """A transaction that concerns local wallet, either incoming or outgoing.""" + payment_id = None + amount = None + local_address = None + + def __init__(self, **kwargs): + super(LocalTransaction, self).__init__(**kwargs) + self.payment_id = kwargs.get('payment_id', self.payment_id) + self.amount = kwargs.get('amount', self.amount) + self.local_address = kwargs.get('local_address', self.local_address) + + +class Payment(LocalTransaction): """Incoming Transaction""" pass -class Transfer(Transaction): +class Transfer(LocalTransaction): """Outgoing Transaction""" key = None - blob = None note = '' def __init__(self, **kwargs): super(Transfer, self).__init__(**kwargs) self.key = kwargs.get('key', self.key) self.note = kwargs.get('note', self.note) - self.blob = kwargs.get('blob', self.blob) diff --git a/monero/wallet.py b/monero/wallet.py index 4eecf0c..74c2369 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -1,6 +1,7 @@ from . import address from . import prio from . import account +from . import transaction class Wallet(object): accounts = None @@ -21,6 +22,12 @@ class Wallet(object): self.accounts.append(_acc) idx += 1 + def get_height(self): + """ + Returns the height of the wallet. + """ + return self._backend.get_height() + def get_view_key(self): """ Returns private view key. @@ -39,6 +46,15 @@ class Wallet(object): self.accounts.append(acc) return acc + def get_transaction(self, hash): + return self._backend.get_transaction(hash) + + def confirmations(self, txn): + txn = self._backend.get_transaction(txn) + if txn.height is None: + return 0 + return max(0, self.get_height() - txn.height) + # Following methods operate on default account (index=0) def get_balances(self): return self.accounts[0].get_balances() @@ -58,11 +74,11 @@ class Wallet(object): def get_payments(self, payment_id=None): return self.accounts[0].get_payments(payment_id=payment_id) - def get_transactions_in(self): - return self.accounts[0].get_transactions_in() + def get_transactions_in(self, confirmed=True, unconfirmed=False): + return self.accounts[0].get_transactions_in(cofirmed=confirmed, unconfirmed=unconfirmed) - def get_transactions_out(self): - return self.accounts[0].get_transactions_out() + def get_transactions_out(self, confirmed=True, unconfirmed=True): + return self.accounts[0].get_transactions_out(confirmed=confirmed, unconfirmed=unconfirmed) def transfer(self, address, amount, priority=prio.NORMAL, ringsize=5, payment_id=None, unlock_time=0, From 0e297c669462309ca4de88aeb46b5fabba32fa38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Mon, 22 Jan 2018 03:57:50 +0100 Subject: [PATCH 3/5] Retrieve mempool txns in wallet dump script --- utils/walletdump.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utils/walletdump.py b/utils/walletdump.py index f1b13a4..04ac632 100644 --- a/utils/walletdump.py +++ b/utils/walletdump.py @@ -36,9 +36,9 @@ _TXHDR = "timestamp height id/hash " amount fee {dir:95s} payment_id" def tx2str(tx): - return "{time} {height} {hash} {amount:17.12f} {fee:13.12f} {addr} {payment_id}".format( + 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, + height=tx.height or 0, hash=tx.hash, amount=tx.amount, fee=tx.fee or 0, @@ -80,26 +80,26 @@ if len(w.accounts) > 1: addresses = acc.get_addresses() print("{num:2d} address(es):".format(num=len(addresses))) print("\n".join(map(a2str, addresses))) - ins = acc.get_transactions_in() + ins = acc.get_transactions_in(unconfirmed=True) if ins: print("\nIncoming transactions:") print(_TXHDR.format(dir='received by')) for tx in ins: print(tx2str(tx)) - outs = acc.get_transactions_out() + 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)) else: - ins = w.get_transactions_in() + 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)) - outs = w.get_transactions_out() + outs = w.get_transactions_out(unconfirmed=True) if outs: print("\nOutgoing transactions:") print(_TXHDR.format(dir='sent from')) From 3854a9e0eb52da404eecf7df6c2ac91e1bae181a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Mon, 22 Jan 2018 04:03:40 +0100 Subject: [PATCH 4/5] Fix crash on empty mempool --- monero/backends/jsonrpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index b7e2caf..e97fb9e 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -40,7 +40,7 @@ class JSONRPCDaemon(object): def get_mempool(self): res = self.raw_request('/get_transaction_pool', {}) txs = [] - for tx in res['transactions']: + for tx in res.get('transactions', []): txs.append(Transaction( hash=tx['id_hash'], fee=from_atomic(tx['fee']), From 6255ca48a74a8f6bbd2fe79b8aaab1a108fb109c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Mon, 22 Jan 2018 04:18:00 +0100 Subject: [PATCH 5/5] Add exception on transaction not found --- monero/backends/jsonrpc.py | 1 + monero/exceptions.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index e97fb9e..762246e 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -295,6 +295,7 @@ _err2exc = { -2: exceptions.WrongAddress, -4: exceptions.NotEnoughUnlockedMoney, -5: exceptions.WrongPaymentId, + -8: exceptions.TransactionNotFound, -16: exceptions.TransactionNotPossible, -20: exceptions.AmountIsZero, -32601: MethodNotFound, diff --git a/monero/exceptions.py b/monero/exceptions.py index 99be44a..fd9d6d4 100644 --- a/monero/exceptions.py +++ b/monero/exceptions.py @@ -30,3 +30,5 @@ class TransactionBroadcastError(BackendException): self.details = details super(TransactionBroadcastError, self).__init__(message) +class TransactionNotFound(AccountException): + pass