@@ -5371,25 +5371,10 @@ fn test_create_node_contract_on_opted_out_node_admin_succeeds() {
53715371}
53725372
53735373#[ 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 ( ) {
5374+ fn test_create_node_contract_opted_out_farmer_cannot_self_deploy ( ) {
5375+ // The farmer who opted out is NOT automatically an admin — they must be explicitly added.
5376+ // This is a distinct case from test_create_node_contract_on_opted_out_node_non_admin_fails
5377+ // which uses an unrelated account (bob). Here the farmer (alice) tries to deploy on their own node.
53935378 new_test_ext ( ) . execute_with ( || {
53945379 run_to_block ( 1 , None ) ;
53955380 prepare_farm_and_node ( ) ;
@@ -5493,10 +5478,8 @@ fn test_create_rent_contract_on_opted_out_node_admin_succeeds() {
54935478
54945479#[ test]
54955480fn 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 ( || {
5481+ let ( mut ext, mut pool_state) = new_test_ext_with_pool_state ( 0 ) ;
5482+ ext. execute_with ( || {
55005483 run_to_block ( 1 , None ) ;
55015484 prepare_farm_and_node ( ) ;
55025485 let node_id = 1 ;
@@ -5518,17 +5501,19 @@ fn test_billing_suppressed_for_opted_out_node_contract() {
55185501 ) ) ;
55195502 let contract_id = 1 ;
55205503
5504+ push_contract_resources_used ( contract_id) ;
5505+
55215506 let balance_before = Balances :: free_balance ( & bob ( ) ) ;
55225507
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- ) ) ;
5508+ // OCW fires at block 11 (billing cycle), but cost is zeroed by should_waive_migration_billing
5509+ pool_state
5510+ . write ( )
5511+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 11 ) ;
5512+ run_to_block ( 11 , Some ( & mut pool_state ) ) ;
55285513
55295514 let balance_after = Balances :: free_balance ( & bob ( ) ) ;
55305515
5531- // No charge — billing suppressed for opted-out node
5516+ // No charge despite real resource usage — billing suppressed for opted-out node
55325517 assert_eq ! ( balance_before, balance_after) ;
55335518
55345519 // Contract remains in Created state (not pushed to GracePeriod)
@@ -5539,8 +5524,7 @@ fn test_billing_suppressed_for_opted_out_node_contract() {
55395524
55405525#[ test]
55415526fn 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 ( || {
5527+ new_test_ext ( ) . execute_with ( || {
55445528 run_to_block ( 1 , None ) ;
55455529 prepare_farm_and_node ( ) ;
55465530 let node_id = 1 ;
@@ -5562,22 +5546,112 @@ fn test_cancel_contract_on_opted_out_node_zero_final_bill() {
55625546 ) ) ;
55635547 let contract_id = 1 ;
55645548
5549+ push_contract_resources_used ( contract_id) ;
5550+
55655551 let balance_before = Balances :: free_balance ( & bob ( ) ) ;
55665552
5567- // Cancel the contract — triggers a final bill_contract call
5553+ // Cancel triggers bill_contract inline (no OCW) with state=Deleted
55685554 assert_ok ! ( SmartContractModule :: cancel_contract(
55695555 RuntimeOrigin :: signed( bob( ) ) ,
55705556 contract_id,
55715557 ) ) ;
55725558
55735559 let balance_after = Balances :: free_balance ( & bob ( ) ) ;
55745560
5575- // No charge at cancellation either
5561+ // No charge despite real resource usage — migration billing suppressed at cancellation too
55765562 assert_eq ! ( balance_before, balance_after) ;
55775563
55785564 // Contract is cleaned up
55795565 assert ! ( SmartContractModule :: contracts( contract_id) . is_none( ) ) ;
5580- let _ = pool_state;
5566+ } ) ;
5567+ }
5568+
5569+ #[ test]
5570+ fn test_grace_period_contract_restored_and_billing_free_after_opt_out ( ) {
5571+ // Scenario:
5572+ // 1. charlie deploys on a normal node, runs out of funds → GracePeriod
5573+ // 2. Farmer opts out of v3 billing while contract is in GracePeriod
5574+ // 3. charlie tops up → next billing cycle restores contract to Created
5575+ // 4. Subsequent billing cycle: resources are present but cost is zero (migration window)
5576+ let ( mut ext, mut pool_state) = new_test_ext_with_pool_state ( 0 ) ;
5577+ ext. execute_with ( || {
5578+ run_to_block ( 1 , None ) ;
5579+ prepare_farm_and_node ( ) ;
5580+ let node_id = 1 ;
5581+
5582+ TFTPriceModule :: set_prices ( RuntimeOrigin :: signed ( alice ( ) ) , 50 , 101 ) . unwrap ( ) ;
5583+
5584+ // charlie (low balance) deploys — node is NOT yet opted out
5585+ assert_ok ! ( TfgridModule :: add_twin_admin( RawOrigin :: Root . into( ) , charlie( ) ) ) ;
5586+ assert_ok ! ( SmartContractModule :: create_node_contract(
5587+ RuntimeOrigin :: signed( charlie( ) ) ,
5588+ node_id,
5589+ generate_deployment_hash( ) ,
5590+ get_deployment_data( ) ,
5591+ 0 ,
5592+ None ,
5593+ ) ) ;
5594+ let contract_id = 1 ;
5595+
5596+ push_contract_resources_used ( contract_id) ;
5597+
5598+ // Cycle 1: charlie can pay
5599+ pool_state
5600+ . write ( )
5601+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 11 ) ;
5602+ run_to_block ( 11 , Some ( & mut pool_state) ) ;
5603+
5604+ // Cycle 2: charlie runs out of funds → GracePeriod
5605+ pool_state
5606+ . write ( )
5607+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 21 ) ;
5608+ run_to_block ( 21 , Some ( & mut pool_state) ) ;
5609+
5610+ let contract = SmartContractModule :: contracts ( contract_id) . unwrap ( ) ;
5611+ assert_eq ! ( contract. state, types:: ContractState :: GracePeriod ( 21 ) ) ;
5612+
5613+ // Farmer opts out while contract is in GracePeriod
5614+ assert_ok ! ( TfgridModule :: opt_out_of_v3_billing(
5615+ RuntimeOrigin :: signed( alice( ) ) ,
5616+ node_id,
5617+ ) ) ;
5618+
5619+ // Cycle 3: charlie still can't pay, stays in GracePeriod
5620+ pool_state
5621+ . write ( )
5622+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 31 ) ;
5623+ run_to_block ( 31 , Some ( & mut pool_state) ) ;
5624+
5625+ let contract = SmartContractModule :: contracts ( contract_id) . unwrap ( ) ;
5626+ assert_eq ! ( contract. state, types:: ContractState :: GracePeriod ( 21 ) ) ;
5627+
5628+ // charlie tops up to cover the pre-opt-out overdraft
5629+ Balances :: transfer ( RuntimeOrigin :: signed ( bob ( ) ) , charlie ( ) , 100000000 ) . unwrap ( ) ;
5630+
5631+ // Cycle 4: sufficient funds → contract restored to Created
5632+ pool_state
5633+ . write ( )
5634+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 41 ) ;
5635+ run_to_block ( 41 , Some ( & mut pool_state) ) ;
5636+
5637+ let contract = SmartContractModule :: contracts ( contract_id) . unwrap ( ) ;
5638+ assert_eq ! ( contract. state, types:: ContractState :: Created ) ;
5639+
5640+ // Cycle 5: contract is Created on opted-out node with resources — billing must be free
5641+ let balance_before = Balances :: free_balance ( & charlie ( ) ) ;
5642+ pool_state
5643+ . write ( )
5644+ . should_call_bill_contract ( contract_id, Ok ( Pays :: Yes . into ( ) ) , 51 ) ;
5645+ run_to_block ( 51 , Some ( & mut pool_state) ) ;
5646+
5647+ let balance_after = Balances :: free_balance ( & charlie ( ) ) ;
5648+
5649+ // No charge — migration window active
5650+ assert_eq ! ( balance_before, balance_after) ;
5651+
5652+ // Contract stays Created (not pushed back to GracePeriod despite no payment)
5653+ let contract = SmartContractModule :: contracts ( contract_id) . unwrap ( ) ;
5654+ assert_eq ! ( contract. state, types:: ContractState :: Created ) ;
55815655 } ) ;
55825656}
55835657
0 commit comments