diff --git a/monero/account.py b/monero/account.py index 3997c4a..3f7292f 100644 --- a/monero/account.py +++ b/monero/account.py @@ -1,4 +1,3 @@ -import warnings from . import prio from .transaction import PaymentManager @@ -68,7 +67,7 @@ class Account(object): priority=prio.NORMAL, payment_id=None, unlock_time=0, relay=True): """ - Sends a transfer. Returns a list of resulting transactions. + Sends a transfer. Returns a set of resulting transactions. :param address: destination :class:`Address ` or subtype :param amount: amount to send @@ -82,7 +81,7 @@ class Account(object): :param relay: if `True`, the wallet will relay the transaction(s) to the network immediately; when `False`, it will only return the transaction(s) so they might be broadcasted later - :rtype: list of :class:`Transaction ` + :rtype: set of :class:`Transaction ` """ return self._backend.transfer( [(address, amount)], @@ -96,7 +95,7 @@ class Account(object): priority=prio.NORMAL, payment_id=None, unlock_time=0, relay=True): """ - Sends a batch of transfers. Returns a list of resulting transactions. + Sends a batch of transfers. Returns a set of resulting transactions. :param destinations: a list of destination and amount pairs: [(:class:`Address `, `Decimal`), ...] @@ -110,7 +109,7 @@ class Account(object): :param relay: if `True`, the wallet will relay the transaction(s) to the network immediately; when `False`, it will only return the transaction(s) so they might be broadcasted later - :rtype: list of :class:`Transaction ` + :rtype: set of :class:`Transaction ` """ return self._backend.transfer( destinations, diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index a6f9948..df97803 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -10,7 +10,9 @@ from ..account import Account from ..address import address, Address, SubAddress from ..numbers import from_atomic, to_atomic, PaymentID from ..seed import Seed -from ..transaction import Transaction, IncomingPayment, OutgoingPayment +from ..transaction import ( + SignedTransaction, UnsignedTransaction, MultisigTransaction, TransactionSet, + IncomingPayment, OutgoingPayment ) _log = logging.getLogger(__name__) @@ -51,7 +53,7 @@ class JSONRPCDaemon(object): res = self.raw_request('/get_transaction_pool', {}) txs = [] for tx in res.get('transactions', []): - txs.append(Transaction( + txs.append(SignedTransaction( hash=tx['id_hash'], fee=from_atomic(tx['fee']), timestamp=datetime.fromtimestamp(tx['receive_time']))) @@ -242,7 +244,7 @@ class JSONRPCWallet(object): 'amount': from_atomic(data['amount']), 'timestamp': datetime.fromtimestamp(data['timestamp']) if 'timestamp' in data else None, 'note': data.get('note', None), - 'transaction': self._tx(data), + 'transaction': self._tx(data, SignedTransaction), 'local_address': laddr, } if 'destinations' in data: @@ -259,8 +261,8 @@ class JSONRPCWallet(object): def _outpayment(self, data): return OutgoingPayment(**self._paymentdict(data)) - def _tx(self, data): - return Transaction(**{ + def _tx(self, data, klass): + return klass(**{ 'hash': data.get('txid', data.get('tx_hash')), 'fee': from_atomic(data['fee']) if 'fee' in data else None, 'key': data.get('key'), @@ -311,7 +313,23 @@ 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 [self._tx(data) for data in _pertx] + result = TransactionSet() + klass = SignedTransaction + try: + if _transfers['unsigned_txset']: + result.unsigned_txset = _transfers['unsigned_txset'] + klass = UnsignedTransaction + except KeyError: + pass + try: + if _transfers['multisig_txset']: + result.multisig_txset = _transfers['multisig_txset'] + klass = MultisigTransaction + except KeyError: + pass + for data in _pertx: + result.add(self._tx(data, klass)) + return result def raw_request(self, method, params=None, squelch_error_logging=False): hdr = {'Content-Type': 'application/json'} diff --git a/monero/daemon.py b/monero/daemon.py index 0cb2486..6d4dc32 100644 --- a/monero/daemon.py +++ b/monero/daemon.py @@ -1,3 +1,6 @@ +from .exceptions import TransactionNotSigned +from .transaction import SignedTransaction + class Daemon(object): """Monero daemon. @@ -28,10 +31,12 @@ class Daemon(object): """ Sends a transaction generated by a :class:`Wallet `. - :param tx: :class:`Transaction ` + :param tx: :class:`SignedTransaction ` :param relay: whether to relay the transaction to peers. If `False`, the daemon will have to mine the transaction itself in order to have it included in the blockchain. """ + if not isinstance(tx, SignedTransaction): + raise TransactionNotSigned(tx.hash) return self._backend.send_transaction(tx.blob, relay=relay) def mempool(self): diff --git a/monero/exceptions.py b/monero/exceptions.py index 389fbb5..25d811c 100644 --- a/monero/exceptions.py +++ b/monero/exceptions.py @@ -41,3 +41,6 @@ class SignatureCheckFailed(MoneroException): class WalletIsNotDeterministic(MoneroException): pass + +class TransactionNotSigned(TransactionBroadcastError): + pass diff --git a/monero/transaction.py b/monero/transaction.py index 20e9bcf..cdcabaf 100644 --- a/monero/transaction.py +++ b/monero/transaction.py @@ -71,6 +71,7 @@ class Transaction(object): key = None blob = None confirmations = None + signed = False def __init__(self, **kwargs): self.hash = kwargs.get('hash', self.hash) @@ -85,6 +86,27 @@ class Transaction(object): return self.hash +class UnsignedTransaction(Transaction): + pass + + +class SignedTransaction(Transaction): + signed = True + + +class MultisigTransaction(Transaction): + pass + + +class TransactionSet(set): + unsigned_txset = None + multisig_txset = None + + @property + def signed(self): + return self.unsigned_txset is None and self.multisig_txset is None + + if sys.version_info < (3,): _str_types = (str, bytes, unicode) else: diff --git a/monero/wallet.py b/monero/wallet.py index 74f8d37..ea2a288 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -188,7 +188,7 @@ class Wallet(object): priority=prio.NORMAL, payment_id=None, unlock_time=0, relay=True): """ - Sends a transfer from the default account. Returns a list of resulting transactions. + Sends a transfer from the default account. Returns a set of resulting transactions. :param address: destination :class:`Address ` or subtype :param amount: amount to send @@ -202,7 +202,7 @@ class Wallet(object): :param relay: if `True`, the wallet will relay the transaction(s) to the network immediately; when `False`, it will only return the transaction(s) so they might be broadcasted later - :rtype: list of :class:`Transaction ` + :rtype: set of :class:`Transaction ` """ return self.accounts[0].transfer( address, @@ -216,7 +216,7 @@ class Wallet(object): priority=prio.NORMAL, payment_id=None, unlock_time=0, relay=True): """ - Sends a batch of transfers from the default account. Returns a list of resulting + Sends a batch of transfers from the default account. Returns a set of resulting transactions. :param destinations: a list of destination and amount pairs: [(address, amount), ...] @@ -230,7 +230,7 @@ class Wallet(object): :param relay: if `True`, the wallet will relay the transaction(s) to the network immediately; when `False`, it will only return the transaction(s) so they might be broadcasted later - :rtype: list of :class:`Transaction ` + :rtype: set of :class:`Transaction ` """ return self.accounts[0].transfer_multiple( destinations, diff --git a/tests/test_jsonrpcwallet.py b/tests/test_jsonrpcwallet.py index 0206e01..4c92fd4 100644 --- a/tests/test_jsonrpcwallet.py +++ b/tests/test_jsonrpcwallet.py @@ -5,12 +5,11 @@ try: from unittest.mock import patch, Mock except ImportError: from mock import patch, Mock -import warnings from monero.wallet import Wallet from monero.address import Address from monero.seed import Seed -from monero.transaction import IncomingPayment, OutgoingPayment, Transaction +from monero.transaction import IncomingPayment, OutgoingPayment, SignedTransaction from monero.backends.jsonrpc import JSONRPCWallet class SubaddrWalletTestCase(unittest.TestCase): @@ -260,7 +259,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt, IncomingPayment) self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, int) @@ -411,7 +410,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt, IncomingPayment) self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, (int, type(None))) @@ -454,7 +453,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt, IncomingPayment) self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIs(pmt.transaction.height, None) @@ -495,7 +494,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt, IncomingPayment) self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) # Fee is not returned by this RPC method! # self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, int) @@ -539,7 +538,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt, IncomingPayment) self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) # Fee is not returned by this RPC method! # self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, int) @@ -709,7 +708,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) self.assertIsInstance(pmt.timestamp, datetime) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, int) @@ -889,7 +888,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) self.assertIsInstance(pmt.timestamp, datetime) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, (int, type(None))) @@ -921,7 +920,7 @@ class SubaddrWalletTestCase(unittest.TestCase): self.assertIsInstance(pmt.local_address, Address) self.assertIsInstance(pmt.amount, Decimal) self.assertIsInstance(pmt.timestamp, datetime) - self.assertIsInstance(pmt.transaction, Transaction) + self.assertIsInstance(pmt.transaction, SignedTransaction) self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIs(pmt.transaction.height, None) @@ -943,8 +942,11 @@ class SubaddrWalletTestCase(unittest.TestCase): '9wFuzNoQDck1pnS9ZhG47kDdLD1BUszSbWpGfWcSRy9m6Npq9NoHWd141KvGag8hu2gajEwzRXJ4iJwmxruv9ofc2CwnYCE', 3) self.assertEqual(len(txns), 1) - txn = txns[0] - self.assertIsInstance(txn, Transaction) + self.assertIsNone(txns.unsigned_txset) + self.assertIsNone(txns.multisig_txset) + txn = txns.pop() + self.assertIsInstance(txn, SignedTransaction) + self.assertTrue(txn.signed) self.assertIsInstance(txn.fee, Decimal) self.assertEqual(txn.hash, '401d8021975a0fee16fe84acbfc4d8ba6312e563fa245baba2aac382e787fb60') diff --git a/utils/pushtx.py b/utils/pushtx.py index 047e3bc..cde045e 100755 --- a/utils/pushtx.py +++ b/utils/pushtx.py @@ -7,7 +7,7 @@ import sys from monero.backends.jsonrpc import JSONRPCDaemon from monero.daemon import Daemon -from monero.transaction import Transaction +from monero.transaction import SignedTransaction from monero import exceptions def url_data(url): @@ -39,7 +39,7 @@ else: d = Daemon(JSONRPCDaemon(**args.daemon_rpc_url)) for name, blob in blobs: logging.debug("Sending {}".format(name)) - tx = Transaction(blob=blob) + tx = SignedTransaction(blob=blob) try: res = d.send_transaction(tx, relay=args.relay) except exceptions.TransactionBroadcastError as e: