diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 363f03d22..56b87e684 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -35,6 +35,7 @@ * [Export Secrets](view-only-accounts/account-secrets/export\_view\_only\_account\_secrets.md) * [Balance](view-only-accounts/balance/README.md) * [Get Balance](view-only-accounts/balance/get\_balance\_for\_view\_only\_account.md) + * [Get Balance For Address](view-only-accounts/balance/get\_balance\_for\_view\_only\_address.md) * [Syncing](view-only-accounts/syncing/README.md) * [Create Account Sync Request](view-only-accounts/syncing/create\_view\_only\_account\_sync\_request.md) * [Sync Account](view-only-accounts/syncing/sync\_view\_only\_account.md) diff --git a/docs/view-only-accounts/balance/get_balance_for_view_only_account.md b/docs/view-only-accounts/balance/get_balance_for_view_only_account.md index 25d426c29..6b8cf764f 100644 --- a/docs/view-only-accounts/balance/get_balance_for_view_only_account.md +++ b/docs/view-only-accounts/balance/get_balance_for_view_only_account.md @@ -1,5 +1,5 @@ --- -description: Get the current balance for a given view only account. +description: Get the current balance for a given account. --- # Get Balance For View Only Account @@ -8,7 +8,7 @@ description: Get the current balance for a given view only account. | Required Param | Purpose | Requirements | | :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | ## Example @@ -32,12 +32,17 @@ description: Get the current balance for a given view only account. "method": "get_balance_for_view_only_account", "result": { "balance": { - "object": "balance", - "balance": "10000000000000", - "network_block_height": "468847", - "local_block_height": "468847", - "account_block_height": "468847", - "is_synced": true + "object": "balance", + "network_block_height": "152918", + "local_block_height": "152918", + "account_block_height": "152003", + "is_synced": false, + "unspent_pmob": "110000000000000000", + "max_spendable_pmob": "110000000000000000", + "pending_pmob": "0", + "spent_pmob": "0", + "secreted_pmob": "0", + "orphaned_pmob": "0" } }, "error": null, diff --git a/docs/view-only-accounts/balance/get_balance_for_view_only_address.md b/docs/view-only-accounts/balance/get_balance_for_view_only_address.md new file mode 100644 index 000000000..f970fed0a --- /dev/null +++ b/docs/view-only-accounts/balance/get_balance_for_view_only_address.md @@ -0,0 +1,53 @@ +--- +description: Get the current balance for a given address. +--- + +# Get Balance For Address + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `address` | The address on which to perform this action. | Address must be assigned for an account in the wallet. | + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_balance_for_view_only_address", + "params": { + "address": "3P4GtGkp5UVBXUzBqirgj7QFetWn4PsFPsHBXbC6A8AXw1a9CMej969jneiN1qKcwdn6e1VtD64EruGVSFQ8wHk5xuBHndpV9WUGQ78vV7Z" + }, + "jsonrpc": "2.0", + "api_version": "2", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_balance_for_view_only_address", + "result": { + "balance": { + "object": "balance", + "network_block_height": "152961", + "local_block_height": "152961", + "account_block_height": "152961", + "is_synced": true, + "unspent_pmob": "11881402222024", + "max_spendable_pmob": "11881402222024", + "pending_pmob": "0", + "spent_pmob": "84493835554166", + "secreted_pmob": "0", + "orphaned_pmob": "0" + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, + "api_version": "2" +} +``` +{% endtab %} +{% endtabs %} + diff --git a/full-service/src/db/view_only_txo.rs b/full-service/src/db/view_only_txo.rs index 490e0f3f4..98ba07751 100644 --- a/full-service/src/db/view_only_txo.rs +++ b/full-service/src/db/view_only_txo.rs @@ -62,6 +62,26 @@ pub trait ViewOnlyTxoModel { conn: &Conn, ) -> Result, WalletDbError>; + fn list_orphaned(account_id_hex: &str, conn: &Conn) -> Result, WalletDbError>; + + fn list_unspent( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError>; + + fn list_pending( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError>; + + fn list_spent( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError>; + /// Select a set of unspent view only Txos to reach a given value. /// /// Returns: @@ -259,6 +279,90 @@ impl ViewOnlyTxoModel for ViewOnlyTxo { Ok(results) } + fn list_orphaned(account_id_hex: &str, conn: &Conn) -> Result, WalletDbError> { + use schema::view_only_txos; + + let txos: Vec = view_only_txos::table + .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) + .filter(view_only_txos::key_image.is_null()) + .filter(view_only_txos::subaddress_index.is_null()) + .load(conn)?; + + Ok(txos) + } + + fn list_unspent( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError> { + use schema::view_only_txos; + + let results = view_only_txos::table + .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) + .filter(view_only_txos::received_block_index.is_not_null()) + .filter(view_only_txos::pending_tombstone_block_index.is_null()) + .filter(view_only_txos::spent_block_index.is_null()); + + let txos = if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { + let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; + results + .filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)) + .load(conn)? + } else { + results.load(conn)? + }; + + Ok(txos) + } + + fn list_pending( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError> { + use schema::view_only_txos; + + let results = view_only_txos::table + .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) + .filter(view_only_txos::pending_tombstone_block_index.is_not_null()) + .filter(view_only_txos::spent_block_index.is_null()); + + let txos = if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { + let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; + results + .filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)) + .load(conn)? + } else { + results.load(conn)? + }; + + Ok(txos) + } + + fn list_spent( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result, WalletDbError> { + use schema::view_only_txos; + + let results = view_only_txos::table + .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) + .filter(view_only_txos::spent_block_index.is_not_null()); + + let txos = if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { + let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; + results + .filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)) + .load(conn)? + } else { + results.load(conn)? + }; + + Ok(txos) + } + // This is a direct port of txo selection and // the whole things needs a nice big refactor // to make it happy. diff --git a/full-service/src/json_rpc/balance.rs b/full-service/src/json_rpc/balance.rs index ee7d9a267..8aa71da9f 100644 --- a/full-service/src/json_rpc/balance.rs +++ b/full-service/src/json_rpc/balance.rs @@ -76,47 +76,3 @@ impl From<&service::balance::Balance> for Balance { } } } - -/// The "balance" for a view-only-account, as well as some information about -/// syncing status needed to interpret the balance correctly. In order for the -/// balance to be accurate, you must mark view_only_txos as spent -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct ViewOnlyBalance { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// Total pico MOB sent to this account minus the total amount marked as - /// spent - pub balance: String, - - /// The block count of MobileCoin's distributed ledger. - pub network_block_height: String, - - /// The local block count downloaded from the ledger. The local database - /// is synced when the local_block_height reaches the network_block_height. - /// The account_block_height can only sync up to local_block_height. - pub local_block_height: String, - - /// The scanned local block count for this account. This value will never - /// be greater than the local_block_height. At fully synced, it will match - /// network_block_height. - pub account_block_height: String, - - /// Whether the account is synced with the network_block_height. Balances - /// may not appear correct if the account is still syncing. - pub is_synced: bool, -} - -impl From<&service::balance::ViewOnlyBalance> for ViewOnlyBalance { - fn from(src: &service::balance::ViewOnlyBalance) -> ViewOnlyBalance { - ViewOnlyBalance { - object: "balance".to_string(), - balance: src.balance.to_string(), - network_block_height: src.network_block_height.to_string(), - local_block_height: src.local_block_height.to_string(), - account_block_height: src.synced_blocks.to_string(), - is_synced: src.synced_blocks == src.network_block_height, - } - } -} diff --git a/full-service/src/json_rpc/json_rpc_response.rs b/full-service/src/json_rpc/json_rpc_response.rs index 0b47f7261..ad11f95fe 100644 --- a/full-service/src/json_rpc/json_rpc_response.rs +++ b/full-service/src/json_rpc/json_rpc_response.rs @@ -9,7 +9,7 @@ use crate::{ account::Account, account_secrets::AccountSecrets, address::Address, - balance::{Balance, ViewOnlyBalance}, + balance::Balance, block::{Block, BlockContents}, confirmation_number::Confirmation, gift_code::GiftCode, @@ -251,10 +251,10 @@ pub enum JsonCommandResponse { balance: Balance, }, get_balance_for_view_only_account { - balance: ViewOnlyBalance, + balance: Balance, }, get_balance_for_view_only_address { - balance: ViewOnlyBalance, + balance: Balance, }, get_block { block: Block, diff --git a/full-service/src/json_rpc/wallet.rs b/full-service/src/json_rpc/wallet.rs index a26f2734a..bc21352de 100644 --- a/full-service/src/json_rpc/wallet.rs +++ b/full-service/src/json_rpc/wallet.rs @@ -8,7 +8,7 @@ use crate::{ json_rpc::{ account_secrets::AccountSecrets, address::Address, - balance::{Balance, ViewOnlyBalance}, + balance::Balance, block::{Block, BlockContents}, confirmation_number::Confirmation, gift_code::GiftCode, @@ -750,7 +750,7 @@ where } JsonCommandRequest::get_balance_for_view_only_account { account_id } => { JsonCommandResponse::get_balance_for_view_only_account { - balance: ViewOnlyBalance::from( + balance: Balance::from( &service .get_balance_for_view_only_account(&account_id) .map_err(format_error)?, @@ -759,7 +759,7 @@ where } JsonCommandRequest::get_balance_for_view_only_address { address } => { JsonCommandResponse::get_balance_for_view_only_address { - balance: ViewOnlyBalance::from( + balance: Balance::from( &service .get_balance_for_view_only_address(&address) .map_err(format_error)?, diff --git a/full-service/src/service/balance.rs b/full-service/src/service/balance.rs index 2f5984a74..5d793aba1 100644 --- a/full-service/src/service/balance.rs +++ b/full-service/src/service/balance.rs @@ -86,14 +86,6 @@ pub struct Balance { pub max_spendable: u128, } -// The balance object for view-only-accounts -pub struct ViewOnlyBalance { - pub balance: u128, - pub network_block_height: u64, - pub local_block_height: u64, - pub synced_blocks: u64, -} - /// The Network Status object. /// This holds the number of blocks in the ledger, on the network and locally. pub struct NetworkStatus { @@ -138,14 +130,14 @@ pub trait BalanceService { fn get_balance_for_view_only_account( &self, account_id: &str, - ) -> Result; + ) -> Result; fn get_balance_for_address(&self, address: &str) -> Result; fn get_balance_for_view_only_address( &self, address: &str, - ) -> Result; + ) -> Result; fn get_network_status(&self) -> Result; @@ -165,7 +157,7 @@ where let conn = self.wallet_db.get_conn()?; let (unspent, max_spendable, pending, spent, secreted, orphaned) = - Self::get_balance_inner(account_id_hex, &conn)?; + Self::get_balance_inner(account_id_hex, None, &conn)?; let network_block_height = self.get_network_block_height()?; let local_block_height = self.ledger_db.num_blocks()?; @@ -187,25 +179,26 @@ where fn get_balance_for_view_only_account( &self, account_id: &str, - ) -> Result { + ) -> Result { let conn = self.wallet_db.get_conn()?; - let txos = ViewOnlyTxo::list_for_account(account_id, None, None, &conn)?; - let total_value = txos.iter().map(|t| (t.value as u64) as u128).sum::(); - let spent = txos - .iter() - .filter(|t| t.spent_block_index.is_some()) - .map(|t| (t.value as u64) as u128) - .sum::(); + + let (unspent, max_spendable, pending, spent, secreted, orphaned) = + Self::get_view_only_balance_inner(account_id, None, &conn)?; let network_block_height = self.get_network_block_height()?; let local_block_height = self.ledger_db.num_blocks()?; let account = ViewOnlyAccount::get(account_id, &conn)?; - Ok(ViewOnlyBalance { - balance: total_value - spent, + Ok(Balance { + unspent, + pending, + spent, + secreted, + orphaned, network_block_height, local_block_height, synced_blocks: account.next_block_index as u64, + max_spendable, }) } @@ -216,30 +209,8 @@ where let conn = self.wallet_db.get_conn()?; let assigned_address = AssignedSubaddress::get(address, &conn)?; - let max_spendable = - Txo::list_spendable(&assigned_address.account_id_hex, None, Some(address), &conn)? - .max_spendable_in_wallet; - - // Orphaned txos have no subaddress assigned, so none of these txos can - // be orphaned. - let orphaned: u128 = 0; - - let unspent = Txo::list_unspent(&assigned_address.account_id_hex, Some(address), &conn)? - .iter() - .map(|txo| (txo.value as u64) as u128) - .sum::(); - let pending = Txo::list_pending(&assigned_address.account_id_hex, Some(address), &conn)? - .iter() - .map(|txo| (txo.value as u64) as u128) - .sum::(); - let spent = Txo::list_spent(&assigned_address.account_id_hex, Some(address), &conn)? - .iter() - .map(|txo| (txo.value as u64) as u128) - .sum::(); - let secreted = Txo::list_secreted(&assigned_address.account_id_hex, &conn)? - .iter() - .map(|txo| (txo.value as u64) as u128) - .sum::(); + let (unspent, max_spendable, pending, spent, secreted, orphaned) = + Self::get_balance_inner(&assigned_address.account_id_hex, Some(address), &conn)?; let account = Account::get(&AccountID(assigned_address.account_id_hex), &conn)?; @@ -259,24 +230,27 @@ where fn get_balance_for_view_only_address( &self, address: &str, - ) -> Result { + ) -> Result { let conn = self.wallet_db.get_conn()?; - let txos = ViewOnlyTxo::list_for_address(address, &conn)?; - let total_value = txos.iter().map(|t| (t.value as u64) as u128).sum::(); - let spent = txos - .iter() - .filter(|t| t.spent_block_index.is_some()) - .map(|t| (t.value as u64) as u128) - .sum::(); + let view_only_subaddress = ViewOnlySubaddress::get(address, &conn)?; + let (unspent, max_spendable, pending, spent, secreted, orphaned) = + Self::get_view_only_balance_inner( + &view_only_subaddress.view_only_account_id_hex, + Some(address), + &conn, + )?; let network_block_height = self.get_network_block_height()?; let local_block_height = self.ledger_db.num_blocks()?; + let account = ViewOnlyAccount::get(&view_only_subaddress.view_only_account_id_hex, &conn)?; - let subaddress = ViewOnlySubaddress::get(address, &conn)?; - let account = ViewOnlyAccount::get(&subaddress.view_only_account_id_hex, &conn)?; - - Ok(ViewOnlyBalance { - balance: total_value - spent, + Ok(Balance { + unspent, + max_spendable, + pending, + spent, + secreted, + orphaned, network_block_height, local_block_height, synced_blocks: account.next_block_index as u64, @@ -310,7 +284,7 @@ where let mut account_ids = Vec::new(); for account in accounts { let account_id = AccountID(account.account_id_hex.clone()); - let balance = Self::get_balance_inner(&account_id.to_string(), &conn)?; + let balance = Self::get_balance_inner(&account_id.to_string(), None, &conn)?; account_map.insert(account_id.clone(), account.clone()); unspent += balance.0; pending += balance.1; @@ -357,34 +331,70 @@ where { fn get_balance_inner( account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, conn: &Conn, ) -> Result<(u128, u128, u128, u128, u128, u128), BalanceServiceError> { let max_spendable = - Txo::list_spendable(account_id_hex, None, None, conn)?.max_spendable_in_wallet; - // Note: We need to cast to u64 first, because i64 could have wrapped, then to - // u128 - let unspent = Txo::list_unspent(account_id_hex, None, conn)? + Txo::list_spendable(account_id_hex, None, assigned_subaddress_b58, conn)? + .max_spendable_in_wallet; + let unspent = Txo::list_unspent(account_id_hex, assigned_subaddress_b58, conn)? + .iter() + .map(|t| (t.value as u64) as u128) + .sum::(); + let spent = Txo::list_spent(account_id_hex, assigned_subaddress_b58, conn)? .iter() .map(|t| (t.value as u64) as u128) .sum::(); - let spent = Txo::list_spent(account_id_hex, None, conn)? + let pending = Txo::list_pending(account_id_hex, assigned_subaddress_b58, conn)? .iter() .map(|t| (t.value as u64) as u128) .sum::(); - let secreted = Txo::list_secreted(account_id_hex, conn)? + + let secreted = if assigned_subaddress_b58.is_some() { + 0 + } else { + Txo::list_secreted(account_id_hex, conn)? + .iter() + .map(|t| t.value as u128) + .sum::() + }; + + let orphaned = if assigned_subaddress_b58.is_some() { + 0 + } else { + Txo::list_orphaned(account_id_hex, conn)? + .iter() + .map(|t| t.value as u128) + .sum::() + }; + + let result = (unspent, max_spendable, pending, spent, secreted, orphaned); + Ok(result) + } + + fn get_view_only_balance_inner( + account_id_hex: &str, + assigned_subaddress_b58: Option<&str>, + conn: &Conn, + ) -> Result<(u128, u128, u128, u128, u128, u128), BalanceServiceError> { + let unspent = ViewOnlyTxo::list_unspent(account_id_hex, assigned_subaddress_b58, conn)? .iter() .map(|t| (t.value as u64) as u128) .sum::(); - let orphaned = Txo::list_orphaned(account_id_hex, conn)? + let spent = ViewOnlyTxo::list_spent(account_id_hex, assigned_subaddress_b58, conn)? .iter() .map(|t| (t.value as u64) as u128) .sum::(); - let pending = Txo::list_pending(account_id_hex, None, conn)? + let orphaned = ViewOnlyTxo::list_orphaned(account_id_hex, conn)? + .iter() + .map(|t| (t.value as u64) as u128) + .sum::(); + let pending = ViewOnlyTxo::list_pending(account_id_hex, assigned_subaddress_b58, conn)? .iter() .map(|t| (t.value as u64) as u128) .sum::(); - let result = (unspent, max_spendable, pending, spent, secreted, orphaned); + let result = (unspent, 0, pending, spent, 0, orphaned); Ok(result) } } @@ -393,12 +403,20 @@ where mod tests { use super::*; use crate::{ - service::{account::AccountService, address::AddressService}, + service::{ + account::AccountService, address::AddressService, + view_only_account::ViewOnlyAccountService, + }, test_utils::{get_test_ledger, manually_sync_account, setup_wallet_service, MOB}, util::b58::b58_encode_public_address, }; - use mc_account_keys::{AccountKey, PublicAddress, RootEntropy, RootIdentity}; + use mc_account_keys::{ + AccountKey, PublicAddress, RootEntropy, RootIdentity, CHANGE_SUBADDRESS_INDEX, + DEFAULT_SUBADDRESS_INDEX, + }; use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; + use mc_transaction_core::{encrypted_fog_hint::EncryptedFogHint, tx::TxOut}; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -500,4 +518,130 @@ mod tests { Err(e) => panic!("Unexpected error {:?}", e), } } + + // The balance for an address should be accurate. + #[test_with_logger] + fn test_view_only_balance(logger: Logger) { + // setup view only account + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let known_recipients: Vec = Vec::new(); + let current_block_height = 12; //index 11 + let ledger_db = get_test_ledger( + 5, + &known_recipients, + current_block_height as usize, + &mut rng, + ); + let service = setup_wallet_service(ledger_db.clone(), logger.clone()); + let conn = service.wallet_db.get_conn().unwrap(); + + let view_private_key = RistrettoPrivate::from_random(&mut rng); + let spend_private_key = RistrettoPrivate::from_random(&mut rng); + + let name = "testing"; + + let account_key = AccountKey::new(&spend_private_key, &view_private_key); + let account_id = AccountID::from(&account_key); + let main_public_address = account_key.default_subaddress(); + let change_public_address = account_key.change_subaddress(); + let mut subaddresses: Vec<(String, u64, String, RistrettoPublic)> = Vec::new(); + subaddresses.push(( + b58_encode_public_address(&main_public_address).unwrap(), + DEFAULT_SUBADDRESS_INDEX, + "Main".to_string(), + *main_public_address.spend_public_key(), + )); + subaddresses.push(( + b58_encode_public_address(&change_public_address).unwrap(), + CHANGE_SUBADDRESS_INDEX, + "Change".to_string(), + *change_public_address.spend_public_key(), + )); + + service + .import_view_only_account( + &account_id.to_string(), + &view_private_key, + DEFAULT_SUBADDRESS_INDEX, + CHANGE_SUBADDRESS_INDEX, + 2, + name.clone(), + subaddresses, + ) + .unwrap(); + + // add funds to account + for _ in 0..2 { + let value = 420 * MOB; + let tx_private_key = RistrettoPrivate::from_random(&mut rng); + let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); + let fake_tx_out = + TxOut::new(value as u64, &main_public_address, &tx_private_key, hint).unwrap(); + ViewOnlyTxo::create( + fake_tx_out.clone(), + value, + Some(DEFAULT_SUBADDRESS_INDEX), + Some(current_block_height), + &account_id.to_string(), + &conn, + ) + .unwrap(); + } + + // test balance for account + let balance: Balance = service + .get_balance_for_view_only_account(&account_id.to_string()) + .unwrap(); + assert_eq!(balance.unspent as u64, 840 * MOB); + // view only accounts have no spendable MOB + assert_eq!(balance.max_spendable, 0); + assert_eq!(balance.spent, 0); + assert_eq!(balance.pending, 0); + assert_eq!(balance.secreted, 0); + assert_eq!(balance.orphaned, 0); + + // add funds to specific address + let subaddress_index = 3; + let subaddress = account_key.subaddress(subaddress_index); + let b58_pub_address = + b58_encode_public_address(&subaddress).expect("Could not encode public address"); + service + .import_subaddresses( + &account_id.to_string(), + [( + b58_pub_address.clone(), + subaddress_index, + "cheese".to_string(), + subaddress.spend_public_key().to_owned(), + )] + .to_vec(), + ) + .unwrap(); + + let value = 100 * MOB; + let tx_private_key = RistrettoPrivate::from_random(&mut rng); + let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); + let fake_tx_out = + TxOut::new(value as u64, &main_public_address, &tx_private_key, hint).unwrap(); + ViewOnlyTxo::create( + fake_tx_out.clone(), + value, + Some(subaddress_index), + Some(current_block_height), + &account_id.to_string(), + &conn, + ) + .unwrap(); + + let balance: Balance = service + .get_balance_for_view_only_address(&b58_pub_address) + .unwrap(); + assert_eq!(balance.unspent as u64, 100 * MOB); + // view only accounts have no spendable MOB + assert_eq!(balance.max_spendable, 0); + assert_eq!(balance.spent, 0); + assert_eq!(balance.pending, 0); + assert_eq!(balance.secreted, 0); + assert_eq!(balance.orphaned, 0); + } }