Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 198de97

Browse files
Fix: explorer rendering for failed transactions (magicblock-labs#1032)
1 parent c9d640a commit 198de97

6 files changed

Lines changed: 1329 additions & 292 deletions

File tree

magicblock-aperture/src/requests/http/get_transaction.rs

Lines changed: 189 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use json::{JsonValueMutTrait, JsonValueTrait};
1+
use json::{JsonContainerTrait, JsonValueMutTrait, JsonValueTrait};
22
use solana_rpc_client_api::config::RpcTransactionConfig;
33
use solana_transaction_status::UiTransactionEncoding;
44

@@ -33,25 +33,19 @@ impl HttpDispatcher {
3333
let encoded_transaction =
3434
transaction.and_then(|tx| tx.encode(encoding, max_version).ok());
3535

36-
if encoding == UiTransactionEncoding::JsonParsed {
37-
let mut encoded_value = value_from_serializable(
38-
&encoded_transaction,
39-
)
36+
let mut encoded_value = value_from_serializable(&encoded_transaction)
4037
.ok_or_else(|| {
41-
RpcError::internal(
42-
"failed to serialize JsonParsed getTransaction response",
43-
)
44-
})?;
38+
RpcError::internal("failed to serialize getTransaction response")
39+
})?;
40+
normalize_failed_transaction_balance_arrays(&mut encoded_value);
41+
42+
if encoding == UiTransactionEncoding::JsonParsed {
4543
sanitize_nan_strings(&mut encoded_value);
46-
return Ok(ResponsePayload::encode_no_context(
47-
&request.id,
48-
encoded_value,
49-
));
5044
}
5145

5246
Ok(ResponsePayload::encode_no_context(
5347
&request.id,
54-
encoded_transaction,
48+
encoded_value,
5549
))
5650
}
5751
}
@@ -62,6 +56,48 @@ fn value_from_serializable<T: json::Serialize>(
6256
json::to_value(value).ok()
6357
}
6458

59+
fn normalize_failed_transaction_balance_arrays(value: &mut json::Value) {
60+
if value["meta"]["err"].is_null() {
61+
return;
62+
}
63+
64+
let Some(pre_balances) = value["meta"]["preBalances"]
65+
.as_array()
66+
.filter(|pre_balances| !pre_balances.is_empty())
67+
.map(|pre_balances| pre_balances.to_vec())
68+
else {
69+
return;
70+
};
71+
72+
let post_balance_len = value["meta"]["postBalances"]
73+
.as_array()
74+
.map_or(0, |post_balances| post_balances.len());
75+
if post_balance_len == pre_balances.len() {
76+
return;
77+
}
78+
79+
let mut repaired_post_balances = pre_balances;
80+
81+
let fee = json_value_as_u64(&value["meta"]["fee"]).unwrap_or(0);
82+
if let Some(first_balance) = repaired_post_balances.first_mut() {
83+
if let Some(balance) = json_value_as_u64(first_balance) {
84+
*first_balance = balance.saturating_sub(fee).into();
85+
}
86+
}
87+
88+
if let Some(encoded_balances) =
89+
value_from_serializable(&repaired_post_balances)
90+
{
91+
value["meta"]["postBalances"] = encoded_balances;
92+
}
93+
}
94+
95+
fn json_value_as_u64(value: &json::Value) -> Option<u64> {
96+
value
97+
.as_u64()
98+
.or_else(|| value.as_str().and_then(|s| s.parse().ok()))
99+
}
100+
65101
fn sanitize_nan_strings(value: &mut json::Value) {
66102
sanitize_nan_strings_for_key(value, None);
67103
}
@@ -85,14 +121,48 @@ fn sanitize_nan_strings_for_key(
85121
}
86122

87123
if let Some(s) = value.as_str() {
88-
if parent_key.is_some_and(is_numeric_amount_field) && is_nan_string(s) {
89-
*value = "0".into();
124+
if let Some(key) = parent_key.filter(|key| is_numeric_json_field(key)) {
125+
if is_nan_string(s) {
126+
*value = nan_replacement_for_field(key);
127+
}
90128
}
91129
}
92130
}
93131

