Skip to main content

winterwallet_client/
wallet.rs

1use solana_address::Address;
2use solana_instruction::{AccountMeta, Instruction};
3use winterwallet_common::{SIGNATURE_LEN, WINTERNITZ_SCALARS};
4use winterwallet_core::WinternitzKeypair;
5
6use crate::{
7    AdvancePlan, Error, WinterWalletAccount, find_wallet_address,
8    transaction::{DEFAULT_ADVANCE_COMPUTE_UNIT_LIMIT, with_compute_budget},
9};
10
11/// Current one-time-signature derivation position.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct SigningPosition {
14    wallet: u32,
15    parent: u32,
16    child: u32,
17}
18
19impl SigningPosition {
20    /// Construct a signing position.
21    pub const fn new(wallet: u32, parent: u32, child: u32) -> Self {
22        Self {
23            wallet,
24            parent,
25            child,
26        }
27    }
28
29    /// Read the current position from a Winternitz keypair.
30    pub fn from_keypair(keypair: &WinternitzKeypair) -> Self {
31        Self::new(keypair.wallet(), keypair.parent(), keypair.child())
32    }
33
34    /// Wallet derivation index.
35    pub const fn wallet(&self) -> u32 {
36        self.wallet
37    }
38
39    /// Parent derivation index.
40    pub const fn parent(&self) -> u32 {
41        self.parent
42    }
43
44    /// Child derivation index.
45    pub const fn child(&self) -> u32 {
46        self.child
47    }
48
49    /// Return the next signing position.
50    pub fn next(&self) -> Result<Self, Error> {
51        match self.child.checked_add(1) {
52            Some(child) => Ok(Self::new(self.wallet, self.parent, child)),
53            None => Ok(Self::new(
54                self.wallet,
55                self.parent.checked_add(1).ok_or(Error::PositionOverflow)?,
56                0,
57            )),
58        }
59    }
60
61    fn tuple(&self) -> (u32, u32, u32) {
62        (self.wallet, self.parent, self.child)
63    }
64
65    fn ensure_can_advance(&self) -> Result<(), Error> {
66        self.next().map(|_| ())
67    }
68}
69
70/// High-level view of a WinterWallet account plus local signer position.
71pub struct WinterWallet {
72    id: [u8; 32],
73    pda: Address,
74    current_root: [u8; 32],
75    position: SigningPosition,
76}
77
78impl WinterWallet {
79    /// Build a wallet facade from explicit account state and local position.
80    pub fn new(id: [u8; 32], current_root: [u8; 32], position: SigningPosition) -> Self {
81        let (pda, _bump) = find_wallet_address(&id);
82        Self {
83            id,
84            pda,
85            current_root,
86            position,
87        }
88    }
89
90    /// Build a wallet facade from a deserialized on-chain account.
91    pub fn from_account(account: &WinterWalletAccount, position: SigningPosition) -> Self {
92        Self::new(account.id, *account.root.as_bytes(), position)
93    }
94
95    /// Wallet ID committed by the account.
96    pub fn id(&self) -> &[u8; 32] {
97        &self.id
98    }
99
100    /// Wallet PDA.
101    pub fn pda(&self) -> &Address {
102        &self.pda
103    }
104
105    /// Current root loaded from chain.
106    pub fn current_root(&self) -> &[u8; 32] {
107        &self.current_root
108    }
109
110    /// Local signer position expected for the next signature.
111    pub fn position(&self) -> SigningPosition {
112        self.position
113    }
114
115    /// Build an unsigned Advance from arbitrary inner CPI instructions.
116    pub fn advance_plan(
117        &self,
118        new_root: &[u8; 32],
119        inner_instructions: &[Instruction],
120    ) -> Result<UnsignedAdvance, Error> {
121        let plan = AdvancePlan::new(&self.pda, new_root, inner_instructions)?;
122        Ok(UnsignedAdvance {
123            wallet_id: self.id,
124            current_root: self.current_root,
125            position: self.position,
126            plan,
127        })
128    }
129
130    /// Build an unsigned Advance wrapping the built-in lamport withdraw CPI.
131    pub fn withdraw_plan(
132        &self,
133        receiver: &Address,
134        lamports: u64,
135        new_root: &[u8; 32],
136    ) -> Result<UnsignedAdvance, Error> {
137        let plan = AdvancePlan::withdraw(&self.pda, receiver, lamports, new_root)?;
138        Ok(UnsignedAdvance {
139            wallet_id: self.id,
140            current_root: self.current_root,
141            position: self.position,
142            plan,
143        })
144    }
145
146    /// Build an unsigned Advance wrapping the built-in close CPI: sweeps all
147    /// lamports to `receiver` and tears the wallet PDA down.
148    pub fn close_plan(
149        &self,
150        receiver: &Address,
151        new_root: &[u8; 32],
152    ) -> Result<UnsignedAdvance, Error> {
153        let plan = AdvancePlan::close(&self.pda, receiver, new_root)?;
154        Ok(UnsignedAdvance {
155            wallet_id: self.id,
156            current_root: self.current_root,
157            position: self.position,
158            plan,
159        })
160    }
161
162    /// Build an unsigned Advance wrapping an SPL Token transfer CPI.
163    pub fn transfer_plan(
164        &self,
165        source_token_account: &Address,
166        destination_token_account: &Address,
167        token_program: &Address,
168        amount: u64,
169        new_root: &[u8; 32],
170    ) -> Result<UnsignedAdvance, Error> {
171        self.advance_plan(
172            new_root,
173            &[token_transfer(
174                source_token_account,
175                destination_token_account,
176                &self.pda,
177                amount,
178                token_program,
179            )],
180        )
181    }
182}
183
184/// Fully constructed Advance that has not burned a Winternitz position yet.
185pub struct UnsignedAdvance {
186    wallet_id: [u8; 32],
187    current_root: [u8; 32],
188    position: SigningPosition,
189    plan: AdvancePlan,
190}
191
192impl UnsignedAdvance {
193    /// Inner plan with payload/account order already fixed.
194    pub fn plan(&self) -> &AdvancePlan {
195        &self.plan
196    }
197
198    /// Position that will be consumed by signing.
199    pub fn signing_position(&self) -> SigningPosition {
200        self.position
201    }
202
203    /// Build the preimage parts that will be signed.
204    pub fn preimage(&self) -> Vec<&[u8]> {
205        self.plan.preimage(&self.wallet_id, &self.current_root)
206    }
207
208    /// Sign the plan and advance the supplied keypair.
209    ///
210    /// This consumes the unsigned value. The returned [`SignedAdvance`] cannot
211    /// be sent until it has been persisted into a [`PersistedAdvance`].
212    pub fn sign(self, keypair: &mut WinternitzKeypair) -> Result<SignedAdvance, Error> {
213        let actual = SigningPosition::from_keypair(keypair);
214        if actual != self.position {
215            return Err(Error::SignerPositionMismatch {
216                expected: self.position.tuple(),
217                actual: actual.tuple(),
218            });
219        }
220        actual.ensure_can_advance()?;
221
222        let derived_root = keypair
223            .derive::<WINTERNITZ_SCALARS>()
224            .to_pubkey()
225            .merklize();
226        if derived_root.as_bytes() != &self.current_root {
227            return Err(Error::RootMismatch);
228        }
229
230        let signature = {
231            let preimage = self.preimage();
232            let sig = keypair.sign_and_increment::<WINTERNITZ_SCALARS>(&preimage);
233            let mut bytes = [0u8; SIGNATURE_LEN];
234            bytes.copy_from_slice(sig.as_bytes());
235            bytes
236        };
237        let next_position = SigningPosition::from_keypair(keypair);
238
239        Ok(SignedAdvance {
240            wallet_id: self.wallet_id,
241            signing_position: self.position,
242            next_position,
243            signature,
244            plan: self.plan,
245        })
246    }
247}
248
249/// Advance after a Winternitz position has been consumed, before persistence.
250pub struct SignedAdvance {
251    wallet_id: [u8; 32],
252    signing_position: SigningPosition,
253    next_position: SigningPosition,
254    signature: [u8; SIGNATURE_LEN],
255    plan: AdvancePlan,
256}
257
258impl SignedAdvance {
259    /// Wallet ID for the signed operation.
260    pub fn wallet_id(&self) -> &[u8; 32] {
261        &self.wallet_id
262    }
263
264    /// Wallet PDA for the signed operation.
265    pub fn wallet_pda(&self) -> &Address {
266        self.plan.wallet_pda()
267    }
268
269    /// Position consumed by this signature.
270    pub fn signing_position(&self) -> SigningPosition {
271        self.signing_position
272    }
273
274    /// Next position that must be persisted before network submission.
275    pub fn next_position(&self) -> SigningPosition {
276        self.next_position
277    }
278
279    /// Raw Winternitz signature bytes.
280    pub fn signature_bytes(&self) -> &[u8; SIGNATURE_LEN] {
281        &self.signature
282    }
283
284    /// Persist the advanced signer position before network submission.
285    pub fn persist<P>(self, persistence: &mut P) -> Result<PersistedAdvance, P::Error>
286    where
287        P: AdvancePersistence,
288    {
289        persistence.persist_signed_advance(&self)?;
290        Ok(PersistedAdvance { signed: self })
291    }
292}
293
294/// Persistence adapter for advancing local one-time-signature state.
295pub trait AdvancePersistence {
296    /// Persistence error type.
297    type Error;
298
299    /// Persist the signed operation's next position durably.
300    fn persist_signed_advance(&mut self, advance: &SignedAdvance) -> Result<(), Self::Error>;
301}
302
303/// Advance whose consumed position has been durably recorded.
304pub struct PersistedAdvance {
305    signed: SignedAdvance,
306}
307
308impl PersistedAdvance {
309    /// Access the signed operation metadata.
310    pub fn signed(&self) -> &SignedAdvance {
311        &self.signed
312    }
313
314    /// Build the signed Advance instruction.
315    pub fn advance_instruction(&self) -> Instruction {
316        self.signed.plan.instruction(&self.signed.signature)
317    }
318
319    /// Build transaction instructions with compute-budget prefix.
320    pub fn transaction_instructions(
321        &self,
322        unit_limit: u32,
323        unit_price_micro_lamports: u64,
324    ) -> Vec<Instruction> {
325        with_compute_budget(
326            &[self.advance_instruction()],
327            unit_limit,
328            unit_price_micro_lamports,
329        )
330    }
331
332    /// Build transaction instructions using the SDK default compute limit.
333    pub fn default_transaction_instructions(
334        &self,
335        unit_price_micro_lamports: u64,
336    ) -> Vec<Instruction> {
337        self.transaction_instructions(
338            DEFAULT_ADVANCE_COMPUTE_UNIT_LIMIT,
339            unit_price_micro_lamports,
340        )
341    }
342
343    /// Send through an adapter that only accepts persisted advances.
344    pub fn send<S>(&self, sender: &mut S) -> Result<String, S::Error>
345    where
346        S: AdvanceSender,
347    {
348        sender.send_persisted_advance(self)
349    }
350}
351
352/// Sender adapter for persisted advances.
353pub trait AdvanceSender {
354    /// Sender error type.
355    type Error;
356
357    /// Submit a persisted operation.
358    fn send_persisted_advance(&mut self, advance: &PersistedAdvance)
359    -> Result<String, Self::Error>;
360}
361
362/// Build an SPL Token `Transfer` instruction for use inside Advance.
363pub fn token_transfer(
364    source: &Address,
365    destination: &Address,
366    authority: &Address,
367    amount: u64,
368    token_program: &Address,
369) -> Instruction {
370    let mut data = Vec::with_capacity(9);
371    data.push(3);
372    data.extend_from_slice(&amount.to_le_bytes());
373
374    Instruction {
375        program_id: *token_program,
376        accounts: vec![
377            AccountMeta::new(*source, false),
378            AccountMeta::new(*destination, false),
379            AccountMeta::new_readonly(*authority, false),
380        ],
381        data,
382    }
383}