diff --git a/src/icp_subaccount_indexer/icp_subaccount_indexer.did b/src/icp_subaccount_indexer/icp_subaccount_indexer.did index 3447882..310b9f3 100644 --- a/src/icp_subaccount_indexer/icp_subaccount_indexer.did +++ b/src/icp_subaccount_indexer/icp_subaccount_indexer.did @@ -68,4 +68,5 @@ service : (Network, nat64, nat32, text, text) -> { set_webhook_url : (text) -> (Result); single_sweep : (text) -> (Result_9); sweep : () -> (Result_9); + sweep_subaccount : (text, float64) -> (Result_8); } diff --git a/src/icp_subaccount_indexer/src/lib.rs b/src/icp_subaccount_indexer/src/lib.rs index 11a3496..1c12572 100644 --- a/src/icp_subaccount_indexer/src/lib.rs +++ b/src/icp_subaccount_indexer/src/lib.rs @@ -1099,6 +1099,51 @@ async fn single_sweep(tx_hash_arg: String) -> Result, Error> { Ok(results) } +#[update] +async fn sweep_subaccount(subaccountid_hex: String, amount: f64) -> Result { + authenticate().map_err(|e| Error { message: e })?; + + let custodian_id = get_custodian_id().map_err(|e| Error { message: e })?; + + let matching_subaccount = LIST_OF_SUBACCOUNTS.with(|subaccounts| { + subaccounts + .borrow() + .iter() + .find(|(_, subaccount)| { + let subaccountid = to_subaccount_id(**subaccount); + subaccountid.to_hex() == subaccountid_hex + }) + .map(|(_, &subaccount)| subaccount) + }); + + let subaccount = matching_subaccount.ok_or_else(|| Error { + message: "Subaccount not found".to_string(), + })?; + + // Convert amount to e8s, handling potential precision issues + let amount_e8s = (amount * 100_000_000.0).round() as u64; + + // Check for potential overflow or underflow + if amount_e8s == u64::MAX || amount < 0.0 { + return Err(Error { + message: "Invalid amount: overflow or negative value".to_string(), + }); + } + + let transfer_args = TransferArgs { + memo: Memo(0), + amount: Tokens::from_e8s(amount_e8s), + fee: Tokens::from_e8s(10_000), + from_subaccount: Some(subaccount), + to: custodian_id, + created_at_time: None, + }; + + InterCanisterCallManager::transfer(transfer_args) + .await + .map_err(|e| Error { message: e }) +} + #[update] async fn set_sweep_failed(tx_hash_arg: String) -> Result, Error> { authenticate().map_err(|e| Error { message: e })?; diff --git a/src/icp_subaccount_indexer/src/tests.rs b/src/icp_subaccount_indexer/src/tests.rs index a95af46..25ce4b8 100644 --- a/src/icp_subaccount_indexer/src/tests.rs +++ b/src/icp_subaccount_indexer/src/tests.rs @@ -542,6 +542,24 @@ mod tests { "The function should return the correct URL" ); } + + #[tokio::test] + async fn test_sweep_subaccount_decimal_amount() { + // Setup + let (_, to_subaccountid, _) = setup_principals(); + let subaccountid_hex = to_subaccountid.to_hex(); + let amount = 1.25; // 1.25 ICP + + // Execute + let result = sweep_subaccount(subaccountid_hex, amount).await; + + // Assert + assert!( + result.is_ok(), + "Sweeping subaccount with decimal amount should succeed" + ); + assert_eq!(result.unwrap(), 1, "BlockIndex should be 1"); + } } #[cfg(feature = "sad_path")] @@ -698,5 +716,91 @@ mod tests { "The function should return the default String" ); } + + #[tokio::test] + async fn test_sweep_subaccount_nonexistent() { + setup_sweep_environment(); + let (_, to_subaccountid, _) = setup_principals(); + + // Setup + let nonexistent_subaccountid = + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let amount = 1.25; + + // Execute + let result = sweep_subaccount(nonexistent_subaccountid.to_string(), amount).await; + + // Assert + assert!( + result.is_err(), + "Sweeping nonexistent subaccount should fail" + ); + assert_eq!( + result.unwrap_err().message, + "Subaccount not found", + "Error message should indicate subaccount not found" + ); + teardown_sweep_environment(); + } + + #[tokio::test] + async fn test_sweep_subaccount_transfer_failure() { + // Setup + let (_, to_subaccountid, _) = setup_principals(); + let subaccountid_hex = to_subaccountid.to_hex(); + let amount = 1.25; + + // Execute + let result = sweep_subaccount(subaccountid_hex, amount).await; + + // Assert + assert!( + result.is_err(), + "Sweeping should fail due to transfer failure" + ); + assert_eq!( + result.unwrap_err().message, + "transfer failed", + "Error message should indicate transfer failure" + ); + } + + #[tokio::test] + async fn test_sweep_subaccount_negative_amount() { + // Setup + let (_, to_subaccountid, _) = setup_principals(); + let subaccountid_hex = to_subaccountid.to_hex(); + let amount = -1.0; + + // Execute + let result = sweep_subaccount(subaccountid_hex, amount).await; + + // Assert + assert!(result.is_err(), "Sweeping with negative amount should fail"); + assert_eq!( + result.unwrap_err().message, + "Invalid amount: overflow or negative value", + "Error message should indicate invalid amount" + ); + } + + #[tokio::test] + async fn test_sweep_subaccount_overflow_amount() { + // Setup + let (_, to_subaccountid, _) = setup_principals(); + let subaccountid_hex = to_subaccountid.to_hex(); + let amount = f64::MAX; + + // Execute + let result = sweep_subaccount(subaccountid_hex, amount).await; + + // Assert + assert!(result.is_err(), "Sweeping with overflow amount should fail"); + assert_eq!( + result.unwrap_err().message, + "Invalid amount: overflow or negative value", + "Error message should indicate invalid amount" + ); + } } }