Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for SEP-0029. #291

Merged
merged 3 commits into from
Mar 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sphinx-rtd-theme = "*"
sphinx-autodoc-typehints = "*"
pytest-timeout = "*"
pytest-asyncio = "*"
pytest-httpserver = "*"

[packages]
crc16 = "*"
Expand Down
4 changes: 4 additions & 0 deletions docs/en/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,8 @@ SEP 0010: Stellar Web Authentication
Exceptions
----------
.. autoclass:: stellar_sdk.sep.exceptions.StellarTomlNotFoundError
.. autoclass:: stellar_sdk.sep.exceptions.InvalidFederationAddress
.. autoclass:: stellar_sdk.sep.exceptions.FederationServerNotFoundError
.. autoclass:: stellar_sdk.sep.exceptions.BadFederationResponseError
.. autoclass:: stellar_sdk.sep.exceptions.InvalidSep10ChallengeError
.. autoclass:: stellar_sdk.sep.exceptions.AccountRequiresMemoError
4 changes: 4 additions & 0 deletions docs/zh_CN/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,8 @@ SEP 0010: Stellar Web Authentication
Exceptions
----------
.. autoclass:: stellar_sdk.sep.exceptions.StellarTomlNotFoundError
.. autoclass:: stellar_sdk.sep.exceptions.InvalidFederationAddress
.. autoclass:: stellar_sdk.sep.exceptions.FederationServerNotFoundError
.. autoclass:: stellar_sdk.sep.exceptions.BadFederationResponseError
.. autoclass:: stellar_sdk.sep.exceptions.InvalidSep10ChallengeError
.. autoclass:: stellar_sdk.sep.exceptions.AccountRequiresMemoError
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pytest
pytest-cov
pytest-timeout
pytest-asyncio
pytest-httpserver
17 changes: 17 additions & 0 deletions stellar_sdk/sep/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,20 @@ def __init__(self, response) -> None:

class InvalidSep10ChallengeError(SdkError):
"""If the SEP 0010 validation fails, the exception will be thrown."""


class AccountRequiresMemoError(SdkError):
"""AccountRequiresMemoError is raised when a transaction is trying to submit an
operation to an account which requires a memo.

This error contains two attributes to help you identify the account requiring
the memo and the operation where the account is the destination.

See `SEP-0029 <https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md>`_ for more
information.
"""

def __init__(self, message, account_id, operation_index) -> None:
super().__init__(message)
self.account_id: str = account_id
self.operation_index: int = operation_index
120 changes: 107 additions & 13 deletions stellar_sdk/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import warnings
from typing import Union, Coroutine, Any, Dict, List


from .account import Account, Thresholds
from .asset import Asset
from .call_builder.accounts_call_builder import AccountsCallBuilder
Expand All @@ -26,9 +25,13 @@
from .client.base_async_client import BaseAsyncClient
from .client.base_sync_client import BaseSyncClient
from .client.requests_client import RequestsClient
from .exceptions import TypeError, raise_request_exception
from .exceptions import TypeError, NotFoundError, raise_request_exception
from .memo import NoneMemo
from .sep.exceptions import AccountRequiresMemoError
from .transaction import Transaction
from .transaction_envelope import TransactionEnvelope
from .utils import urljoin_with_query
from .xdr import Xdr

__all__ = ["Server"]

Expand Down Expand Up @@ -72,38 +75,74 @@ def __init__(
)

def submit_transaction(
self, transaction_envelope: Union[TransactionEnvelope, str]
self,
transaction_envelope: Union[TransactionEnvelope, str],
skip_memo_required_check: bool = False,
) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
"""Submits a transaction to the network.

:param transaction_envelope: :class:`stellar_sdk.transaction_envelope.TransactionEnvelope` object
or base64 encoded xdr
:return: the response from horizon
:raises:
:exc:`ConnectionError <stellar_sdk.exceptions.ConnectionError>`
:exc:`NotFoundError <stellar_sdk.exceptions.NotFoundError>`
:exc:`BadRequestError <stellar_sdk.exceptions.BadRequestError>`
:exc:`BadResponseError <stellar_sdk.exceptions.BadResponseError>`
:exc:`UnknownRequestError <stellar_sdk.exceptions.UnknownRequestError>`
:exc:`AccountRequiresMemoError <stellar_sdk.sep.exceptions.AccountRequiresMemoError>`
"""
xdr = transaction_envelope
if isinstance(transaction_envelope, TransactionEnvelope):
xdr = transaction_envelope.to_xdr()

