1use 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 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 pub prepared: PreparedMessage,
44 pub signature_mode: WalletSignatureMode,
45 #[serde(with = "serde_bytes")]
47 pub message_bytes: Vec<u8>,
48 pub clear_sign_message: Option<String>,
50}
51
52pub 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
101pub 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
111pub 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}