@@ -5312,6 +5312,275 @@ fn prepare_solution_provider(origin: AccountId) {
53125312 ) ) ;
53135313}
53145314
5315+ // ------------------------------------------ //
5316+ // V3 BILLING OPT-OUT TESTS //
5317+ // ------------------------------------------ //
5318+
5319+ #[ test]
5320+ fn test_create_node_contract_on_opted_out_node_non_admin_fails ( ) {
5321+ new_test_ext ( ) . execute_with ( || {
5322+ run_to_block ( 1 , None ) ;
5323+ prepare_farm_and_node ( ) ;
5324+ let node_id = 1 ;
5325+
5326+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5327+ RuntimeOrigin :: signed( alice( ) ) ,
5328+ node_id,
5329+ ) ) ;
5330+
5331+ assert_noop ! (
5332+ SmartContractModule :: create_node_contract(
5333+ RuntimeOrigin :: signed( bob( ) ) ,
5334+ node_id,
5335+ generate_deployment_hash( ) ,
5336+ get_deployment_data( ) ,
5337+ 0 ,
5338+ None ,
5339+ ) ,
5340+ Error :: <TestRuntime >:: OnlyTwinAdminCanDeployOnThisNode
5341+ ) ;
5342+ } ) ;
5343+ }
5344+
5345+ #[ test]
5346+ fn test_create_node_contract_on_opted_out_node_admin_succeeds ( ) {
5347+ new_test_ext ( ) . execute_with ( || {
5348+ run_to_block ( 1 , None ) ;
5349+ prepare_farm_and_node ( ) ;
5350+ let node_id = 1 ;
5351+
5352+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5353+ RuntimeOrigin :: signed( alice( ) ) ,
5354+ node_id,
5355+ ) ) ;
5356+
5357+ assert_ok ! ( TfgridModule :: add_twin_admin(
5358+ RawOrigin :: Root . into( ) ,
5359+ bob( ) ,
5360+ ) ) ;
5361+
5362+ assert_ok ! ( SmartContractModule :: create_node_contract(
5363+ RuntimeOrigin :: signed( bob( ) ) ,
5364+ node_id,
5365+ generate_deployment_hash( ) ,
5366+ get_deployment_data( ) ,
5367+ 0 ,
5368+ None ,
5369+ ) ) ;
5370+ } ) ;
5371+ }
5372+
5373+ #[ test]
5374+ fn test_create_node_contract_on_normal_node_unaffected ( ) {
5375+ new_test_ext ( ) . execute_with ( || {
5376+ run_to_block ( 1 , None ) ;
5377+ prepare_farm_and_node ( ) ;
5378+ let node_id = 1 ;
5379+
5380+ assert_ok ! ( SmartContractModule :: create_node_contract(
5381+ RuntimeOrigin :: signed( bob( ) ) ,
5382+ node_id,
5383+ generate_deployment_hash( ) ,
5384+ get_deployment_data( ) ,
5385+ 0 ,
5386+ None ,
5387+ ) ) ;
5388+ } ) ;
5389+ }
5390+
5391+ #[ test]
5392+ fn test_create_node_contract_opted_out_empty_admin_list_fails ( ) {
5393+ new_test_ext ( ) . execute_with ( || {
5394+ run_to_block ( 1 , None ) ;
5395+ prepare_farm_and_node ( ) ;
5396+ let node_id = 1 ;
5397+
5398+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5399+ RuntimeOrigin :: signed( alice( ) ) ,
5400+ node_id,
5401+ ) ) ;
5402+
5403+ assert_noop ! (
5404+ SmartContractModule :: create_node_contract(
5405+ RuntimeOrigin :: signed( alice( ) ) ,
5406+ node_id,
5407+ generate_deployment_hash( ) ,
5408+ get_deployment_data( ) ,
5409+ 0 ,
5410+ None ,
5411+ ) ,
5412+ Error :: <TestRuntime >:: OnlyTwinAdminCanDeployOnThisNode
5413+ ) ;
5414+ } ) ;
5415+ }
5416+
5417+ #[ test]
5418+ fn test_admin_removed_cannot_deploy_on_opted_out_node ( ) {
5419+ new_test_ext ( ) . execute_with ( || {
5420+ run_to_block ( 1 , None ) ;
5421+ prepare_farm_and_node ( ) ;
5422+ let node_id = 1 ;
5423+
5424+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5425+ RuntimeOrigin :: signed( alice( ) ) ,
5426+ node_id,
5427+ ) ) ;
5428+
5429+ assert_ok ! ( TfgridModule :: add_twin_admin( RawOrigin :: Root . into( ) , bob( ) ) ) ;
5430+ assert_ok ! ( TfgridModule :: remove_twin_admin( RawOrigin :: Root . into( ) , bob( ) ) ) ;
5431+
5432+ assert_noop ! (
5433+ SmartContractModule :: create_node_contract(
5434+ RuntimeOrigin :: signed( bob( ) ) ,
5435+ node_id,
5436+ generate_deployment_hash( ) ,
5437+ get_deployment_data( ) ,
5438+ 0 ,
5439+ None ,
5440+ ) ,
5441+ Error :: <TestRuntime >:: OnlyTwinAdminCanDeployOnThisNode
5442+ ) ;
5443+ } ) ;
5444+ }
5445+
5446+ #[ test]
5447+ fn test_create_rent_contract_on_opted_out_node_non_admin_fails ( ) {
5448+ new_test_ext ( ) . execute_with ( || {
5449+ run_to_block ( 1 , None ) ;
5450+ prepare_dedicated_farm_and_node ( ) ;
5451+ let node_id = 1 ;
5452+
5453+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5454+ RuntimeOrigin :: signed( alice( ) ) ,
5455+ node_id,
5456+ ) ) ;
5457+
5458+ assert_noop ! (
5459+ SmartContractModule :: create_rent_contract(
5460+ RuntimeOrigin :: signed( charlie( ) ) ,
5461+ node_id,
5462+ None ,
5463+ ) ,
5464+ Error :: <TestRuntime >:: OnlyTwinAdminCanDeployOnThisNode
5465+ ) ;
5466+ } ) ;
5467+ }
5468+
5469+ #[ test]
5470+ fn test_create_rent_contract_on_opted_out_node_admin_succeeds ( ) {
5471+ new_test_ext ( ) . execute_with ( || {
5472+ run_to_block ( 1 , None ) ;
5473+ prepare_dedicated_farm_and_node ( ) ;
5474+ let node_id = 1 ;
5475+
5476+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5477+ RuntimeOrigin :: signed( alice( ) ) ,
5478+ node_id,
5479+ ) ) ;
5480+
5481+ assert_ok ! ( TfgridModule :: add_twin_admin(
5482+ RawOrigin :: Root . into( ) ,
5483+ charlie( ) ,
5484+ ) ) ;
5485+
5486+ assert_ok ! ( SmartContractModule :: create_rent_contract(
5487+ RuntimeOrigin :: signed( charlie( ) ) ,
5488+ node_id,
5489+ None ,
5490+ ) ) ;
5491+ } ) ;
5492+ }
5493+
5494+ #[ test]
5495+ fn test_billing_suppressed_for_opted_out_node_contract ( ) {
5496+ // Note: should_bill_contract returns false for a node contract with no resources/IPs/NU/overdraft,
5497+ // so the OCW never submits bill_contract_for_block. We verify billing suppression by directly
5498+ // calling bill_contract and checking that balance is unchanged and state stays Created.
5499+ new_test_ext ( ) . execute_with ( || {
5500+ run_to_block ( 1 , None ) ;
5501+ prepare_farm_and_node ( ) ;
5502+ let node_id = 1 ;
5503+
5504+ assert_ok ! ( TfgridModule :: add_twin_admin( RawOrigin :: Root . into( ) , bob( ) ) ) ;
5505+
5506+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5507+ RuntimeOrigin :: signed( alice( ) ) ,
5508+ node_id,
5509+ ) ) ;
5510+
5511+ assert_ok ! ( SmartContractModule :: create_node_contract(
5512+ RuntimeOrigin :: signed( bob( ) ) ,
5513+ node_id,
5514+ generate_deployment_hash( ) ,
5515+ get_deployment_data( ) ,
5516+ 0 ,
5517+ None ,
5518+ ) ) ;
5519+ let contract_id = 1 ;
5520+
5521+ let balance_before = Balances :: free_balance ( & bob ( ) ) ;
5522+
5523+ // Directly invoke bill_contract (the on-chain extrinsic path)
5524+ assert_ok ! ( SmartContractModule :: bill_contract_for_block(
5525+ RuntimeOrigin :: signed( alice( ) ) ,
5526+ contract_id,
5527+ ) ) ;
5528+
5529+ let balance_after = Balances :: free_balance ( & bob ( ) ) ;
5530+
5531+ // No charge — billing suppressed for opted-out node
5532+ assert_eq ! ( balance_before, balance_after) ;
5533+
5534+ // Contract remains in Created state (not pushed to GracePeriod)
5535+ let contract = SmartContractModule :: contracts ( contract_id) . unwrap ( ) ;
5536+ assert_eq ! ( contract. state, types:: ContractState :: Created ) ;
5537+ } ) ;
5538+ }
5539+
5540+ #[ test]
5541+ fn test_cancel_contract_on_opted_out_node_zero_final_bill ( ) {
5542+ let ( mut ext, mut pool_state) = new_test_ext_with_pool_state ( 0 ) ;
5543+ ext. execute_with ( || {
5544+ run_to_block ( 1 , None ) ;
5545+ prepare_farm_and_node ( ) ;
5546+ let node_id = 1 ;
5547+
5548+ assert_ok ! ( TfgridModule :: add_twin_admin( RawOrigin :: Root . into( ) , bob( ) ) ) ;
5549+
5550+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5551+ RuntimeOrigin :: signed( alice( ) ) ,
5552+ node_id,
5553+ ) ) ;
5554+
5555+ assert_ok ! ( SmartContractModule :: create_node_contract(
5556+ RuntimeOrigin :: signed( bob( ) ) ,
5557+ node_id,
5558+ generate_deployment_hash( ) ,
5559+ get_deployment_data( ) ,
5560+ 0 ,
5561+ None ,
5562+ ) ) ;
5563+ let contract_id = 1 ;
5564+
5565+ let balance_before = Balances :: free_balance ( & bob ( ) ) ;
5566+
5567+ // Cancel the contract — triggers a final bill_contract call
5568+ assert_ok ! ( SmartContractModule :: cancel_contract(
5569+ RuntimeOrigin :: signed( bob( ) ) ,
5570+ contract_id,
5571+ ) ) ;
5572+
5573+ let balance_after = Balances :: free_balance ( & bob ( ) ) ;
5574+
5575+ // No charge at cancellation either
5576+ assert_eq ! ( balance_before, balance_after) ;
5577+
5578+ // Contract is cleaned up
5579+ assert ! ( SmartContractModule :: contracts( contract_id) . is_none( ) ) ;
5580+ let _ = pool_state;
5581+ } ) ;
5582+ }
5583+
53155584fn record ( event : RuntimeEvent ) -> EventRecord < RuntimeEvent , H256 > {
53165585 EventRecord {
53175586 phase : Phase :: Initialization ,
0 commit comments