-
Notifications
You must be signed in to change notification settings - Fork 141
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
New Feature: Adding methods to use the simulate endpoint #420
Changes from 19 commits
7bebd3f
8ad7d0c
601abe5
a388e37
26e63e4
17a1b56
661e08c
c6105af
ffb8e64
e9398f4
26e7265
9dd2db6
c2f98f8
179c67d
7fbbb26
882052e
5382250
5148785
16f0275
8f4a4df
7d28c75
53155b8
ffef864
63a4b6a
d56aa55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -479,6 +479,71 @@ def submit(self, client: algod.AlgodClient) -> List[str]: | |
self.status = AtomicTransactionComposerStatus.SUBMITTED | ||
return self.tx_ids | ||
|
||
def simulate( | ||
self, client: algod.AlgodClient | ||
) -> "SimulateAtomicTransactionResponse": | ||
barnjamin marked this conversation as resolved.
Show resolved
Hide resolved
barnjamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Send the transaction group to the `simulate` endpoint and wait for results. | ||
An error will be thrown if submission or execution fails. | ||
The composer's status must be SUBMITTED or lower before calling this method, | ||
since execution is only allowed once. | ||
|
||
Args: | ||
client (AlgodClient): Algod V2 client | ||
|
||
Returns: | ||
SimulateAtomicTransactionResponse: Object with simulation results for this | ||
transaction group, a list of txIDs of the simulated transactions, | ||
an array of results for each method call transaction in this group. | ||
If a method has no return value (void), then the method results array | ||
will contain None for that method's return value. | ||
""" | ||
|
||
if self.status <= AtomicTransactionComposerStatus.SUBMITTED: | ||
self.gather_signatures() | ||
barnjamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
raise error.AtomicTransactionComposerError( | ||
"AtomicTransactionComposerStatus must be submitted or " | ||
"lower to simulate a group" | ||
) | ||
|
||
simulation_result: Dict[str, Any] = client.simulate_transactions( | ||
self.signed_txns | ||
) | ||
# Only take the first group in the simulate response | ||
txn_group: Dict[str, Any] = simulation_result["txn-groups"][0] | ||
|
||
# Parse out abi results | ||
txn_results = [t["txn-result"] for t in txn_group["txn-results"]] | ||
method_results = self.parse_response(txn_results) | ||
|
||
# build up data structure with fields we'd want | ||
sim_results = [] | ||
for idx, result in enumerate(method_results): | ||
sim_txn: Dict[str, Any] = txn_group["txn-results"][idx] | ||
|
||
sim_results.append( | ||
SimulateABIResult( | ||
tx_id=result.tx_id, | ||
raw_value=result.raw_value, | ||
return_value=result.return_value, | ||
decode_error=result.decode_error, | ||
tx_info=result.tx_info, | ||
method=result.method, | ||
missing_signature=sim_txn.get("missing-signature", False), | ||
) | ||
) | ||
|
||
return SimulateAtomicTransactionResponse( | ||
version=simulation_result.get("version", 0), | ||
would_succeed=simulation_result.get("would-succeed", False), | ||
failure_message=txn_group.get("failure-message", ""), | ||
failed_at=txn_group.get("failed-at"), | ||
simulate_response=simulation_result, | ||
tx_ids=self.tx_ids, | ||
results=sim_results, | ||
) | ||
|
||
def execute( | ||
self, client: algod.AlgodClient, wait_rounds: int | ||
) -> "AtomicTransactionResponse": | ||
|
@@ -517,20 +582,33 @@ def execute( | |
self.status = AtomicTransactionComposerStatus.COMMITTED | ||
|
||
confirmed_round = resp["confirmed-round"] | ||
method_results = [] | ||
|
||
for i, tx_id in enumerate(self.tx_ids): | ||
tx_results = [ | ||
client.pending_transaction_info(tx_id) for tx_id in self.tx_ids | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. caveat/nit: I don't think we need to get the txn info for every transaction, just the ones with method calls ( |
||
] | ||
|
||
method_results = self.parse_response(tx_results) | ||
|
||
return AtomicTransactionResponse( | ||
confirmed_round=confirmed_round, | ||
tx_ids=self.tx_ids, | ||
results=method_results, | ||
) | ||
|
||
def parse_response(self, txns: List[Dict[str, Any]]) -> List["ABIResult"]: | ||
|
||
method_results = [] | ||
for i, tx_info in enumerate(txns): | ||
tx_id = self.tx_ids[i] | ||
raw_value: Optional[bytes] = None | ||
return_value = None | ||
decode_error = None | ||
tx_info: Optional[Any] = None | ||
|
||
if i not in self.method_dict: | ||
continue | ||
|
||
# Parse log for ABI method return value | ||
try: | ||
tx_info = client.pending_transaction_info(tx_id) | ||
if self.method_dict[i].returns.type == abi.Returns.VOID: | ||
method_results.append( | ||
ABIResult( | ||
|
@@ -569,21 +647,18 @@ def execute( | |
except Exception as e: | ||
decode_error = e | ||
|
||
abi_result = ABIResult( | ||
tx_id=tx_id, | ||
raw_value=cast(bytes, raw_value), | ||
return_value=return_value, | ||
decode_error=decode_error, | ||
tx_info=cast(Any, tx_info), | ||
method=self.method_dict[i], | ||
method_results.append( | ||
ABIResult( | ||
tx_id=tx_id, | ||
raw_value=cast(bytes, raw_value), | ||
return_value=return_value, | ||
decode_error=decode_error, | ||
tx_info=tx_info, | ||
method=self.method_dict[i], | ||
) | ||
) | ||
method_results.append(abi_result) | ||
|
||
return AtomicTransactionResponse( | ||
confirmed_round=confirmed_round, | ||
tx_ids=self.tx_ids, | ||
results=method_results, | ||
) | ||
return method_results | ||
|
||
|
||
class TransactionSigner(ABC): | ||
|
@@ -740,3 +815,43 @@ def __init__( | |
self.confirmed_round = confirmed_round | ||
self.tx_ids = tx_ids | ||
self.abi_results = results | ||
|
||
|
||
class SimulateABIResult(ABIResult): | ||
def __init__( | ||
self, | ||
tx_id: str, | ||
raw_value: bytes, | ||
return_value: Any, | ||
decode_error: Optional[Exception], | ||
tx_info: dict, | ||
method: abi.Method, | ||
missing_signature: bool, | ||
) -> None: | ||
self.tx_id = tx_id | ||
self.raw_value = raw_value | ||
self.return_value = return_value | ||
self.decode_error = decode_error | ||
self.tx_info = tx_info | ||
self.method = method | ||
self.missing_signature = missing_signature | ||
|
||
|
||
class SimulateAtomicTransactionResponse: | ||
def __init__( | ||
self, | ||
version: int, | ||
would_succeed: bool, | ||
failure_message: str, | ||
failed_at: Optional[List[int]], | ||
simulate_response: Dict[str, Any], | ||
tx_ids: List[str], | ||
results: List[SimulateABIResult], | ||
) -> None: | ||
self.version = version | ||
self.would_succeed = would_succeed | ||
self.failure_message = failure_message | ||
self.failed_at = failed_at | ||
self.simulate_response = simulate_response | ||
self.tx_ids = tx_ids | ||
self.abi_results = results |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ | |
@rekey_v1 | ||
@send | ||
@send.keyregtxn | ||
@simulate |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -19,6 +19,10 @@ | |||||
source_map, | ||||||
transaction, | ||||||
) | ||||||
|
||||||
from algosdk.atomic_transaction_composer import ( | ||||||
SimulateAtomicTransactionResponse, | ||||||
) | ||||||
from algosdk.error import AlgodHTTPError | ||||||
from algosdk.testing.dryrun import DryrunTestCaseMixin | ||||||
from algosdk.v2client import * | ||||||
|
@@ -1414,3 +1418,44 @@ def transaction_proof(context, round, txid, hashtype): | |||||
@when("we make a Lookup Block Hash call against round {round}") | ||||||
def get_block_hash(context, round): | ||||||
context.response = context.acl.get_block_hash(round) | ||||||
|
||||||
|
||||||
@when("I simulate the transaction") | ||||||
def simulate_transaction(context): | ||||||
context.simulate_response = context.app_acl.simulate_transactions( | ||||||
[context.stx] | ||||||
) | ||||||
|
||||||
|
||||||
@then("the simulation should succeed without any failure message") | ||||||
def simulate_transaction_succeed(context): | ||||||
if hasattr(context, "simulate_response"): | ||||||
assert context.simulate_response["would-succeed"] is True | ||||||
barnjamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
else: | ||||||
assert context.simulate_atc_response.would_succeed is True | ||||||
|
||||||
|
||||||
@then("I simulate the current transaction group with the composer") | ||||||
def simulate_atc(context): | ||||||
context.simulate_atc_response = ( | ||||||
context.atomic_transaction_composer.simulate(context.app_acl) | ||||||
) | ||||||
|
||||||
|
||||||
@then( | ||||||
'the simulation should report a failure at group "{group}", path "{path}" with message "{message}"' | ||||||
) | ||||||
def simulate_atc_failure(context, group, path, message): | ||||||
resp: SimulateAtomicTransactionResponse = context.simulate_atc_response | ||||||
group_idx: int = int(group) | ||||||
fail_path = ",".join( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
[ | ||||||
str(pe) | ||||||
for pe in resp.simulate_response["txn-groups"][group_idx][ | ||||||
"failed-at" | ||||||
] | ||||||
] | ||||||
) | ||||||
assert resp.would_succeed is False | ||||||
assert fail_path == path | ||||||
assert message in resp.failure_message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remember to change this back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i looked at the trap ray
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#445