@@ -63,6 +63,8 @@ pub enum ConversionError {
6363 FieldConversionFailed { field : String , reason : String } ,
6464 #[ error( "Missing transaction receipt for {tx_hash:#x}" ) ]
6565 MissingTransactionReceipt { tx_hash : Felt } ,
66+ #[ error( "Missing proof facts in RPC response for transaction {tx_hash:#x}" ) ]
67+ MissingProofFacts { tx_hash : Felt } ,
6668}
6769
6870// ================================================================================================
@@ -306,8 +308,15 @@ pub(crate) fn transaction_receipt_hash(receipt: &TransactionReceipt) -> Felt {
306308 }
307309}
308310
309- fn proof_facts_from_rpc ( proof_facts : Option < Vec < Felt > > ) -> starknet_api:: transaction:: fields:: ProofFacts {
310- proof_facts. unwrap_or_default ( ) . into ( )
311+ #[ expect(
312+ clippy:: result_large_err,
313+ reason = "ConversionError is shared across transaction conversions and not worth boxing here"
314+ ) ]
315+ fn proof_facts_from_rpc (
316+ tx_hash : Felt ,
317+ proof_facts : Option < Vec < Felt > > ,
318+ ) -> Result < starknet_api:: transaction:: fields:: ProofFacts , ConversionError > {
319+ proof_facts. ok_or ( ConversionError :: MissingProofFacts { tx_hash } ) . map ( Into :: into)
311320}
312321
313322#[ allow( clippy:: result_large_err) ]
@@ -425,7 +434,7 @@ impl TryIntoBlockifierAsync<TransactionConversionResult> for InvokeTransactionV3
425434 account_deployment_data : starknet_api:: transaction:: fields:: AccountDeploymentData (
426435 self . account_deployment_data ,
427436 ) ,
428- proof_facts : proof_facts_from_rpc ( self . proof_facts ) ,
437+ proof_facts : proof_facts_from_rpc ( self . transaction_hash , self . proof_facts ) ? ,
429438 } ) ;
430439
431440 let invoke_tx = starknet_api:: executable_transaction:: InvokeTransaction :: create ( api_tx, ctx. chain_id ) ?;
@@ -795,4 +804,75 @@ mod tests {
795804 assert_eq ! ( converted_tx. proof_facts_length( ) , 3 ) ;
796805 assert_eq ! ( converted_tx. tx_hash( ) . 0 , tx. transaction_hash) ;
797806 }
807+
808+ #[ tokio:: test]
809+ async fn invoke_v3_conversion_rejects_missing_proof_facts ( ) {
810+ let chain_id = ChainId :: Sepolia ;
811+ let rpc_client = RpcClient :: try_new ( "http://localhost:9545" ) . expect ( "valid dummy rpc url" ) ;
812+ let transaction_receipts = HashMap :: new ( ) ;
813+ let ctx = ConversionContext :: new ( & chain_id, 9112643 , & rpc_client, & transaction_receipts) ;
814+ let tx = InvokeTransactionV3 {
815+ transaction_hash : Felt :: from ( 123_u64 ) ,
816+ sender_address : Felt :: from_hex_unchecked (
817+ "0x041c9dbe8ab9b414fa0ec4d22b7a41d80a3911b77a2c9c819ce949faa5edb9f9" ,
818+ ) ,
819+ calldata : vec ! [ Felt :: ONE ] ,
820+ signature : vec ! [ Felt :: TWO ] ,
821+ nonce : Felt :: from ( 7_u64 ) ,
822+ resource_bounds : ResourceBoundsMapping {
823+ l1_gas : ResourceBounds { max_amount : 1_000_000 , max_price_per_unit : 1 } ,
824+ l2_gas : ResourceBounds { max_amount : 2_000_000 , max_price_per_unit : 3 } ,
825+ l1_data_gas : ResourceBounds { max_amount : 4_000_000 , max_price_per_unit : 5 } ,
826+ } ,
827+ tip : 0 ,
828+ paymaster_data : vec ! [ ] ,
829+ account_deployment_data : vec ! [ ] ,
830+ nonce_data_availability_mode : DataAvailabilityMode :: L1 ,
831+ fee_data_availability_mode : DataAvailabilityMode :: L1 ,
832+ proof_facts : None ,
833+ } ;
834+
835+ let error = tx. try_into_blockifier_async ( & ctx) . await . expect_err ( "missing proof facts must fail" ) ;
836+
837+ assert ! ( matches!( error, ConversionError :: MissingProofFacts { tx_hash } if tx_hash == Felt :: from( 123_u64 ) ) ) ;
838+ }
839+
840+ #[ tokio:: test]
841+ async fn invoke_v3_conversion_accepts_empty_proof_facts ( ) {
842+ let chain_id = ChainId :: Sepolia ;
843+ let rpc_client = RpcClient :: try_new ( "http://localhost:9545" ) . expect ( "valid dummy rpc url" ) ;
844+ let transaction_receipts = HashMap :: new ( ) ;
845+ let ctx = ConversionContext :: new ( & chain_id, 9112643 , & rpc_client, & transaction_receipts) ;
846+ let tx = InvokeTransactionV3 {
847+ transaction_hash : Felt :: ZERO ,
848+ sender_address : Felt :: from_hex_unchecked (
849+ "0x041c9dbe8ab9b414fa0ec4d22b7a41d80a3911b77a2c9c819ce949faa5edb9f9" ,
850+ ) ,
851+ calldata : vec ! [ Felt :: ONE ] ,
852+ signature : vec ! [ Felt :: TWO ] ,
853+ nonce : Felt :: from ( 7_u64 ) ,
854+ resource_bounds : ResourceBoundsMapping {
855+ l1_gas : ResourceBounds { max_amount : 1_000_000 , max_price_per_unit : 1 } ,
856+ l2_gas : ResourceBounds { max_amount : 2_000_000 , max_price_per_unit : 3 } ,
857+ l1_data_gas : ResourceBounds { max_amount : 4_000_000 , max_price_per_unit : 5 } ,
858+ } ,
859+ tip : 0 ,
860+ paymaster_data : vec ! [ ] ,
861+ account_deployment_data : vec ! [ ] ,
862+ nonce_data_availability_mode : DataAvailabilityMode :: L1 ,
863+ fee_data_availability_mode : DataAvailabilityMode :: L1 ,
864+ proof_facts : Some ( vec ! [ ] ) ,
865+ } ;
866+
867+ let result = tx. try_into_blockifier_async ( & ctx) . await . expect ( "empty proof facts should be accepted" ) ;
868+
869+ let converted_tx = match result. starknet_api_tx {
870+ starknet_api:: executable_transaction:: Transaction :: Account (
871+ starknet_api:: executable_transaction:: AccountTransaction :: Invoke ( invoke_tx) ,
872+ ) => invoke_tx,
873+ other => panic ! ( "expected invoke account transaction, got {other:?}" ) ,
874+ } ;
875+
876+ assert_eq ! ( converted_tx. proof_facts_length( ) , 0 ) ;
877+ }
798878}
0 commit comments