Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions zebra-crosslink/wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,50 @@ pub static USER_UFVK_STRING: Mutex<Option<String>> = Mutex::new(None);
pub static GUI_ENABLE_MINE: Mutex<bool> = Mutex::new(true);

pub static STAKING_STAGE: Mutex<Option<(StakingActionRequest, tokio::sync::oneshot::Sender<Result<String, String>>)>> = Mutex::new(None);
// Read side, complementing `wallet_staking_action`: the headless wallet publishes a
// clone of its live `WalletState` handle here at startup so an RPC can report staking
// state (bonds, balances, sync height) without a round-trip through the wallet loop.
// None until the wallet starts => the query fails closed with an explanatory error.
pub static STAKING_STATUS_HANDLE: Mutex<Option<Arc<Mutex<WalletState>>>> = Mutex::new(None);

/// Serialise the headless wallet's live staking view — bonds (key, target, amount),
/// balances, and sync height — for the read-only `wallet_staking_status` RPC. This is
/// the query complement to `wallet_staking_action`: it lets a headless operator see the
/// result of the actions they submit. Fails closed when no wallet is running.
pub fn wallet_staking_status_json() -> Result<String, String> {
let handle = STAKING_STATUS_HANDLE.lock().unwrap().clone();
let Some(handle) = handle else {
return Err("headless wallet is not running (is `disable_the_headless_wallet` set?)".to_string());
};
let s = handle.lock().unwrap();

fn hex32(b: &[u8; 32]) -> String {
let mut o = String::with_capacity(64);
for x in b { o.push_str(&format!("{:02x}", x)); }
o
}
fn bonds_json(v: &[([u8; 32], [u8; 32], u64)]) -> String {
let items: Vec<String> = v.iter().map(|(k, t, z)| format!(
"{{\"bond_key\":\"{}\",\"target_finalizer\":\"{}\",\"initial_zats\":{}}}",
hex32(k), hex32(t), z)).collect();
format!("[{}]", items.join(","))
}

let own_finalizer = hex32(&TENDERLINK_PUBLIC_KEY.lock().unwrap().0);

Ok(format!(
"{{\"wallet_is_init\":{},\"sync_height\":{},\"tip_height\":{},\
\"user_address\":\"{}\",\"user_ufvk\":\"{}\",\"own_finalizer_pubkey\":\"{}\",\
\"user_unshielded_zats\":{},\"user_shielded_spendable_zats\":{},\"user_shielded_pending_zats\":{},\
\"staked_zats\":{},\"withdrawable_zats\":{},\
\"bonded\":{},\"unbonded\":{}}}",
s.wallet_is_init, s.wallets_sync_h, s.wallets_tip_h,
s.user_recv_ua, s.user_ufvk, own_finalizer,
s.user_unshielded_funds, s.user_shielded_spendable_funds, s.user_shielded_pending_funds,
s.staked_balance, s.withdrawable_balance,
bonds_json(&s.stake_positions_bonded), bonds_json(&s.stake_positions_unbonded),
))
}

#[derive(Clone)]
pub struct RecencyRequestClosure(pub Arc<dyn Fn() -> Option<String> + Sync + Send + 'static>);
Expand Down Expand Up @@ -2882,6 +2926,9 @@ fn read_compact_tx(wallet: &mut ManualWallet, account_i: usize, keys: &PreparedK


pub async fn wallet_main(wallet_state: Arc<Mutex<WalletState>>) {
// Publish the live state handle for the read-only `wallet_staking_status` RPC.
*STAKING_STATUS_HANDLE.lock().unwrap() = Some(wallet_state.clone());

fn wallet_from_usk<P: Parameters + 'static>(params: P, name: &'static str, usk: &UnifiedSpendingKey) -> (ManualWallet, ManualAccount) {
// TODO: skip this by changing API slightly
let account_id = zip32::AccountId::try_from(0).unwrap();
Expand Down
3 changes: 3 additions & 0 deletions zebra-crosslink/zebra-crosslink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,9 @@ async fn tfl_service_incoming_request(
}
})),

TFLServiceRequest::WalletStakingStatus =>
Ok(TFLServiceResponse::WalletStakingStatus(wallet::wallet_staking_status_json())),

// workshop - mining & staking via PoW
TFLServiceRequest::TotalIssuanceFromKey(ufvk_str, first_height, last_height) => {
Ok(TFLServiceResponse::TotalIssuanceFromKey({
Expand Down
30 changes: 30 additions & 0 deletions zebra-crosslink/zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ pub trait Rpc {
#[method(name = "wallet_staking_action")]
async fn wallet_staking_action(&self, staking_action: StakingActionRequest) -> Result<String>;

/// read the wallet's staking status (bonds, balances, sync height) as a JSON object
#[method(name = "wallet_staking_status")]
async fn wallet_staking_status(&self) -> Result<String>;

/// Returns the requested block header by hash or height, as a [`GetBlockHeader`] JSON string.
/// If the block is not in Zebra's state,
/// returns [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
Expand Down Expand Up @@ -2389,6 +2393,32 @@ where
}
}

async fn wallet_staking_status(&self) -> Result<String> {
let res = self
.tfl_service
.clone()
.ready()
.await
.unwrap()
.call(TFLServiceRequest::WalletStakingStatus)
.await;

match res {
Ok(TFLServiceResponse::WalletStakingStatus(Ok(res))) => Ok(res),
Ok(TFLServiceResponse::WalletStakingStatus(Err(err))) => Err(ErrorObject::owned(
server::error::LegacyCode::Verify.into(),
format!("wallet_staking_status failed: {err}"),
None::<()>,
)),
Err(err) => Err(ErrorObject::owned(
server::error::LegacyCode::Verify.into(),
format!("wallet_staking_status failed: {err}"),
None::<()>,
)),
_ => unreachable!(""),
}
}

async fn get_block_header(
&self,
hash_or_height: String,
Expand Down
4 changes: 4 additions & 0 deletions zebra-crosslink/zebra-state/src/crosslink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub enum TFLServiceRequest {
WalletUfvk,
/// Send staking action from wallet
WalletStakingAction(StakingActionRequest),
/// Read the wallet's staking status (bonds, balances, sync height)
WalletStakingStatus,
}

/// Types of responses that can be returned by the TFLService.
Expand Down Expand Up @@ -98,6 +100,8 @@ pub enum TFLServiceResponse {
WalletUfvk(Option<String>),
/// Send staking action from wallet
WalletStakingAction(Result<String, String>),
/// Wallet staking status as a JSON object string
WalletStakingStatus(Result<String, String>),
}

/// Errors that can occur when interacting with the TFLService.
Expand Down