Skip to main content

bulk_keychain/
wallet.rs

1//! Explicit wallet signature modes. Raw preparation and ordinary signing are unchanged.
2
3use crate::{Error, PreparedMessage, Pubkey, Result, SignatureDomain, SignedTransaction, Signer};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum WalletSignatureMode {
9    Raw,
10    Base58,
11    Offchain,
12}
13
14impl WalletSignatureMode {
15    /// Value for the Bulk signature-mode transport hint.
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            Self::Raw => "raw",
19            Self::Base58 => "base58",
20            Self::Offchain => "offchain",
21        }
22    }
23}
24
25impl std::str::FromStr for WalletSignatureMode {
26    type Err = Error;
27    fn from_str(value: &str) -> Result<Self> {
28        match value {
29            "raw" => Ok(Self::Raw),
30            "base58" => Ok(Self::Base58),
31            "offchain" => Ok(Self::Offchain),
32            _ => Err(Error::SigningFailed(
33                "signature mode must be raw, base58 or offchain".into(),
34            )),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct WalletPreparedMessage {
42    /// Original trusted raw preparation output. Do not edit action JSON or order IDs.
43    pub prepared: PreparedMessage,
44    pub signature_mode: WalletSignatureMode,
45    /// Exact bytes to sign, including the envelope for Offchain mode.
46    #[serde(with = "serde_bytes")]
47    pub message_bytes: Vec<u8>,
48    /// Display text for Offchain mode; this alone is not the signature input.
49    pub clear_sign_message: Option<String>,
50}
51
52/// Select a wallet encoding for original trusted raw preparation output.
53/// Offchain rejects action shapes whose canonical text has not been verified.
54pub fn prepare_wallet_message(
55    prepared: PreparedMessage,
56    signature_mode: WalletSignatureMode,
57) -> Result<WalletPreparedMessage> {
58    let domain = prepared_domain(&prepared)?;
59    let clear_sign_message = if signature_mode == WalletSignatureMode::Offchain {
60        Some(crate::clear_sign::canonical_message(&prepared)?)
61    } else {
62        None
63    };
64    let message_bytes = match signature_mode {
65        WalletSignatureMode::Raw => prepared.message_bytes.clone(),
66        WalletSignatureMode::Base58 => bs58::encode(&prepared.message_bytes)
67            .into_string()
68            .into_bytes(),
69        WalletSignatureMode::Offchain => {
70            let payload = clear_sign_message
71                .as_ref()
72                .expect("offchain text constructed above")
73                .as_bytes();
74            let length = u16::try_from(payload.len())
75                .map_err(|_| Error::SigningFailed("offchain message exceeds 65535 bytes".into()))?;
76            let mut envelope = Vec::with_capacity(85 + payload.len());
77            envelope.extend_from_slice(b"\xffsolana offchain");
78            envelope.push(0);
79            envelope.push(domain as u8);
80            envelope.extend_from_slice(&[0; 31]);
81            envelope.push(if payload.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
82                0
83            } else {
84                1
85            });
86            envelope.push(1);
87            envelope.extend_from_slice(Pubkey::from_base58(&prepared.signer)?.as_bytes());
88            envelope.extend_from_slice(&length.to_le_bytes());
89            envelope.extend_from_slice(payload);
90            envelope
91        }
92    };
93    Ok(WalletPreparedMessage {
94        prepared,
95        signature_mode,
96        message_bytes,
97        clear_sign_message,
98    })
99}
100
101/// Verify the selected signature bytes and finalize the original Bulk payload.
102/// This retains the raw finalizer's trust requirement for action JSON and order IDs.
103pub fn finalize_wallet_message(
104    wallet: WalletPreparedMessage,
105    signature: &str,
106) -> Result<SignedTransaction> {
107    let wallet = validate_wallet_message(wallet)?;
108    crate::prepare::finalize_with_message(wallet.prepared, signature, Some(&wallet.message_bytes))
109}
110
111/// Sign an explicit wallet mode using the configured signer and network.
112pub fn sign_wallet_message(
113    signer: &Signer,
114    wallet: WalletPreparedMessage,
115) -> Result<SignedTransaction> {
116    let wallet = validate_wallet_message(wallet)?;
117    if prepared_domain(&wallet.prepared)? != signer.signature_domain()
118        || Pubkey::from_base58(&wallet.prepared.signer)? != signer.pubkey()
119    {
120        return Err(Error::SigningFailed(
121            "prepared wallet signer or network does not match configured signer".into(),
122        ));
123    }
124    let signature = signer.sign_bytes(&wallet.message_bytes);
125    crate::prepare::finalize_with_message(wallet.prepared, &signature, Some(&wallet.message_bytes))
126}
127
128fn validate_wallet_message(wallet: WalletPreparedMessage) -> Result<WalletPreparedMessage> {
129    let rebuilt = prepare_wallet_message(wallet.prepared, wallet.signature_mode)?;
130    if rebuilt.message_bytes != wallet.message_bytes
131        || rebuilt.clear_sign_message != wallet.clear_sign_message
132    {
133        return Err(Error::SigningFailed(
134            "wallet message differs from selected encoding of the raw preparation".into(),
135        ));
136    }
137    Ok(rebuilt)
138}
139
140pub(crate) fn prepared_domain(prepared: &PreparedMessage) -> Result<SignatureDomain> {
141    if prepared.actions.is_empty() {
142        return Err(Error::EmptyOrders);
143    }
144    let suffix = prepared
145        .message_bytes
146        .len()
147        .checked_sub(41)
148        .and_then(|offset| prepared.message_bytes.get(offset..))
149        .ok_or_else(|| {
150            Error::SigningFailed("prepared message is missing its signing suffix".into())
151        })?;
152    if suffix[..8] != prepared.nonce.to_le_bytes()
153        || suffix[8..40] != Pubkey::from_base58(&prepared.account)?.as_bytes()[..]
154    {
155        return Err(Error::SigningFailed(
156            "prepared account or nonce differs from raw signing bytes".into(),
157        ));
158    }
159    Pubkey::from_base58(&prepared.signer)?;
160    match suffix[40] {
161        1 => Ok(SignatureDomain::Mainnet),
162        2 => Ok(SignatureDomain::Testnet),
163        3 => Ok(SignatureDomain::Devnet),
164        _ => Err(Error::SigningFailed(
165            "prepared message has an invalid network domain".into(),
166        )),
167    }
168}