-
Notifications
You must be signed in to change notification settings - Fork 144
Dryrun response #283
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
Dryrun response #283
Changes from all commits
7b62bab
6785baa
6bb2533
1e4990f
598a5b6
062ce94
072e947
508025b
0d2f81c
4ce2a14
8815d72
0fa8d8e
dd1b093
31d49f1
d1083ce
c010e75
efb281e
33bb685
5e2ef10
1e35187
edd8328
69a891a
357e1c6
0e036ec
89182e8
c01d415
07eca10
ede274a
5287739
eaebf36
f661cca
fa9211a
83dca7e
2972067
3dfe4c4
48ffbda
a81d5b8
8190e2b
0d2620c
e9022c1
bd2a723
ed3605a
0185f87
5181e2d
8b54442
78aa402
8ded7ec
371875f
846e7e9
5efa115
b7fb15c
d336364
8e562b9
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 |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| import base64 | ||
| from typing import List | ||
|
|
||
|
|
||
| class StackPrinterConfig: | ||
| DEFAULT_MAX_VALUE_WIDTH: int = 30 | ||
|
|
||
| def __init__( | ||
| self, max_value_width=DEFAULT_MAX_VALUE_WIDTH, top_of_stack_first=True | ||
| ): | ||
| self.max_value_width = max_value_width | ||
| self.top_of_stack_first = top_of_stack_first | ||
|
|
||
|
|
||
| class DryrunResponse: | ||
| def __init__(self, drrjson: dict): | ||
| for param in ["error", "protocol-version", "txns"]: | ||
| assert ( | ||
| param in drrjson | ||
| ), f"expecting dryrun response object to have key '{param}' but it is missing" | ||
|
|
||
| # These are all required fields | ||
| self.error = drrjson["error"] | ||
| self.protocol = drrjson["protocol-version"] | ||
| self.txns = [DryrunTransactionResult(txn) for txn in drrjson["txns"]] | ||
|
|
||
|
|
||
| class DryrunTransactionResult: | ||
| def __init__(self, dr): | ||
| assert ( | ||
| "disassembly" in dr | ||
| ), "expecting dryrun transaction result to have key 'disassembly' but its missing" | ||
|
|
||
| self.disassembly = dr["disassembly"] | ||
barnjamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| optionals = [ | ||
| "app-call-messages", | ||
| "local-deltas", | ||
| "global-delta", | ||
| "cost", | ||
| "logic-sig-messages", | ||
| "logic-sig-disassembly", | ||
| "logs", | ||
| ] | ||
|
|
||
| def attrname(field): | ||
| return field.replace("-", "_") | ||
|
|
||
| for field in optionals: | ||
| setattr(self, attrname(field), dr.get(field)) | ||
|
|
||
| traces = ["app-call-trace", "logic-sig-trace"] | ||
| for trace_field in traces: | ||
| if trace_field in dr: | ||
| setattr( | ||
| self, | ||
| attrname(trace_field), | ||
| DryrunTrace(dr[trace_field]), | ||
| ) | ||
|
|
||
| def app_call_rejected(self) -> bool: | ||
| return ( | ||
| False | ||
| if self.app_call_messages is None | ||
| else "REJECT" in self.app_call_messages | ||
| ) | ||
|
|
||
| def logic_sig_rejected(self) -> bool: | ||
| if self.logic_sig_messages is not None: | ||
| return "REJECT" in self.logic_sig_messages | ||
| return False | ||
|
|
||
| @classmethod | ||
| def trace( | ||
| cls, | ||
| dr_trace: "DryrunTrace", | ||
| disassembly: List[str], | ||
| spc: StackPrinterConfig, | ||
| ) -> str: | ||
|
|
||
| # 16 for length of the header up to spaces | ||
| lines = [["pc#", "ln#", "source", "scratch", "stack"]] | ||
| for idx in range(len(dr_trace.trace)): | ||
|
|
||
| trace_line = dr_trace.trace[idx] | ||
|
|
||
| src = disassembly[trace_line.line] | ||
| if trace_line.error != "": | ||
| src = "!! {} !!".format(trace_line.error) | ||
|
|
||
| prev_scratch = [] | ||
| if idx > 0: | ||
| prev_scratch = dr_trace.trace[idx - 1].scratch | ||
|
|
||
| scratch = scratch_to_string(prev_scratch, trace_line.scratch) | ||
| stack = stack_to_string(trace_line.stack, spc.top_of_stack_first) | ||
| lines.append( | ||
| [ | ||
| "{}".format(trace_line.pc), | ||
| "{}".format(trace_line.line), | ||
| truncate(src, spc.max_value_width), | ||
| truncate(scratch, spc.max_value_width), | ||
| truncate(stack, spc.max_value_width), | ||
| ] | ||
| ) | ||
|
|
||
| cols = len(lines[0]) | ||
| max_widths = [0] * cols | ||
| for line in lines: | ||
| for i in range(cols): | ||
| if len(line[i]) > max_widths[i]: | ||
| max_widths[i] = len(line[i]) | ||
|
|
||
| trace = [] | ||
| for line in lines: | ||
| trace.append( | ||
| " |".join( | ||
| [str(line[i]).ljust(max_widths[i]) for i in range(cols)] | ||
| ).strip() | ||
| ) | ||
|
|
||
| return "\n".join(trace) + "\n" | ||
|
|
||
| def app_trace(self, spc: StackPrinterConfig = None) -> str: | ||
| if not hasattr(self, "app_call_trace"): | ||
| return "" | ||
|
|
||
| if spc == None: | ||
| spc = StackPrinterConfig(top_of_stack_first=False) | ||
|
|
||
| return self.trace(self.app_call_trace, self.disassembly, spc=spc) | ||
|
|
||
| def lsig_trace(self, spc: StackPrinterConfig = None) -> str: | ||
| if not hasattr(self, "logic_sig_trace"): | ||
| return "" | ||
|
|
||
| if getattr(self, "logic_sig_disassembly", None) is None: | ||
| return "" | ||
|
|
||
| if spc is None: | ||
| spc = StackPrinterConfig(top_of_stack_first=False) | ||
|
|
||
| return self.trace( | ||
| self.logic_sig_trace, self.logic_sig_disassembly, spaces=spc | ||
| ) | ||
|
|
||
|
|
||
| class DryrunTrace: | ||
| def __init__(self, trace: List[dict]): | ||
| self.trace = [DryrunTraceLine(line) for line in trace] | ||
|
|
||
| def get_trace(self) -> List[str]: | ||
| return [line.trace_line() for line in self.trace] | ||
|
|
||
|
|
||
| class DryrunTraceLine: | ||
| def __init__(self, tl): | ||
| self.line = tl["line"] | ||
| self.pc = tl["pc"] | ||
|
|
||
| self.error = "" | ||
| if "error" in tl: | ||
| self.error = tl["error"] | ||
|
|
||
| self.scratch = [] | ||
| if "scratch" in tl: | ||
| self.scratch = [DryrunStackValue(sv) for sv in tl["scratch"]] | ||
|
|
||
| self.stack = [DryrunStackValue(sv) for sv in tl["stack"]] | ||
|
|
||
|
|
||
| class DryrunStackValue: | ||
| def __init__(self, v): | ||
| self.type = v["type"] | ||
| self.bytes = v["bytes"] | ||
| self.int = v["uint"] | ||
|
|
||
| def __str__(self) -> str: | ||
| if len(self.bytes) > 0: | ||
| return "0x" + base64.b64decode(self.bytes).hex() | ||
| return str(self.int) | ||
|
|
||
| def __eq__(self, other: "DryrunStackValue"): | ||
| return ( | ||
| self.type == other.type | ||
| and self.bytes == other.bytes | ||
| and self.int == other.int | ||
| ) | ||
|
|
||
|
|
||
| def truncate(s: str, max_width: int) -> str: | ||
|
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. looks like the column width is really 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. All of the SDKs have the same type of logic where we truncate at the max width, then add the 3 period ellipses. I went back and forth on this but I think having max width describe where we truncate the real value then adding ellipses to denote that the value is truncated is the right way to go. It is tough though so if you feel strongly I can change it. 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. No worries. Since users have an option to specify no |
||
| if len(s) > max_width and max_width > 0: | ||
|
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. There is a weird edge case here when So maybe it would be better to have the condition be: if len(s) >= max_width + 3 and max_width > 0:But that could break the test, unfortunately. 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. Maybe I should change the flag from max_width to max_value_width to prevent confusion on this? 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. Seems like a good compromise |
||
| return s[:max_width] + "..." | ||
| return s | ||
|
|
||
|
|
||
| def scratch_to_string( | ||
| prev_scratch: List[DryrunStackValue], curr_scratch: List[DryrunStackValue] | ||
| ) -> str: | ||
| if not curr_scratch: | ||
| return "" | ||
|
|
||
| new_idx = None | ||
| for idx in range(len(curr_scratch)): | ||
| if idx >= len(prev_scratch): | ||
| new_idx = idx | ||
| continue | ||
|
|
||
| if curr_scratch[idx] != prev_scratch[idx]: | ||
| new_idx = idx | ||
|
|
||
| if new_idx == None: | ||
| return "" | ||
|
|
||
| return "{} = {}".format(new_idx, curr_scratch[new_idx]) | ||
|
|
||
|
|
||
| def stack_to_string(stack: List[DryrunStackValue], reverse: bool) -> str: | ||
| if reverse: | ||
| stack.reverse() | ||
| return "[{}]".format(", ".join([str(sv) for sv in stack])) | ||
Uh oh!
There was an error while loading. Please reload this page.