Skip to main content

signer_svm/
lib.rs

1//! Solana transaction signer built on [`ed25519-dalek`].
2//!
3//! [`Signer`] wraps an [`ed25519_dalek::SigningKey`], adding Base58 address
4//! support, keypair import/export, and convenient constructors.
5//!
6//! **Zero hand-rolled cryptography.**
7//!
8//! # Signing
9//!
10//! `Signer` implements `Deref<Target = SigningKey>`, so all
11//! [`ed25519_dalek::Signer`](ed25519_dalek::Signer) methods are available
12//! directly (e.g. `signer.sign(msg)`).
13//!
14//! | Method | Description |
15//! |---|---|
16//! | `sign` (via Deref) | Ed25519 signature on arbitrary bytes |
17//! | [`Signer::verify`] | Verify an Ed25519 signature |
18//! | [`Signer::sign_transaction_message`] | Sign serialized Solana tx message bytes |
19
20mod error;
21
22use core::ops::Deref;
23
24pub use ed25519_dalek::{self, Signature, VerifyingKey};
25use ed25519_dalek::{SigningKey, Verifier};
26pub use error::Error;
27use zeroize::Zeroizing;
28
29/// Solana transaction signer.
30///
31/// Wraps an [`ed25519_dalek::SigningKey`] with [`Deref`] for full upstream
32/// access. The inner key implements [`ZeroizeOnDrop`](zeroize::ZeroizeOnDrop).
33///
34/// # Examples
35///
36/// ```
37/// use signer_svm::Signer;
38/// use ed25519_dalek::Signer as _;
39///
40/// let signer = Signer::random();
41/// let sig = signer.sign(b"hello solana");
42/// signer.verify(b"hello solana", &sig).unwrap();
43/// ```
44#[derive(Debug, Clone)]
45pub struct Signer {
46    key: SigningKey,
47}
48
49impl Deref for Signer {
50    type Target = SigningKey;
51
52    #[inline]
53    fn deref(&self) -> &Self::Target {
54        &self.key
55    }
56}
57
58impl Signer {
59    /// Create a signer from raw 32-byte secret key bytes.
60    #[must_use]
61    pub fn from_bytes(bytes: &[u8; 32]) -> Self {
62        Self {
63            key: SigningKey::from_bytes(bytes),
64        }
65    }
66
67    /// Create a signer from a hex-encoded 32-byte private key.
68    ///
69    /// Accepts keys with or without `0x` prefix.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the hex string is invalid or the key length is wrong.
74    pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
75        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
76        let bytes: [u8; 32] = hex::decode(hex_str)?.try_into().map_err(|v: Vec<u8>| {
77            Error::InvalidKey(format!("expected 32 bytes, got {}", v.len()))
78        })?;
79        Ok(Self::from_bytes(&bytes))
80    }
81
82    /// Create a signer from a Base58-encoded keypair (64 bytes: secret ‖ public).
83    ///
84    /// This is the standard format used by Phantom, Backpack, and Solflare.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the Base58 string is invalid or not 64 bytes.
89    pub fn from_keypair_base58(b58: &str) -> Result<Self, Error> {
90        let decoded = bs58::decode(b58)
91            .into_vec()
92            .map_err(|e| Error::InvalidKeypair(e.to_string()))?;
93
94        if decoded.len() != 64 {
95            return Err(Error::InvalidKeypair(format!(
96                "expected 64 bytes, got {}",
97                decoded.len()
98            )));
99        }
100
101        let mut secret = [0u8; 32];
102        secret.copy_from_slice(&decoded[..32]);
103        let signer = Self::from_bytes(&secret);
104        secret.fill(0);
105        Ok(signer)
106    }
107
108    /// Generate a random signer.
109    ///
110    /// # Panics
111    ///
112    /// Panics if the system CSPRNG is unavailable.
113    #[must_use]
114    pub fn random() -> Self {
115        let mut bytes = [0u8; 32];
116        getrandom::fill(&mut bytes).expect("system CSPRNG unavailable");
117        let signer = Self::from_bytes(&bytes);
118        bytes.fill(0);
119        signer
120    }
121
122    /// Verify an Ed25519 signature against this signer's public key.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the signature is invalid.
127    pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> {
128        self.key.verifying_key().verify(msg, signature)?;
129        Ok(())
130    }
131
132    /// Sign serialized Solana transaction message bytes.
133    ///
134    /// A Solana transaction signature is an Ed25519 signature over the
135    /// serialized message. Use this with any serialization method
136    /// (e.g. `solana-sdk`, `solana-transaction`).
137    #[must_use]
138    pub fn sign_transaction_message(&self, message_bytes: &[u8]) -> Signature {
139        use ed25519_dalek::Signer as _;
140        self.key.sign(message_bytes)
141    }
142
143    /// Get the Solana address (Base58-encoded 32-byte public key).
144    #[must_use]
145    pub fn address(&self) -> String {
146        bs58::encode(self.key.verifying_key().as_bytes()).into_string()
147    }
148
149    /// Get the public key in hex format.
150    #[must_use]
151    pub fn public_key_hex(&self) -> String {
152        hex::encode(self.key.verifying_key().as_bytes())
153    }
154
155    /// Export the keypair as Base58 (64 bytes: secret ‖ public).
156    ///
157    /// Compatible with Phantom, Backpack, and Solflare wallet format.
158    #[must_use]
159    pub fn keypair_base58(&self) -> Zeroizing<String> {
160        let vk = self.key.verifying_key();
161        let mut buf = [0u8; 64];
162        buf[..32].copy_from_slice(self.key.as_bytes());
163        buf[32..].copy_from_slice(vk.as_bytes());
164        let encoded = bs58::encode(&buf).into_string();
165        buf.fill(0);
166        Zeroizing::new(encoded)
167    }
168}
169
170#[cfg(feature = "kobe")]
171impl Signer {
172    /// Create a signer from a [`kobe_svm::DerivedAddress`].
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the private key hex is invalid.
177    pub fn from_derived(derived: &kobe_svm::DerivedAddress) -> Result<Self, Error> {
178        Self::from_hex(&derived.private_key_hex)
179    }
180
181    /// Create a signer from a [`kobe_svm::StandardWallet`].
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the private key hex is invalid.
186    pub fn from_standard_wallet(wallet: &kobe_svm::StandardWallet) -> Result<Self, Error> {
187        Self::from_hex(&wallet.secret_hex())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use ed25519_dalek::Signer as _;
194
195    use super::*;
196
197    #[test]
198    fn assert_send_sync() {
199        fn assert<T: Send + Sync>() {}
200        assert::<Signer>();
201    }
202
203    #[test]
204    fn assert_clone() {
205        let s = Signer::random();
206        let s2 = s.clone();
207        assert_eq!(s.address(), s2.address());
208    }
209
210    #[test]
211    fn random_address() {
212        let s = Signer::random();
213        let addr = s.address();
214        assert!(addr.len() >= 32 && addr.len() <= 44);
215    }
216
217    #[test]
218    fn hex_roundtrip() {
219        let s = Signer::random();
220        let hex_key = hex::encode(s.key.as_bytes());
221        let restored = Signer::from_hex(&hex_key).unwrap();
222        assert_eq!(s.address(), restored.address());
223    }
224
225    #[test]
226    fn keypair_base58_roundtrip() {
227        let s = Signer::random();
228        let b58 = s.keypair_base58();
229        let restored = Signer::from_keypair_base58(&b58).unwrap();
230        assert_eq!(s.address(), restored.address());
231    }
232
233    #[test]
234    fn from_bytes_deterministic() {
235        let key = [42u8; 32];
236        assert_eq!(
237            Signer::from_bytes(&key).address(),
238            Signer::from_bytes(&key).address()
239        );
240    }
241
242    #[test]
243    fn sign_and_verify() {
244        let s = Signer::random();
245        let sig = s.sign(b"hello solana");
246        s.verify(b"hello solana", &sig).unwrap();
247    }
248
249    #[test]
250    fn verify_wrong_message_fails() {
251        let s = Signer::random();
252        let sig = s.sign(b"correct");
253        assert!(s.verify(b"wrong", &sig).is_err());
254    }
255
256    #[test]
257    fn sign_transaction_message() {
258        let s = Signer::random();
259        let fake_msg = [0u8; 128];
260        let sig = s.sign_transaction_message(&fake_msg);
261        s.verify(&fake_msg, &sig).unwrap();
262    }
263
264    #[test]
265    fn public_key_hex_length() {
266        let s = Signer::random();
267        assert_eq!(s.public_key_hex().len(), 64);
268    }
269
270    #[test]
271    fn deref_to_signing_key() {
272        let s = Signer::random();
273        let _vk: VerifyingKey = s.verifying_key();
274    }
275}