Skip to main content

near_kit/client/
signer.rs

1//! Signer trait and implementations.
2//!
3//! A `Signer` knows which account it signs for and provides keys for signing.
4//! The `key()` method returns a `SigningKey` that bundles together the public key
5//! and signing capability, ensuring atomic key claiming for rotating signers.
6//!
7//! # Implementations
8//!
9//! - [`InMemorySigner`] - Single key stored in memory
10//! - [`FileSigner`] - Key loaded from ~/.near-credentials
11//! - [`EnvSigner`] - Key loaded from environment variables
12//! - [`RotatingSigner`] - Multiple keys with round-robin rotation
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use near_kit::{Near, InMemorySigner};
18//!
19//! # async fn example() -> Result<(), near_kit::Error> {
20//! let signer = InMemorySigner::new(
21//!     "alice.testnet",
22//!     "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
23//! )?;
24//!
25//! let near = Near::testnet()
26//!     .signer(signer)
27//!     .build();
28//!
29//! near.transfer("bob.testnet", "1 NEAR").await?;
30//! # Ok(())
31//! # }
32//! ```
33
34use std::future::Future;
35use std::path::Path;
36use std::pin::Pin;
37use std::sync::Arc;
38use std::sync::atomic::{AtomicUsize, Ordering};
39
40use crate::error::SignerError;
41use crate::types::nep413::{self, SignMessageParams, SignedMessage};
42use crate::types::{AccountId, PublicKey, SecretKey, Signature};
43
44// ============================================================================
45// Signer Trait
46// ============================================================================
47
48/// Trait for signing transactions.
49///
50/// A signer knows which account it signs for and provides keys for signing.
51/// The `key()` method returns a [`SigningKey`] that bundles together the public
52/// key and signing capability, ensuring atomic key claiming.
53///
54/// # Example Implementation
55///
56/// ```rust,ignore
57/// use near_kit::{Signer, SigningKey, AccountId, SecretKey};
58///
59/// struct MyCustomSigner {
60///     account_id: AccountId,
61///     secret_key: SecretKey,
62/// }
63///
64/// impl Signer for MyCustomSigner {
65///     fn account_id(&self) -> &AccountId {
66///         &self.account_id
67///     }
68///
69///     fn key(&self) -> SigningKey {
70///         SigningKey::new(self.secret_key.clone())
71///     }
72/// }
73/// ```
74pub trait Signer: Send + Sync {
75    /// The account this signer signs for.
76    fn account_id(&self) -> &AccountId;
77
78    /// Get a key for signing.
79    ///
80    /// Returns a [`SigningKey`] that contains both the public key and the
81    /// capability to sign with the corresponding private key.
82    ///
83    /// For single-key signers, this always returns the same key.
84    /// For rotating signers, this atomically claims the next key in rotation.
85    fn key(&self) -> SigningKey;
86}
87
88/// Implement `Signer` for `Arc<dyn Signer>` for convenience.
89impl Signer for Arc<dyn Signer> {
90    fn account_id(&self) -> &AccountId {
91        (**self).account_id()
92    }
93
94    fn key(&self) -> SigningKey {
95        (**self).key()
96    }
97}
98
99// ============================================================================
100// SigningKey
101// ============================================================================
102
103/// A key that can sign messages.
104///
105/// This bundles together a public key and the ability to sign with the
106/// corresponding private key. For in-memory keys, signing is instant.
107/// For hardware wallets or KMS, signing may involve async operations.
108///
109/// # Example
110///
111/// ```rust
112/// use near_kit::{InMemorySigner, Signer};
113///
114/// # async fn example() -> Result<(), near_kit::Error> {
115/// let signer = InMemorySigner::new("alice.testnet", "ed25519:...")?;
116///
117/// let key = signer.key();
118/// println!("Public key: {}", key.public_key());
119///
120/// let signature = key.sign(b"message").await?;
121/// # Ok(())
122/// # }
123/// ```
124pub struct SigningKey {
125    /// The public key.
126    public_key: PublicKey,
127    /// The signing backend.
128    backend: Arc<dyn SigningBackend>,
129}
130
131impl SigningKey {
132    /// Create a new signing key from a secret key.
133    pub fn new(secret_key: SecretKey) -> Self {
134        let public_key = secret_key.public_key();
135        Self {
136            public_key,
137            backend: Arc::new(SecretKeyBackend { secret_key }),
138        }
139    }
140
141    /// Get the public key.
142    pub fn public_key(&self) -> &PublicKey {
143        &self.public_key
144    }
145
146    /// Sign a message.
147    ///
148    /// For in-memory keys, this returns immediately.
149    /// For hardware wallets or KMS, this may involve user confirmation or
150    /// network requests.
151    pub async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
152        self.backend.sign(message).await
153    }
154
155    /// Sign a NEP-413 message for off-chain authentication.
156    ///
157    /// # Example
158    ///
159    /// ```rust,ignore
160    /// use near_kit::{InMemorySigner, Signer, nep413};
161    ///
162    /// let signer = InMemorySigner::new("alice.testnet", "ed25519:...")?;
163    /// let key = signer.key();
164    ///
165    /// let params = nep413::SignMessageParams {
166    ///     message: "Login to MyApp".to_string(),
167    ///     recipient: "myapp.com".to_string(),
168    ///     nonce: nep413::generate_nonce(),
169    ///     callback_url: None,
170    ///     state: None,
171    /// };
172    ///
173    /// let signed = key.sign_nep413(&signer.account_id(), &params).await?;
174    /// ```
175    pub async fn sign_nep413(
176        &self,
177        account_id: &AccountId,
178        params: &SignMessageParams,
179    ) -> Result<SignedMessage, SignerError> {
180        let hash = nep413::serialize_message(params);
181        let signature = self.sign(hash.as_bytes()).await?;
182
183        Ok(SignedMessage {
184            account_id: account_id.clone(),
185            public_key: self.public_key.clone(),
186            signature,
187            state: params.state.clone(),
188        })
189    }
190}
191
192impl Clone for SigningKey {
193    fn clone(&self) -> Self {
194        Self {
195            public_key: self.public_key.clone(),
196            backend: self.backend.clone(),
197        }
198    }
199}
200
201impl std::fmt::Debug for SigningKey {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.debug_struct("SigningKey")
204            .field("public_key", &self.public_key)
205            .finish()
206    }
207}
208
209// ============================================================================
210// SigningBackend (internal)
211// ============================================================================
212
213/// Internal trait for signing backends.
214///
215/// This allows different implementations (in-memory, hardware wallet, KMS)
216/// to provide signing capability.
217trait SigningBackend: Send + Sync {
218    fn sign(
219        &self,
220        message: &[u8],
221    ) -> Pin<Box<dyn Future<Output = Result<Signature, SignerError>> + Send + '_>>;
222}
223
224/// In-memory signing backend using a secret key.
225struct SecretKeyBackend {
226    secret_key: SecretKey,
227}
228
229impl SigningBackend for SecretKeyBackend {
230    fn sign(
231        &self,
232        message: &[u8],
233    ) -> Pin<Box<dyn Future<Output = Result<Signature, SignerError>> + Send + '_>> {
234        let sig = self.secret_key.sign(message);
235        Box::pin(async move { Ok(sig) })
236    }
237}
238
239// ============================================================================
240// InMemorySigner
241// ============================================================================
242
243/// A signer with a single key stored in memory.
244///
245/// This is the simplest signer implementation, suitable for scripts,
246/// bots, and testing.
247///
248/// # Example
249///
250/// ```rust
251/// use near_kit::InMemorySigner;
252///
253/// let signer = InMemorySigner::new(
254///     "alice.testnet",
255///     "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
256/// ).unwrap();
257/// ```
258#[derive(Clone)]
259pub struct InMemorySigner {
260    account_id: AccountId,
261    secret_key: SecretKey,
262    public_key: PublicKey,
263}
264
265impl InMemorySigner {
266    /// Create a new signer with an account ID and secret key.
267    ///
268    /// # Arguments
269    ///
270    /// * `account_id` - The NEAR account ID (e.g., "alice.testnet")
271    /// * `secret_key` - The secret key in string format (e.g., "ed25519:...")
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if the account ID or secret key cannot be parsed.
276    pub fn new(
277        account_id: impl AsRef<str>,
278        secret_key: impl AsRef<str>,
279    ) -> Result<Self, crate::error::Error> {
280        let account_id: AccountId = account_id.as_ref().parse()?;
281        let secret_key: SecretKey = secret_key.as_ref().parse()?;
282        let public_key = secret_key.public_key();
283
284        Ok(Self {
285            account_id,
286            secret_key,
287            public_key,
288        })
289    }
290
291    /// Create a signer from a SecretKey directly.
292    pub fn from_secret_key(account_id: AccountId, secret_key: SecretKey) -> Self {
293        let public_key = secret_key.public_key();
294        Self {
295            account_id,
296            secret_key,
297            public_key,
298        }
299    }
300
301    /// Create a signer from a BIP-39 seed phrase.
302    ///
303    /// Uses SLIP-10 derivation with the default NEAR HD path (`m/44'/397'/0'`).
304    ///
305    /// # Arguments
306    ///
307    /// * `account_id` - The NEAR account ID (e.g., "alice.testnet")
308    /// * `phrase` - BIP-39 mnemonic phrase (12, 15, 18, 21, or 24 words)
309    ///
310    /// # Example
311    ///
312    /// ```rust
313    /// use near_kit::InMemorySigner;
314    ///
315    /// let signer = InMemorySigner::from_seed_phrase(
316    ///     "alice.testnet",
317    ///     "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
318    /// ).unwrap();
319    /// ```
320    pub fn from_seed_phrase(
321        account_id: impl AsRef<str>,
322        phrase: impl AsRef<str>,
323    ) -> Result<Self, crate::error::Error> {
324        let account_id: AccountId = account_id.as_ref().parse()?;
325        let secret_key = SecretKey::from_seed_phrase(phrase)?;
326        Ok(Self::from_secret_key(account_id, secret_key))
327    }
328
329    /// Create a signer from a BIP-39 seed phrase with custom HD path.
330    ///
331    /// # Arguments
332    ///
333    /// * `account_id` - The NEAR account ID
334    /// * `phrase` - BIP-39 mnemonic phrase
335    /// * `hd_path` - BIP-32 derivation path (e.g., `"m/44'/397'/0'"`)
336    ///
337    /// # Example
338    ///
339    /// ```rust
340    /// use near_kit::InMemorySigner;
341    ///
342    /// let signer = InMemorySigner::from_seed_phrase_with_path(
343    ///     "alice.testnet",
344    ///     "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
345    ///     "m/44'/397'/1'"
346    /// ).unwrap();
347    /// ```
348    pub fn from_seed_phrase_with_path(
349        account_id: impl AsRef<str>,
350        phrase: impl AsRef<str>,
351        hd_path: impl AsRef<str>,
352    ) -> Result<Self, crate::error::Error> {
353        let account_id: AccountId = account_id.as_ref().parse()?;
354        let secret_key = SecretKey::from_seed_phrase_with_path(phrase, hd_path)?;
355        Ok(Self::from_secret_key(account_id, secret_key))
356    }
357
358    /// Get the public key.
359    pub fn public_key(&self) -> &PublicKey {
360        &self.public_key
361    }
362}
363
364impl std::fmt::Debug for InMemorySigner {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        f.debug_struct("InMemorySigner")
367            .field("account_id", &self.account_id)
368            .field("public_key", &self.public_key)
369            .finish()
370    }
371}
372
373impl Signer for InMemorySigner {
374    fn account_id(&self) -> &AccountId {
375        &self.account_id
376    }
377
378    fn key(&self) -> SigningKey {
379        SigningKey::new(self.secret_key.clone())
380    }
381}
382
383// ============================================================================
384// FileSigner
385// ============================================================================
386
387/// A signer that loads its key from `~/.near-credentials/{network}/{account}.json`.
388///
389/// Compatible with credentials created by near-cli and near-cli-rs.
390///
391/// # Example
392///
393/// ```rust,no_run
394/// use near_kit::FileSigner;
395///
396/// // Load from ~/.near-credentials/testnet/alice.testnet.json
397/// let signer = FileSigner::new("testnet", "alice.testnet").unwrap();
398/// ```
399#[derive(Clone)]
400pub struct FileSigner {
401    inner: InMemorySigner,
402}
403
404/// Credential file format compatible with near-cli.
405#[derive(serde::Deserialize)]
406struct CredentialFile {
407    #[serde(alias = "secret_key")]
408    private_key: String,
409}
410
411impl FileSigner {
412    /// Load credentials for an account from the standard NEAR credentials directory.
413    ///
414    /// Looks for the file at `~/.near-credentials/{network}/{account_id}.json`.
415    ///
416    /// # Arguments
417    ///
418    /// * `network` - Network name (e.g., "testnet", "mainnet")
419    /// * `account_id` - The NEAR account ID
420    ///
421    /// # Errors
422    ///
423    /// Returns an error if:
424    /// - The home directory cannot be determined
425    /// - The credentials file doesn't exist
426    /// - The file cannot be parsed
427    pub fn new(
428        network: impl AsRef<str>,
429        account_id: impl AsRef<str>,
430    ) -> Result<Self, crate::error::Error> {
431        let home = dirs::home_dir().ok_or_else(|| {
432            crate::error::Error::Config("Could not determine home directory".to_string())
433        })?;
434        let path = home
435            .join(".near-credentials")
436            .join(network.as_ref())
437            .join(format!("{}.json", account_id.as_ref()));
438
439        Self::from_file(&path, account_id)
440    }
441
442    /// Load credentials from a specific file path.
443    ///
444    /// # Arguments
445    ///
446    /// * `path` - Path to the credentials JSON file
447    /// * `account_id` - The NEAR account ID
448    pub fn from_file(
449        path: impl AsRef<Path>,
450        account_id: impl AsRef<str>,
451    ) -> Result<Self, crate::error::Error> {
452        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
453            crate::error::Error::Config(format!(
454                "Failed to read credentials file {}: {}",
455                path.as_ref().display(),
456                e
457            ))
458        })?;
459
460        let cred: CredentialFile = serde_json::from_str(&content).map_err(|e| {
461            crate::error::Error::Config(format!(
462                "Failed to parse credentials file {}: {}",
463                path.as_ref().display(),
464                e
465            ))
466        })?;
467
468        let inner = InMemorySigner::new(account_id, &cred.private_key)?;
469        Ok(Self { inner })
470    }
471
472    /// Get the public key.
473    pub fn public_key(&self) -> &PublicKey {
474        self.inner.public_key()
475    }
476
477    /// Unwrap into the underlying [`InMemorySigner`].
478    pub fn into_inner(self) -> InMemorySigner {
479        self.inner
480    }
481}
482
483impl std::fmt::Debug for FileSigner {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        f.debug_struct("FileSigner")
486            .field("account_id", &self.inner.account_id)
487            .field("public_key", &self.inner.public_key)
488            .finish()
489    }
490}
491
492impl Signer for FileSigner {
493    fn account_id(&self) -> &AccountId {
494        self.inner.account_id()
495    }
496
497    fn key(&self) -> SigningKey {
498        self.inner.key()
499    }
500}
501
502// ============================================================================
503// EnvSigner
504// ============================================================================
505
506/// A signer that loads credentials from environment variables.
507///
508/// By default, reads from:
509/// - `NEAR_ACCOUNT_ID` - The account ID
510/// - `NEAR_PRIVATE_KEY` - The private key
511///
512/// # Example
513///
514/// ```rust,no_run
515/// use near_kit::EnvSigner;
516///
517/// // With NEAR_ACCOUNT_ID and NEAR_PRIVATE_KEY set:
518/// let signer = EnvSigner::new().unwrap();
519/// ```
520#[derive(Clone)]
521pub struct EnvSigner {
522    inner: InMemorySigner,
523}
524
525impl EnvSigner {
526    /// Load from `NEAR_ACCOUNT_ID` and `NEAR_PRIVATE_KEY` environment variables.
527    ///
528    /// # Errors
529    ///
530    /// Returns an error if:
531    /// - Either environment variable is not set
532    /// - The values cannot be parsed
533    pub fn new() -> Result<Self, crate::error::Error> {
534        Self::from_env_vars("NEAR_ACCOUNT_ID", "NEAR_PRIVATE_KEY")
535    }
536
537    /// Load from custom environment variable names.
538    ///
539    /// # Arguments
540    ///
541    /// * `account_var` - Name of the environment variable containing the account ID
542    /// * `key_var` - Name of the environment variable containing the private key
543    pub fn from_env_vars(account_var: &str, key_var: &str) -> Result<Self, crate::error::Error> {
544        let account_id = std::env::var(account_var).map_err(|_| {
545            crate::error::Error::Config(format!("Environment variable {} not set", account_var))
546        })?;
547
548        let private_key = std::env::var(key_var).map_err(|_| {
549            crate::error::Error::Config(format!("Environment variable {} not set", key_var))
550        })?;
551
552        let inner = InMemorySigner::new(&account_id, &private_key)?;
553        Ok(Self { inner })
554    }
555
556    /// Get the public key.
557    pub fn public_key(&self) -> &PublicKey {
558        self.inner.public_key()
559    }
560
561    /// Unwrap into the underlying [`InMemorySigner`].
562    pub fn into_inner(self) -> InMemorySigner {
563        self.inner
564    }
565}
566
567impl std::fmt::Debug for EnvSigner {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        f.debug_struct("EnvSigner")
570            .field("account_id", &self.inner.account_id)
571            .field("public_key", &self.inner.public_key)
572            .finish()
573    }
574}
575
576impl Signer for EnvSigner {
577    fn account_id(&self) -> &AccountId {
578        self.inner.account_id()
579    }
580
581    fn key(&self) -> SigningKey {
582        self.inner.key()
583    }
584}
585
586// ============================================================================
587// RotatingSigner
588// ============================================================================
589
590/// A signer that rotates through multiple keys for the same account.
591///
592/// This solves the nonce collision problem for high-throughput applications.
593/// Each call to `key()` atomically claims the next key in round-robin order.
594///
595/// # Use Case
596///
597/// NEAR uses per-key nonces. When sending concurrent transactions with a single key,
598/// they can collide on nonce values. By rotating through multiple keys, each
599/// concurrent transaction uses a different key with its own nonce sequence.
600///
601/// # Loading Keys from Other Sources
602///
603/// Keys can be loaded from any storage backend (file, keyring, env) via
604/// [`from_signers()`](Self::from_signers):
605///
606/// ```rust,no_run
607/// # use near_kit::*;
608/// let rotating = RotatingSigner::from_signers(vec![
609///     FileSigner::from_file("keys/bot-key-0.json", "bot.testnet")?.into_inner(),
610///     FileSigner::from_file("keys/bot-key-1.json", "bot.testnet")?.into_inner(),
611/// ])?;
612/// # Ok::<(), near_kit::Error>(())
613/// ```
614///
615/// # Sequential Sends
616///
617/// Use [`into_per_key_signers()`](Self::into_per_key_signers) to split into
618/// per-key [`InMemorySigner`] instances for building sequential send queues.
619/// See the `sequential_sends` example.
620///
621/// # Example
622///
623/// ```rust
624/// use near_kit::{RotatingSigner, SecretKey, Signer};
625///
626/// let keys = vec![
627///     SecretKey::generate_ed25519(),
628///     SecretKey::generate_ed25519(),
629///     SecretKey::generate_ed25519(),
630/// ];
631///
632/// let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
633///
634/// // Each key() call atomically claims the next key in sequence
635/// let key1 = signer.key();
636/// let key2 = signer.key();
637/// let key3 = signer.key();
638/// // key4 wraps back to the first key
639/// let key4 = signer.key();
640/// ```
641pub struct RotatingSigner {
642    account_id: AccountId,
643    keys: Vec<SecretKey>,
644    counter: AtomicUsize,
645}
646
647impl RotatingSigner {
648    /// Create a rotating signer with multiple keys.
649    ///
650    /// # Arguments
651    ///
652    /// * `account_id` - The NEAR account ID
653    /// * `keys` - Vector of secret keys (must not be empty)
654    ///
655    /// # Errors
656    ///
657    /// Returns an error if:
658    /// - The account ID cannot be parsed
659    /// - The keys vector is empty
660    pub fn new(
661        account_id: impl AsRef<str>,
662        keys: Vec<SecretKey>,
663    ) -> Result<Self, crate::error::Error> {
664        if keys.is_empty() {
665            return Err(crate::error::Error::Config(
666                "RotatingSigner requires at least one key".to_string(),
667            ));
668        }
669
670        let account_id: AccountId = account_id.as_ref().parse()?;
671
672        Ok(Self {
673            account_id,
674            keys,
675            counter: AtomicUsize::new(0),
676        })
677    }
678
679    /// Create a rotating signer from [`InMemorySigner`] instances.
680    ///
681    /// This accepts signers loaded from any source (keyring, file, env, etc.)
682    /// via their `into_inner()` method. All signers must share the same account ID.
683    ///
684    /// # Example
685    ///
686    /// ```rust,no_run
687    /// use near_kit::{RotatingSigner, FileSigner};
688    ///
689    /// // Load keys from separate credential files for the same account
690    /// let signers = vec![
691    ///     FileSigner::from_file("keys/bot-key-0.json", "bot.testnet")?.into_inner(),
692    ///     FileSigner::from_file("keys/bot-key-1.json", "bot.testnet")?.into_inner(),
693    /// ];
694    /// let rotating = RotatingSigner::from_signers(signers)?;
695    /// # Ok::<(), near_kit::Error>(())
696    /// ```
697    pub fn from_signers(signers: Vec<InMemorySigner>) -> Result<Self, crate::error::Error> {
698        if signers.is_empty() {
699            return Err(crate::error::Error::Config(
700                "RotatingSigner requires at least one signer".to_string(),
701            ));
702        }
703
704        let account_id = signers[0].account_id().clone();
705        for signer in &signers[1..] {
706            if signer.account_id() != &account_id {
707                return Err(crate::error::Error::Config(format!(
708                    "All signers must share the same account ID, got {} and {}",
709                    account_id,
710                    signer.account_id()
711                )));
712            }
713        }
714
715        let keys = signers.into_iter().map(|s| s.secret_key).collect();
716        Ok(Self {
717            account_id,
718            keys,
719            counter: AtomicUsize::new(0),
720        })
721    }
722
723    /// Create a rotating signer from key strings.
724    ///
725    /// # Arguments
726    ///
727    /// * `account_id` - The NEAR account ID
728    /// * `keys` - Slice of secret keys in string format (e.g., "ed25519:...")
729    pub fn from_key_strings(
730        account_id: impl AsRef<str>,
731        keys: &[impl AsRef<str>],
732    ) -> Result<Self, crate::error::Error> {
733        let parsed_keys: Result<Vec<SecretKey>, _> =
734            keys.iter().map(|k| k.as_ref().parse()).collect();
735        Self::new(account_id, parsed_keys?)
736    }
737
738    /// Get the number of keys in rotation.
739    pub fn key_count(&self) -> usize {
740        self.keys.len()
741    }
742
743    /// Get all public keys.
744    pub fn public_keys(&self) -> Vec<PublicKey> {
745        self.keys.iter().map(|sk| sk.public_key()).collect()
746    }
747
748    /// Get all signing keys without advancing the rotation counter.
749    ///
750    /// Useful for building per-key data structures like sequential send queues.
751    /// Unlike [`key()`](Signer::key), calling this does not affect the rotation.
752    pub fn signing_keys(&self) -> Vec<SigningKey> {
753        self.keys
754            .iter()
755            .map(|sk| SigningKey::new(sk.clone()))
756            .collect()
757    }
758
759    /// Split into per-key [`InMemorySigner`] instances.
760    ///
761    /// Each signer uses a single key from the rotation pool, allowing
762    /// independent send ordering per key. The global nonce manager
763    /// automatically tracks nonces per `(account_id, public_key)`,
764    /// so per-key signers coordinate correctly without extra setup.
765    ///
766    /// # Example
767    ///
768    /// ```rust
769    /// use near_kit::{RotatingSigner, SecretKey};
770    ///
771    /// let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
772    /// let rotating = RotatingSigner::new("bot.testnet", keys).unwrap();
773    ///
774    /// // Create per-key signers for sequential queue workers
775    /// let per_key: Vec<_> = rotating.into_per_key_signers();
776    /// assert_eq!(per_key.len(), 2);
777    /// ```
778    pub fn into_per_key_signers(self) -> Vec<InMemorySigner> {
779        let account_id = self.account_id;
780        self.keys
781            .into_iter()
782            .map(|sk| InMemorySigner::from_secret_key(account_id.clone(), sk))
783            .collect()
784    }
785}
786
787impl std::fmt::Debug for RotatingSigner {
788    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789        f.debug_struct("RotatingSigner")
790            .field("account_id", &self.account_id)
791            .field("key_count", &self.keys.len())
792            .field("counter", &self.counter.load(Ordering::Relaxed))
793            .finish()
794    }
795}
796
797impl Signer for RotatingSigner {
798    fn account_id(&self) -> &AccountId {
799        &self.account_id
800    }
801
802    fn key(&self) -> SigningKey {
803        // Atomically claim the next key in rotation
804        let idx = self.counter.fetch_add(1, Ordering::Relaxed) % self.keys.len();
805        SigningKey::new(self.keys[idx].clone())
806    }
807}
808
809// ============================================================================
810// Tests
811// ============================================================================
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use crate::types::{Action, CryptoHash, NearToken, Transaction};
817
818    #[tokio::test]
819    async fn test_in_memory_signer() {
820        let signer = InMemorySigner::new(
821            "alice.testnet",
822            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
823        )
824        .unwrap();
825
826        assert_eq!(signer.account_id().as_str(), "alice.testnet");
827
828        let key = signer.key();
829        let message = b"test message";
830        let signature = key.sign(message).await.unwrap();
831
832        // Verify the key matches
833        assert_eq!(key.public_key(), signer.public_key());
834        assert!(!signature.as_bytes().is_empty());
835    }
836
837    #[tokio::test]
838    async fn test_signature_consistency() {
839        // Same message should produce the same signature (Ed25519 is deterministic)
840        let signer = InMemorySigner::new(
841            "alice.testnet",
842            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
843        )
844        .unwrap();
845
846        let key = signer.key();
847        let message = b"test message";
848        let sig1 = key.sign(message).await.unwrap();
849        let sig2 = key.sign(message).await.unwrap();
850
851        assert_eq!(sig1.as_bytes(), sig2.as_bytes());
852    }
853
854    #[tokio::test]
855    async fn test_different_messages_different_signatures() {
856        let signer = InMemorySigner::new(
857            "alice.testnet",
858            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
859        )
860        .unwrap();
861
862        let key = signer.key();
863        let sig1 = key.sign(b"message 1").await.unwrap();
864        let sig2 = key.sign(b"message 2").await.unwrap();
865
866        assert_ne!(sig1.as_bytes(), sig2.as_bytes());
867    }
868
869    #[tokio::test]
870    async fn test_transaction_signing_with_signer_trait() {
871        let secret_key = SecretKey::generate_ed25519();
872        let signer = InMemorySigner::from_secret_key("alice.testnet".parse().unwrap(), secret_key);
873
874        // Get a key for signing
875        let key = signer.key();
876
877        // Build a transaction
878        let tx = Transaction::new(
879            signer.account_id().clone(),
880            key.public_key().clone(),
881            1,
882            "bob.testnet".parse().unwrap(),
883            CryptoHash::ZERO,
884            vec![Action::transfer(NearToken::from_near(1))],
885        );
886
887        // Sign using the key
888        let tx_hash = tx.get_hash();
889        let signature = key.sign(tx_hash.as_bytes()).await.unwrap();
890
891        // Verify signature is 64 bytes (Ed25519)
892        assert_eq!(signature.as_bytes().len(), 64);
893
894        // Create signed transaction
895        let signed_tx = crate::types::SignedTransaction {
896            transaction: tx,
897            signature,
898        };
899
900        // Verify serialization works
901        let bytes = signed_tx.to_bytes();
902        assert!(!bytes.is_empty());
903
904        // Verify base64 encoding works
905        let base64 = signed_tx.to_base64();
906        assert!(!base64.is_empty());
907    }
908
909    #[tokio::test]
910    async fn test_rotating_signer() {
911        let keys = vec![
912            SecretKey::generate_ed25519(),
913            SecretKey::generate_ed25519(),
914            SecretKey::generate_ed25519(),
915        ];
916        let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
917
918        let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
919
920        // Verify round-robin rotation
921        let key1 = signer.key();
922        assert_eq!(key1.public_key(), &expected_public_keys[0]);
923
924        let key2 = signer.key();
925        assert_eq!(key2.public_key(), &expected_public_keys[1]);
926
927        let key3 = signer.key();
928        assert_eq!(key3.public_key(), &expected_public_keys[2]);
929
930        // Wraps around
931        let key4 = signer.key();
932        assert_eq!(key4.public_key(), &expected_public_keys[0]);
933    }
934
935    #[tokio::test]
936    async fn test_rotating_signer_atomic_key_claiming() {
937        // Verify that key() atomically claims a key that can be used for signing
938        let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
939        let expected_pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
940
941        let signer = RotatingSigner::new("bot.testnet", keys.clone()).unwrap();
942        let message = b"test";
943
944        // Claim first key and sign
945        let key1 = signer.key();
946        assert_eq!(key1.public_key(), &expected_pks[0]);
947        let sig1 = key1.sign(message).await.unwrap();
948        // Verify signature matches what the raw key would produce
949        let expected_sig1 = keys[0].sign(message);
950        assert_eq!(sig1.as_bytes(), expected_sig1.as_bytes());
951
952        // Claim second key and sign
953        let key2 = signer.key();
954        assert_eq!(key2.public_key(), &expected_pks[1]);
955        let sig2 = key2.sign(message).await.unwrap();
956        let expected_sig2 = keys[1].sign(message);
957        assert_eq!(sig2.as_bytes(), expected_sig2.as_bytes());
958
959        // Different keys produce different signatures
960        assert_ne!(sig1.as_bytes(), sig2.as_bytes());
961    }
962
963    #[test]
964    fn test_rotating_signer_empty_keys() {
965        let result = RotatingSigner::new("bot.testnet", vec![]);
966        assert!(result.is_err());
967    }
968
969    #[test]
970    fn test_env_signer_missing_vars() {
971        // This should fail because the env vars aren't set
972        let result = EnvSigner::from_env_vars("NONEXISTENT_VAR_1", "NONEXISTENT_VAR_2");
973        assert!(result.is_err());
974    }
975
976    #[test]
977    fn test_signer_from_secret_key() {
978        let secret = SecretKey::generate_ed25519();
979        let expected_pk = secret.public_key();
980
981        let signer = InMemorySigner::from_secret_key("alice.testnet".parse().unwrap(), secret);
982
983        assert_eq!(signer.account_id().as_str(), "alice.testnet");
984        assert_eq!(signer.public_key(), &expected_pk);
985    }
986
987    #[test]
988    fn test_rotating_signer_key_count() {
989        let keys = vec![
990            SecretKey::generate_ed25519(),
991            SecretKey::generate_ed25519(),
992            SecretKey::generate_ed25519(),
993        ];
994
995        let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
996
997        assert_eq!(signer.key_count(), 3);
998        assert_eq!(signer.public_keys().len(), 3);
999    }
1000
1001    #[test]
1002    fn test_rotating_signer_from_key_strings() {
1003        let keys = [
1004            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
1005            "ed25519:4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi5kL6YJt9Z6iLqMBkVfqDH2Zj8bxqXTdMkNmvPcAD8LqCZ",
1006        ];
1007
1008        let signer = RotatingSigner::from_key_strings("bot.testnet", &keys).unwrap();
1009
1010        assert_eq!(signer.key_count(), 2);
1011        assert_eq!(signer.account_id().as_str(), "bot.testnet");
1012    }
1013
1014    #[test]
1015    fn test_in_memory_signer_debug_hides_secret() {
1016        let signer = InMemorySigner::new(
1017            "alice.testnet",
1018            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
1019        )
1020        .unwrap();
1021
1022        let debug_str = format!("{:?}", signer);
1023
1024        // Should show account_id and public_key but NOT the secret key
1025        assert!(debug_str.contains("alice.testnet"));
1026        assert!(debug_str.contains("public_key"));
1027        assert!(!debug_str.contains("secret_key"));
1028        assert!(!debug_str.contains("3D4YudUahN1nawWogh"));
1029    }
1030
1031    #[test]
1032    fn test_rotating_signer_from_signers() {
1033        let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
1034        let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
1035
1036        let signers: Vec<InMemorySigner> = keys
1037            .into_iter()
1038            .map(|sk| InMemorySigner::from_secret_key("bot.testnet".parse().unwrap(), sk))
1039            .collect();
1040
1041        let rotating = RotatingSigner::from_signers(signers).unwrap();
1042        assert_eq!(rotating.key_count(), 2);
1043        assert_eq!(rotating.public_keys(), expected_public_keys);
1044    }
1045
1046    #[test]
1047    fn test_rotating_signer_from_signers_mismatched_accounts() {
1048        let signers = vec![
1049            InMemorySigner::from_secret_key(
1050                "alice.testnet".parse().unwrap(),
1051                SecretKey::generate_ed25519(),
1052            ),
1053            InMemorySigner::from_secret_key(
1054                "bob.testnet".parse().unwrap(),
1055                SecretKey::generate_ed25519(),
1056            ),
1057        ];
1058
1059        let result = RotatingSigner::from_signers(signers);
1060        assert!(result.is_err());
1061    }
1062
1063    #[test]
1064    fn test_rotating_signer_signing_keys() {
1065        let keys = vec![
1066            SecretKey::generate_ed25519(),
1067            SecretKey::generate_ed25519(),
1068            SecretKey::generate_ed25519(),
1069        ];
1070        let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
1071
1072        let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
1073
1074        // signing_keys() should not advance the counter
1075        let counter_before = signer.counter.load(Ordering::Relaxed);
1076        let signing_keys = signer.signing_keys();
1077        let counter_after = signer.counter.load(Ordering::Relaxed);
1078        assert_eq!(counter_before, counter_after);
1079
1080        // Should return all keys with matching public keys
1081        assert_eq!(signing_keys.len(), 3);
1082        for (sk, expected_pk) in signing_keys.iter().zip(&expected_public_keys) {
1083            assert_eq!(sk.public_key(), expected_pk);
1084        }
1085    }
1086
1087    #[test]
1088    fn test_rotating_signer_into_per_key_signers() {
1089        let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
1090        let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
1091
1092        let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
1093        let per_key = signer.into_per_key_signers();
1094
1095        assert_eq!(per_key.len(), 2);
1096        for (ims, expected_pk) in per_key.iter().zip(&expected_public_keys) {
1097            assert_eq!(ims.account_id().as_str(), "bot.testnet");
1098            assert_eq!(ims.public_key(), expected_pk);
1099        }
1100    }
1101
1102    #[test]
1103    fn test_signing_key_is_clone() {
1104        let signer = InMemorySigner::new(
1105            "alice.testnet",
1106            "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
1107        )
1108        .unwrap();
1109
1110        let key = signer.key();
1111        let key_clone = key.clone();
1112
1113        assert_eq!(key.public_key(), key_clone.public_key());
1114    }
1115}