Skip to content

Commit

Permalink
Merge pull request #52 from fjarri/return-from-transact
Browse files Browse the repository at this point in the history
Add "return value"-like functionality to `transact()`, based on events
  • Loading branch information
fjarri authored Sep 25, 2023
2 parents 4a1db2a + 2727829 commit 38d615c
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 2 deletions.
7 changes: 7 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ Changelog
0.7.1 (unreleased)
~~~~~~~~~~~~~~~~~~

Added
^^^^^

- `Client.transact()` takes an optional `return_events` argument, allowing one to get "return values" from the transaction via events. (PR_52_)


Fixed
^^^^^

- Process unnamed arguments in JSON entries correctly (as positional arguments). (PR_51_)


.. _PR_51: https://github.com/fjarri/pons/pull/51
.. _PR_52: https://github.com/fjarri/pons/pull/52


0.7.0 (09-07-2023)
Expand Down
39 changes: 38 additions & 1 deletion pons/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Dict,
List,
Tuple,
Sequence,
Callable,
Mapping,
TypeVar,
Expand All @@ -36,6 +37,7 @@
BoundConstructorCall,
BoundMethodCall,
BoundEventFilter,
BoundEvent,
)
from ._contract_abi import EventFilter, PANIC_ERROR, LEGACY_ERROR, UnknownError, ContractABI, Error
from ._provider import (
Expand Down Expand Up @@ -697,13 +699,19 @@ async def transact(
call: BoundMethodCall,
amount: Amount = Amount(0),
gas: Optional[int] = None,
) -> None:
return_events: Optional[Sequence[BoundEvent]] = None,
) -> Dict[BoundEvent, List[Dict[str, Any]]]:
"""
Transacts with the contract using a prepared method call.
If ``gas`` is ``None``, the required amount of gas is estimated first,
otherwise the provided value is used.
Waits for the transaction to be confirmed.
If any bound events are given in `return_events`, the provider will be queried
for any firing of these events originating from the hash of the completed transaction
(from the contract addresses the events are bound to),
and the results will be returned as a dictionary keyed by the corresponding event object.
Raises :py:class:`TransactionFailed` if the transaction was submitted successfully,
but could not be processed.
If gas estimation is run, see the additional errors that may be raised in the docs for
Expand All @@ -714,6 +722,35 @@ async def transact(
if not receipt.succeeded:
raise TransactionFailed(f"Transact failed (receipt: {receipt})")

if return_events is None:
return {}

results = {}
for event in return_events:
event_filter = event()
log_filter = await self.eth_new_filter(
source=event_filter.contract_address,
event_filter=EventFilter(event_filter.topics),
from_block=receipt.block_number,
to_block=receipt.block_number,
)
log_entries = await self.eth_get_filter_changes(log_filter)
event_results = []
for log_entry in log_entries:
# We can't ensure it statically, since `eth_getFilterChanges` return type depends
# on the filter passed to it.
log_entry = cast(LogEntry, log_entry)

if log_entry.transaction_hash != receipt.transaction_hash:
continue

decoded = event_filter.decode_log_entry(log_entry)
event_results.append(decoded)

results[event] = event_results

return results

@rpc_call("eth_newBlockFilter")
async def eth_new_block_filter(self) -> BlockFilter:
"""
Expand Down
16 changes: 15 additions & 1 deletion tests/TestClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,22 @@ contract BasicContract {
function deposit2(bytes4 id) public payable {
emit Deposit2(msg.sender, id, msg.value, msg.value + 1);
}
}

event Event1(
uint32 indexed value
);

event Event2(
uint32 value
);

function emitMultipleEvents(uint32 x) public {
emit Event1(x);
emit Event1(x + 1);
emit Event2(x + 2);
emit Event2(x + 3);
}
}

contract PayableConstructor {
uint256 public state;
Expand Down
55 changes: 55 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,61 @@ async def test_transact(test_provider, session, compiled_contracts, root_signer)
await session.transact(root_signer, deployed_contract.method.faultySetState(0), gas=300000)


async def test_transact_and_return_events(
autojump_clock, test_provider, session, compiled_contracts, root_signer, another_signer
):
await session.transfer(root_signer, another_signer.address, Amount.ether(1))

basic_contract = compiled_contracts["BasicContract"]

deployed_contract = await session.deploy(root_signer, basic_contract.constructor(123))

Event1 = deployed_contract.event.Event1
Event2 = deployed_contract.event.Event2

results_for = lambda x: {
Event1: [{"value": x}, {"value": x + 1}],
Event2: [{"value": x + 2}, {"value": x + 3}],
}

# Normal operation: one relevant transaction in the block

x = 1
result = await session.transact(
root_signer,
deployed_contract.method.emitMultipleEvents(x),
return_events=[Event1, Event2],
)
assert result == results_for(x)

# Two transactions for the same method in the same block -
# we need to be able to only pick up the results from the relevant transaction receipt

test_provider.disable_auto_mine_transactions()

results = {}

async def transact(signer, x):
result = await session.transact(
signer, deployed_contract.method.emitMultipleEvents(x), return_events=[Event1, Event2]
)
results[x] = result

async def delayed_enable_mining():
await trio.sleep(5)
test_provider.enable_auto_mine_transactions()

x1 = 1
x2 = 2
async with trio.open_nursery() as nursery:
nursery.start_soon(transact, root_signer, x1)
nursery.start_soon(transact, another_signer, x2)
nursery.start_soon(delayed_enable_mining)

assert results[x1] == results_for(x1)
assert results[x2] == results_for(x2)


async def test_get_block(test_provider, session, root_signer, another_signer):
to_transfer = Amount.ether(10)
await session.transfer(root_signer, another_signer.address, to_transfer)
Expand Down

0 comments on commit 38d615c

Please sign in to comment.