Skip to content

Commit

Permalink
Merge branch 'master' into assets-transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
emesik committed Aug 8, 2021
2 parents 4679450 + 39351d9 commit 4cb5d9d
Show file tree
Hide file tree
Showing 10 changed files with 479 additions and 38 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include *requirements*.txt
21 changes: 10 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
Python Cardano module
=====================

**This software is in early development phase. Please consider it experimental and don't rely on any
API to be stable in the future.**
**This software is in development phase. Please consider it experimental and don't rely on any
API to be stable before version 1.0 comes.**

There's release 0.6 available for those brave enough to try it.

This module is the implementation of `the idea`_ submitted to Catalyst Project.

.. _`the idea`: https://cardano.ideascale.com/a/dtd/Python-module/333770-48088
There's release 0.6.1 available for those brave enough to try it.

Prerequisites
-------------
Expand All @@ -26,6 +22,10 @@ the Cardano *testnet* for any software development and testing.
Roadmap
-------

This module is the implementation of `the idea`_ submitted to Catalyst Project.

.. _`the idea`: https://cardano.ideascale.com/a/dtd/Python-module/333770-48088

+------------+---------+--------------------------------------------------------------------------+
| date | version | features |
+============+=========+==========================================================================+
Expand All @@ -52,16 +52,15 @@ Roadmap
| 2021-05-17 | 0.6 | - UTXO stats |
| | | - docs for 0.5 + 0.6 features |
+------------+---------+--------------------------------------------------------------------------+
| 2021-06-XX | | - advanced filtering of incoming and outgoing transfers |
| | | - address validation |
| 2021-08-10 | 0.7 | - advanced filtering of incoming and outgoing transfers |
| | | - native assets transfer |
+------------+---------+--------------------------------------------------------------------------+
| | | **End of the Catalyst-funded phase** |
+------------+---------+--------------------------------------------------------------------------+
| future | | - native assets transfer |
| future | | - address validation |
| | | - key operations (HD wallet key generation) |
| | | - seed to key and vice versa conversion |
| | | - coin selection |
| | | - advanced filtering of incoming and outgoing transfers |
| | | - transaction forgetting |
| | | - handling of Byron wallets |
| | | - Goguen features (smart contracts?) |
Expand Down
2 changes: 1 addition & 1 deletion cardano/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6"
__version__ = "0.6.1"
9 changes: 3 additions & 6 deletions cardano/backends/walletrest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,14 @@ def _txdata2tx(self, txd, addresses=None):
(serializers.get_amount(w["amount"]), w["stake_address"])
for w in txd.get("withdrawals", [])
],
status=txd["status"],
metadata=metadata,
)

