Skip to content
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
28 changes: 17 additions & 11 deletions src/story_protocol_python_sdk/utils/transaction_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# src/story_protcol_python_sdk/utils/transaction_utils.py

from web3 import Web3

TRANSACTION_TIMEOUT = 300
Expand All @@ -19,9 +17,16 @@ def build_and_send_transaction(
:param account: The account to use for signing the transaction.
:param client_function: The client function to build the transaction.
:param client_args: Arguments to pass to the client function.
:param tx_options dict: Optional transaction options. Can include 'nonce' to use a custom nonce value.
If not provided, nonce will be fetched from web3.eth.get_transaction_count().
:return dict: A dictionary with the transaction hash and receipt, or encoded data if encodedTxDataOnly is True.
:param tx_options dict: Optional transaction options. Can include:
- 'nonce': Custom nonce value (int). If not provided, nonce will be fetched from web3.eth.get_transaction_count().
- 'wait_for_receipt': Whether to wait for transaction receipt (bool, default True).
- 'timeout': Custom timeout in seconds for waiting for receipt (int/float, default TRANSACTION_TIMEOUT).
- 'encodedTxDataOnly': If True, returns encoded transaction data without sending.
- 'value': Transaction value in wei.
- 'gasPrice': Gas price in gwei.
- 'maxFeePerGas': Max fee per gas in wei.
:return dict: A dictionary with the transaction hash and optionally receipt (if wait_for_receipt is True),
or encoded data if encodedTxDataOnly is True.
:raises Exception: If there is an error during the transaction process.
"""
try:
Expand All @@ -43,7 +48,6 @@ def build_and_send_transaction(
account.address
)

# Add value if it exists in tx_options
if "value" in tx_options:
transaction_options["value"] = tx_options["value"]

Expand All @@ -56,18 +60,20 @@ def build_and_send_transaction(

transaction = client_function(*client_args, transaction_options)

# If encodedTxDataOnly is True, return the transaction data without sending
if tx_options.get("encodedTxDataOnly"):
return {"encodedTxData": transaction}

signed_txn = account.sign_transaction(transaction)
tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction)

tx_receipt = web3.eth.wait_for_transaction_receipt(
tx_hash, timeout=TRANSACTION_TIMEOUT
)
wait_for_receipt = tx_options.get("wait_for_receipt", True)

return {"tx_hash": tx_hash.hex(), "tx_receipt": tx_receipt}
if wait_for_receipt:
timeout = tx_options.get("timeout", TRANSACTION_TIMEOUT)
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
return {"tx_hash": tx_hash.hex(), "tx_receipt": tx_receipt}
else:
return {"tx_hash": tx_hash.hex()}

except Exception as e:
raise e
181 changes: 180 additions & 1 deletion tests/integration/test_integration_transaction_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# tests/integration/test_integration_transaction_utils.py
import time

import pytest
from web3.exceptions import TimeExhausted

from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction

Expand Down Expand Up @@ -122,3 +123,181 @@ def approve_func(tx_options):
assert result["tx_receipt"]["status"] == 1
tx = web3.eth.get_transaction(result["tx_hash"])
assert tx["nonce"] == current_nonce

def test_wait_for_receipt_false_returns_only_tx_hash(self):
"""Test that wait_for_receipt=False returns immediately with only tx_hash."""

def create_transfer_tx(to_address, value):
def build_tx(tx_options):
return {
"to": to_address,
"value": value,
"data": "0x",
"gas": 21000,
"gasPrice": web3.eth.gas_price,
"chainId": 1315,
**tx_options,
}

return build_tx

tx_func = create_transfer_tx(account.address, 0)
result = build_and_send_transaction(
web3, account, tx_func, tx_options={"wait_for_receipt": False}
)
assert "tx_hash" in result
assert "tx_receipt" not in result
assert len(result["tx_hash"]) == 64

tx_receipt = web3.eth.wait_for_transaction_receipt(result["tx_hash"])
assert tx_receipt["status"] == 1

def test_wait_for_receipt_true_returns_receipt(self):
"""Test that wait_for_receipt=True (default) returns both tx_hash and receipt."""

def create_transfer_tx(to_address, value):
def build_tx(tx_options):
return {
"to": to_address,
"value": value,
"data": "0x",
"gas": 21000,
"gasPrice": web3.eth.gas_price,
"chainId": 1315,
**tx_options,
}

return build_tx

tx_func = create_transfer_tx(account.address, 0)
result = build_and_send_transaction(
web3, account, tx_func, tx_options={"wait_for_receipt": True}
)

assert "tx_hash" in result
assert "tx_receipt" in result
assert result["tx_receipt"]["status"] == 1
assert "blockNumber" in result["tx_receipt"]
assert "gasUsed" in result["tx_receipt"]

def test_custom_timeout_with_transaction(self):
"""Test that custom timeout is used when specified."""

def create_transfer_tx(to_address, value):
def build_tx(tx_options):
return {
"to": to_address,
"value": value,
"data": "0x",
"gas": 21000,
"gasPrice": web3.eth.gas_price,
"chainId": 1315,
**tx_options,
}

return build_tx

tx_func = create_transfer_tx(account.address, 0)
result = build_and_send_transaction(
web3,
account,
tx_func,
tx_options={"wait_for_receipt": True, "timeout": 120},
)

assert "tx_hash" in result
assert "tx_receipt" in result
assert result["tx_receipt"]["status"] == 1

def test_combined_options_nonce_wait_timeout(self):
"""Test that all new options work correctly together."""
current_nonce = web3.eth.get_transaction_count(account.address)

def create_transfer_tx(to_address, value):
def build_tx(tx_options):
return {
"to": to_address,
"value": value,
"data": "0x",
"gas": 21000,
"gasPrice": web3.eth.gas_price,
"chainId": 1315,
**tx_options,
}

return build_tx

tx_func = create_transfer_tx(account.address, 0)
result = build_and_send_transaction(
web3,
account,
tx_func,
tx_options={
"nonce": current_nonce,
"wait_for_receipt": True,
"timeout": 120,
},
)

assert "tx_hash" in result
assert "tx_receipt" in result
assert result["tx_receipt"]["status"] == 1

tx = web3.eth.get_transaction(result["tx_hash"])
assert tx["nonce"] == current_nonce

def test_wait_for_receipt_false_with_contract_call(self):
"""Test wait_for_receipt=False with actual contract interaction."""
erc20_contract = web3.eth.contract(
address=MockERC20,
abi=[
{
"inputs": [
{"name": "spender", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
}
],
)

def approve_func(tx_options):
return erc20_contract.functions.approve(
account.address, 200
).build_transaction(tx_options)

time.time()
result = build_and_send_transaction(
web3, account, approve_func, tx_options={"wait_for_receipt": False}
)

assert "tx_hash" in result
assert "tx_receipt" not in result

tx_receipt = web3.eth.wait_for_transaction_receipt(result["tx_hash"])
assert tx_receipt["status"] == 1

def test_timeout_too_short_raises_exception(self):
"""Test that very short timeout raises TimeExhausted exception."""

def build_tx(tx_options):
return {
"to": account.address,
"value": 0,
"data": "0x",
"gas": 21000,
"gasPrice": web3.eth.gas_price,
"chainId": 1315,
**tx_options,
}

with pytest.raises(TimeExhausted):
build_and_send_transaction(
web3,
account,
build_tx,
tx_options={"wait_for_receipt": True, "timeout": 0.001},
)
2 changes: 1 addition & 1 deletion tests/unit/utils/test_derivative_data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest.mock import MagicMock, patch

import pytest
from _pytest.raises import raises
from pytest import raises

from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import (
IPAssetRegistryClient,
Expand Down
Loading
Loading