Skip to content

Comments

rpc: Add z_getbalances#362

Draft
str4d wants to merge 1 commit intomainfrom
z_getbalances
Draft

rpc: Add z_getbalances#362
str4d wants to merge 1 commit intomainfrom
z_getbalances

Conversation

@str4d
Copy link
Collaborator

@str4d str4d commented Feb 3, 2026

Closes #101.

@str4d str4d added this to the Zallet alpha phase milestone Feb 3, 2026
@str4d str4d added the A-rpc-interface Area: RPC interface label Feb 3, 2026
@str4d str4d force-pushed the z_getbalances branch 2 times, most recently from 4f60c0b to 542c349 Compare February 3, 2026 19:25
legacy_transparent: Option<TransparentBalance>,

/// The total of all funds for which this wallet controls spending keys.
total: Balance,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A downside to the current design including total fields for each account as well as the wallet balance, is that it is very duplicative:

$ zallet -d local/wallet-testnet rpc z_getbalances 1 true
{
  "accounts": [
    {
      "account_uuid": "46416286-4799-4ac4-be4b-dd879274d340",
      "sapling": {
        "value": 4.69997000,
        "valueZat": 469997000
      },
      "total": {
        "value": 4.69997000,
        "valueZat": 469997000
      }
    }
  ],
  "total": {
    "value": 4.69997000,
    "valueZat": 469997000
  }
}

But maybe this is only visually annoying, and doesn't actually matter? From an automated consumption perspective, this mostly just doubles the JSON-RPC response size, which is linear in the number of accounts in the wallet, so likely not large.

Copy link
Contributor

@daira daira Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine I think. But personally, I would just drop "value" and only include "valueZat". Decimals were always a mistake.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now made things slightly more verbose by including pending etc. value, and choosing to always show a spendable object anywhere a balance is show (so always for totals, and present whenever pending etc is present even if spendable is zero).

For a wallet with a single empty account (either due to being new, or just after starting account recovery), we see this in the current PR:

$ zallet -d local/wallet-testnet rpc z_getbalances 1 true
{
  "accounts": [
    {
      "account_uuid": "6ee0fac8-0621-4dc3-a507-9107ad47dc07",
      "total": {
        "spendable": {
          "value": 0.00000000,
          "valueZat": 0
        }
      }
    }
  ],
  "total": {
    "spendable": {
      "value": 0.00000000,
      "valueZat": 0
    }
  }
}

After starting a previously-synced wallet that has been offline for some time, we see this in the current PR:

$ zallet -d local/wallet-testnet rpc z_getbalances 1 true
{
  "accounts": [
    {
      "account_uuid": "46416286-4799-4ac4-be4b-dd879274d340",
      "sapling": {
        "spendable": {
          "value": 0.00000000,
          "valueZat": 0
        },
        "pending": {
          "value": 4.69997000,
          "valueZat": 469997000
        }
      },
      "orchard": {
        "spendable": {
          "value": 0.00000000,
          "valueZat": 0
        },
        "pending": {
          "value": 9.60000000,
          "valueZat": 960000000
        }
      },
      "total": {
        "spendable": {
          "value": 0.00000000,
          "valueZat": 0
        },
        "pending": {
          "value": 14.29997000,
          "valueZat": 1429997000
        }
      }
    }
  ],
  "total": {
    "spendable": {
      "value": 0.00000000,
      "valueZat": 0
    },
    "pending": {
      "value": 14.29997000,
      "valueZat": 1429997000
    }
  }
}

And once synced, we see this:

