Skip to content
This repository has been archived by the owner on Aug 12, 2024. It is now read-only.

Commit

Permalink
feat: high level API to access transaction properties (#338)
Browse files Browse the repository at this point in the history
* implement parser for tx data
* add easy access to nested tx properties via get(), meta() and ga_meta() methods

closes #272
  • Loading branch information
noandrea authored Jan 29, 2020
1 parent 1bd7929 commit 65f490b
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 31 deletions.
80 changes: 62 additions & 18 deletions aeternity/transactions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from aeternity.hashing import _int, _int_decode, _binary, _binary_decode, _id, _id_decode, encode, decode, hash_encode
from aeternity.openapi import OpenAPICli
from aeternity import identifiers as idf
from aeternity import defaults

Expand All @@ -8,6 +7,7 @@
import pprint
import copy
from munch import Munch
from deprecated import deprecated


_INT = 0 # int type
Expand Down Expand Up @@ -393,21 +393,37 @@ def __repr__(self):
class TxObject:
"""
This is a TxObject that is used throughout the SDK for transactions
It contains all the info associated to a transaction like transaction data,transaction hash, etx
It contains all the info associated with a transaction
"""

def __init__(self, **kwargs):
self.set_data(kwargs.get("data", None))
self._index = {}
self.set_data(kwargs.get("data", {}))
self.tx = kwargs.get("tx", None)
self.hash = kwargs.get("hash", None)
self.set_metadata(kwargs.get("metadata", None))
self.set_metadata(kwargs.get("metadata", {}))
# self._build_index() # the index building is triggered by the set metadata

def set_data(self, data):
self.data = Munch.fromDict(data)

def set_metadata(self, metadata):
self.metadata = Munch.fromDict(metadata)
self._build_index()

def _build_index(self):
self._index = {}
for k, v in Munch.toDict(self.metadata).items():
self._index[f"meta.{k}"] = v

def __bi(data: dict):
prefix = "ga." if data.get("tag", -1) == idf.OBJECT_TAG_GA_META_TRANSACTION else ""
for k, v in Munch.toDict(data).items():
if k == "tx":
__bi(Munch.toDict(v.data))
continue
self._index[f"{prefix}{k}"] = v
__bi(Munch.toDict(self.data))

def asdict(self):
t = dict(
Expand All @@ -422,6 +438,38 @@ def asdict(self):

return t

def get(self, name):
"""
Get the value of a property of the transaction by name,
searching recursively in the TxObject structure.
For GA transaction the properties of the GA use the ga_meta() method
below
:param name: the name of the property to get
:return: the property value or None if there is no such property
"""
return self._index.get(name)

def meta(self, name):
"""
Get the value of a meta property such as the min_fee.
:param name: the name of the meta property
:return: the value of the meta property or none if not found
"""
return self._index.get(f"meta.{name}")

def ga_meta(self, name):
"""
Get the value of a GA meta transaction property
:param name: the name of the property to get
:return: the property value or None if there is no such property
"""
return self._index.get(f"ga.{name}")

@deprecated(reason="This method has been deprecated in favour of get('signatures')")
def get_signatures(self):
"""
retrieves the list of signatures for a signed transaction, otherwise returns a empty list
Expand All @@ -430,6 +478,7 @@ def get_signatures(self):
return self.data.signatures
return []

@deprecated(reason="This method has been deprecated in favour of get('signatures')[index]")
def get_signature(self, index):
"""
get the signature at the requested index, or raise type error if there is no such index
Expand Down Expand Up @@ -473,6 +522,9 @@ def sign_transaction(self, transaction: TxObject, metadata: dict = None) -> str:
# pack and encode the transaction
return encode(idf.SIGNATURE, signature)

def __str__(self):
return f"{self.network_id}:{self.account.get_address()}"


class TxBuilder:
"""
Expand Down Expand Up @@ -547,6 +599,8 @@ def _jsontx_to_txobject(self, api_data):
A specific case is the one for signed transactions that in the api are treated in
a different ways from other transactions, and their data is mixed with block informations
:param api_data: a namedtuple of the response obtained from the node api
"""
# transform the data to a dict since the openapi module maps them to a named tuple
tx_data = Munch.toDict(api_data)
Expand Down Expand Up @@ -788,7 +842,7 @@ def tx_name_preclaim(self, account_id, commitment_id, fee, ttl, nonce) -> TxObje
return self._build_txobject(body)
# return self.api.post_name_preclaim(body=body).tx

def tx_name_claim(self, account_id, name, name_salt, fee, ttl, nonce) -> TxObject:
def tx_name_claim(self, account_id, name, name_salt, fee, ttl, nonce) -> TxObject: # lgtm [py/similar-function]
"""
create a preclaim transaction
:param account_id: the account registering the name
Expand Down Expand Up @@ -834,7 +888,7 @@ def tx_name_claim_v2(self, account_id, name, name_salt, name_fee, fee, ttl, nonc
)
return self._build_txobject(body)

def tx_name_update(self, account_id, name_id, pointers, name_ttl, client_ttl, fee, ttl, nonce) -> TxObject:
def tx_name_update(self, account_id, name_id, pointers, name_ttl, client_ttl, fee, ttl, nonce) -> TxObject: # lgtm [py/similar-function]
"""
create an update transaction
:param account_id: the account updating the name
Expand All @@ -861,7 +915,7 @@ def tx_name_update(self, account_id, name_id, pointers, name_ttl, client_ttl, fe
return self._build_txobject(body)
# return self.api.post_name_update(body=body).tx

def tx_name_transfer(self, account_id, name_id, recipient_id, fee, ttl, nonce) -> TxObject:
def tx_name_transfer(self, account_id, name_id, recipient_id, fee, ttl, nonce) -> TxObject: # lgtm [py/similar-function]
"""
create a transfer transaction
:param account_id: the account transferring the name
Expand Down Expand Up @@ -1132,13 +1186,3 @@ def tx_ga_meta(self, ga_id,
tx=tx,
)
return self._build_txobject(body)


class TxBuilderDebug:
def __init__(self, api: OpenAPICli):
"""
:param native: if the transactions should be built by the sdk (True) or requested to the debug api (False)
"""
if api is None:
raise ValueError("A initialized api rest client has to be provided to build a transaction using the node internal API ")
self.api = api
58 changes: 58 additions & 0 deletions docs/howto/amounts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
=======
Amounts
=======

The Aeternity blockchain tokens are an ERC-20 compatible token,
where the unit is the Aetto (1e-18).

The ``utils`` submodule provides functions to manipulate the
amounts.


Format amount to a human readable format
========================================

The following snippets show how to format
an amount from ``aetto`` to a human readable format.

::
# import the utils submodule
from aeternity import utils

amount = 1000000000000000000
human_value = utils.format_amount(amount)
print(human_value) # prints 1AE


Parse amounts
=============

This snippets shows how to parse amounts
expressed in a human friendly, float or scientific notation.

::
# import the utils submodule
from aeternity import utils

amount = utils.amount_to_aettos(1.2)
print(amount) # prints 1200000000000000000

amount = utils.amount_to_aettos("10AE")
print(amount) # prints 10000000000000000000

amount = utils.amount_to_aettos(1e1)
print(amount) # prints 10000000000000000000

amount = utils.amount_to_aettos(1)
print(amount) # prints 1


.. Important::
when using amount_to_aettos function, the maximum value as imput is 1e9,
that will result in an aetto value of 1000000000000000000000000000 (1e27).
Bigger values wil return in an error.



1 change: 1 addition & 0 deletions docs/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ you quickly accomplish common tasks.

cli_accounts
validate_contract_bytecode
amounts

.. seealso::

Expand Down
86 changes: 83 additions & 3 deletions docs/ref/txobject.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,87 @@
============
========
TxObject
============
========

``TxObject`` is one of the central entity of the Python SDK,
and it represent a transaction object.

.. autoclass:: aeternity.transactions.TxObject
:members:
:members: get, meta, ga_meta

The fields of a ``TxObject`` are

- ``hash``: the transaction hash
- ``data``: the transaction data, it varies for every transaction type
- ``metadata``: it contains additional data that may be relevant in the transaction context
- ``tx``: the rlp + base64 check encoded string of the transaction that is used to broadcast a transaction

Since a transaction can be a nested structured, the ``TxObject`` is nested as well:
considering a simple spend transaction, the actual structure of the transaction is:

::

SignedTx(
tag: - signed transaction type (11)
version - transaction version, 1 in this case
[signature] - the list of signatures for the signed transaction
tx: SpendTx( - the inner spend transaction
tag - spend transaction type (12)
version - spend transaction version, 1 in this case
sender_id - spend sender
recipient_id - spend recipient
amount - amount being transferred
fee - fee for the miner
ttl - the mempool time to live
nonce - sender account nonce
payload - arbitrary payload
)
)

This means that to access, for example, the spend transaction ``recipient_id`` from a ``TxObject``,
the code would be:

::

tx_object = node_cli.spend(sender, recipient, "100AE")
# access the recipient_id
tx_object.data.tx.data.recipient_id

unless the transaction has been posted from a generalized account, in which case there
are 4 levels of nesting:

::

tx_object = node_cli.spend(sender, recipient, "100AE")
# access the recipient_id for a GA generated transaction
tx_object.data.tx.data.tx.data.tx.data.recipient_id

This is of course somewhat awkward, and therefore the ``TxObject`` provides the ``get(NAME)``, ``meta(NAME)``, ``ga_meta(NAME)`` functions.

The functions are used to access the values of the properties without worrying about the structure of the transaction,
so the example above will become:

::

tx_object = node_cli.spend(sender, recipient, "100AE")
# access the recipient_id for any spend transaction
tx_object.get("recipient_id")

Metadata
--------
Metadatas are special informations that are not part of the transaction itself but my be generated
ad a additional output while creating or parsing a transaction, in particular metadata fields are:

- ``min_fee`` minimum fee for a transaction, this value is always calculaed and can be used to
evaluate the actual fee used for the transaction.
- ``contract_id`` the id of a contract, only present when deploying a new contract (starts with prefix ``ct_``).
- ``salt`` the random generated salt to prepare the ``commitment_id`` of a name pre-claim transaction. The
salt must be used then to prepare a claim transaction.

TxObject data fields
--------------------
Here is the complete list of transactions and available fields:


.. literalinclude:: ../../aeternity/transactions.py
:lines: 65-389
:dedent: 4
21 changes: 12 additions & 9 deletions tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@


def _test_node_spend(node_cli, sender_account):
account = Account.generate().get_address()
# WARNING THIS EXAMPLE IS USED IN THE DOCUMENTATION

recipient_id = Account.generate().get_address()
# with numbers
tx = node_cli.spend(sender_account, account, 100)
tx = node_cli.spend(sender_account, recipient_id, 100)
print("DATA", tx)
assert account == tx.data.tx.data.recipient_id
assert recipient_id == tx.data.tx.data.recipient_id
assert sender_account.get_address() == tx.data.tx.data.sender_id
account_balance = node_cli.get_account_by_pubkey(pubkey=account).balance
account_balance = node_cli.get_account_by_pubkey(pubkey=recipient_id).balance
assert account_balance == 100
# with strings
tx = node_cli.spend(sender_account, account, "0.5ae")
print("DATA", tx)
assert account == tx.data.tx.data.recipient_id
# spend some string
tx = node_cli.spend(sender_account, recipient_id, "0.5ae")

assert recipient_id == tx.data.tx.data.recipient_id
assert sender_account.get_address() == tx.data.tx.data.sender_id
account_balance = node_cli.get_account_by_pubkey(pubkey=account).balance

account_balance = node_cli.get_account_by_pubkey(pubkey=recipient_id).balance
assert account_balance == 500000000000000100


Expand Down
Loading

0 comments on commit 65f490b

Please sign in to comment.