// Copyright 2025 DataHaven // This file is part of DataHaven. // DataHaven is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // DataHaven is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with DataHaven. If not, see . #[path = "common.rs"] mod common; use codec::Encode; use common::*; use datahaven_testnet_runtime::{ configs::EthereumSovereignAccount, currency::HAVE, AccountId, Balance, Balances, DataHavenNativeTransfer, Runtime, RuntimeEvent, RuntimeOrigin, SnowbridgeSystemV2, System, }; use dhp_bridge::NativeTokenTransferMessageProcessor; use frame_support::{assert_noop, assert_ok, traits::fungible::Inspect}; use pallet_datahaven_native_transfer::Event as NativeTransferEvent; use snowbridge_core::TokenIdOf; use snowbridge_inbound_queue_primitives::v2::{ EthereumAsset, Message as SnowbridgeMessage, MessageProcessor, Payload, }; use snowbridge_pallet_outbound_queue_v2::Event as OutboundQueueEvent; use snowbridge_pallet_system::NativeToForeignId; use sp_core::Get; use sp_core::{H160, H256}; use sp_runtime::DispatchError; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; const TRANSFER_AMOUNT: Balance = 1000 * HAVE; const FEE_AMOUNT: Balance = 10 * HAVE; const ETH_ALICE: H160 = H160([0x11; 20]); const ETH_BOB: H160 = H160([0x22; 20]); // Get the gateway address from runtime configuration fn gateway_address() -> H160 { use datahaven_testnet_runtime::configs::runtime_params::dynamic_params::runtime_config::EthereumGatewayAddress; EthereumGatewayAddress::get() } fn register_native_token() -> H256 { let asset_location = Location::here(); let _ = SnowbridgeSystemV2::register_token( root_origin(), Box::new(VersionedLocation::V5(asset_location.clone())), Box::new(VersionedLocation::V5(asset_location.clone())), datahaven_token_metadata(), ); let reanchored = SnowbridgeSystemV2::reanchor(asset_location).unwrap(); TokenIdOf::convert_location(&reanchored).unwrap() } fn setup_sovereign_balance(amount: Balance) { let _ = Balances::force_set_balance(root_origin(), EthereumSovereignAccount::get(), amount); } fn create_message(token_id: H256, amount: Balance, claimer: H160, nonce: u64) -> SnowbridgeMessage { SnowbridgeMessage { gateway: gateway_address(), nonce, origin: H160::zero(), assets: vec![EthereumAsset::ForeignTokenERC20 { token_id, value: amount, }], xcm: Payload::Raw(vec![0x01, 0x02, 0x03]), claimer: Some(claimer.encode()), value: 0, execution_fee: 100, relayer_fee: 50, } } // === Token Registration Tests === #[test] fn native_token_registration_works() { ExtBuilder::default().build().execute_with(|| { let asset_location = Location::here(); // Register the native HAVE token with Snowbridge assert_ok!(SnowbridgeSystemV2::register_token( root_origin(), Box::new(VersionedLocation::V5(asset_location.clone())), Box::new(VersionedLocation::V5(asset_location.clone())), datahaven_token_metadata() )); // Verify token was registered and assigned a valid token ID let reanchored = SnowbridgeSystemV2::reanchor(asset_location).unwrap(); let token_id = TokenIdOf::convert_location(&reanchored).unwrap(); assert_ne!(token_id, H256::zero()); assert_eq!( NativeToForeignId::::get(&reanchored), Some(token_id) ); }); } // === Outbound Transfer Tests === #[test] fn transfer_to_ethereum_works() { ExtBuilder::default().build().execute_with(|| { let _token_id = register_native_token(); let alice = account_id(ALICE); let alice_initial = Balances::balance(&alice); let sovereign_initial = Balances::balance(&EthereumSovereignAccount::get()); assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(alice.clone()), ETH_ALICE, TRANSFER_AMOUNT, FEE_AMOUNT )); assert_eq!( Balances::balance(&alice), alice_initial - TRANSFER_AMOUNT - FEE_AMOUNT ); assert_eq!( Balances::balance(&EthereumSovereignAccount::get()), sovereign_initial + TRANSFER_AMOUNT ); // Check event was emitted assert!(System::events().iter().any(|e| matches!( &e.event, RuntimeEvent::DataHavenNativeTransfer( NativeTransferEvent::TokensTransferredToEthereum { .. } ) ))); }); } #[test] fn transfer_fails_when_paused() { ExtBuilder::default().build().execute_with(|| { let _token_id = register_native_token(); let alice = account_id(ALICE); assert_ok!(DataHavenNativeTransfer::pause(root_origin())); assert_noop!( DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(alice), ETH_ALICE, TRANSFER_AMOUNT, FEE_AMOUNT ), pallet_datahaven_native_transfer::Error::::TransfersDisabled ); }); } #[test] fn multiple_transfers_work() { ExtBuilder::default().build().execute_with(|| { let _token_id = register_native_token(); let alice = account_id(ALICE); let bob = account_id(BOB); assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(alice), ETH_ALICE, TRANSFER_AMOUNT, FEE_AMOUNT )); assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(bob), ETH_BOB, TRANSFER_AMOUNT, FEE_AMOUNT )); let expected_sovereign_balance = TRANSFER_AMOUNT * 2; assert_eq!( Balances::balance(&EthereumSovereignAccount::get()), expected_sovereign_balance ); }); } #[test] fn treasury_collects_fees_from_multiple_transfers() { ExtBuilder::default().build().execute_with(|| { let _token_id = register_native_token(); let alice = account_id(ALICE); let bob = account_id(BOB); let treasury_account = datahaven_testnet_runtime::configs::TreasuryAccount::get(); let initial_treasury_balance = Balances::balance(&treasury_account); let fee1 = 5 * HAVE; let fee2 = 10 * HAVE; assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(alice), ETH_ALICE, TRANSFER_AMOUNT, fee1 )); assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(bob), ETH_BOB, TRANSFER_AMOUNT, fee2 )); let expected_treasury_balance = initial_treasury_balance + fee1 + fee2; assert_eq!( Balances::balance(&treasury_account), expected_treasury_balance ); }); } // === Inbound Message Processing Tests === #[test] fn message_processor_accepts_registered_token() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); assert!( NativeTokenTransferMessageProcessor::::can_process_message(&alice, &message) ); }); } #[test] fn message_processor_rejects_unregistered_token() { ExtBuilder::default().build().execute_with(|| { let fake_token_id = H256::from_low_u64_be(0x9999); let alice = account_id(ALICE); let message = create_message(fake_token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); assert!( !NativeTokenTransferMessageProcessor::::can_process_message(&alice, &message) ); }); } #[test] fn message_processor_rejects_empty_assets() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let mut message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); message.assets = vec![]; assert!( !NativeTokenTransferMessageProcessor::::can_process_message(&alice, &message) ); }); } #[test] fn inbound_message_processing_works() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); setup_sovereign_balance(TRANSFER_AMOUNT * 2); let sovereign_initial = Balances::balance(&EthereumSovereignAccount::get()); assert_ok!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message) ); let recipient: AccountId = ETH_ALICE.into(); assert_eq!(Balances::balance(&recipient), TRANSFER_AMOUNT); assert_eq!( Balances::balance(&EthereumSovereignAccount::get()), sovereign_initial - TRANSFER_AMOUNT ); // Check unlock event was emitted assert!(System::events().iter().any(|e| matches!( &e.event, RuntimeEvent::DataHavenNativeTransfer(NativeTransferEvent::TokensUnlocked { .. }) ))); }); } #[test] fn multiple_assets_processing_sums_amounts() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let mut message = create_message(token_id, 0, ETH_ALICE, 1); message.assets = vec![ EthereumAsset::ForeignTokenERC20 { token_id, value: 300 * HAVE, }, EthereumAsset::ForeignTokenERC20 { token_id, value: 200 * HAVE, }, EthereumAsset::ForeignTokenERC20 { token_id, value: 500 * HAVE, }, ]; setup_sovereign_balance(2000 * HAVE); assert_ok!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message) ); let recipient: AccountId = ETH_ALICE.into(); let total_amount = 1000 * HAVE; assert_eq!(Balances::balance(&recipient), total_amount); }); } #[test] fn processing_fails_without_claimer() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let mut message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); message.claimer = None; assert_noop!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message), DispatchError::Other("No claimer specified in message") ); }); } #[test] fn processing_fails_with_zero_amount() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let message = create_message(token_id, 0, ETH_ALICE, 1); assert_noop!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message), DispatchError::Other("No native token found in assets") ); }); } #[test] fn processing_fails_with_insufficient_sovereign_balance() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); let message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); setup_sovereign_balance(TRANSFER_AMOUNT / 2); // Insufficient balance assert_noop!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message), pallet_datahaven_native_transfer::Error::::InsufficientSovereignBalance ); }); } // === Integration Tests === #[test] fn end_to_end_transfer_flow() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); // Outbound transfer assert_ok!(DataHavenNativeTransfer::transfer_to_ethereum( RuntimeOrigin::signed(alice.clone()), ETH_ALICE, TRANSFER_AMOUNT, FEE_AMOUNT )); // Verify message was queued assert!(System::events().iter().any(|e| matches!( &e.event, RuntimeEvent::EthereumOutboundQueueV2(OutboundQueueEvent::MessageQueued { .. }) ))); // Simulate inbound processing let message = create_message(token_id, TRANSFER_AMOUNT, ETH_BOB, 1); setup_sovereign_balance(TRANSFER_AMOUNT * 3); assert_ok!( snowbridge_pallet_inbound_queue_v2::Pallet::::process_message(alice, message) ); let recipient: AccountId = ETH_BOB.into(); assert_eq!(Balances::balance(&recipient), TRANSFER_AMOUNT); }); } #[test] fn message_routing_works_correctly() { ExtBuilder::default().build().execute_with(|| { let token_id = register_native_token(); let alice = account_id(ALICE); // Native token message should be accepted let native_message = create_message(token_id, TRANSFER_AMOUNT, ETH_ALICE, 1); assert!( NativeTokenTransferMessageProcessor::::can_process_message( &alice, &native_message ) ); // Non-native token message should be rejected let fake_token_id = H256::from_low_u64_be(0x8888); let non_native_message = create_message(fake_token_id, TRANSFER_AMOUNT, ETH_ALICE, 2); assert!( !NativeTokenTransferMessageProcessor::::can_process_message( &alice, &non_native_message ) ); }); }