$ zallet -d local/wallet-testnet rpc z_getbalances 1 true
{
  "accounts": [
    {
      "account_uuid": "46416286-4799-4ac4-be4b-dd879274d340",
      "sapling": {
        "spendable": {
          "value": 4.69997000,
          "valueZat": 469997000
        }
      },
      "orchard": {
        "spendable": {
          "value": 9.60000000,
          "valueZat": 960000000
        }
      },
      "total": {
        "spendable": {
          "value": 14.29997000,
          "valueZat": 1429997000
        }
      }
    }
  ],
  "total": {
    "spendable": {
      "value": 14.29997000,
      "valueZat": 1429997000
    }
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question of whether the duplication is useful is actually the same question as whether or not we should include the "value" field. If our position is that this API output is not intended to be directly consumed by humans, then both "value" and the overall total have no utility; an API consumer is going to be able to compute an overall total and doesn't need the decimal notation. In general, I don't think we should be pursuing a "humans should read JSON" path. So: I vote for "valueZat" only, and no overall total.

Comment on lines 150 to 156
// TODO: Separate out transparent coinbase.
let transparent_regular = account.unshielded_balance().spendable_value();
let transparent_coinbase = Zatoshis::ZERO;
let transparent_coinbase_immature = Zatoshis::ZERO;

let sapling = account.sapling_balance().spendable_value();
let orchard = account.orchard_balance().spendable_value();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using Balance::spendable_value() here because, at least as being designed for zcashd, the returned values were gated on the provided minconf argument. Setting that to 0 should result in spendable_value() being equal to the total discovered balance, for wallets that are not in the middle of being recovered.

We should consider whether this RPC should return something indicating that the wallet is fully-synced or not (i.e. whether minconf=0 will show all unspent funds or not).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading the relevant zcashd issues and PR again, I decided to have fields for every part of the balance:

  • spendable: what the user expects to be their "balance".
  • locked: anything that is spendable and trying to be spent elsewhere. Always omitted until we add support for locking outputs.
  • pending: Anything that is temporarily unspendable (e.g. due to the requested minconf).
  • dust: Anything that is (essentially, modulo usage of already-paid-for dummy spends) permanently unspendable.

@str4d str4d force-pushed the z_getbalances branch 2 times, most recently from 9e6b5c0 to 8cc445d Compare February 4, 2026 18:42
Comment on lines +349 to +350
/// - `minconf` (numeric, optional, default=1) Only include transactions confirmed at
/// least this many times.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - `minconf` (numeric, optional, default=1) Only include transactions confirmed at
/// least this many times.
/// - `minconf` (numeric, optional, default=1) Only include unspent outputs confirmed at
/// least this many times.

legacy_transparent: Option<TransparentBalance>,

/// The total of all funds for which this wallet controls spending keys.
total: Balance,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question of whether the duplication is useful is actually the same question as whether or not we should include the "value" field. If our position is that this API output is not intended to be directly consumed by humans, then both "value" and the overall total have no utility; an API consumer is going to be able to compute an overall total and doesn't need the decimal notation. In general, I don't think we should be pursuing a "humans should read JSON" path. So: I vote for "valueZat" only, and no overall total.

///
/// Omitted if no coinbase funds are present.
#[serde(skip_serializing_if = "Option::is_none")]
coinbase: Option<Balance>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we also need immature_coinbase in order to present a complete picture? Or is the documentation here wrong, and immature coinbase outputs will be reflected in the pending field of the balance?

Comment on lines +123 to +124
pub(super) const PARAM_INCLUDE_WATCHONLY_DESC: &str =
"Also include balance in watchonly addresses.";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't like the phrase "in watchonly addresses" because it's the wrong mental model for Sapling/Orchard funds, which aren't "in an address" per se.

Comment on lines +133 to +134
None | Some(false) => Err(LegacyCode::Misc
.with_message("include_watchonly argument must be set to true (for now)")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the default for include_watchonly should be true and only Some(false) should raise this error. In the future, the balance should report whether or not the wallet controls spending keys for the value it represents.

Thinking further, even though include_watchonly follows the pattern of the existing RPC APIs, I don't think that it's the right concept at all. The only important consideration is whether or not the wallet has local spend authority for a balance. Balance is not available at all for IVK-only accounts, and any account associated with an FVK could potentially be spendable via a hardware wallet, so the only relevant information that the caller needs is whether the balance represented can be spent using spending keys that are directly available to the wallet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-rpc-interface Area: RPC interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rpc: Design and implement z_getbalances

3 participants