94-
fn is_numeric_amount_field(key: &str) -> bool {
95-
matches!(key, "amount" | "uiAmount" | "uiAmountString")
132+
fn nan_replacement_for_field(key: &str) -> json::Value {
133+
match key {
134+
"amount" | "uiAmountString" => "0".into(),
135+
_ => 0.into(),
136+
}
137+
}
138+
139+
fn is_numeric_json_field(key: &str) -> bool {
140+
matches!(
141+
key,
142+
"accountDataSizeLimit"
143+
| "activationEpoch"
144+
| "additionalFee"
145+
| "amount"
146+
| "bytes"
147+
| "commission"
148+
| "computeUnitsConsumed"
149+
| "costUnits"
150+
| "deactivationEpoch"
151+
| "epoch"
152+
| "fee"
153+
| "lamports"
154+
| "microLamports"
155+
| "recentSlot"
156+
| "rentEpoch"
157+
| "rentExemptReserve"
158+
| "space"
159+
| "stake"
160+
| "timestamp"
161+
| "uiAmount"
162+
| "uiAmountString"
163+
| "unixTimestamp"
164+
| "units"
165+
)
96166
}
97167

98168
fn is_nan_string(value: &str) -> bool {
@@ -118,12 +188,21 @@ mod tests {
118188
"parsed": {
119189
"info": {
120190
"amount": "nan",
191+
"lamports": "nan",
192+
"microLamports": "+nan",
193+
"recentSlot": "-nan",
121194
"uiAmount": "+nan",
122195
"uiAmountString": "-nan",
123196
"note": "nan"
124197
}
125198
}
126-
}]
199+
}],
200+
"extra": {
201+
"rentEpoch": "nan",
202+
"space": "+nan",
203+
"timestamp": "-nan",
204+
"description": "nan"
205+
}
127206
}
128207
}
129208
});
@@ -139,10 +218,25 @@ mod tests {
139218
["info"]["amount"],
140219
"0"
141220
);
221+
assert_eq!(
222+
value["transaction"]["message"]["instructions"][0]["parsed"]
223+
["info"]["lamports"],
224+
0
225+
);
226+
assert_eq!(
227+
value["transaction"]["message"]["instructions"][0]["parsed"]
228+
["info"]["microLamports"],
229+
0
230+
);
231+
assert_eq!(
232+
value["transaction"]["message"]["instructions"][0]["parsed"]
233+
["info"]["recentSlot"],
234+
0
235+
);
142236
assert_eq!(
143237
value["transaction"]["message"]["instructions"][0]["parsed"]
144238
["info"]["uiAmount"],
145-
"0"
239+
0
146240
);
147241
assert_eq!(
148242
value["transaction"]["message"]["instructions"][0]["parsed"]
@@ -154,5 +248,80 @@ mod tests {
154248
["info"]["note"],
155249
"nan"
156250
);
251+
assert_eq!(value["transaction"]["message"]["extra"]["rentEpoch"], 0);
252+
assert_eq!(value["transaction"]["message"]["extra"]["space"], 0);
253+
assert_eq!(value["transaction"]["message"]["extra"]["timestamp"], 0);
254+
assert_eq!(
255+
value["transaction"]["message"]["extra"]["description"],
256+
"nan"
257+
);
258+
}
259+
260+
#[test]
261+
fn normalize_failed_transaction_balance_arrays_repairs_post_balances() {
262+
let mut value = json::json!({
263+
"meta": {
264+
"err": "InvalidWritableAccount",
265+
"fee": 5000,
266+
"preBalances": [10000, 20000, 30000],
267+
"postBalances": []
268+
}
269+
});
270+
271+
normalize_failed_transaction_balance_arrays(&mut value);
272+
273+
assert_eq!(value["meta"]["postBalances"][0], 5000);
274+
assert_eq!(value["meta"]["postBalances"][1], 20000);
275+
assert_eq!(value["meta"]["postBalances"][2], 30000);
276+
}
277+
278+
#[test]
279+
fn normalize_failed_transaction_balance_arrays_keeps_successful_tx() {
280+
let mut value = json::json!({
281+
"meta": {
282+
"err": null,
283+
"fee": 5000,
284+
"preBalances": [10000, 20000],
285+
"postBalances": []
286+
}
287+
});
288+
289+
normalize_failed_transaction_balance_arrays(&mut value);
290+
291+
assert_eq!(value["meta"]["postBalances"], json::json!([]));
292+
}
293+
294+
#[test]
295+
fn normalize_failed_transaction_balance_arrays_handles_fee_exceeding_balance(
296+
) {
297+
let mut value = json::json!({
298+
"meta": {
299+
"err": "SomeError",
300+
"fee": 15000,
301+
"preBalances": [10000, 20000],
302+
"postBalances": []
303+
}
304+
});
305+
306+
normalize_failed_transaction_balance_arrays(&mut value);
307+
308+
assert_eq!(value["meta"]["postBalances"][0], 0);
309+
assert_eq!(value["meta"]["postBalances"][1], 20000);
310+
}
311+
312+
#[test]
313+
fn normalize_failed_transaction_balance_arrays_handles_missing_fee() {
314+
let mut value = json::json!({
315+
"meta": {
316+
"err": "SomeError",
317+
"preBalances": [10000, 20000],
318+
"postBalances": []
319+
}
320+
});
321+
322+
normalize_failed_transaction_balance_arrays(&mut value);
323+
324+
assert_eq!(value["meta"]["postBalances"][0], 10000);
325+
assert_eq!(value["meta"]["postBalances"][1], 20000);
157326
}
158327
}

0 commit comments

Comments
 (0)