near_api/signer/
mod.rs

1//! Transaction signing functionality for NEAR Protocol
2//!
3//! The [`Signer`] provides various ways to sign transactions on NEAR, including:
4//! - Secret key signing
5//! - Seed phrase (mnemonic) signing
6//! - Access key file signing
7//! - Hardware wallet (`Ledger`) signing
8//! - System keychain signing
9//!
10//! # Examples
11//!
12//! ## Creating a signer using a secret key
13//! ```rust,no_run
14//! use near_api::{*, types::SecretKey};
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let secret_key: SecretKey = "ed25519:2vVTQWpoZvYZBS4HYFZtzU2rxpoQSrhyFWdaHLqSdyaEfgjefbSKiFpuVatuRqax3HFvVq2tkkqWH2h7tso2nK8q".parse()?;
18//! let signer = Signer::new(Signer::from_secret_key(secret_key))?;
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! ## Creating a signer using a seed phrase
24//! ```rust,no_run
25//! use near_api::*;
26//!
27//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! let seed_phrase = "witch collapse practice feed shame open despair creek road again ice least";
29//! let signer = Signer::new(Signer::from_seed_phrase(seed_phrase, None)?)?;
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Creating a `Ledger` signer
35//! ```rust,no_run
36//! # #[cfg(feature = "ledger")]
37//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
38//! use near_api::*;
39//!
40//! let signer = Signer::new(Signer::from_ledger())?;
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! ## Creating a `keystore` signer
46//! ```rust,no_run
47//! # #[cfg(feature = "keystore")]
48//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
49//! use near_api::*;
50//!
51//! let preloaded_keychain = Signer::from_keystore_with_search_for_keys("account_id.testnet".parse()?, &NetworkConfig::testnet()).await?;
52//! let signer = Signer::new(preloaded_keychain)?;
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! ## Example signing with [Signer](`Signer`)
58//!
59//! ```rust,no_run
60//! # use near_api::*;
61//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
62//! # let signer = Signer::new(Signer::from_secret_key("ed25519:2vVTQWpoZvYZBS4HYFZtzU2rxpoQSrhyFWdaHLqSdyaEfgjefbSKiFpuVatuRqax3HFvVq2tkkqWH2h7tso2nK8q".parse()?))?;
63//! let transaction_result = Tokens::account("alice.testnet".parse()?)
64//!     .send_to("bob.testnet".parse()?)
65//!     .near(NearToken::from_near(1))
66//!     .with_signer(signer)
67//!     .send_to_testnet()
68//!     .await?;
69//!
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! # Advanced: [Access Key Pooling](https://github.com/akorchyn/near-api/issues/2)
75//!
76//! The signer supports pooling multiple access keys for improved transaction throughput.
77//! It helps to mitigate concurrency issues that arise when multiple transactions are signed but the
78//! transaction with the highest nonce arrives first which would fail transaction with a lower nonce.
79//!
80//! By using, account key pooling, each transaction is signed with a different key, so that the nonce issue
81//! is mitigated as long as the keys are more or equal to the number of signed transactions.
82//! ```rust,no_run
83//! use near_api::{*, types::SecretKey};
84//!
85//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
86//! let signer = Signer::new(Signer::from_secret_key("ed25519:2vVTQWpoZvYZBS4HYFZtzU2rxpoQSrhyFWdaHLqSdyaEfgjefbSKiFpuVatuRqax3HFvVq2tkkqWH2h7tso2nK8q".parse()?))?;
87//!
88//! // Add additional keys to the pool
89//! signer.add_signer_to_pool(Signer::from_seed_phrase("witch collapse practice feed shame open despair creek road again ice least", None)?).await?;
90//! signer.add_signer_to_pool(Signer::from_seed_phrase("return cactus real attack meat pitch trash found autumn upgrade mystery pupil", None)?).await?;
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! # Nonce Management
96//!
97//! The signer automatically manages nonces for transactions:
98//! - Caches nonces per (account_id, public_key) pair
99//! - Automatically increments nonces for sequential transactions
100//! - Supports concurrent transactions as long as the `Arc<Signer>` is same
101//!
102//! # Secret generation
103//! The crate provides utility functions to generate new secret keys and seed phrases
104//!
105//! See [functions](#functions) section for details
106//!
107//! # Custom signer
108//! The user can instantiate [`Signer`] with a custom signing logic by utilizing the [`SignerTrait`] trait.
109
110use std::{
111    collections::HashMap,
112    path::{Path, PathBuf},
113    str::FromStr,
114    sync::{
115        atomic::{AtomicU64, AtomicUsize, Ordering},
116        Arc,
117    },
118};
119
120use near_api_types::{
121    transaction::{
122        delegate_action::{NonDelegateAction, SignedDelegateAction},
123        PrepopulateTransaction, SignedTransaction, Transaction, TransactionV0,
124    },
125    AccountId, BlockHeight, CryptoHash, Nonce, PublicKey, SecretKey, Signature,
126};
127
128use borsh::{BorshDeserialize, BorshSerialize};
129use serde::{Deserialize, Serialize};
130use slipped10::BIP32Path;
131use tracing::{debug, info, instrument, trace, warn};
132
133use crate::{
134    config::NetworkConfig,
135    errors::{AccessKeyFileError, MetaSignError, SecretError, SignerError},
136};
137
138use secret_key::SecretKeySigner;
139
140#[cfg(feature = "keystore")]
141pub mod keystore;
142#[cfg(feature = "ledger")]
143pub mod ledger;
144pub mod secret_key;
145
146const SIGNER_TARGET: &str = "near_api::signer";
147/// Default HD path for seed phrases and secret keys generation
148pub const DEFAULT_HD_PATH: &str = "m/44'/397'/0'";
149/// Default word count for seed phrases generation
150pub const DEFAULT_WORD_COUNT: usize = 12;
151
152/// A struct representing a pair of public and private keys for an account.
153/// This might be useful for getting keys from a file. E.g. `~/.near-credentials`.
154#[derive(Debug, Serialize, Deserialize, Clone)]
155pub struct AccountKeyPair {
156    pub public_key: PublicKey,
157    pub private_key: SecretKey,
158}
159
160impl AccountKeyPair {
161    fn load_access_key_file(path: &Path) -> Result<Self, AccessKeyFileError> {
162        let data = std::fs::read_to_string(path)?;
163        Ok(serde_json::from_str(&data)?)
164    }
165}
166
167/// [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) input for the signing message.
168#[derive(Debug, Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
169pub struct NEP413Payload {
170    /// The message that wants to be transmitted.
171    pub message: String,
172    /// A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array.
173    pub nonce: [u8; 32],
174    /// The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
175    pub recipient: String,
176    /// A callback URL that will be called with the signed message as a query parameter.
177    pub callback_url: Option<String>,
178}
179
180#[cfg(feature = "ledger")]
181impl From<NEP413Payload> for near_ledger::NEP413Payload {
182    fn from(payload: NEP413Payload) -> Self {
183        Self {
184            message: payload.message,
185            nonce: payload.nonce,
186            recipient: payload.recipient,
187            callback_url: payload.callback_url,
188        }
189    }
190}
191
192/// A trait for implementing custom signing logic.
193///
194/// This trait provides the core functionality needed to sign transactions and delegate actions.
195/// It is used by the [`Signer`] to abstract over different signing methods (secret key, `ledger`, `keystore`, etc.).
196///
197/// # Examples
198///
199/// ## Implementing a custom signer
200/// ```rust,no_run
201/// use near_api::{*, signer::*, types::transaction::{PrepopulateTransaction, Transaction}, errors::SignerError};
202///
203/// struct CustomSigner {
204///     secret_key: SecretKey,
205/// }
206///
207/// #[async_trait::async_trait]
208/// impl SignerTrait for CustomSigner {
209///     async fn get_secret_key(
210///         &self,
211///         _signer_id: &AccountId,
212///         _public_key: &PublicKey
213///     ) -> Result<SecretKey, SignerError> {
214///         Ok(self.secret_key.clone())
215///     }
216///
217///     fn get_public_key(&self) -> Result<PublicKey, SignerError> {
218///         Ok(self.secret_key.public_key().into())
219///     }
220/// }
221/// ```
222///
223/// ## Using a custom signer
224/// ```rust,no_run
225/// # use near_api::{AccountId, signer::*, types::{transaction::{Transaction, PrepopulateTransaction}, PublicKey, SecretKey}, errors::SignerError};
226/// # struct CustomSigner;
227/// # impl CustomSigner {
228/// #     fn new(_: SecretKey) -> Self { Self }
229/// # }
230/// # #[async_trait::async_trait]
231/// # impl SignerTrait for CustomSigner {
232/// #     async fn get_secret_key(&self, _: &AccountId, _: &PublicKey) -> Result<SecretKey, near_api::errors::SignerError> { unimplemented!() }
233/// #     fn get_public_key(&self) -> Result<PublicKey, SignerError> { unimplemented!() }
234/// # }
235/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
236/// let secret_key = "ed25519:2vVTQWpoZvYZBS4HYFZtzU2rxpoQSrhyFWdaHLqSdyaEfgjefbSKiFpuVatuRqax3HFvVq2tkkqWH2h7tso2nK8q".parse()?;
237/// let custom_signer = CustomSigner::new(secret_key);
238/// let signer = Signer::new(custom_signer)?;
239/// # Ok(())
240/// # }
241/// ```
242///
243/// ## Example of implementing `sign_meta` and `sign` methods
244/// The default implementation of `sign_meta` and `sign` methods should work for most cases.
245/// If you need to implement custom logic, you can override these methods.
246/// See [`near_ledger`](`ledger::LedgerSigner`) implementation for an example.
247#[async_trait::async_trait]
248pub trait SignerTrait {
249    /// Signs a delegate action for meta transactions.
250    ///
251    /// This method is used for meta-transactions where one account can delegate transaction delivery and gas payment to another account.
252    /// The delegate action is signed with a maximum block height to ensure the delegation expiration after some point in time.
253    ///
254    /// The default implementation should work for most cases.
255    #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))]
256    async fn sign_meta(
257        &self,
258        tr: PrepopulateTransaction,
259        public_key: PublicKey,
260        nonce: Nonce,
261        block_hash: CryptoHash,
262        max_block_height: BlockHeight,
263    ) -> Result<SignedDelegateAction, MetaSignError> {
264        let signer_secret_key = self.get_secret_key(&tr.signer_id, &public_key).await?;
265        let unsigned_transaction = Transaction::V0(TransactionV0 {
266            signer_id: tr.signer_id.clone(),
267            public_key,
268            nonce,
269            receiver_id: tr.receiver_id,
270            block_hash,
271            actions: tr.actions,
272        });
273
274        get_signed_delegate_action(unsigned_transaction, signer_secret_key, max_block_height)
275    }
276
277    /// Signs a regular transaction.
278    ///
279    /// This method is used for standard transactions. It creates a signed transaction
280    /// that can be sent to the `NEAR` network.
281    ///
282    /// The default implementation should work for most cases.
283    #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))]
284    async fn sign(
285        &self,
286        tr: PrepopulateTransaction,
287        public_key: PublicKey,
288        nonce: Nonce,
289        block_hash: CryptoHash,
290    ) -> Result<SignedTransaction, SignerError> {
291        let signer_secret_key = self.get_secret_key(&tr.signer_id, &public_key).await?;
292        let unsigned_transaction = Transaction::V0(TransactionV0 {
293            signer_id: tr.signer_id.clone(),
294            public_key,
295            nonce,
296            receiver_id: tr.receiver_id,
297            block_hash,
298            actions: tr.actions,
299        });
300
301        let signature = signer_secret_key.sign(unsigned_transaction.get_hash().0.as_ref());
302
303        Ok(SignedTransaction::new(signature, unsigned_transaction))
304    }
305
306    /// Signs a [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) message that is widely used for the [authentication](https://docs.near.org/build/web3-apps/backend/)
307    /// and off-chain proof of account ownership.
308    ///
309    /// The default implementation should work for most cases.
310    #[instrument(skip(self), fields(signer_id = %signer_id, receiver_id = %payload.recipient, message = %payload.message))]
311    async fn sign_message_nep413(
312        &self,
313        signer_id: AccountId,
314        public_key: PublicKey,
315        payload: NEP413Payload,
316    ) -> Result<Signature, SignerError> {
317        const NEP413_413_SIGN_MESSAGE_PREFIX: u32 = (1u32 << 31u32) + 413u32;
318        let mut bytes = NEP413_413_SIGN_MESSAGE_PREFIX.to_le_bytes().to_vec();
319        borsh::to_writer(&mut bytes, &payload)?;
320        let hash = CryptoHash::hash(&bytes);
321        let secret = self.get_secret_key(&signer_id, &public_key).await?;
322        let signature = secret.sign(hash.0.as_ref());
323        Ok(signature)
324    }
325
326    /// Returns the secret key associated with this signer.
327    /// This is a `helper` method that should be implemented by the signer or fail with [`SignerError`].
328    /// As long as this method works, the default implementation of the [sign_meta](`SignerTrait::sign_meta`) and [sign](`SignerTrait::sign`) methods should work.
329    ///
330    /// If you can't provide a [`SecretKey`] for some reason (E.g. `Ledger``),
331    /// you can fail with SignerError and override `sign_meta` and `sign`, `sign_message_nep413` methods.
332    async fn get_secret_key(
333        &self,
334        signer_id: &AccountId,
335        public_key: &PublicKey,
336    ) -> Result<SecretKey, SignerError>;
337
338    /// Returns the public key associated with this signer.
339    ///
340    /// This method is used by the [`Signer`] to manage the pool of signing keys.
341    fn get_public_key(&self) -> Result<PublicKey, SignerError>;
342}
343
344/// A [Signer](`Signer`) is a wrapper around a single or multiple signer implementations
345/// of [SignerTrait](`SignerTrait`).
346///
347/// It provides an access key pooling and a nonce caching mechanism to improve transaction throughput.
348pub struct Signer {
349    pool: tokio::sync::RwLock<HashMap<PublicKey, Box<dyn SignerTrait + Send + Sync + 'static>>>,
350    nonce_cache: tokio::sync::RwLock<HashMap<(AccountId, PublicKey), AtomicU64>>,
351    current_public_key: AtomicUsize,
352}
353
354impl Signer {
355    /// Creates a new signer and instantiates nonce cache.
356    #[instrument(skip(signer))]
357    pub fn new<T: SignerTrait + Send + Sync + 'static>(
358        signer: T,
359    ) -> Result<Arc<Self>, SignerError> {
360        let public_key = signer.get_public_key()?;
361        Ok(Arc::new(Self {
362            pool: tokio::sync::RwLock::new(HashMap::from([(
363                public_key,
364                Box::new(signer) as Box<dyn SignerTrait + Send + Sync + 'static>,
365            )])),
366            nonce_cache: tokio::sync::RwLock::new(HashMap::new()),
367            current_public_key: AtomicUsize::new(0),
368        }))
369    }
370
371    /// Adds a signer to the pool of signers.
372    /// The [Signer](`Signer`) will rotate the provided implementation of [SignerTrait](`SignerTrait`) on each call to [get_public_key](`Signer::get_public_key`).
373    #[instrument(skip(self, signer))]
374    pub async fn add_signer_to_pool<T: SignerTrait + Send + Sync + 'static>(
375        &self,
376        signer: T,
377    ) -> Result<(), SignerError> {
378        let public_key = signer.get_public_key()?;
379        debug!(target: SIGNER_TARGET, "Adding signer to pool");
380        self.pool.write().await.insert(public_key, Box::new(signer));
381        Ok(())
382    }
383
384    /// Fetches the transaction nonce and block hash associated to the access key. Internally
385    /// caches the nonce as to not need to query for it every time, and ending up having to run
386    /// into contention with others.
387    #[instrument(skip(self, network), fields(account_id = %account_id))]
388    pub async fn fetch_tx_nonce(
389        &self,
390        account_id: AccountId,
391        public_key: PublicKey,
392        network: &NetworkConfig,
393    ) -> Result<(Nonce, CryptoHash, BlockHeight), SignerError> {
394        debug!(target: SIGNER_TARGET, "Fetching transaction nonce");
395        let nonce_data = crate::account::Account(account_id.clone())
396            .access_key(public_key.clone())
397            .fetch_from(network)
398            .await
399            .map_err(|e| SignerError::FetchNonceError(Box::new(e)))?;
400        let nonce_cache = self.nonce_cache.read().await;
401
402        if let Some(nonce) = nonce_cache.get(&(account_id.clone(), public_key.clone())) {
403            let nonce = nonce.fetch_add(1, Ordering::SeqCst);
404            drop(nonce_cache);
405            trace!(target: SIGNER_TARGET, "Nonce fetched from cache");
406            return Ok((nonce + 1, nonce_data.block_hash, nonce_data.block_height));
407        } else {
408            drop(nonce_cache);
409        }
410
411        // It's initialization, so it's better to take write lock, so other will wait
412
413        // case where multiple writers end up at the same lock acquisition point and tries
414        // to overwrite the cached value that a previous writer already wrote.
415        let nonce = self
416            .nonce_cache
417            .write()
418            .await
419            .entry((account_id.clone(), public_key))
420            .or_insert_with(|| AtomicU64::new(nonce_data.data.nonce.0 + 1))
421            .fetch_max(nonce_data.data.nonce.0 + 1, Ordering::SeqCst)
422            .max(nonce_data.data.nonce.0 + 1);
423
424        info!(target: SIGNER_TARGET, "Nonce fetched and cached");
425        Ok((nonce, nonce_data.block_hash, nonce_data.block_height))
426    }
427
428    /// Creates a [SecretKeySigner](`SecretKeySigner`) using seed phrase with default HD path.
429    pub fn from_seed_phrase(
430        seed_phrase: &str,
431        password: Option<&str>,
432    ) -> Result<SecretKeySigner, SecretError> {
433        Self::from_seed_phrase_with_hd_path(
434            seed_phrase,
435            BIP32Path::from_str("m/44'/397'/0'").expect("Valid HD path"),
436            password,
437        )
438    }
439
440    /// Creates a [SecretKeySigner](`SecretKeySigner`) using a secret key.
441    pub fn from_secret_key(secret_key: SecretKey) -> SecretKeySigner {
442        SecretKeySigner::new(secret_key)
443    }
444
445    /// Creates a [SecretKeySigner](`SecretKeySigner`) using seed phrase with a custom HD path.
446    pub fn from_seed_phrase_with_hd_path(
447        seed_phrase: &str,
448        hd_path: BIP32Path,
449        password: Option<&str>,
450    ) -> Result<SecretKeySigner, SecretError> {
451        let secret_key = get_secret_key_from_seed(hd_path, seed_phrase, password)?;
452        Ok(SecretKeySigner::new(secret_key))
453    }
454
455    /// Creates a [SecretKeySigner](`secret_key::SecretKeySigner`) using a path to the access key file.
456    pub fn from_access_keyfile(path: PathBuf) -> Result<SecretKeySigner, AccessKeyFileError> {
457        let keypair = AccountKeyPair::load_access_key_file(&path)?;
458        debug!(target: SIGNER_TARGET, "Access key file loaded successfully");
459
460        if keypair.public_key != keypair.private_key.public_key() {
461            return Err(AccessKeyFileError::PrivatePublicKeyMismatch);
462        }
463
464        Ok(SecretKeySigner::new(keypair.private_key))
465    }
466
467    /// Creates a [LedgerSigner](`ledger::LedgerSigner`) using default HD path.
468    #[cfg(feature = "ledger")]
469    pub fn from_ledger() -> ledger::LedgerSigner {
470        ledger::LedgerSigner::new(BIP32Path::from_str("44'/397'/0'/0'/1'").expect("Valid HD path"))
471    }
472
473    /// Creates a [LedgerSigner](`ledger::LedgerSigner`) using a custom HD path.
474    #[cfg(feature = "ledger")]
475    pub const fn from_ledger_with_hd_path(hd_path: BIP32Path) -> ledger::LedgerSigner {
476        ledger::LedgerSigner::new(hd_path)
477    }
478
479    /// Creates a [KeystoreSigner](`keystore::KeystoreSigner`) with predefined public key.
480    #[cfg(feature = "keystore")]
481    pub fn from_keystore(pub_key: PublicKey) -> keystore::KeystoreSigner {
482        keystore::KeystoreSigner::new_with_pubkey(pub_key)
483    }
484
485    /// Creates a [KeystoreSigner](`keystore::KeystoreSigner`). The provided function will query provided account for public keys and search
486    /// in the system keychain for the corresponding secret keys.
487    #[cfg(feature = "keystore")]
488    pub async fn from_keystore_with_search_for_keys(
489        account_id: AccountId,
490        network: &NetworkConfig,
491    ) -> Result<keystore::KeystoreSigner, crate::errors::KeyStoreError> {
492        keystore::KeystoreSigner::search_for_keys(account_id, network).await
493    }
494
495    /// Retrieves the public key from the pool of signers.
496    /// The public key is rotated on each call.
497    #[instrument(skip(self))]
498    pub async fn get_public_key(&self) -> Result<PublicKey, SignerError> {
499        let index = self.current_public_key.fetch_add(1, Ordering::SeqCst);
500        let public_key = {
501            let pool = self.pool.read().await;
502            pool.keys()
503                .nth(index % pool.len())
504                .ok_or(SignerError::PublicKeyIsNotAvailable)?
505                .clone()
506        };
507        debug!(target: SIGNER_TARGET, "Public key retrieved");
508        Ok(public_key)
509    }
510
511    #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))]
512    pub async fn sign_meta(
513        &self,
514        tr: PrepopulateTransaction,
515        public_key: PublicKey,
516        nonce: Nonce,
517        block_hash: CryptoHash,
518        max_block_height: BlockHeight,
519    ) -> Result<SignedDelegateAction, MetaSignError> {
520        let signer = self.pool.read().await;
521
522        signer
523            .get(&public_key)
524            .ok_or(SignerError::PublicKeyIsNotAvailable)?
525            .sign_meta(tr, public_key, nonce, block_hash, max_block_height)
526            .await
527    }
528
529    #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))]
530    pub async fn sign(
531        &self,
532        tr: PrepopulateTransaction,
533        public_key: PublicKey,
534        nonce: Nonce,
535        block_hash: CryptoHash,
536    ) -> Result<SignedTransaction, SignerError> {
537        let pool = self.pool.read().await;
538
539        pool.get(&public_key)
540            .ok_or(SignerError::PublicKeyIsNotAvailable)?
541            .sign(tr, public_key, nonce, block_hash)
542            .await
543    }
544}
545
546#[instrument(skip(unsigned_transaction, private_key))]
547fn get_signed_delegate_action(
548    mut unsigned_transaction: Transaction,
549    private_key: SecretKey,
550    max_block_height: u64,
551) -> core::result::Result<SignedDelegateAction, MetaSignError> {
552    use near_api_types::signable_message::{SignableMessage, SignableMessageType};
553    let actions: Vec<NonDelegateAction> = unsigned_transaction
554        .take_actions()
555        .into_iter()
556        .map(|action| {
557            NonDelegateAction::try_from(action)
558                .map_err(|_| MetaSignError::DelegateActionIsNotSupported)
559        })
560        .collect::<Result<Vec<_>, _>>()?;
561    let delegate_action = near_api_types::transaction::delegate_action::DelegateAction {
562        sender_id: unsigned_transaction.signer_id().clone(),
563        receiver_id: unsigned_transaction.receiver_id().clone(),
564        actions,
565        nonce: unsigned_transaction.nonce(),
566        max_block_height,
567        public_key: unsigned_transaction.public_key().clone(),
568    };
569
570    // create a new signature here signing the delegate action + discriminant
571    let signable = SignableMessage::new(&delegate_action, SignableMessageType::DelegateAction);
572    let bytes = borsh::to_vec(&signable).expect("Failed to serialize");
573    let hash = CryptoHash::hash(&bytes);
574    let signature = private_key.sign(hash.0.as_ref());
575
576    Ok(SignedDelegateAction {
577        delegate_action,
578        signature,
579    })
580}
581
582/// Generates a secret key from a seed phrase.
583///
584/// Prefer using [generate_secret_key_from_seed_phrase](`generate_secret_key_from_seed_phrase`) if you don't need to customize the HD path and passphrase.
585#[instrument(skip(seed_phrase_hd_path, master_seed_phrase, password))]
586pub fn get_secret_key_from_seed(
587    seed_phrase_hd_path: BIP32Path,
588    master_seed_phrase: &str,
589    password: Option<&str>,
590) -> Result<SecretKey, SecretError> {
591    let master_seed =
592        bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(password.unwrap_or_default());
593    let derived_private_key = slipped10::derive_key_from_path(
594        &master_seed,
595        slipped10::Curve::Ed25519,
596        &seed_phrase_hd_path,
597    )
598    .map_err(|_| SecretError::DeriveKeyInvalidIndex)?;
599
600    let secret_key = SecretKey::ED25519(
601        near_api_types::crypto::secret_key::ED25519SecretKey::from_secret_key(
602            derived_private_key.key,
603        ),
604    );
605
606    Ok(secret_key)
607}
608
609/// Generates a new seed phrase with optional customization.
610///
611/// Prefer using [generate_seed_phrase](`generate_seed_phrase`) or [generate_secret_key](`generate_secret_key`) if you don't need to customize the seed phrase.
612pub fn generate_seed_phrase_custom(
613    word_count: Option<usize>,
614    hd_path: Option<BIP32Path>,
615    passphrase: Option<&str>,
616) -> Result<(String, PublicKey), SecretError> {
617    let mnemonic = bip39::Mnemonic::generate(word_count.unwrap_or(DEFAULT_WORD_COUNT))?;
618    let seed_phrase = mnemonic.words().collect::<Vec<&str>>().join(" ");
619
620    let secret_key = get_secret_key_from_seed(
621        hd_path.unwrap_or_else(|| DEFAULT_HD_PATH.parse().expect("Valid HD path")),
622        &seed_phrase,
623        passphrase,
624    )?;
625    let public_key = secret_key.public_key();
626
627    Ok((seed_phrase, public_key))
628}
629
630/// Generates a new seed phrase with default settings (12 words, [default HD path](`DEFAULT_HD_PATH`))
631pub fn generate_seed_phrase() -> Result<(String, PublicKey), SecretError> {
632    generate_seed_phrase_custom(None, None, None)
633}
634
635/// Generates a new 12-words seed phrase with a custom HD path
636pub fn generate_seed_phrase_with_hd_path(
637    hd_path: BIP32Path,
638) -> Result<(String, PublicKey), SecretError> {
639    generate_seed_phrase_custom(None, Some(hd_path), None)
640}
641
642/// Generates a new 12-words seed phrase with a custom passphrase and [default HD path](`DEFAULT_HD_PATH`)
643pub fn generate_seed_phrase_with_passphrase(
644    passphrase: &str,
645) -> Result<(String, PublicKey), SecretError> {
646    generate_seed_phrase_custom(None, None, Some(passphrase))
647}
648
649/// Generates a new seed phrase with a custom word count and [default HD path](`DEFAULT_HD_PATH`)
650pub fn generate_seed_phrase_with_word_count(
651    word_count: usize,
652) -> Result<(String, PublicKey), SecretError> {
653    generate_seed_phrase_custom(Some(word_count), None, None)
654}
655
656/// Generates a secret key from a new seed phrase using default settings
657pub fn generate_secret_key() -> Result<SecretKey, SecretError> {
658    let (seed_phrase, _) = generate_seed_phrase()?;
659    let secret_key = get_secret_key_from_seed(
660        DEFAULT_HD_PATH.parse().expect("Valid HD path"),
661        &seed_phrase,
662        None,
663    )?;
664    Ok(secret_key)
665}
666
667/// Generates a secret key from a seed phrase using [default HD path](`DEFAULT_HD_PATH`)
668pub fn generate_secret_key_from_seed_phrase(seed_phrase: String) -> Result<SecretKey, SecretError> {
669    get_secret_key_from_seed(
670        DEFAULT_HD_PATH.parse().expect("Valid HD path"),
671        &seed_phrase,
672        None,
673    )
674}
675
676#[cfg(test)]
677mod nep_413_tests {
678    use base64::{prelude::BASE64_STANDARD, Engine};
679    use near_api_types::{crypto::KeyType, Signature};
680
681    use crate::SignerTrait;
682
683    use super::{NEP413Payload, Signer};
684
685    fn from_base64(base64: &str) -> Vec<u8> {
686        BASE64_STANDARD.decode(base64).unwrap()
687    }
688
689    // The mockup data is created using the sender/my-near-wallet NEP413 implementation
690    // The meteor wallet ignores the callback url on time of writing.
691    #[tokio::test]
692    pub async fn with_callback_url() {
693        let payload: NEP413Payload = NEP413Payload {
694            message: "Hello NEAR!".to_string(),
695            nonce: from_base64("KNV0cOpvJ50D5vfF9pqWom8wo2sliQ4W+Wa7uZ3Uk6Y=")
696                .try_into()
697                .unwrap(),
698            recipient: "example.near".to_string(),
699            // callback_url: None,
700            callback_url: Some("http://localhost:3000".to_string()),
701        };
702
703        let signer = Signer::from_seed_phrase(
704            "fatal edge jacket cash hard pass gallery fabric whisper size rain biology",
705            None,
706        )
707        .unwrap();
708        let signature = signer
709            .sign_message_nep413(
710                "round-toad.testnet".parse().unwrap(),
711                signer.get_public_key().unwrap(),
712                payload,
713            )
714            .await
715            .unwrap();
716
717        let expected_signature = from_base64(
718            "zzZQ/GwAjrZVrTIFlvmmQbDQHllfzrr8urVWHaRt5cPfcXaCSZo35c5LDpPpTKivR6BxLyb3lcPM0FfCW5lcBQ==",
719        );
720        assert_eq!(
721            signature,
722            Signature::from_parts(KeyType::ED25519, expected_signature.as_slice()).unwrap()
723        );
724    }
725
726    // The mockup data is created using the sender/meteor NEP413 implementation.
727    // My near wallet adds the callback url to the payload if it is not provided on time of writing.
728    #[tokio::test]
729    pub async fn without_callback_url() {
730        let payload: NEP413Payload = NEP413Payload {
731            message: "Hello NEAR!".to_string(),
732            nonce: from_base64("KNV0cOpvJ50D5vfF9pqWom8wo2sliQ4W+Wa7uZ3Uk6Y=")
733                .try_into()
734                .unwrap(),
735            recipient: "example.near".to_string(),
736            callback_url: None,
737        };
738
739        let signer = Signer::from_seed_phrase(
740            "fatal edge jacket cash hard pass gallery fabric whisper size rain biology",
741            None,
742        )
743        .unwrap();
744        let signature = signer
745            .sign_message_nep413(
746                "round-toad.testnet".parse().unwrap(),
747                signer.get_public_key().unwrap(),
748                payload,
749            )
750            .await
751            .unwrap();
752
753        let expected_signature = from_base64(
754            "NnJgPU1Ql7ccRTITIoOVsIfElmvH1RV7QAT4a9Vh6ShCOnjIzRwxqX54JzoQ/nK02p7VBMI2vJn48rpImIJwAw==",
755        );
756        assert_eq!(
757            signature,
758            Signature::from_parts(KeyType::ED25519, expected_signature.as_slice()).unwrap()
759        );
760    }
761}