Skip to content

Commit

Permalink
fix: Corrected the issue where TransactionBuilder.from_xdr and `Fee…
Browse files Browse the repository at this point in the history
…BumpTransactionEnvelope.from_xdr` could not properly parse transactions containing Soroban operations. (#954)

- fix: Corrected the issue where `TransactionBuilder.from_xdr` could not properly parse transactions containing Soroban operations.
- fix: Corrected the issue where `FeeBumpTransactionEnvelope.from_xdr` could not properly parse transactions containing Soroban operations.
- refactor: `TransactionBuilder.from_xdr` previously could return `TransactionBuilder` or `FeeBumpTransactionEnvelope`. Now it will no longer return `TransactionBuilder`, but will return `TransactionEnvelope` or `FeeBumpTransactionEnvelope`.
- feat: `TransactionBuilder.build_fee_bump_transaction` now supports transactions containing Soroban operations.
  • Loading branch information
overcat authored Jul 16, 2024
1 parent ea3450e commit d41352c
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 112 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@ Release History
==============

### Pending
feat: `SorobanServer.send_transaction` supports sending FeeBumpTransactionEnvelope.
#### Update
- feat: `SorobanServer.send_transaction` supports sending FeeBumpTransactionEnvelope.
- fix: Corrected the issue where `TransactionBuilder.from_xdr` could not properly parse transactions containing Soroban operations.
- fix: Corrected the issue where `FeeBumpTransactionEnvelope.from_xdr` could not properly parse transactions containing Soroban operations.
- refactor: `TransactionBuilder.from_xdr` previously could return `TransactionBuilder` or `FeeBumpTransactionEnvelope`. Now it will no longer return `TransactionBuilder`, but will return `TransactionEnvelope` or `FeeBumpTransactionEnvelope`.
- feat: `TransactionBuilder.build_fee_bump_transaction` now supports transactions containing Soroban operations.

#### Breaking changes
- refactor: `FeeBumpTransactionEnvelope.base_fee` has been removed. Please use `FeeBumpTransactionEnvelope.fee instead`. Note that their meanings are different:
- `FeeBumpTransactionEnvelope.base_fee` represented the maximum fee you were willing to pay per operation for this transaction.
- `FeeBumpTransactionEnvelope.fee` represents the maximum fee you are willing to pay for this transaction.
- refactor: `TransactionBuilder.from_xdr` previously could return `TransactionBuilder` or `FeeBumpTransactionEnvelope`. Now it will no longer return `TransactionBuilder`, but will return `TransactionEnvelope` or `FeeBumpTransactionEnvelope`.
- refactor: `helpers.parse_transaction_envelope_from_xdr` has been marked as deprecated. Please use the refactored `TransactionEnvelope.from_xdr` instead.

### Version 10.0.0

Expand Down
5 changes: 5 additions & 0 deletions stellar_sdk/base_soroban_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def _assemble_transaction(
)
te = copy.deepcopy(transaction_envelope)
te.signatures = []

# Reset fee to the original value, excluding the resource fee
if te.transaction.soroban_data:
te.transaction.fee -= te.transaction.soroban_data.resource_fee.int64

assert min_resource_fee is not None
te.transaction.fee += min_resource_fee
te.transaction.soroban_data = soroban_data
Expand Down
32 changes: 8 additions & 24 deletions stellar_sdk/fee_bump_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
from .transaction import Transaction
from .transaction_envelope import TransactionEnvelope

BASE_FEE = 100

__all__ = ["FeeBumpTransaction"]


Expand All @@ -19,14 +17,14 @@ class FeeBumpTransaction:
See `CAP-0015 <https://github.com/stellar/stellar-protocol/blob/master/core/cap-0015.md>`__ for more information.
:param fee_source: The account paying for the transaction.
:param base_fee: The max fee willing to pay per operation in inner transaction (**in stroops**).
:param fee: The max fee willing to pay for the transaction (**in stroops**).
:param inner_transaction_envelope: The TransactionEnvelope to be bumped by the fee bump transaction.
"""

def __init__(
self,
fee_source: Union[MuxedAccount, Keypair, str],
base_fee: int,
fee: int,
inner_transaction_envelope: TransactionEnvelope,
) -> None:
if isinstance(fee_source, str):
Expand All @@ -35,39 +33,29 @@ def __init__(
fee_source = MuxedAccount.from_account(fee_source.public_key)

self.fee_source: MuxedAccount = fee_source
self.base_fee: int = base_fee
self.fee: int = fee
self.inner_transaction_envelope: TransactionEnvelope = (
inner_transaction_envelope.to_transaction_envelope_v1()
)
self._inner_transaction: Transaction = (
self.inner_transaction_envelope.transaction
)

inner_operations_length: int = len(self._inner_transaction.operations)
inner_base_fee_rate: int = int(
self._inner_transaction.fee / inner_operations_length
)
if self.base_fee < inner_base_fee_rate or self.base_fee < BASE_FEE:
raise ValueError(
f"Invalid `base_fee`, it should be at least {self._inner_transaction.fee if self._inner_transaction.fee > BASE_FEE else BASE_FEE} stroops."
)

def to_xdr_object(self) -> stellar_xdr.FeeBumpTransaction:
"""Get an XDR object representation of this :class:`FeeBumpTransaction`.
:return: XDR Transaction object
"""

fee_source = self.fee_source.to_xdr_object()
fee = self.base_fee * (len(self._inner_transaction.operations) + 1)
ext = stellar_xdr.FeeBumpTransactionExt(0)
inner_tx = stellar_xdr.FeeBumpTransactionInnerTx(
type=stellar_xdr.EnvelopeType.ENVELOPE_TYPE_TX,
v1=self.inner_transaction_envelope.to_xdr_object().v1,
)
return stellar_xdr.FeeBumpTransaction(
fee_source=fee_source,
fee=stellar_xdr.Int64(fee),
fee=stellar_xdr.Int64(self.fee),
inner_tx=inner_tx,
ext=ext,
)
Expand All @@ -90,13 +78,9 @@ def from_xdr_object(
inner_transaction_envelope = TransactionEnvelope.from_xdr_object(
te, network_passphrase
)
inner_transaction_operation_length = len(
inner_transaction_envelope.transaction.operations
)
base_fee = int(xdr_object.fee.int64 / (inner_transaction_operation_length + 1))
tx = cls(
fee_source=source,
base_fee=base_fee,
fee=xdr_object.fee.int64,
inner_transaction_envelope=inner_transaction_envelope,
)
return tx
Expand All @@ -117,7 +101,7 @@ def __hash__(self):
return hash(
(
self.fee_source,
self.base_fee,
self.fee,
self.inner_transaction_envelope,
)
)
Expand All @@ -127,12 +111,12 @@ def __eq__(self, other: object) -> bool:
return NotImplemented
return (
self.fee_source == other.fee_source
and self.base_fee == other.base_fee
and self.fee == other.fee
and self.inner_transaction_envelope == other.inner_transaction_envelope
)

def __repr__(self):
return (
f"<FeeBumpTransaction [fee_source={self.fee_source}, "
f"base_fee={self.base_fee}, inner_transaction_envelope={self.inner_transaction_envelope}]>"
f"fee={self.fee}, inner_transaction_envelope={self.inner_transaction_envelope}]>"
)
5 changes: 5 additions & 0 deletions stellar_sdk/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from typing import Union

from .fee_bump_transaction_envelope import FeeBumpTransactionEnvelope
Expand Down Expand Up @@ -29,6 +30,10 @@ def parse_transaction_envelope_from_xdr(
:raises: :exc:`ValueError <stellar_sdk.exceptions.ValueError>` - XDR is neither :py:class:`TransactionEnvelope <stellar_sdk.transaction_envelope.TransactionEnvelope>`
nor :py:class:`FeeBumpTransactionEnvelope <stellar_sdk.fee_bump_transaction_envelope.FeeBumpTransactionEnvelope>`
"""
warnings.warn(
"This function is deprecated. Use `TransactionBuilder.from_xdr` instead.",
DeprecationWarning,
)
if FeeBumpTransactionEnvelope.is_fee_bump_transaction_envelope(xdr):
return FeeBumpTransactionEnvelope.from_xdr(xdr, network_passphrase)
return TransactionEnvelope.from_xdr(xdr, network_passphrase)
5 changes: 2 additions & 3 deletions stellar_sdk/sep/txrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def to_txrep(
)
_add_line(
"feeBump.tx.fee",
fee_bump_transaction.base_fee * (len(transaction.operations) + 1),
fee_bump_transaction.fee,
lines,
)
_add_line(
Expand Down Expand Up @@ -182,13 +182,12 @@ def from_txrep(
if is_fee_bump:
fee_bump_fee_source = _get_value(raw_data_map, "feeBump.tx.feeSource")
fee_bump_fee = _get_int_value(raw_data_map, "feeBump.tx.fee")
fee_bump_base_fee = int(fee_bump_fee / (len(operations) + 1))

fee_bump_transaction_signatures = _get_signatures(raw_data_map, "feeBump.")

fee_bump_transaction = FeeBumpTransaction(
fee_source=fee_bump_fee_source,
base_fee=fee_bump_base_fee,
fee=fee_bump_fee,
inner_transaction_envelope=transaction_envelope,
)
fee_bump_transaction_envelope = FeeBumpTransactionEnvelope(
Expand Down
109 changes: 55 additions & 54 deletions stellar_sdk/transaction_builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import binascii
import math
import os
import time
import warnings
Expand Down Expand Up @@ -32,6 +33,8 @@

__all__ = ["TransactionBuilder"]

MIN_BASE_FEE = 100


class TransactionBuilder:
"""Transaction builder helps constructs a new :class:`TransactionEnvelope
Expand Down Expand Up @@ -96,7 +99,7 @@ def __init__(
self,
source_account: Account,
network_passphrase: str = Network.TESTNET_NETWORK_PASSPHRASE,
base_fee: int = 100,
base_fee: int = MIN_BASE_FEE,
v1: bool = True,
):
self.source_account: Account = source_account
Expand Down Expand Up @@ -138,10 +141,13 @@ def build(self) -> TransactionEnvelope:
min_sequence_ledger_gap=self.min_sequence_ledger_gap,
extra_signers=self.extra_signers,
)
fee = self.base_fee * len(self.operations)
if self.soroban_data:
fee += self.soroban_data.resource_fee.int64
transaction = Transaction(
source=source,
sequence=sequence,
fee=self.base_fee * len(self.operations),
fee=fee,
operations=self.operations,
memo=self.memo,
preconditions=preconditions,
Expand Down Expand Up @@ -173,9 +179,36 @@ def build_fee_bump_transaction(
:param network_passphrase: The network to connect to for verifying and retrieving additional attributes from.
:return: a :class:`TransactionBuilder <stellar_sdk.transaction_envelope.TransactionBuilder>` via the XDR object.
"""

if base_fee < MIN_BASE_FEE:
raise ValueError(
f"Invalid `base_fee`, it should be at least {MIN_BASE_FEE} stroops."
)

soroban_resource_fee = 0
if inner_transaction_envelope.transaction.soroban_data:
soroban_resource_fee = (
inner_transaction_envelope.transaction.soroban_data.resource_fee.int64
)

inner_include_fee = (
inner_transaction_envelope.transaction.fee - soroban_resource_fee
) # dont include soroban resource fee
inner_base_fee = math.ceil(
inner_include_fee / len(inner_transaction_envelope.transaction.operations)
)

if base_fee < inner_base_fee:
raise ValueError(
f"Invalid `base_fee`, it should be at least {inner_base_fee} stroops."
)

fee = base_fee * (len(inner_transaction_envelope.transaction.operations) + 1)
fee += soroban_resource_fee

fee_bump_transaction = FeeBumpTransaction(
fee_source=fee_source,
base_fee=base_fee,
fee=fee,
inner_transaction_envelope=inner_transaction_envelope,
)
transaction_envelope = FeeBumpTransactionEnvelope(
Expand All @@ -187,61 +220,29 @@ def build_fee_bump_transaction(
@staticmethod
def from_xdr(
xdr: str, network_passphrase: str
) -> Union["TransactionBuilder", FeeBumpTransactionEnvelope]:
"""Create a :class:`TransactionBuilder
<stellar_sdk.transaction_envelope.TransactionBuilder>` or
:py:class:`FeeBumpTransactionEnvelope <stellar_sdk.fee_bump_transaction_envelope.FeeBumpTransactionEnvelope>`
via an XDR object.
) -> Union[TransactionEnvelope, FeeBumpTransactionEnvelope]:
"""When you are not sure whether your XDR belongs to
:py:class:`TransactionEnvelope <stellar_sdk.transaction_envelope.TransactionEnvelope>`
or :py:class:`FeeBumpTransactionEnvelope <stellar_sdk.fee_bump_transaction_envelope.FeeBumpTransactionEnvelope>`,
you can use this function.
.. warning::
I don't recommend you to use this function,
because it loses its signature when constructing TransactionBuilder.
Please use :py:func:`stellar_sdk.helpers.parse_transaction_envelope_from_xdr` instead.
An example::
In addition, if xdr is not of
:py:class:`TransactionEnvelope <stellar_sdk.transaction_envelope.TransactionEnvelope>`,
it sets the fields of this builder (the transaction envelope,
transaction, operations, source, etc.) to all of the fields in the
provided XDR transaction envelope, but the signature will not be added to it.
from stellar_sdk import Network, TransactionBuilder
:param xdr: The XDR object representing the transaction envelope to
which this builder is setting its state to.
:param network_passphrase: The network to connect to for verifying and retrieving additional attributes from.
:return: a :class:`TransactionBuilder <stellar_sdk.transaction_envelope.TransactionBuilder>` or
:py:class:`FeeBumpTransactionEnvelope <stellar_sdk.fee_bump_transaction_envelope.FeeBumpTransactionEnvelope>`
via the XDR object.
"""
xdr_object = stellar_xdr.TransactionEnvelope.from_xdr(xdr)
te_type = xdr_object.type
if te_type == stellar_xdr.EnvelopeType.ENVELOPE_TYPE_TX_FEE_BUMP:
return FeeBumpTransactionEnvelope.from_xdr_object(
xdr_object, network_passphrase
)
transaction_envelope = TransactionEnvelope.from_xdr(
xdr=xdr, network_passphrase=network_passphrase
)
xdr = "AAAAAgAAAADHJNEDn33/C1uDkDfzDfKVq/4XE9IxDfGiLCfoV7riZQAAA+gCI4TVABpRPgAAAAAAAAAAAAAAAQAAAAAAAAADAAAAAUxpcmEAAAAAabIaDgm0ypyJpsVfEjZw2mO3Enq4Q4t5URKfWtqukSUAAAABVVNEAAAAAADophqGHmCvYPgHc+BjRuXHLL5Z3K3aN2CNWO9CUR2f3AAAAAAAAAAAE8G9mAADcH8AAAAAMYdBWgAAAAAAAAABV7riZQAAAEARGCGwYk/kEB2Z4UL20y536evnwmmSc4c2FnxlvUcPZl5jgWHcNwY8LTpFhdrUN9TZWciCRp/JCZYa0SJh8cYB"
te = TransactionBuilder.from_xdr(xdr, Network.PUBLIC_NETWORK_PASSPHRASE)
print(te)
source_account = Account(
transaction_envelope.transaction.source,
transaction_envelope.transaction.sequence - 1,
)
transaction_builder = TransactionBuilder(
source_account=source_account,
network_passphrase=network_passphrase,
base_fee=int(
transaction_envelope.transaction.fee
/ len(transaction_envelope.transaction.operations)
),
v1=transaction_envelope.transaction.v1,
)
if transaction_envelope.transaction.preconditions:
transaction_builder.time_bounds = (
transaction_envelope.transaction.preconditions.time_bounds
)
transaction_builder.operations = transaction_envelope.transaction.operations
transaction_builder.memo = transaction_envelope.transaction.memo
transaction_builder.soroban_data = transaction_envelope.transaction.soroban_data
return transaction_builder
:param xdr: Transaction envelope XDR
:param network_passphrase: The network to connect to for verifying and retrieving
additional attributes from. (ex. ``"Public Global Stellar Network ; September 2015"``)
:raises: :exc:`ValueError <stellar_sdk.exceptions.ValueError>` - XDR is neither :py:class:`TransactionEnvelope <stellar_sdk.transaction_envelope.TransactionEnvelope>`
nor :py:class:`FeeBumpTransactionEnvelope <stellar_sdk.fee_bump_transaction_envelope.FeeBumpTransactionEnvelope>`
"""
if FeeBumpTransactionEnvelope.is_fee_bump_transaction_envelope(xdr):
return FeeBumpTransactionEnvelope.from_xdr(xdr, network_passphrase)
return TransactionEnvelope.from_xdr(xdr, network_passphrase)

def add_time_bounds(self, min_time: int, max_time: int) -> "TransactionBuilder":
"""Sets a timeout precondition on the transaction.
Expand Down
Loading

0 comments on commit d41352c

Please sign in to comment.