data = {"tx": xdr}
url = urljoin_with_query(self.horizon_url, "transactions")
if self.__async:
return self.__submit_transaction_async(url, data)
return self.__submit_transaction_sync(url, data)
return self.__submit_transaction_async(
transaction_envelope, skip_memo_required_check
)
return self.__submit_transaction_sync(
transaction_envelope, skip_memo_required_check
)

def __submit_transaction_sync(
self, url: str, data: Dict[str, str]
self,
transaction_envelope: Union[TransactionEnvelope, str],
skip_memo_required_check: bool,
) -> Dict[str, Any]:
url = urljoin_with_query(self.horizon_url, "transactions")
xdr, tx = self.__get_xdr_and_transaction_from_transaction_envelope(
transaction_envelope
)
if not skip_memo_required_check:
self.__check_memo_required_sync(tx)
data = {"tx": xdr}
resp = self._client.post(url=url, data=data)
raise_request_exception(resp)
return resp.json()

async def __submit_transaction_async(
self, url: str, data: Dict[str, str]
self,
transaction_envelope: Union[TransactionEnvelope, str],
skip_memo_required_check: bool,
) -> Dict[str, Any]:
url = urljoin_with_query(self.horizon_url, "transactions")
xdr, tx = self.__get_xdr_and_transaction_from_transaction_envelope(
transaction_envelope
)
if not skip_memo_required_check:
await self.__check_memo_required_async(tx)
data = {"tx": xdr}
resp = await self._client.post(url=url, data=data)
raise_request_exception(resp)
return resp.json()

def __get_xdr_and_transaction_from_transaction_envelope(
self, transaction_envelope: Union[TransactionEnvelope, str]
):
if isinstance(transaction_envelope, TransactionEnvelope):
xdr = transaction_envelope.to_xdr()
tx = transaction_envelope.transaction
else:
xdr = transaction_envelope
tx = Transaction.from_xdr(xdr)
return xdr, tx

def root(self) -> RootCallBuilder:
"""
:return: New :class:`stellar_sdk.call_builder.RootCallBuilder` object configured
Expand Down Expand Up @@ -361,6 +400,61 @@ def __load_account_sync(self, account_id: str) -> Account:
account.thresholds = thresholds
return account

def __check_memo_required_sync(self, transaction: Transaction) -> None:
if not (transaction.memo is None or isinstance(transaction.memo, NoneMemo)):
return
for index, destination in self.__get_check_memo_required_destinations(
transaction
):
try:
account_resp = self.accounts().account_id(destination).call()
except NotFoundError:
continue
self.__check_destination_memo(account_resp, index, destination)

async def __check_memo_required_async(self, transaction: Transaction) -> None:
if not (transaction.memo is None or isinstance(transaction.memo, NoneMemo)):
return
for index, destination in self.__get_check_memo_required_destinations(
transaction
):
try:
account_resp = await self.accounts().account_id(destination).call()
except NotFoundError:
continue
self.__check_destination_memo(account_resp, index, destination)

def __check_destination_memo(
self, account_resp: dict, index: int, destination: str
) -> None:
memo_required_config_key = "config.memo_required"
memo_required_config_value = "MQ=="
data = account_resp["data"]
if data.get(memo_required_config_key) == memo_required_config_value:
raise AccountRequiresMemoError(
"Destination account requires a memo in the transaction.",
destination,
index,
)

def __get_check_memo_required_destinations(self, transaction: Transaction):
destinations = set()
memo_required_operation_code = (
Xdr.const.PAYMENT,
Xdr.const.ACCOUNT_MERGE,
Xdr.const.PATH_PAYMENT_STRICT_RECEIVE,
Xdr.const.PATH_PAYMENT_STRICT_SEND,
)
for index, operation in enumerate(transaction.operations):
if operation.type_code() in memo_required_operation_code:
destination = operation.destination
else:
continue
if destination in destinations:
continue
destinations.add(destination)
yield index, destination

def fetch_base_fee(self) -> Union[int, Coroutine[Any, Any, int]]:
"""Fetch the base fee. Since this hits the server, if the server call fails,
you might get an error. You should be prepared to use a default value if that happens.
Expand Down
11 changes: 11 additions & 0 deletions stellar_sdk/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,14 @@ def from_xdr_object(cls, tx_xdr_object) -> "Transaction":
fee=fee,
operations=operations,
)

@classmethod
def from_xdr(cls, xdr: str) -> "Transaction":
"""Create a new :class:`Transaction` from an XDR string.

:param xdr: The XDR string that represents a transaction.

:return: A new :class:`TransactionEnvelope` object from the given XDR TransactionEnvelope base64 string object.
"""
xdr_object = Xdr.types.Transaction.from_xdr(xdr)
return cls.from_xdr_object(xdr_object)
Loading