Compare commits

...

15 Commits

Author SHA1 Message Date
Michał Sałaban aa8bf90561 Bump up to 0.8
3 years ago
Michał Sałaban a0bb867df0 Remove deprecated features
3 years ago
Michał Sałaban 7307ea5e8d Add docs
3 years ago
Michał Sałaban 70be9453a1 s/OneTimeOutput/Output/
3 years ago
Michał Sałaban 3fd58b2ed0 Add test with unknown extra tags and ExtraParser initialized by either str or bytes
3 years ago
Michał Sałaban 24e3e4f822 Add test of v1 transactions
3 years ago
Michał Sałaban 691de449d9 Add py2 compatibility
3 years ago
Michał Sałaban 9239b40a62 Merge branch 'master' into onetimeout
3 years ago
Michał Sałaban 9674aa1e9b Add tests on coinbase transactions
3 years ago
Michał Sałaban ee03a86d8c Add first working version of output recognition; add tests
3 years ago
Michał Sałaban 1e98fe1cc0 Add draft of output recognition
3 years ago
Michał Sałaban 7d3ec1a2e5 Add reference to tx in OneTimeOutput; add exception
3 years ago
Michał Sałaban 646ae2ba2e Merge branch 'onetimeout' of https://github.com/jeffro256/monero-python into jeffro256-onetimeout
3 years ago
Jeffrey Ryan 4b2ae16f95 Adding support for one-time outputs
3 years ago
Jeffrey Ryan 7749596e47 Add support for one-time outputs
3 years ago

@ -14,7 +14,7 @@ Python Monero module
A comprehensive Python module for handling Monero cryptocurrency.
* release 0.7.7
* release 0.8
* open source: https://github.com/monero-ecosystem/monero-python
* works with Monero 0.13.x and `the latest source`_ (at least we try to keep up)
* Python 2.x and 3.x compatible

@ -55,9 +55,9 @@ author = 'Michal Salaban'
# built documents.
#
# The short X.Y version.
version = '0.7.7'
version = '0.8'
# The full version, including alpha/beta/rc tags.
release = '0.7.7'
release = '0.8'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

@ -23,6 +23,7 @@ Project homepage: https://github.com/monero-ecosystem/monero-python
address
transactions
daemon
outputs
backends
seed
misc

@ -0,0 +1,53 @@
Output recognition
==================
The module provides means to obtain output information from transactions as well as recognize and
decrypt those destined to user's own wallet.
That functionality is a part of ``Transaction.outputs(wallet=None)`` method which may take a wallet
as optional keyword, which will make it analyze outputs against all wallet's addresses.
The wallet **must have the secret view key** while secret spend key is not required (which means
a view-only wallet is enough).
.. note:: Be aware that ed25519 cryptography used there is written in pure Python. Don't expect
high efficiency there. If you plan a massive analysis of transactions, please check if
using Monero source code wouldn't be better for you.
.. note:: Please make sure the wallet you provide has all existing subaddresses generated.
If you run another copy of the wallet and use subaddresses, the wallet you pass to
``.outputs()`` **must have the same or bigger set of subaddressses present**. For those
missing from the wallet, no recognition will happen.
Output data
-----------
The method will return a set of ``Output`` objects. Each of them contains the following attributes:
* ``stealth_address`` — the stealth address of the output as hexadecimal string,
* ``amount`` — the amount of the output, ``None`` if unknown,
* ``index`` — the index of the output,
* ``transaction`` — the ``Transaction`` the output is a part of,
* ``payment`` — a ``Payment`` object if the output is destined to provided wallet,
otherwise ``None``,
An example usage:
.. code-block:: python
In [1]: from monero.daemon import Daemon
In [2]: from monero.wallet import Wallet
In [3]: daemon = Daemon(port=28081)
In [4]: tx = daemon.transactions("f79a10256859058b3961254a35a97a3d4d5d40e080c6275a3f9779acde73ca8d")[0]
In [5]: wallet = Wallet(port=28088)
In [6]: outs = tx.outputs(wallet=wallet)
In [7]: outs[0].payment.local_address
Out [7]: 76Qt2xMZ3m7b2tagubEgkvG81pwf9P3JYdxR65H2BEv8c79A9pCBTacEFv87tfdcqXRemBsZLFVGHTWbqBpkoBJENBoJJS9
In [8]: outs[0].payment.amount
Out [8]: Decimal('4.000000000000')

@ -1,6 +1,15 @@
Release Notes
=============
0.8
---
Backward-incompatible changes:
1. The ``monero.prio`` submodule has been removed. Switch to ``monero.const``.
2. Methods ``.is_mainnet()``, ``.is_testnet()``, ``.is_stagenet()`` have been removed from
``monero.address.Address`` instances. Use ``.net`` attribute instead.
0.7
---

@ -1,3 +1,3 @@
from . import address, account, const, daemon, wallet, numbers, wordlists, seed
__version__ = "0.7.7"
__version__ = "0.8"

