Skip to main content

df_papertrade_core/account/session/
lifecycle.rs

1//! The client's side of the session key: reading and keeping the stored record, making a key,
2//! having the wallet register it, and handing it out as a signer.
3
4use df_hyperliquid_core::client::Http;
5use df_hyperliquid_core::utils::Address;
6use zeroize::Zeroizing;
7
8use super::{KeyState, SessionRecord, key_state};
9use crate::account::Account;
10use crate::account::session_key::{self, RENEW_BEFORE_MS, SESSION_TTL_MS, SessionKey};
11use crate::account::store::{Slot, Store};
12use crate::account::wallet::{Purpose, Wallet};
13use crate::client::{Client, Error};
14use crate::trade::Intent;
15use crate::utils::Lane;
16
17impl<H: Http + Clone> Client<H> {
18    /// The session key stored for `owner`, if readable.
19    pub async fn session(&self, store: &impl Store, owner: Address) -> Result<Option<SessionRecord>, Error> {
20        let bytes = store.get(owner, Slot::Session).await?.map(Zeroizing::new);
21        Ok(bytes.as_deref().and_then(|bytes| SessionRecord::from_bytes(bytes)))
22    }
23
24    async fn keep(&self, store: &impl Store, owner: Address, record: &SessionRecord) -> Result<(), Error> {
25        let ttl = record.held_until_ms.saturating_sub(self.now_ms());
26        Ok(store.put(owner, Slot::Session, record.to_bytes().to_vec(), Some(ttl)).await?)
27    }
28
29    /// Where `owner`'s stored key stands, given the account as last streamed.
30    pub async fn key_state(
31        &self,
32        store: &impl Store,
33        owner: Address,
34        account: Option<&Account>,
35    ) -> Result<KeyState, Error> {
36        Ok(key_state(self.session(store, owner).await?.as_ref(), account, self.now_ms()))
37    }
38
39    /// Makes and stores a session key for `owner`, unless the stored one is still usable.
40    pub async fn new_session(&self, store: &impl Store, owner: Address) -> Result<SessionRecord, Error> {
41        let now = self.now_ms();
42        if let Some(record) = self.session(store, owner).await?
43            && record.held_until_ms > now.saturating_add(RENEW_BEFORE_MS)
44        {
45            return Ok(record);
46        }
47        let record = SessionRecord {
48            secret: Zeroizing::new(self.random::<32>()?),
49            held_until_ms: now.saturating_add(SESSION_TTL_MS),
50            registered_at_ms: None,
51        };
52        // a secret that is not a valid scalar (about 1 in 2^128) fails here and is redrawn next call
53        record.address()?;
54        self.keep(store, owner, &record).await?;
55        Ok(record)
56    }
57
58    /// Has the wallet register the stored session key (made first if none is usable) and posts
59    /// the registration to the relayer once.
60    pub async fn register(&self, store: &impl Store, wallet: &impl Wallet) -> Result<SessionRecord, Error> {
61        let owner = wallet.address();
62        let mut record = self.new_session(store, owner).await?;
63        let nonce = self.take_nonce(store, owner, Lane::Exchange).await?;
64        let registration =
65            session_key::registration(owner, record.address()?, record.held_until_ms, nonce, self.now_ms())?;
66        let signed = self
67            .wallet_sign(wallet, owner, Intent::RegisterSessionKey(registration), Purpose::RegisterSessionKey)
68            .await?;
69        self.submit(&signed).await?;
70        record.registered_at_ms = Some(self.now_ms());
71        self.keep(store, owner, &record).await?;
72        Ok(record)
73    }
74
75    /// Enables trading: makes a session key if needed and registers it unless it is ready. At
76    /// most one wallet prompt.
77    pub async fn enable(
78        &self,
79        store: &impl Store,
80        wallet: &impl Wallet,
81        account: Option<&Account>,
82    ) -> Result<KeyState, Error> {
83        if self.key_state(store, wallet.address(), account).await? == KeyState::Ready {
84            return Ok(KeyState::Ready);
85        }
86        self.register(store, wallet).await?;
87        Ok(KeyState::Ready)
88    }
89
90    /// `owner`'s session key as a signer, or [`Error::NeedsKey`] with the step to take first.
91    pub async fn session_key(
92        &self,
93        store: &impl Store,
94        owner: Address,
95        account: Option<&Account>,
96    ) -> Result<SessionKey, Error> {
97        let record = self.session(store, owner).await?;
98        match key_state(record.as_ref(), account, self.now_ms()) {
99            KeyState::Ready => record.ok_or(Error::NeedsKey(KeyState::Missing))?.signer(owner),
100            other => Err(Error::NeedsKey(other)),
101        }
102    }
103}