1- use json:: { JsonValueMutTrait , JsonValueTrait } ;
1+ use json:: { JsonContainerTrait , JsonValueMutTrait , JsonValueTrait } ;
22use solana_rpc_client_api:: config:: RpcTransactionConfig ;
33use 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+
65101fn 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
98168fn 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