@ -42,39 +42,6 @@ class BaseAddress(object):
def net(self):
return const.NETS[self._valid_netbytes.index(self._decoded[0])]
def is_mainnet(self):
"""Returns `True` if the address belongs to mainnet.
:rtype: bool
"""
warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated "
"and will be gone in 0.8; use Address.net property and constants form monero.const "
"instead",
DeprecationWarning)
return self.net == const.NET_MAIN
def is_testnet(self):
"""Returns `True` if the address belongs to testnet.
:rtype: bool
"""
warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated "
"and will be gone in 0.8; use Address.net property and constants form monero.const "
"instead",
DeprecationWarning)
return self.net == const.NET_TEST
def is_stagenet(self):
"""Returns `True` if the address belongs to stagenet.
:rtype: bool
"""
warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated "
"and will be gone in 0.8; use Address.net property and constants form monero.const "
"instead",
DeprecationWarning)
return self.net == const.NET_STAGE
def _decode(self, address):
self._decoded = bytearray(unhexlify(base58.decode(address)))
checksum = self._decoded[-4:]

@ -163,6 +163,7 @@ class JSONRPCDaemon(object):
timestamp=datetime.fromtimestamp(
tx['block_timestamp']) if 'block_timestamp' in tx else None,
blob=binascii.unhexlify(tx['as_hex']) or None,
output_indices=None if tx['in_pool'] else tx['output_indices'],
json=as_json))
return txs

@ -59,3 +59,6 @@ class TransactionIncomplete(MoneroException):
class TransactionWithoutBlob(TransactionIncomplete):
pass
class TransactionWithoutJSON(TransactionIncomplete):
pass

@ -1,9 +0,0 @@
import warnings
warnings.warn(
"monero.prio is deprecated and will be gone in 0.8; use monero.const.PRIO_* consts instead",
DeprecationWarning)
UNIMPORTANT=1
NORMAL=2
ELEVATED=3
PRIORITY=4

