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