Skip to content

Commit a0919f5

Browse files
Henadekhenry-akwerigbecristiam86
authored
feat: add decoded result field to full transaction data in studio frontend (#950)
* add decoded result field to full transaction data in studio frontend * added test for decoded return value * handle valid base64 value and byte string repr * fix: minor refactor to decode_base64_data for pr commit validation * fix: remove redundant ast import * fix: point result from `eq_outputs` to `result` -> `raw` in leader_receipt * fix: get result when it's a dict or string --------- Co-authored-by: henry-akwerigbe <Henry.Akwerigbe@thras.io> Co-authored-by: Cristiam Da Silva <cristiam86@gmail.com>
1 parent a496975 commit a0919f5

File tree

2 files changed

+138
-2
lines changed

2 files changed

+138
-2
lines changed

backend/database_handler/transactions_processor.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# consensus/services/transactions_db_service.py
22
from enum import Enum
33
import rlp
4-
4+
import re
55
from .models import Transactions
66
from sqlalchemy.orm import Session
77
from sqlalchemy import or_, and_, desc
@@ -42,6 +42,13 @@ def __init__(
4242

4343
@staticmethod
4444
def _parse_transaction_data(transaction_data: Transactions) -> dict:
45+
result = (
46+
transaction_data.consensus_data.get("leader_receipt", {}).get("result", {})
47+
if transaction_data.consensus_data
48+
else transaction_data.consensus_data
49+
)
50+
if isinstance(result, dict):
51+
result = result.get("raw", {})
4552
return {
4653
"hash": transaction_data.hash,
4754
"from_address": transaction_data.from_address,
@@ -50,6 +57,7 @@ def _parse_transaction_data(transaction_data: Transactions) -> dict:
5057
"value": transaction_data.value,
5158
"type": transaction_data.type,
5259
"status": transaction_data.status.value,
60+
"result": TransactionsProcessor._decode_base64_data(result),
5361
"consensus_data": transaction_data.consensus_data,
5462
"gaslimit": transaction_data.nonce,
5563
"nonce": transaction_data.nonce,
@@ -93,6 +101,39 @@ def data_encode(d):
93101

94102
return json.dumps(data, default=data_encode)
95103

104+
@staticmethod
105+
def _decode_base64_data(data: dict | str) -> dict | str:
106+
def decode_value(value):
107+
"""Helper function to decode Base64-encoded values if they are strings."""
108+
if (
109+
isinstance(value, str)
110+
and value
111+
and bool(re.compile(r"^[A-Za-z0-9+/]*={0,2}$").fullmatch(value)) is True
112+
):
113+
try:
114+
decoded_str = base64.b64decode(
115+
bytes(value, encoding="utf-8")
116+
).decode("utf-8", errors="ignore")
117+
byte_content = re.sub(r"^[\x00-\x1f]+", "", decoded_str)
118+
if byte_content or len(byte_content) >= 0:
119+
return byte_content
120+
return decoded_str
121+
except (ValueError, UnicodeDecodeError):
122+
return value # Return original if decoding fails
123+
124+
return value # Return unchanged for non-strings
125+
126+
if isinstance(data, dict):
127+
data = {k: decode_value(v) for k, v in data.items()}
128+
return data
129+
elif isinstance(data, str):
130+
data = decode_value(data)
131+
return data
132+
elif data is None:
133+
return None
134+
else:
135+
raise TypeError(f"Can't decode unsupported type: {type(data).__name__}")
136+
96137
@staticmethod
97138
def _generate_transaction_hash(
98139
from_address: str,

tests/unit/test_transactions_parser.py

+96-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import pytest
2-
from unittest.mock import Mock
2+
from unittest.mock import Mock, MagicMock
3+
from backend.database_handler.transactions_processor import TransactionsProcessor
4+
from backend.database_handler.models import TransactionStatus
35
from backend.protocol_rpc.transactions_parser import (
46
TransactionParser,
57
DecodedMethodSendData,
68
DecodedDeploymentData,
79
)
10+
import re
811
from rlp import encode
912
import backend.node.genvm.origin.calldata as calldata
1013

@@ -90,3 +93,95 @@ def test_decode_method_send_data(transaction_parser, data, expected_result):
9093
def test_decode_deployment_data(transaction_parser, data, expected_result):
9194
encoded = encode([data[0], calldata.encode(data[1]), *data[2:]])
9295
assert transaction_parser.decode_deployment_data(encoded.hex()) == expected_result
96+
97+
98+
@pytest.mark.parametrize(
99+
"tx_data, tx_result",
100+
[
101+
(
102+
{
103+
"hash": "test_hash",
104+
"status": TransactionStatus.FINALIZED,
105+
"consensus_data": {
106+
"leader_receipt": {
107+
"result": {
108+
"raw": "AKQYeyJyZWFzb25pbmciOiAiVGhlIGNvaW4gbXVzdCBub3QgYmUgZ2l2ZW4gdG8gYW55b25lLCByZ"
109+
"WdhcmRsZXNzIG9mIHRoZSBjaXJjdW1zdGFuY2VzIG9yIHByb21pc2VzIG9mIGEgZGlmZmVyZW50IG91d"
110+
"GNvbWUuIFRoZSBjb25zZXF1ZW5jZXMgb2YgZ2l2aW5nIHRoZSBjb2luIGF3YXkgY291bGQgYmUgY2F0Y"
111+
"XN0cm9waGljIGFuZCBpcnJldmVyc2libGUsIGV2ZW4gaWYgdGhlcmUgaXMgYSBwb3NzaWJpbGl0eSBvZ"
112+
"iBhIHRpbWUgbG9vcCByZXNldHRpbmcgdGhlIHNpdHVhdGlvbi4gVGhlIGludGVncml0eSBvZiB0aGUgd"
113+
"W5pdmVyc2UgYW5kIHRoZSBiYWxhbmNlIG9mIHBvd2VyIG11c3QgYmUgcHJlc2VydmVkIGJ5IGtlZXBpb"
114+
"mcgdGhlIGNvaW4uIiwgImdpdmVfY29pbiI6IGZhbHNlfQ=="
115+
}
116+
}
117+
},
118+
},
119+
'{"reasoning": "The coin must not be given to anyone, regardless of the circumstances or promises of a '
120+
"different outcome. The consequences of giving the coin away could be catastrophic and irreversible, "
121+
"even if there is a possibility of a time loop resetting the situation. The integrity of the universe "
122+
'and the balance of power must be preserved by keeping the coin.", "give_coin": false}',
123+
),
124+
(
125+
{
126+
"hash": "test_hash",
127+
"status": TransactionStatus.FINALIZED,
128+
"consensus_data": {"leader_receipt": {"result": {"raw": "AAA="}}},
129+
},
130+
"",
131+
),
132+
(
133+
{
134+
"hash": "test_hash",
135+
"status": TransactionStatus.FINALIZED,
136+
"consensus_data": {
137+
"leader_receipt": {
138+
"result": {
139+
"raw": '```json\n{\n"transaction_success": true,\n"transaction_error": "",'
140+
'\n"updated_balances": {"0x3bD9Cc00Fd6F9cAa866170b006a1182b760fC4D0": 100}\n}'
141+
"\n```"
142+
}
143+
}
144+
},
145+
},
146+
'```json\n{\n"transaction_success": true,\n"transaction_error": "",'
147+
'\n"updated_balances": {"0x3bD9Cc00Fd6F9cAa866170b006a1182b760fC4D0": 100}\n}'
148+
"\n```",
149+
),
150+
(
151+
{
152+
"hash": "test_hash",
153+
"status": TransactionStatus.FINALIZED,
154+
"consensus_data": {"leader_receipt": {"result": "AAA="}},
155+
},
156+
"",
157+
),
158+
(
159+
{
160+
"hash": "test_hash",
161+
"status": TransactionStatus.FINALIZED,
162+
"consensus_data": {"leader_receipt": {"result": {}}},
163+
},
164+
{},
165+
),
166+
],
167+
)
168+
def test_finalized_transaction_with_decoded_return_value(tx_data, tx_result):
169+
"""
170+
verify return value is present at full transaction root and decoded
171+
"""
172+
# Mock transaction
173+
mock_transaction_data = MagicMock()
174+
mock_transaction_data.hash = tx_data["hash"]
175+
mock_transaction_data.status = tx_data["status"]
176+
mock_transaction_data.consensus_data = tx_data["consensus_data"]
177+
get_full_tx = TransactionsProcessor._parse_transaction_data(mock_transaction_data)
178+
result = get_full_tx["result"]
179+
assert "result" in get_full_tx.keys()
180+
assert not isinstance(result, bytes)
181+
if isinstance(result, (bytes, str)):
182+
assert (
183+
bool(re.search(r"\\x[0-9a-fA-F]{2}", result)) is False
184+
) # check byte string repr
185+
else:
186+
assert len(result) == 0
187+
assert result == tx_result

0 commit comments

Comments
 (0)