@ -159,12 +159,6 @@ class Seed(object):
"""
# backward compatibility
_net = net[:-3] if net.endswith('net') else net
if _net != net:
warnings.warn(
"Argument '{:s}' is deprecated and will not be accepted in 0.8, "
"use one of monero.const.NET_*".format(net),
DeprecationWarning)
net = _net
if net not in const.NETS:
raise ValueError(
"Invalid net argument '{:s}'. Must be one of monero.const.NET_*".format(net))

@ -1,9 +1,17 @@
import binascii
import itertools
import operator
import re
import sha3
import six
import struct
import varint
import warnings
from .address import address
from .numbers import PaymentID
from . import exceptions
from ..address import address
from ..numbers import from_atomic, PaymentID
from .. import ed25519
from .. import exceptions
from .extra import ExtraParser
class Payment(object):
"""
@ -73,8 +81,19 @@ class Transaction(object):
timestamp = None
key = None
blob = None
json = None
confirmations = None
output_indices = None
json = None
version = None
pubkeys = None
@property
def is_coinbase(self):
if self.json:
return "gen" in self.json['vin'][0]
raise exceptions.TransactionWithoutJSON(
"Tx {:s} has no .json attribute and it cannot be determined "
"if it's coinbase tx.".format(self.hash))
@property
def size(self):
@ -91,13 +110,154 @@ class Transaction(object):
self.timestamp = kwargs.get('timestamp', self.timestamp)
self.key = kwargs.get('key', self.key)
self.blob = kwargs.get('blob', self.blob)
self.json = kwargs.get('json', self.json)
self.confirmations = kwargs.get('confirmations', self.confirmations)
self.output_indices = kwargs.get('output_indices', self.output_indices)
self.json = kwargs.get('json', self.json)
self.pubkeys = self.pubkeys or []
if self.json:
if "rct_signatures" in self.json:
self.version = 2
else:
self.version = 1
def outputs(self, wallet=None):
"""
Returns a list of outputs. If wallet is given, decodes destinations and amounts
for outputs directed to the wallet, provided that matching subaddresses have been
already generated.
"""
def _scan_pubkeys(svk, psk, stealth_address, amount, encamount):
for keyidx, tx_key in enumerate(self.pubkeys):
hsdata = b"".join(
[
ed25519.encodepoint(
ed25519.scalarmult(
ed25519.decodepoint(tx_key), ed25519.decodeint(svk) * 8
)
),
varint.encode(idx),
]
)
Hs_ur = sha3.keccak_256(hsdata).digest()
# sc_reduce32:
Hsint_ur = ed25519.decodeint(Hs_ur)
Hsint = Hsint_ur % ed25519.l
Hs = ed25519.encodeint(Hsint)
k = ed25519.encodepoint(
ed25519.edwards_add(
ed25519.scalarmult_B(Hsint), ed25519.decodepoint(psk),
)
)
if k != stealth_address:
continue
if not encamount:
# Tx ver 1
return Payment(
amount=amount,
timestamp=self.timestamp,
transaction=self,
local_address=addr)
amount_hs = sha3.keccak_256(b"amount" + Hs).digest()
xormask = amount_hs[:len(encamount)]
dec_amount = bytearray(a ^ b for a, b in zip(*map(bytearray, (encamount, xormask))))
int_amount = struct.unpack("<Q", dec_amount)[0]
amount = from_atomic(int_amount)
return Payment(
amount=amount,
timestamp=self.timestamp,
transaction=self,
local_address=addr)
if not self.json:
raise exceptions.TransactionWithoutJSON(
'Tx {:s} has no .json attribute'.format(self.hash))
if wallet:
ep = ExtraParser(self.json["extra"])
extra = ep.parse()
self.pubkeys = extra.get("pubkeys", [])
svk = binascii.unhexlify(wallet.view_key())
# fetch before loop to save on calls; cast to list to preserve over multiple iterations
addresses = list(
itertools.chain(*map(operator.methodcaller("addresses"), wallet.accounts)))
outs = []
for idx, vout in enumerate(self.json['vout']):
stealth_address = binascii.unhexlify(vout['target']['key'])
encamount = None
if self.version == 2 and not self.is_coinbase:
encamount = binascii.unhexlify(
self.json["rct_signatures"]["ecdhInfo"][idx]["amount"]
)
payment = None
amount = from_atomic(vout['amount']) if self.version == 1 or self.is_coinbase else None
if wallet:
for addridx, addr in enumerate(addresses):
psk = binascii.unhexlify(addr.spend_key())
payment = _scan_pubkeys(svk, psk, stealth_address, amount, encamount)
if payment:
break
outs.append(Output(
stealth_address=vout['target']['key'],
amount=payment.amount if payment else amount,
index=self.output_indices[idx] if self.output_indices else None,
transaction=self,
payment=payment))
return outs
def __repr__(self):
return self.hash
class Output(object):
"""
A Monero one-time public output (A.K.A stealth address).
Identified by `stealth_address`, or `index` and `amount`
together, it can contain differing levels of information on an output.
This class is not intended to be turned into objects by the user,
it is used by backends.
"""
stealth_address = None
amount = None
index = None
transaction = None
payment = None
def __init__(self, **kwargs):
self.stealth_address = kwargs.get('stealth_address', self.stealth_address)
self.amount = kwargs.get('amount', self.amount)
self.index = kwargs.get('index', self.index)
self.transaction = kwargs.get('transaction', self.transaction)
self.payment = kwargs.get('payment', self.payment)
def __repr__(self):
# Try to represent output as (index, amount) pair if applicable because there is no RPC
# daemon command to lookup outputs by their stealth_address ;(
if self.stealth_address:
res = self.stealth_address
else:
res = "(index={},amount={})".format(self.index, self.amount)
if self.payment:
return "{:s}, {:.12f} to [{:s}]".format(
res, self.payment.amount, str(self.payment.local_address)[:6])
return res
def __eq__(self, other):
# Try to compare stealth_addresses, then try to compare (index,amount) pairs, else raise error
if self.stealth_address and other.stealth_address:
return self.stealth_address == other.stealth_address
elif None not in (self.index, other.index, self.amount, other.amount):
return self.index == other.index and self.amount == other.amount
else:
raise TypeError('Given one-time outputs (%r,%r) are not comparable'.format(self, other))
def __ne__(self, other):
return not(self == other)
class PaymentManager(object):
"""
A payment query manager, handling either incoming or outgoing payments of

@ -0,0 +1,97 @@
import binascii
import six
import varint
class ExtraParser(object):
TX_EXTRA_TAG_PADDING = 0x00
TX_EXTRA_TAG_PUBKEY = 0x01
TX_EXTRA_TAG_EXTRA_NONCE = 0x02
TX_EXTRA_TAG_ADDITIONAL_PUBKEYS = 0x04
KNOWN_TAGS = (
TX_EXTRA_TAG_PADDING,
TX_EXTRA_TAG_PUBKEY,
TX_EXTRA_TAG_EXTRA_NONCE,
TX_EXTRA_TAG_ADDITIONAL_PUBKEYS,
)
def __init__(self, extra):
if isinstance(extra, str):
extra = binascii.unhexlify(extra)
if isinstance(extra, bytes):
extra = list(extra)
self.extra = extra
def parse(self):
self.data = {}
self.offset = 0
self._parse(self.extra)
return self.data
def _strip_padding(self, extra):
while extra and extra[0] == self.TX_EXTRA_TAG_PADDING:
extra = extra[1:]
self.offset += 1
return extra
def _pop_pubkey(self, extra):
key = bytes(bytearray(extra[:32])) # bytearray() is for py2 compatibility
if len(key) < 32:
raise ValueError(
"offset {:d}: only {:d} bytes of key data, expected 32".format(
self.offset, len(key)
)
)
if "pubkeys" in self.data:
self.data["pubkeys"].append(key)
else:
self.data["pubkeys"] = [key]
extra = extra[32:]
self.offset += 32
return extra
def _extract_pubkey(self, extra):
if extra:
if extra[0] == self.TX_EXTRA_TAG_PUBKEY:
extra = extra[1:]
self.offset += 1
extra = self._pop_pubkey(extra)
elif extra[0] == self.TX_EXTRA_TAG_ADDITIONAL_PUBKEYS:
extra = extra[1:]
self.offset += 1
keycount = varint.decode_bytes(bytearray(extra))
valen = len(varint.encode(keycount))
extra = extra[valen:]
self.offset += valen
for i in range(keycount):
extra = self._pop_pubkey(extra)
return extra
def _extract_nonce(self, extra):
if extra and extra[0] == self.TX_EXTRA_TAG_EXTRA_NONCE:
noncelen = extra[1]
extra = extra[2:]
self.offset += 2
if noncelen > len(extra):
raise ValueError(
"offset {:d}: extra nonce exceeds field size".format(self.offset)
)
return []
nonce = bytearray(extra[:noncelen])
if "nonces" in self.data:
self.data["nonces"].append(nonce)
else:
self.data["nonces"] = [nonce]
extra = extra[noncelen:]
self.offset += noncelen
return extra
def _parse(self, extra):
while extra:
if extra[0] not in self.KNOWN_TAGS:
raise ValueError(
"offset {:d}: unknown tag 0x{:x}".format(self.offset, extra[0])
)
extra = self._strip_padding(extra)
extra = self._extract_pubkey(extra)
extra = self._extract_nonce(extra)

@ -1,3 +1,4 @@
pysha3
requests
six>=1.12.0
varint

@ -0,0 +1,15 @@
{
"as_hex": "",
"as_json": "{\n \"version\": 2, \n \"unlock_time\": 766519, \n \"vin\": [ {\n \"gen\": {\n \"height\": 766459\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 8415513145431, \n \"target\": {\n \"key\": \"6c4a7168678f9d5c504e72c54d55a1cfc118cce9af944b6ecca10556541af056\"\n }\n }\n ], \n \"extra\": [ 1, 128, 93, 146, 154, 55, 1, 170, 165, 76, 172, 184, 227, 115, 121, 76, 30, 233, 193, 8, 228, 133, 96, 186, 122, 30, 54, 92, 179, 188, 229, 65, 243\n ], \n \"rct_signatures\": {\n \"type\": 0\n }\n}",
"block_height": 766459,
"block_timestamp": 1612470441,
"double_spend_seen": false,
"in_pool": false,
"output_indices": [
3129279
],
"prunable_as_hex": "",
"prunable_hash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
"pruned_as_hex": "02b7e42e01fffbe32e01d7e0af9df6f401026c4a7168678f9d5c504e72c54d55a1cfc118cce9af944b6ecca10556541af0562101805d929a3701aaa54cacb8e373794c1ee9c108e48560ba7a1e365cb3bce541f300",
"tx_hash": "26dcb55c3c93a2176949fd9ec4e20a9d97ece7c420408d9353c390a909e9a7c1"
}

@ -0,0 +1,57 @@
{
"version": 2,
"unlock_time": 518207,
"vin": [
{
"gen": {
"height": 518147
}
}
],
"vout": [
{
"amount": 13515927959357,
"target": {
"key": "5b695861b1f0b409e1e51f0fed323fbb9dc2fe020146c060f85520e96468659d"
}
}
],
"extra": [
1,
220,
188,
34,
119,
62,
166,
51,
229,
198,
46,
210,
214,
191,
54,
140,
193,
246,
219,
234,
35,
107,
202,
9,
213,
173,
245,
16,
4,
39,
197,
32,
231
],
"rct_signatures": {
"type": 0
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"subaddress_accounts": [
{
"account_index": 0,
"balance": 13387591827179,
"base_address": "56eDKfprZtQGfB4y6gVLZx5naKVHw6KEKLDoq2WWtLng9ANuBvsw67wfqyhQECoLmjQN4cKAdvMp2WsC5fnw9seKLcCSfjj",
"label": "Primary account",
"tag": "",
"unlocked_balance": 13387591827179
},
{
"account_index": 1,
"balance": 221310000000,
"base_address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"label": "(Untitled account)",
"tag": "",
"unlocked_balance": 221310000000
}
],
"total_balance": 13608901827179,
"total_unlocked_balance": 13608901827179
}
}

@ -0,0 +1,7 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"key": "e507923516f52389eae889b6edc182ada82bb9354fb405abedbe0772a15aea0a"
}
}

@ -0,0 +1,15 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"addresses": [
{
"address": "7BC3q5ogPCfTkBHZajDdkhSLxN3wSSULEN52Q2XzGebeetyG4oumiCHJjPpSyNvP6qR2idCYiUEqmHjKwc66fmcKN4dxW5u",
"address_index": 0,
"label": "(Untitled account)",
"used": true
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -50,7 +50,6 @@ class Tests(object):
self.assertEqual(ia.payment_id(), self.pid)
self.assertEqual(str(ia), self.iaddr)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_recognition_and_comparisons(self):
a = Address(self.addr)
a2 = address(self.addr)
@ -60,19 +59,7 @@ class Tests(object):
self.assertEqual(self.addr, a)
self.assertEqual(hash(a), hash(self.addr))
self.assertEqual(a.net, self.net)
with pytest.deprecated_call():
self.assertEqual(a.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(a.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(a.is_stagenet(), self.net == const.NET_STAGE)
self.assertEqual(a2.net, self.net)
with pytest.deprecated_call():
self.assertEqual(a2.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(a2.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(a2.is_stagenet(), self.net == const.NET_STAGE)
ia = IntegratedAddress(self.iaddr)
ia2 = address(self.iaddr)
@ -82,19 +69,7 @@ class Tests(object):
self.assertEqual(self.iaddr, ia)
self.assertEqual(hash(ia), hash(self.iaddr))
self.assertEqual(ia.net, self.net)
with pytest.deprecated_call():
self.assertEqual(ia.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(ia.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(ia.is_stagenet(), self.net == const.NET_STAGE)
self.assertEqual(ia2.net, self.net)
with pytest.deprecated_call():
self.assertEqual(ia2.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(ia2.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(ia2.is_stagenet(), self.net == const.NET_STAGE)
self.assertEqual(ia2.base_address(), a)
self.assertEqual(ia.view_key(), a.view_key())
@ -108,19 +83,7 @@ class Tests(object):
self.assertEqual(self.subaddr, sa)
self.assertEqual(hash(sa), hash(self.subaddr))
self.assertEqual(sa.net, self.net)
with pytest.deprecated_call():
self.assertEqual(sa.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(sa.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(sa.is_stagenet(), self.net == const.NET_STAGE)
self.assertEqual(sa2.net, self.net)
with pytest.deprecated_call():
self.assertEqual(sa2.is_mainnet(), self.net == const.NET_MAIN)
with pytest.deprecated_call():
self.assertEqual(sa2.is_testnet(), self.net == const.NET_TEST)
with pytest.deprecated_call():
self.assertEqual(sa2.is_stagenet(), self.net == const.NET_STAGE)
self.assertNotEqual(a, 0)

@ -0,0 +1,204 @@
from decimal import Decimal
import json
try:
from unittest.mock import patch, Mock
except ImportError:
from mock import patch, Mock
import responses
from monero.backends.jsonrpc import JSONRPCDaemon, JSONRPCWallet
from monero.backends.offline import OfflineWallet
from monero.daemon import Daemon
from monero.transaction import Transaction
from monero.transaction.extra import ExtraParser
from monero.wallet import Wallet
from .base import JSONTestCase
class OutputTestCase(JSONTestCase):
data_subdir = "test_outputs"
daemon_transactions_url = "http://127.0.0.1:38081/get_transactions"
wallet_jsonrpc_url = "http://127.0.0.1:38083/json_rpc"
@responses.activate
def test_multiple_outputs(self):
daemon = Daemon(JSONRPCDaemon(host="127.0.0.1", port=38081))
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-00-get_accounts.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-01-query_key.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-02-addresses-account-0.json"),
status=200)
responses.add(responses.POST, self.wallet_jsonrpc_url,
json=self._read("test_multiple_outputs-wallet-02-addresses-account-1.json"),
status=200)
wallet = Wallet(JSONRPCWallet(host="127.0.0.1", port=38083))
responses.add(responses.POST, self.daemon_transactions_url,
json=self._read("test_multiple_outputs-daemon-00-get_transactions.json"),
status=200)
tx = daemon.transactions(
"f79a10256859058b3961254a35a97a3d4d5d40e080c6275a3f9779acde73ca8d")[0]
outs = tx.outputs(wallet=wallet)
self.assertEqual(len(outs), 5)
self.assertEqual(
outs[0].stealth_address,
"d3eb42322566c1d48685ee0d1ad7aed2ba6210291a785ec051d8b13ae797d202")
self.assertEqual(
outs[1].stealth_address,
"5bda44d7953e27b84022399850b59ed87408facdf00bbd1a2d4fda4bf9ebf72f")
self.assertEqual(
outs[2].stealth_address,
"4c79c14d5d78696e72959a28a734ec192059ebabb931040b5a0714c67b507e76")
self.assertEqual(
outs[3].stealth_address,
"64de2b358cdf96d498a9688edafcc0e25c60179e813304747524c876655a8e55")
self.assertEqual(
outs[4].stealth_address,
"966240954892294091a48c599c6db2b028e265c67677ed113d2263a7538f9a43")
self.assertIsNotNone(outs[0].payment)
self.assertIsNone(outs[1].payment) # FIXME: isn't that change we should recognize?
self.assertIsNotNone(outs[2].payment)
self.assertIsNotNone(outs[3].payment)
self.assertIsNotNone(outs[4].payment)
self.assertEqual(outs[0].amount, outs[0].payment.amount)
self.assertEqual(outs[2].amount, outs[2].payment.amount)
self.assertEqual(outs[3].amount, outs[3].payment.amount)
self.assertEqual(outs[4].amount, outs[4].payment.amount)
self.assertEqual(outs[0].amount, Decimal(4))
self.assertIsNone(outs[1].amount)
self.assertEqual(outs[2].amount, Decimal(1))
self.assertEqual(outs[3].amount, Decimal(2))
self.assertEqual(outs[4].amount, Decimal(8))
self.assertEqual(
outs[0].payment.local_address,
"76Qt2xMZ3m7b2tagubEgkvG81pwf9P3JYdxR65H2BEv8c79A9pCBTacEFv87tfdcqXRemBsZLFVGHTWbqBpkoBJENBoJJS9")
self.assertEqual(
outs[2].payment.local_address,
"78zGgzb45TEL8uvRFjCayUjHS98RFry1f7P4PE4LU7oeLh42s9AtP8fYXVzWqUW4r3Nz4g3V64w9RSiV7o3zUbPZVs5DVaU")
self.assertEqual(
outs[3].payment.local_address,
"73ndji4W2bu4WED87rJDVALMvUsZLLYstZsigbcGfb5YG9SuNyCSYk7Qbttez2mXciKtWRzRN9aYGJbF9TPBidNQNZppnFw")
self.assertEqual(
outs[4].payment.local_address,
"7BJxHKTa4p5USJ9Z5GY15ZARXL6Qe84qT3FnWkMbSJSoEj9ugGjnpQ1N9H1jqkjsTzLiN5VTbCP8f4MYYVPAcXhr36bHXzP")
self.assertEqual(
repr(outs[0]),
"d3eb42322566c1d48685ee0d1ad7aed2ba6210291a785ec051d8b13ae797d202, 4.000000000000 "
"to [76Qt2x]")
def test_coinbase_no_own_output(self):
txdata = self._read("test_coinbase_no_own_output-26dcb5.json")
tx = Transaction(
hash="26dcb55c3c93a2176949fd9ec4e20a9d97ece7c420408d9353c390a909e9a7c1",
height=766459,
output_indices=txdata["output_indices"],
json=json.loads(txdata["as_json"]))
self.assertTrue(tx.is_coinbase)
wallet = Wallet(OfflineWallet(
address="56eDKfprZtQGfB4y6gVLZx5naKVHw6KEKLDoq2WWtLng9ANuBvsw67wfqyhQECoLmjQN4cKAdvMp2WsC5fnw9seKLcCSfjj",
view_key="e507923516f52389eae889b6edc182ada82bb9354fb405abedbe0772a15aea0a"))
outs = tx.outputs(wallet=wallet)
self.assertEqual(len(outs), 1)
self.assertIsNone(outs[0].payment)
self.assertEqual(outs[0].amount, Decimal("8.415513145431"))
self.assertEqual(outs[0].index, 3129279)
def test_coinbase_own_output(self):
txdata = self._read("test_coinbase_own_output-dc0861.json")
tx = Transaction(
hash="dc08610685b8a55dc7d64454ecbe12868e4e73c766e2d19ee092885a06fc092d",
height=518147,
json=txdata)
self.assertTrue(tx.is_coinbase)
wallet = Wallet(OfflineWallet(
address="56eDKfprZtQGfB4y6gVLZx5naKVHw6KEKLDoq2WWtLng9ANuBvsw67wfqyhQECoLmjQN4cKAdvMp2WsC5fnw9seKLcCSfjj",
view_key="e507923516f52389eae889b6edc182ada82bb9354fb405abedbe0772a15aea0a"))
outs = tx.outputs(wallet=wallet)
self.assertEqual(len(outs), 1)
self.assertIsNotNone(outs[0].payment)
self.assertEqual(
outs[0].payment.local_address,
"56eDKfprZtQGfB4y6gVLZx5naKVHw6KEKLDoq2WWtLng9ANuBvsw67wfqyhQECoLmjQN4cKAdvMp2WsC5fnw9seKLcCSfjj")
self.assertEqual(outs[0].amount, outs[0].payment.amount)
self.assertEqual(outs[0].payment.amount, Decimal("13.515927959357"))
def test_v1_tx(self):
tx1 = Transaction(
hash="2634445086cc48b89f1cd241e89e6f37195008807264684d8fad4a16f479c45a",
height=2022660,
json=self._read("test_v1_tx-263444.json"))
tx2 = Transaction(
hash="3586a81f051bcb265a45c99f11b19fc4b55bb2abb3332c515a8b88a559cd9f7b",
height=2022660,
json=self._read("test_v1_tx-3586a8.json"))
outs1 = tx1.outputs()
self.assertEqual(len(outs1), 14)
self.assertEqual(outs1[0].stealth_address, "b1ef76960fe245f73131be22e9b548e861f93b727ab8a2a3ff64d86521512382")
self.assertEqual(outs1[0].amount, Decimal("0.000000000300"))
self.assertEqual(outs1[1].stealth_address, "dcd66bbcb6e72602dd876e1dad65a3464a2bd831f09ec7c8131147315152e29b")
self.assertEqual(outs1[1].amount, Decimal("0.000008000000"))
self.assertEqual(outs1[2].stealth_address, "71efdb68dfd33f5c89a5fa8312ec6e346681f6f60fb406e9426231a5f230351a")
self.assertEqual(outs1[2].amount, Decimal("0.007000000000"))
self.assertEqual(outs1[3].stealth_address, "499fb727f61f2ce0fbc3419b309601f2cbf672eeef2cc827aef423b0b70e2529")
self.assertEqual(outs1[3].amount, Decimal("0.000000010000"))
self.assertEqual(outs1[4].stealth_address, "297ef9bb654dd6e26472a4f07f037eddb3f8b458cf4315e2cc40d9fd725e28b9")
self.assertEqual(outs1[4].amount, Decimal("0.000000500000"))
self.assertEqual(outs1[5].stealth_address, "b2bf18a500afe1775305b19d16d0d5afec0f72096b9f15cca6604d7f5ad6e5f8")
self.assertEqual(outs1[5].amount, Decimal("0.300000000000"))
self.assertEqual(outs1[6].stealth_address, "f7a95b33912077e3aca425270f76be13af503919b6230368a591e1053b3c7436")
self.assertEqual(outs1[6].amount, Decimal("5.000000000000"))
self.assertEqual(outs1[7].stealth_address, "1e93e243a865b71e14fe4df6de0902ca634749b48002c52adc7f046053c2b921")
self.assertEqual(outs1[7].amount, Decimal("0.000200000000"))
self.assertEqual(outs1[8].stealth_address, "513822bad9697e8494ff82cb4b58a5a693aa433c16f0aafdaaf4a27b026a32e4")
self.assertEqual(outs1[8].amount, Decimal("0.000000000009"))
self.assertEqual(outs1[9].stealth_address, "6e1ace4cfdf3f5363d72c241382e3b9927af1093b549a62f2902f56137d153bc")
self.assertEqual(outs1[9].amount, Decimal("0.000000000070"))
self.assertEqual(outs1[10].stealth_address, "1df18bd04f42c9da8f6b49afe418aabc8ab973448a941d365534b5d0862a3d46")
self.assertEqual(outs1[10].amount, Decimal("0.000000002000"))
self.assertEqual(outs1[11].stealth_address, "caf3e6c07f8172fc31a56ba7f541ba8d6cc601f2c7da1a135126f8f3455e3ffc")
self.assertEqual(outs1[11].amount, Decimal("20.000000000000"))
self.assertEqual(outs1[12].stealth_address, "1ce506bc1ee041dfe36df3e085156023be26e133fb14f5e529b60a2d769a7c7c")
self.assertEqual(outs1[12].amount, Decimal("0.000030000000"))
self.assertEqual(outs1[13].stealth_address, "ee1a22b1f49db4df0df56161801974326cda4ceacbbf2a17c795ebe945790281")
self.assertEqual(outs1[13].amount, Decimal("0.030000000000"))
outs2 = tx2.outputs()
self.assertEqual(len(outs2), 10)
self.assertEqual(outs2[0].stealth_address, "ddd1d47e5d419cf5e2298e4d9e828364b929976912dfc1bbed25fb20cc681f9f")
self.assertEqual(outs2[0].amount, Decimal("3.000000000000"))
self.assertEqual(outs2[1].stealth_address, "a0c0edc478a3448a0d371755bd614854505d2f158499d9881bfffa8b05c5b3e8")
self.assertEqual(outs2[1].amount, Decimal("0.600000000000"))
self.assertEqual(outs2[2].stealth_address, "f9aeb5f16117f363adcd22f6b73d6e35eda64c25fee2f59208bd68d411b6d0c6")
self.assertEqual(outs2[2].amount, Decimal("0.000000000700"))
self.assertEqual(outs2[3].stealth_address, "17e36384cf11a4d85be1320c0e221505818edbb2d6634dd54db24e25570d0f75")
self.assertEqual(outs2[3].amount, Decimal("0.000000500000"))
self.assertEqual(outs2[4].stealth_address, "8b7e5dac3e0e45f9e7213ec3d4a465c5301b20f8ef30a5b2b5baba80867952b3")
self.assertEqual(outs2[4].amount, Decimal("0.000000000070"))
self.assertEqual(outs2[5].stealth_address, "d1e24eeaa62232cb0e4be536fc785e03075416457dd2b704437bced16da52500")
self.assertEqual(outs2[5].amount, Decimal("0.000000001000"))
self.assertEqual(outs2[6].stealth_address, "52c26fcce9d0a41f91ec57074e2cbfe301ca96b556e861deba51cd54e3e5b3e3")
self.assertEqual(outs2[6].amount, Decimal("0.000010000000"))
self.assertEqual(outs2[7].stealth_address, "c5859574278889dede61d5aa341e14d2fb2acf45941486276f61dd286e7f8895")
self.assertEqual(outs2[7].amount, Decimal("0.000000010000"))
self.assertEqual(outs2[8].stealth_address, "a3556072b7c8f77abdd16fe762fe1099c10c5ab071e16075ce0c667a3eacf1cc")
self.assertEqual(outs2[8].amount, Decimal("0.090000000000"))
self.assertEqual(outs2[9].stealth_address, "d72affedd142c6a459c42318169447f22042dba0d93c0f7ade42ddb222de8914")
self.assertEqual(outs2[9].amount, Decimal("0.009000000000"))
def test_extra_unknown_tag(self):
# try initializing as string
ep = ExtraParser(
"0169858d0d6de79c2dfd94b3f97745a12c9a7a61ffbae16b7a34bbf5b36b75084302086003c919"
"1772d1f90300786a796a227d07e9ae41ff9248a6b2e55adb3f6a42eb4c7ccc1c1b3d0f42c524")
with self.assertRaises(ValueError):
pdata = ep.parse()
# also try initializing as bytes
ep = ExtraParser(
b"015894c0e5a8d376e931df4b4ae45b753d9442de52ec8d94036253fba5aeff9782020901cb0e5e"
b"5a80a4e135ff301ea13334dfbe1508dafcaa32762a86cf12cd4fd193ee9807edcb91bc87f6ccb6"
b"02384b54dff4664b232a058b8d28ad7d")
with self.assertRaises(ValueError):
pdata = ep.parse()

@ -6,7 +6,8 @@ import unittest
from monero.address import address
from monero.numbers import PaymentID
from monero.transaction import IncomingPayment, Transaction, _ByHeight
from monero.transaction import IncomingPayment, Transaction, Output, _ByHeight
from monero import exceptions
class FiltersTestCase(unittest.TestCase):
def setUp(self):
@ -21,6 +22,23 @@ class FiltersTestCase(unittest.TestCase):
payment_id=PaymentID('0166d8da6c0045c51273dd65d6f63734beb8a84e0545a185b2cfd053fced9f5d'),
transaction=self.tx1)
# setup for one-time output tests
self.json1 = { # Actual as_json response from TX ee5bcb6430c39757ff27f8d607287572f3956a0ee16bb1d2378891f93746c8f9
'version': 2, 'unlock_time': 0, 'vin': [{'key': {'amount': 0, 'key_offsets':
[25471133, 261981, 36602, 18967, 13096, 16260, 54279, 3105, 5403, 786, 555],
'k_image': '4b48346e954a74be9a334b03cadf8aa020542d201fb6ae7416246d19fd04fdb7'}}],
'vout': [{'amount': 0, 'target': {'key': 'c55e793b4d673dcf73587e5141b777ef24e255d48826c75ce110ffc23ff762b9'}},
{'amount': 0, 'target': {'key': '93b263454cd3cc349245ad60c9c248332b885a1f2d7b5792cfc24fd87434d62a'}}],
'extra': [1, 209, 170, 43, 245, 190, 68, 82, 131, 116, 79, 134, 175, 104, 216, 127, 99, 49, 127, 141, 255, 65, 204, 101,
81, 244, 111, 253, 155, 75, 111, 14, 159, 2, 9, 1, 24, 56, 108, 94, 20, 88, 150, 94], 'rct_signatures': {'type': 5,
'txnFee': 58560000, 'ecdhInfo': [{'amount': '6c13cf459cb9ed96'}, {'amount': '373bc40c7f600bf4'}], 'outPk':
['80521a77ebe954a5daa6f14b13cc74337f999bc68177a58e76f768c18f2fa421',
'5997e64b90d59f7f810ddbc801f747c4fa43e2de593e4ea48531e16d776c00fd']}}
self.outind1 = [25884175, 25884176]
self.tx2 = Transaction(json=self.json1, output_indices=self.outind1)
self.oto1 = Output(index=25973289, amount=Decimal('0.000000000000'))
self.oto2 = Output(pubkey='0faff18f7149a0db5aa0dc3c9116887740ccbb5dc4d1eeff87895288e55e5052')
def test_hash(self):
self.assertIn(
'a0b876ebcf7c1d499712d84cedec836f9d50b608bb22d6cb49fd2feae3ffed14',
@ -29,6 +47,21 @@ class FiltersTestCase(unittest.TestCase):
'a0b876ebcf7c1d499712d84cedec836f9d50b608bb22d6cb49fd2feae3ffed14',
repr(self.pm1))
def test_outputs(self):
out1, out2 = self.tx2.outputs()
self.assertEqual(out1.transaction, self.tx2)
self.assertEqual(out2.transaction, self.tx2)
self.assertIn(self.json1['vout'][0]['target']['key'], repr(out1))
self.assertFalse(out2 != Output(stealth_address=self.json1['vout'][1]['target']['key']))
self.assertIn('(index=25973289,amount=0E-12)', repr(self.oto1))
self.assertEqual(self.oto1, Output(index=25973289, amount=Decimal('0.000000000000')))
with self.assertRaises(exceptions.TransactionWithoutJSON):
self.tx1.outputs()
with self.assertRaises(TypeError):
self.oto1 == self.oto2
class SortingTestCase(unittest.TestCase):
def test_sorting(self):

Loading…
Cancel
Save