def transactions(self, wid, start=None, end=None, order="ascending"):
def transactions(self, wid):
data = {
"order": order,
"order": "ascending"
}
if start is not None:
data["start"] = start.isoformat(timespec="seconds")
if end is not None:
data["end"] = end.isoformat(timespec="seconds")
return [
self._txdata2tx(txd, addresses=self._addresses_set(wid))
for txd in self.raw_request(
Expand Down
10 changes: 9 additions & 1 deletion cardano/backends/walletrest/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ def get_percent(data):
return Decimal(data["quantity"]) / Decimal(100)


def get_height(data):
assert data["unit"] == "block"
return data["quantity"]


def get_block_position(data):
return BlockPosition(
data["epoch_number"], data["slot_number"], data["absolute_slot_number"]
data["epoch_number"],
data["slot_number"],
data["absolute_slot_number"],
get_height(data["height"]) if "height" in data else None,
)


Expand Down
3 changes: 2 additions & 1 deletion cardano/simpletypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@


BlockPosition = collections.namedtuple(
"BlockPosition", ["epoch", "slot", "absolute_slot"]
"BlockPosition", ["epoch", "slot", "absolute_slot", "height"]
)
BlockPosition.__doc__ = "Represents block's position within the blockchain"
BlockPosition.epoch.__doc__ = "Epoch number"
BlockPosition.slot.__doc__ = "Slot number"
BlockPosition.absolute_slot.__doc__ = "Absolute slot number"
BlockPosition.height.__doc__ = "Block number (height of the chain) [optional]"


Epoch = collections.namedtuple("Epoch", ["number", "starts"])
Expand Down
212 changes: 211 additions & 1 deletion cardano/transaction.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import collections
import operator
import re
import warnings

from .address import Address
from .address import Address, address
from .metadata import Metadata
from .numbers import as_ada

__all__ = ("Transaction", "Input", "Output", "validate_txid")


class Transaction(object):
"""
Expand All @@ -31,10 +35,12 @@ class Transaction(object):
inserted_at = None
expires_at = None
pending_since = None
status = None
metadata = None

def __init__(self, txid=None, **kwargs):
self.txid = txid or self.txid
validate_txid(self.txid)
fee = kwargs.pop("fee", None)
self.fee = fee if fee is not None else self.fee
self.inputs = kwargs.pop("inputs", []) or (
Expand All @@ -55,10 +61,17 @@ def __init__(self, txid=None, **kwargs):
self.inserted_at = kwargs.pop("inserted_at", None) or self.inserted_at
self.expires_at = kwargs.pop("expires_at", None) or self.expires_at
self.pending_since = kwargs.pop("pending_since", None) or self.pending_since
self.status = kwargs.pop("status", None) or self.status
self.metadata = kwargs.pop("metadata", Metadata()) or (
self.metadata if self.metadata is not None else Metadata()
)

def __repr__(self):
return "<Cardano tx: {:s}>".format(self.txid)

def __format__(self, spec):
return format(str(self), spec)

@property
def local_inputs_sum(self):
return as_ada(sum(map(operator.attrgetter("amount"), self.local_inputs)))
Expand Down Expand Up @@ -143,3 +156,200 @@ class Output(IOBase):
"""

pass


def validate_txid(txid):
if not bool(re.compile("^[0-9a-f]{64}$").match(txid)):
raise ValueError(
"Transaction ID must be a 64-character hexadecimal string, not "
"'{}'".format(txid)
)
return txid


class TransactionManager(object):
wid = None
backend = None

def __init__(self, wid, backend):
self.wid = wid
self.backend = backend

def __call__(self, **filterparams):
filter_ = TxFilter(**filterparams)
return filter_.filter(self.backend.transactions(self.wid))


class _ByHeight(object):
"""A helper class used as key in sorting of payments by height.
Mempool goes on top, blockchain payments are ordered with descending block numbers.
**WARNING:** Integer sorting is reversed here.
"""

def __init__(self, tx):
self.tx = tx

def _cmp(self, other):
sh = self.tx.inserted_at
oh = other.tx.inserted_at
if sh is oh is None:
return 0
if sh is None:
return 1
if oh is None:
return -1
return (sh.absolute_slot > oh.absolute_slot) - (
sh.absolute_slot < oh.absolute_slot
)

def __lt__(self, other):
return self._cmp(other) > 0

def __le__(self, other):
return self._cmp(other) >= 0

def __eq__(self, other):
return self._cmp(other) == 0

def __ge__(self, other):
return self._cmp(other) <= 0

def __gt__(self, other):
return self._cmp(other) < 0

def __ne__(self, other):
return self._cmp(other) != 0


class TxFilter(object):
#
# Available filters:
# - txid
# - src_addr
# - dest_addr
# - min_epoch
# - max_epoch
# - min_slot
# - max_slot
# - min_height
# - max_height
# - confirmed
# - unconfirmed
#
def __init__(self, **filterparams):
self.min_epoch = filterparams.pop("min_epoch", None)
self.max_epoch = filterparams.pop("max_epoch", None)
self.min_slot = filterparams.pop("min_slot", None)
self.max_slot = filterparams.pop("max_slot", None)
self.min_absolute_slot = filterparams.pop("min_absolute_slot", None)
self.max_absolute_slot = filterparams.pop("max_absolute_slot", None)
self.min_height = filterparams.pop("min_height", None)
self.max_height = filterparams.pop("max_height", None)
self.unconfirmed = filterparams.pop("unconfirmed", False)
self.confirmed = filterparams.pop("confirmed", True)
_txid = filterparams.pop("txid", None)
_src_addr = filterparams.pop("src_addr", None)
_dest_addr = filterparams.pop("dest_addr", None)
if len(filterparams) > 0:
raise ValueError(
"Excessive arguments for payment query: {}".format(filterparams)
)
self._asks_chain_position = any(
map(
lambda x: x is not None,
(
self.min_epoch,
self.max_epoch,
self.min_slot,
self.max_slot,
self.min_absolute_slot,
self.max_absolute_slot,
self.min_height,
self.max_height,
),
)
)
if self.unconfirmed and self._asks_chain_position:
warnings.warn(
"Blockchain position filtering ({max,min}_{epoch,slot,block}) has been "
"requested while also asking for transactions not in ledger. "
"These are mutually exclusive. "
"As mempool transactions have no height at all, they will be excluded "
"from the result.",
RuntimeWarning,
)
self.src_addrs = self._get_addrset(_src_addr)
self.dest_addrs = self._get_addrset(_dest_addr)
if _txid is None:
self.txids = []
else:
if isinstance(_txid, (bytes, str)):
txids = [_txid]
else:
iter(_txid)
txids = _txid
self.txids = list(map(validate_txid, txids))

def _get_addrset(self, addr):
if addr is None:
return set()
else:
if isinstance(addr, (str, bytes)):
addrs = [addr]
else:
try:
iter(addr)
addrs = addr
except TypeError:
addrs = [addr]
return set(map(address, addrs))

def check(self, tx):
assert (tx.status == "in_ledger" and tx.inserted_at is not None) or (
tx.status != "in_ledger" and tx.inserted_at is None
)
ht = tx.inserted_at
if ht is None:
if not self.unconfirmed:
return False
if self._asks_chain_position:
# mempool txns are filtered out if any height range check is present
return False
else:
if not self.confirmed:
return False
if self.min_epoch is not None and ht.epoch < self.min_epoch:
return False
if self.max_epoch is not None and ht.epoch > self.max_epoch:
return False
if self.min_slot is not None and ht.slot < self.min_slot:
return False
if self.max_slot is not None and ht.slot > self.max_slot:
return False
if (
self.min_absolute_slot is not None
and ht.absolute_slot < self.min_absolute_slot
):
return False
if (
self.max_absolute_slot is not None
and ht.absolute_slot > self.max_absolute_slot
):
return False
if self.min_height is not None and ht.height < self.min_height:
return False
if self.max_height is not None and ht.height > self.max_height:
return False
if self.txids and tx.txid not in self.txids:
return False
srcs = set(filter(None, map(operator.attrgetter("address"), tx.inputs)))
dests = set(filter(None, map(operator.attrgetter("address"), tx.inputs)))
if self.src_addrs and not self.src_addrs.intersection(srcs):
return False
if self.dest_addrs and not self.dest_addrs.intersection(dests):
return False
return True

def filter(self, txns):
return sorted(filter(self.check, txns), key=_ByHeight)
15 changes: 2 additions & 13 deletions cardano/wallet.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .address import Address
from .simpletypes import StakePoolInfo
from .transaction import TransactionManager
from . import exceptions


Expand Down Expand Up @@ -68,6 +69,7 @@ def __init__(self, wid, backend, passphrase=None):
self.passphrase = passphrase or self.passphrase
if not self.backend.wallet_exists(wid):
raise ValueError("Wallet of id '{:s}' doesn't exist.".format(wid))
self.transactions = TransactionManager(self.wid, self.backend)

def sync_progress(self):
"""
Expand Down Expand Up @@ -125,19 +127,6 @@ def delete(self):
"""
return self.backend.delete_wallet(self.wid)

def transactions(self): # , start=None, end=None, order="ascending"):
"""
Returns the list of all wallet's :class:`Transactions <cardano.transaction.Transaction>`.
"""
# WARNING: parameters don't really work; this is a known bug
# if start:
# start = start.astimezone(tz=timezone.utc)
# if end:
# end = end.astimezone(tz=timezone.utc)
return self.backend.transactions(
self.wid
) # , start=start, end=end, order=order)

def _resolve_passphrase(self, passphrase):
passphrase = passphrase or self.passphrase
if passphrase is None:
Expand Down
Loading

0 comments on commit 4cb5d9d

Please sign in to comment.