Skip to main content

near_kit/client/
transaction.rs

1//! Transaction builder for fluent multi-action transactions.
2//!
3//! Allows chaining multiple actions (transfers, function calls, account creation, etc.)
4//! into a single atomic transaction. All actions either succeed together or fail together.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! # use near_kit::*;
10//! # async fn example() -> Result<(), near_kit::Error> {
11//! let near = Near::testnet()
12//!     .credentials("ed25519:...", "alice.testnet")?
13//!     .build();
14//!
15//! // Create a new sub-account with funding and a key
16//! let new_public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse()?;
17//! let wasm_code = std::fs::read("contract.wasm").expect("failed to read wasm");
18//! near.transaction("new.alice.testnet")
19//!     .create_account()
20//!     .transfer(NearToken::near(5))
21//!     .add_full_access_key(new_public_key)
22//!     .deploy(wasm_code)
23//!     .call("init")
24//!         .args(serde_json::json!({ "owner": "alice.testnet" }))
25//!     .send()
26//!     .await?;
27//! # Ok(())
28//! # }
29//! ```
30
31use std::collections::BTreeMap;
32use std::future::{Future, IntoFuture};
33use std::pin::Pin;
34use std::sync::{Arc, OnceLock};
35
36use crate::error::{Error, RpcError};
37use crate::types::{
38    AccountId, Action, BlockReference, CryptoHash, DelegateAction, DeterministicAccountStateInit,
39    DeterministicAccountStateInitV1, FinalExecutionOutcome, Finality, Gas,
40    GlobalContractIdentifier, IntoGas, IntoNearToken, NearToken, NonDelegateAction, PublicKey,
41    SignedDelegateAction, SignedTransaction, Transaction, TxExecutionStatus,
42};
43
44use super::nonce_manager::NonceManager;
45use super::rpc::RpcClient;
46use super::signer::Signer;
47
48/// Global nonce manager shared across all TransactionBuilder instances.
49/// This is an implementation detail - not exposed to users.
50fn nonce_manager() -> &'static NonceManager {
51    static NONCE_MANAGER: OnceLock<NonceManager> = OnceLock::new();
52    NONCE_MANAGER.get_or_init(NonceManager::new)
53}
54
55// ============================================================================
56// Delegate Action Types
57// ============================================================================
58
59/// Options for creating a delegate action (meta-transaction).
60#[derive(Clone, Debug, Default)]
61pub struct DelegateOptions {
62    /// Explicit block height at which the delegate action expires.
63    /// If omitted, uses the current block height plus `block_height_offset`.
64    pub max_block_height: Option<u64>,
65
66    /// Number of blocks after the current height when the delegate action should expire.
67    /// Defaults to 200 blocks if neither this nor `max_block_height` is provided.
68    pub block_height_offset: Option<u64>,
69
70    /// Override nonce to use for the delegate action. If omitted, fetches
71    /// from the access key and uses nonce + 1.
72    pub nonce: Option<u64>,
73}
74
75impl DelegateOptions {
76    /// Create options with a specific block height offset.
77    pub fn with_offset(offset: u64) -> Self {
78        Self {
79            block_height_offset: Some(offset),
80            ..Default::default()
81        }
82    }
83
84    /// Create options with a specific max block height.
85    pub fn with_max_height(height: u64) -> Self {
86        Self {
87            max_block_height: Some(height),
88            ..Default::default()
89        }
90    }
91}
92
93/// Result of creating a delegate action.
94///
95/// Contains the signed delegate action plus a pre-encoded payload for transport.
96#[derive(Clone, Debug)]
97pub struct DelegateResult {
98    /// The fully signed delegate action.
99    pub signed_delegate_action: SignedDelegateAction,
100    /// Base64-encoded payload for HTTP/JSON transport.
101    pub payload: String,
102}
103
104impl DelegateResult {
105    /// Get the raw bytes of the signed delegate action.
106    pub fn to_bytes(&self) -> Vec<u8> {
107        self.signed_delegate_action.to_bytes()
108    }
109
110    /// Get the sender account ID.
111    pub fn sender_id(&self) -> &AccountId {
112        self.signed_delegate_action.sender_id()
113    }
114
115    /// Get the receiver account ID.
116    pub fn receiver_id(&self) -> &AccountId {
117        self.signed_delegate_action.receiver_id()
118    }
119}
120
121// ============================================================================
122// TransactionBuilder
123// ============================================================================
124
125/// Builder for constructing multi-action transactions.
126///
127/// Created via [`crate::Near::transaction`]. Supports chaining multiple actions
128/// into a single atomic transaction.
129///
130/// # Example
131///
132/// ```rust,no_run
133/// # use near_kit::*;
134/// # async fn example() -> Result<(), near_kit::Error> {
135/// let near = Near::testnet()
136///     .credentials("ed25519:...", "alice.testnet")?
137///     .build();
138///
139/// // Single action
140/// near.transaction("bob.testnet")
141///     .transfer(NearToken::near(1))
142///     .send()
143///     .await?;
144///
145/// // Multiple actions (atomic)
146/// let key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse()?;
147/// near.transaction("new.alice.testnet")
148///     .create_account()
149///     .transfer(NearToken::near(5))
150///     .add_full_access_key(key)
151///     .send()
152///     .await?;
153/// # Ok(())
154/// # }
155/// ```
156pub struct TransactionBuilder {
157    rpc: Arc<RpcClient>,
158    signer: Option<Arc<dyn Signer>>,
159    receiver_id: AccountId,
160    actions: Vec<Action>,
161    signer_override: Option<Arc<dyn Signer>>,
162    wait_until: TxExecutionStatus,
163    max_nonce_retries: u32,
164}
165
166impl TransactionBuilder {
167    pub(crate) fn new(
168        rpc: Arc<RpcClient>,
169        signer: Option<Arc<dyn Signer>>,
170        receiver_id: AccountId,
171        max_nonce_retries: u32,
172    ) -> Self {
173        Self {
174            rpc,
175            signer,
176            receiver_id,
177            actions: Vec::new(),
178            signer_override: None,
179            wait_until: TxExecutionStatus::ExecutedOptimistic,
180            max_nonce_retries,
181        }
182    }
183
184    // ========================================================================
185    // Action methods
186    // ========================================================================
187
188    /// Add a create account action.
189    ///
190    /// Creates a new sub-account. Must be followed by `transfer` and `add_key`
191    /// to properly initialize the account.
192    pub fn create_account(mut self) -> Self {
193        self.actions.push(Action::create_account());
194        self
195    }
196
197    /// Add a transfer action.
198    ///
199    /// Transfers NEAR tokens to the receiver account.
200    ///
201    /// # Example
202    ///
203    /// ```rust,no_run
204    /// # use near_kit::*;
205    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
206    /// near.transaction("bob.testnet")
207    ///     .transfer(NearToken::near(1))
208    ///     .send()
209    ///     .await?;
210    /// # Ok(())
211    /// # }
212    /// ```
213    ///
214    /// # Panics
215    ///
216    /// Panics if the amount string cannot be parsed.
217    pub fn transfer(mut self, amount: impl IntoNearToken) -> Self {
218        let amount = amount
219            .into_near_token()
220            .expect("invalid transfer amount - use NearToken::from_str() for user input");
221        self.actions.push(Action::transfer(amount));
222        self
223    }
224
225    /// Add a deploy contract action.
226    ///
227    /// Deploys WASM code to the receiver account.
228    pub fn deploy(mut self, code: impl Into<Vec<u8>>) -> Self {
229        self.actions.push(Action::deploy_contract(code.into()));
230        self
231    }
232
233    /// Add a function call action.
234    ///
235    /// Returns a [`CallBuilder`] for configuring the call with args, gas, and deposit.
236    ///
237    /// # Example
238    ///
239    /// ```rust,no_run
240    /// # use near_kit::*;
241    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
242    /// near.transaction("contract.testnet")
243    ///     .call("set_greeting")
244    ///         .args(serde_json::json!({ "greeting": "Hello" }))
245    ///         .gas(Gas::tgas(10))
246    ///         .deposit(NearToken::ZERO)
247    ///     .call("another_method")
248    ///         .args(serde_json::json!({ "value": 42 }))
249    ///     .send()
250    ///     .await?;
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub fn call(self, method: &str) -> CallBuilder {
255        CallBuilder::new(self, method.to_string())
256    }
257
258    /// Add a full access key to the account.
259    pub fn add_full_access_key(mut self, public_key: PublicKey) -> Self {
260        self.actions.push(Action::add_full_access_key(public_key));
261        self
262    }
263
264    /// Add a function call access key to the account.
265    ///
266    /// # Arguments
267    ///
268    /// * `public_key` - The public key to add
269    /// * `receiver_id` - The contract this key can call
270    /// * `method_names` - Methods this key can call (empty = all methods)
271    /// * `allowance` - Maximum amount this key can spend (None = unlimited)
272    pub fn add_function_call_key(
273        mut self,
274        public_key: PublicKey,
275        receiver_id: impl AsRef<str>,
276        method_names: Vec<String>,
277        allowance: Option<NearToken>,
278    ) -> Self {
279        let receiver_id = AccountId::parse_lenient(receiver_id);
280        self.actions.push(Action::add_function_call_key(
281            public_key,
282            receiver_id,
283            method_names,
284            allowance,
285        ));
286        self
287    }
288
289    /// Delete an access key from the account.
290    pub fn delete_key(mut self, public_key: PublicKey) -> Self {
291        self.actions.push(Action::delete_key(public_key));
292        self
293    }
294
295    /// Delete the account and transfer remaining balance to beneficiary.
296    pub fn delete_account(mut self, beneficiary_id: impl AsRef<str>) -> Self {
297        let beneficiary_id = AccountId::parse_lenient(beneficiary_id);
298        self.actions.push(Action::delete_account(beneficiary_id));
299        self
300    }
301
302    /// Add a stake action.
303    ///
304    /// # Panics
305    ///
306    /// Panics if the amount string cannot be parsed.
307    pub fn stake(mut self, amount: impl IntoNearToken, public_key: PublicKey) -> Self {
308        let amount = amount
309            .into_near_token()
310            .expect("invalid stake amount - use NearToken::from_str() for user input");
311        self.actions.push(Action::stake(amount, public_key));
312        self
313    }
314
315    /// Add a signed delegate action to this transaction (for relayers).
316    ///
317    /// This is used by relayers to wrap a user's signed delegate action
318    /// and submit it to the blockchain, paying for the gas on behalf of the user.
319    ///
320    /// # Example
321    ///
322    /// ```rust,no_run
323    /// # use near_kit::*;
324    /// # async fn example(relayer: Near, payload: &str) -> Result<(), near_kit::Error> {
325    /// // Relayer receives base64 payload from user
326    /// let signed_delegate = SignedDelegateAction::from_base64(payload)?;
327    ///
328    /// // Relayer submits it, paying the gas
329    /// let result = relayer
330    ///     .transaction(signed_delegate.sender_id().as_str())
331    ///     .signed_delegate_action(signed_delegate)
332    ///     .send()
333    ///     .await?;
334    /// # Ok(())
335    /// # }
336    /// ```
337    pub fn signed_delegate_action(mut self, signed_delegate: SignedDelegateAction) -> Self {
338        // Set receiver_id to the sender of the delegate action (the original user)
339        self.receiver_id = signed_delegate.sender_id().clone();
340        self.actions.push(Action::delegate(signed_delegate));
341        self
342    }
343
344    // ========================================================================
345    // Meta-transactions (Delegate Actions)
346    // ========================================================================
347
348    /// Build and sign a delegate action for meta-transactions (NEP-366).
349    ///
350    /// This allows the user to sign a set of actions off-chain, which can then
351    /// be submitted by a relayer who pays the gas fees. The user's signature
352    /// authorizes the actions, but they don't need to hold NEAR for gas.
353    ///
354    /// # Example
355    ///
356    /// ```rust,no_run
357    /// # use near_kit::*;
358    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
359    /// // User builds and signs a delegate action
360    /// let result = near
361    ///     .transaction("contract.testnet")
362    ///     .call("add_message")
363    ///         .args(serde_json::json!({ "text": "Hello!" }))
364    ///         .gas(Gas::tgas(30))
365    ///     .delegate(Default::default())
366    ///     .await?;
367    ///
368    /// // Send payload to relayer via HTTP
369    /// println!("Payload to send: {}", result.payload);
370    /// # Ok(())
371    /// # }
372    /// ```
373    pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, Error> {
374        if self.actions.is_empty() {
375            return Err(Error::InvalidTransaction(
376                "Delegate action requires at least one action".to_string(),
377            ));
378        }
379
380        // Verify no nested delegates
381        for action in &self.actions {
382            if matches!(action, Action::Delegate(_)) {
383                return Err(Error::InvalidTransaction(
384                    "Delegate actions cannot contain nested signed delegate actions".to_string(),
385                ));
386            }
387        }
388
389        // Get the signer
390        let signer = self
391            .signer_override
392            .as_ref()
393            .or(self.signer.as_ref())
394            .ok_or(Error::NoSigner)?;
395
396        let sender_id = signer.account_id().clone();
397
398        // Get a signing key atomically
399        let key = signer.key();
400        let public_key = key.public_key().clone();
401
402        // Get nonce
403        let nonce = if let Some(n) = options.nonce {
404            n
405        } else {
406            let access_key = self
407                .rpc
408                .view_access_key(
409                    &sender_id,
410                    &public_key,
411                    BlockReference::Finality(Finality::Optimistic),
412                )
413                .await?;
414            access_key.nonce + 1
415        };
416
417        // Get max block height
418        let max_block_height = if let Some(h) = options.max_block_height {
419            h
420        } else {
421            let status = self.rpc.status().await?;
422            let offset = options.block_height_offset.unwrap_or(200);
423            status.sync_info.latest_block_height + offset
424        };
425
426        // Convert actions to NonDelegateAction
427        let delegate_actions: Vec<NonDelegateAction> = self
428            .actions
429            .into_iter()
430            .filter_map(NonDelegateAction::from_action)
431            .collect();
432
433        // Create delegate action
434        let delegate_action = DelegateAction {
435            sender_id,
436            receiver_id: self.receiver_id,
437            actions: delegate_actions,
438            nonce,
439            max_block_height,
440            public_key: public_key.clone(),
441        };
442
443        // Sign the delegate action
444        let hash = delegate_action.get_hash();
445        let signature = key.sign(hash.as_bytes()).await?;
446
447        // Create signed delegate action
448        let signed_delegate_action = delegate_action.sign(signature);
449        let payload = signed_delegate_action.to_base64();
450
451        Ok(DelegateResult {
452            signed_delegate_action,
453            payload,
454        })
455    }
456
457    // ========================================================================
458    // Global Contract Actions
459    // ========================================================================
460
461    /// Publish a contract to the global registry.
462    ///
463    /// Global contracts are deployed once and can be referenced by multiple accounts,
464    /// saving storage costs. Two modes are available:
465    ///
466    /// - `by_hash = false` (default): Contract is identified by the signer's account ID.
467    ///   The signer can update the contract later, and all users will automatically
468    ///   use the updated version.
469    ///
470    /// - `by_hash = true`: Contract is identified by its code hash. This creates
471    ///   an immutable contract that cannot be updated.
472    ///
473    /// # Example
474    ///
475    /// ```rust,no_run
476    /// # use near_kit::*;
477    /// # async fn example(near: Near) -> Result<(), Box<dyn std::error::Error>> {
478    /// let wasm_code = std::fs::read("contract.wasm")?;
479    ///
480    /// // Publish updatable contract (identified by your account)
481    /// near.transaction("alice.testnet")
482    ///     .publish_contract(wasm_code.clone(), false)
483    ///     .send()
484    ///     .await?;
485    ///
486    /// // Publish immutable contract (identified by its hash)
487    /// near.transaction("alice.testnet")
488    ///     .publish_contract(wasm_code, true)
489    ///     .send()
490    ///     .await?;
491    /// # Ok(())
492    /// # }
493    /// ```
494    pub fn publish_contract(mut self, code: impl Into<Vec<u8>>, by_hash: bool) -> Self {
495        self.actions
496            .push(Action::publish_contract(code.into(), by_hash));
497        self
498    }
499
500    /// Deploy a contract from the global registry by code hash.
501    ///
502    /// References a previously published immutable contract.
503    ///
504    /// # Example
505    ///
506    /// ```rust,no_run
507    /// # use near_kit::*;
508    /// # async fn example(near: Near, code_hash: CryptoHash) -> Result<(), near_kit::Error> {
509    /// near.transaction("alice.testnet")
510    ///     .deploy_from_hash(code_hash)
511    ///     .send()
512    ///     .await?;
513    /// # Ok(())
514    /// # }
515    /// ```
516    pub fn deploy_from_hash(mut self, code_hash: CryptoHash) -> Self {
517        self.actions.push(Action::deploy_from_hash(code_hash));
518        self
519    }
520
521    /// Deploy a contract from the global registry by publisher account.
522    ///
523    /// References a contract published by the given account.
524    /// The contract can be updated by the publisher.
525    ///
526    /// # Example
527    ///
528    /// ```rust,no_run
529    /// # use near_kit::*;
530    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
531    /// near.transaction("alice.testnet")
532    ///     .deploy_from_publisher("contract-publisher.near")
533    ///     .send()
534    ///     .await?;
535    /// # Ok(())
536    /// # }
537    /// ```
538    pub fn deploy_from_publisher(mut self, publisher_id: impl AsRef<str>) -> Self {
539        let publisher_id = AccountId::parse_lenient(publisher_id);
540        self.actions.push(Action::deploy_from_account(publisher_id));
541        self
542    }
543
544    /// Create a NEP-616 deterministic state init action with code hash reference.
545    ///
546    /// The receiver_id is automatically set to the deterministically derived account ID:
547    /// `"0s" + hex(keccak256(borsh(state_init))[12..32])`
548    ///
549    /// # Example
550    ///
551    /// ```rust,no_run
552    /// # use near_kit::*;
553    /// # async fn example(near: Near, code_hash: CryptoHash) -> Result<(), near_kit::Error> {
554    /// // Note: the receiver_id passed to transaction() is ignored for state_init -
555    /// // it will be replaced with the derived deterministic account ID
556    /// let outcome = near.transaction("alice.testnet")
557    ///     .state_init_by_hash(code_hash, Default::default(), NearToken::near(1))
558    ///     .send()
559    ///     .await?;
560    /// # Ok(())
561    /// # }
562    /// ```
563    ///
564    /// # Panics
565    ///
566    /// Panics if the deposit amount string cannot be parsed.
567    pub fn state_init_by_hash(
568        mut self,
569        code_hash: CryptoHash,
570        data: BTreeMap<Vec<u8>, Vec<u8>>,
571        deposit: impl IntoNearToken,
572    ) -> Self {
573        let deposit = deposit
574            .into_near_token()
575            .expect("invalid deposit amount - use NearToken::from_str() for user input");
576
577        // Build the state init to derive the account ID
578        let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
579            code: GlobalContractIdentifier::CodeHash(code_hash),
580            data: data.clone(),
581        });
582
583        // Set receiver_id to the derived deterministic account ID
584        self.receiver_id = state_init.derive_account_id();
585
586        self.actions
587            .push(Action::state_init_by_hash(code_hash, data, deposit));
588        self
589    }
590
591    /// Create a NEP-616 deterministic state init action with publisher account reference.
592    ///
593    /// The receiver_id is automatically set to the deterministically derived account ID.
594    ///
595    /// # Example
596    ///
597    /// ```rust,no_run
598    /// # use near_kit::*;
599    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
600    /// // Note: the receiver_id passed to transaction() is ignored for state_init -
601    /// // it will be replaced with the derived deterministic account ID
602    /// let outcome = near.transaction("alice.testnet")
603    ///     .state_init_by_publisher("contract-publisher.near", Default::default(), NearToken::near(1))
604    ///     .send()
605    ///     .await?;
606    /// # Ok(())
607    /// # }
608    /// ```
609    ///
610    /// # Panics
611    ///
612    /// Panics if the deposit amount string cannot be parsed.
613    pub fn state_init_by_publisher(
614        mut self,
615        publisher_id: impl AsRef<str>,
616        data: BTreeMap<Vec<u8>, Vec<u8>>,
617        deposit: impl IntoNearToken,
618    ) -> Self {
619        let publisher_id = AccountId::parse_lenient(publisher_id);
620        let deposit = deposit
621            .into_near_token()
622            .expect("invalid deposit amount - use NearToken::from_str() for user input");
623
624        // Build the state init to derive the account ID
625        let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
626            code: GlobalContractIdentifier::AccountId(publisher_id.clone()),
627            data: data.clone(),
628        });
629
630        // Set receiver_id to the derived deterministic account ID
631        self.receiver_id = state_init.derive_account_id();
632
633        self.actions
634            .push(Action::state_init_by_account(publisher_id, data, deposit));
635        self
636    }
637
638    // ========================================================================
639    // Configuration methods
640    // ========================================================================
641
642    /// Override the signer for this transaction.
643    pub fn sign_with(mut self, signer: impl Signer + 'static) -> Self {
644        self.signer_override = Some(Arc::new(signer));
645        self
646    }
647
648    /// Set the execution wait level.
649    pub fn wait_until(mut self, status: TxExecutionStatus) -> Self {
650        self.wait_until = status;
651        self
652    }
653
654    // ========================================================================
655    // Execution
656    // ========================================================================
657
658    /// Sign the transaction without sending it.
659    ///
660    /// Returns a `SignedTransaction` that can be inspected or sent later.
661    ///
662    /// # Example
663    ///
664    /// ```rust,no_run
665    /// # use near_kit::*;
666    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
667    /// let signed = near.transaction("bob.testnet")
668    ///     .transfer(NearToken::near(1))
669    ///     .sign()
670    ///     .await?;
671    ///
672    /// // Inspect the transaction
673    /// println!("Hash: {}", signed.transaction.get_hash());
674    /// println!("Actions: {:?}", signed.transaction.actions);
675    ///
676    /// // Send it later
677    /// let outcome = near.send(&signed).await?;
678    /// # Ok(())
679    /// # }
680    /// ```
681    pub async fn sign(self) -> Result<SignedTransaction, Error> {
682        if self.actions.is_empty() {
683            return Err(Error::InvalidTransaction(
684                "Transaction must have at least one action".to_string(),
685            ));
686        }
687
688        let signer = self
689            .signer_override
690            .or(self.signer)
691            .ok_or(Error::NoSigner)?;
692
693        let signer_id = signer.account_id().clone();
694
695        // Get a signing key atomically. For RotatingSigner, this claims the next
696        // key in rotation. The key contains both the public key and signing capability.
697        let key = signer.key();
698        let public_key = key.public_key().clone();
699        let public_key_str = public_key.to_string();
700
701        // Get nonce for the key
702        let rpc = self.rpc.clone();
703        let network = rpc.url().to_string();
704        let signer_id_clone = signer_id.clone();
705        let public_key_clone = public_key.clone();
706
707        let nonce = nonce_manager()
708            .get_next_nonce(&network, signer_id.as_ref(), &public_key_str, || async {
709                let access_key = rpc
710                    .view_access_key(
711                        &signer_id_clone,
712                        &public_key_clone,
713                        BlockReference::Finality(Finality::Optimistic),
714                    )
715                    .await?;
716                Ok(access_key.nonce)
717            })
718            .await?;
719
720        // Get recent block hash
721        let block = self
722            .rpc
723            .block(BlockReference::Finality(Finality::Final))
724            .await?;
725
726        // Build transaction
727        let tx = Transaction::new(
728            signer_id,
729            public_key,
730            nonce,
731            self.receiver_id,
732            block.header.hash,
733            self.actions,
734        );
735
736        // Sign with the key
737        let signature = key.sign(tx.get_hash().as_bytes()).await?;
738
739        Ok(SignedTransaction {
740            transaction: tx,
741            signature,
742        })
743    }
744
745    /// Sign the transaction offline without network access.
746    ///
747    /// This is useful for air-gapped signing workflows where you need to
748    /// provide the block hash and nonce manually (obtained from a separate
749    /// online machine).
750    ///
751    /// # Arguments
752    ///
753    /// * `block_hash` - A recent block hash (transaction expires ~24h after this block)
754    /// * `nonce` - The next nonce for the signing key (current nonce + 1)
755    ///
756    /// # Example
757    ///
758    /// ```rust,ignore
759    /// # use near_kit::*;
760    /// // On online machine: get block hash and nonce
761    /// // let block = near.rpc().block(BlockReference::latest()).await?;
762    /// // let access_key = near.rpc().view_access_key(...).await?;
763    ///
764    /// // On offline machine: sign with pre-fetched values
765    /// let block_hash: CryptoHash = "11111111111111111111111111111111".parse().unwrap();
766    /// let nonce = 12345u64;
767    ///
768    /// let signed = near.transaction("bob.testnet")
769    ///     .transfer(NearToken::near(1))
770    ///     .sign_offline(block_hash, nonce)
771    ///     .await?;
772    ///
773    /// // Transport signed_tx.to_base64() back to online machine
774    /// ```
775    pub async fn sign_offline(
776        self,
777        block_hash: CryptoHash,
778        nonce: u64,
779    ) -> Result<SignedTransaction, Error> {
780        if self.actions.is_empty() {
781            return Err(Error::InvalidTransaction(
782                "Transaction must have at least one action".to_string(),
783            ));
784        }
785
786        let signer = self
787            .signer_override
788            .or(self.signer)
789            .ok_or(Error::NoSigner)?;
790
791        let signer_id = signer.account_id().clone();
792
793        // Get a signing key atomically
794        let key = signer.key();
795        let public_key = key.public_key().clone();
796
797        // Build transaction with provided block_hash and nonce
798        let tx = Transaction::new(
799            signer_id,
800            public_key,
801            nonce,
802            self.receiver_id,
803            block_hash,
804            self.actions,
805        );
806
807        // Sign
808        let signature = key.sign(tx.get_hash().as_bytes()).await?;
809
810        Ok(SignedTransaction {
811            transaction: tx,
812            signature,
813        })
814    }
815
816    /// Send the transaction.
817    ///
818    /// This is equivalent to awaiting the builder directly.
819    pub fn send(self) -> TransactionSend {
820        TransactionSend { builder: self }
821    }
822
823    /// Internal method to add an action (used by CallBuilder).
824    fn push_action(&mut self, action: Action) {
825        self.actions.push(action);
826    }
827}
828
829// ============================================================================
830// CallBuilder
831// ============================================================================
832
833/// Builder for configuring a function call within a transaction.
834///
835/// Created via [`TransactionBuilder::call`]. Allows setting args, gas, and deposit
836/// before continuing to chain more actions or sending.
837pub struct CallBuilder {
838    builder: TransactionBuilder,
839    method: String,
840    args: Vec<u8>,
841    gas: Gas,
842    deposit: NearToken,
843}
844
845impl CallBuilder {
846    fn new(builder: TransactionBuilder, method: String) -> Self {
847        Self {
848            builder,
849            method,
850            args: Vec::new(),
851            gas: Gas::DEFAULT,
852            deposit: NearToken::ZERO,
853        }
854    }
855
856    /// Set JSON arguments.
857    pub fn args<A: serde::Serialize>(mut self, args: A) -> Self {
858        self.args = serde_json::to_vec(&args).unwrap_or_default();
859        self
860    }
861
862    /// Set raw byte arguments.
863    pub fn args_raw(mut self, args: Vec<u8>) -> Self {
864        self.args = args;
865        self
866    }
867
868    /// Set Borsh-encoded arguments.
869    pub fn args_borsh<A: borsh::BorshSerialize>(mut self, args: A) -> Self {
870        self.args = borsh::to_vec(&args).unwrap_or_default();
871        self
872    }
873
874    /// Set gas limit.
875    ///
876    /// # Example
877    ///
878    /// ```rust,no_run
879    /// # use near_kit::*;
880    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
881    /// near.transaction("contract.testnet")
882    ///     .call("method")
883    ///         .gas(Gas::tgas(50))
884    ///     .send()
885    ///     .await?;
886    /// # Ok(())
887    /// # }
888    /// ```
889    ///
890    /// # Panics
891    ///
892    /// Panics if the gas string cannot be parsed. Use [`Gas`]'s `FromStr` impl
893    /// for fallible parsing of user input.
894    pub fn gas(mut self, gas: impl IntoGas) -> Self {
895        self.gas = gas
896            .into_gas()
897            .expect("invalid gas format - use Gas::from_str() for user input");
898        self
899    }
900
901    /// Set attached deposit.
902    ///
903    /// # Example
904    ///
905    /// ```rust,no_run
906    /// # use near_kit::*;
907    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
908    /// near.transaction("contract.testnet")
909    ///     .call("method")
910    ///         .deposit(NearToken::near(1))
911    ///     .send()
912    ///     .await?;
913    /// # Ok(())
914    /// # }
915    /// ```
916    ///
917    /// # Panics
918    ///
919    /// Panics if the amount string cannot be parsed. Use [`NearToken`]'s `FromStr`
920    /// impl for fallible parsing of user input.
921    pub fn deposit(mut self, amount: impl IntoNearToken) -> Self {
922        self.deposit = amount
923            .into_near_token()
924            .expect("invalid deposit amount - use NearToken::from_str() for user input");
925        self
926    }
927
928    /// Finish this call and return to the transaction builder.
929    fn finish(self) -> TransactionBuilder {
930        let mut builder = self.builder;
931        builder.push_action(Action::function_call(
932            self.method,
933            self.args,
934            self.gas,
935            self.deposit,
936        ));
937        builder
938    }
939
940    // ========================================================================
941    // Chaining methods (delegate to TransactionBuilder after finishing)
942    // ========================================================================
943
944    /// Add another function call.
945    pub fn call(self, method: &str) -> CallBuilder {
946        self.finish().call(method)
947    }
948
949    /// Add a create account action.
950    pub fn create_account(self) -> TransactionBuilder {
951        self.finish().create_account()
952    }
953
954    /// Add a transfer action.
955    pub fn transfer(self, amount: impl IntoNearToken) -> TransactionBuilder {
956        self.finish().transfer(amount)
957    }
958
959    /// Add a deploy contract action.
960    pub fn deploy(self, code: impl Into<Vec<u8>>) -> TransactionBuilder {
961        self.finish().deploy(code)
962    }
963
964    /// Add a full access key.
965    pub fn add_full_access_key(self, public_key: PublicKey) -> TransactionBuilder {
966        self.finish().add_full_access_key(public_key)
967    }
968
969    /// Add a function call access key.
970    pub fn add_function_call_key(
971        self,
972        public_key: PublicKey,
973        receiver_id: impl AsRef<str>,
974        method_names: Vec<String>,
975        allowance: Option<NearToken>,
976    ) -> TransactionBuilder {
977        self.finish()
978            .add_function_call_key(public_key, receiver_id, method_names, allowance)
979    }
980
981    /// Delete an access key.
982    pub fn delete_key(self, public_key: PublicKey) -> TransactionBuilder {
983        self.finish().delete_key(public_key)
984    }
985
986    /// Delete the account.
987    pub fn delete_account(self, beneficiary_id: impl AsRef<str>) -> TransactionBuilder {
988        self.finish().delete_account(beneficiary_id)
989    }
990
991    /// Add a stake action.
992    pub fn stake(self, amount: impl IntoNearToken, public_key: PublicKey) -> TransactionBuilder {
993        self.finish().stake(amount, public_key)
994    }
995
996    /// Publish a contract to the global registry.
997    pub fn publish_contract(self, code: impl Into<Vec<u8>>, by_hash: bool) -> TransactionBuilder {
998        self.finish().publish_contract(code, by_hash)
999    }
1000
1001    /// Deploy a contract from the global registry by code hash.
1002    pub fn deploy_from_hash(self, code_hash: CryptoHash) -> TransactionBuilder {
1003        self.finish().deploy_from_hash(code_hash)
1004    }
1005
1006    /// Deploy a contract from the global registry by publisher account.
1007    pub fn deploy_from_publisher(self, publisher_id: impl AsRef<str>) -> TransactionBuilder {
1008        self.finish().deploy_from_publisher(publisher_id)
1009    }
1010
1011    /// Create a NEP-616 deterministic state init action with code hash reference.
1012    pub fn state_init_by_hash(
1013        self,
1014        code_hash: CryptoHash,
1015        data: BTreeMap<Vec<u8>, Vec<u8>>,
1016        deposit: impl IntoNearToken,
1017    ) -> TransactionBuilder {
1018        self.finish().state_init_by_hash(code_hash, data, deposit)
1019    }
1020
1021    /// Create a NEP-616 deterministic state init action with publisher account reference.
1022    pub fn state_init_by_publisher(
1023        self,
1024        publisher_id: impl AsRef<str>,
1025        data: BTreeMap<Vec<u8>, Vec<u8>>,
1026        deposit: impl IntoNearToken,
1027    ) -> TransactionBuilder {
1028        self.finish()
1029            .state_init_by_publisher(publisher_id, data, deposit)
1030    }
1031
1032    /// Override the signer.
1033    pub fn sign_with(self, signer: impl Signer + 'static) -> TransactionBuilder {
1034        self.finish().sign_with(signer)
1035    }
1036
1037    /// Set the execution wait level.
1038    pub fn wait_until(self, status: TxExecutionStatus) -> TransactionBuilder {
1039        self.finish().wait_until(status)
1040    }
1041
1042    /// Build and sign a delegate action for meta-transactions (NEP-366).
1043    ///
1044    /// This finishes the current function call and then creates a delegate action.
1045    pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, crate::Error> {
1046        self.finish().delegate(options).await
1047    }
1048
1049    /// Sign the transaction offline without network access.
1050    ///
1051    /// See [`TransactionBuilder::sign_offline`] for details.
1052    pub async fn sign_offline(
1053        self,
1054        block_hash: CryptoHash,
1055        nonce: u64,
1056    ) -> Result<SignedTransaction, Error> {
1057        self.finish().sign_offline(block_hash, nonce).await
1058    }
1059
1060    /// Sign the transaction without sending it.
1061    ///
1062    /// See [`TransactionBuilder::sign`] for details.
1063    pub async fn sign(self) -> Result<SignedTransaction, Error> {
1064        self.finish().sign().await
1065    }
1066
1067    /// Send the transaction.
1068    pub fn send(self) -> TransactionSend {
1069        self.finish().send()
1070    }
1071}
1072
1073impl IntoFuture for CallBuilder {
1074    type Output = Result<FinalExecutionOutcome, Error>;
1075    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1076
1077    fn into_future(self) -> Self::IntoFuture {
1078        self.send().into_future()
1079    }
1080}
1081
1082// ============================================================================
1083// TransactionSend
1084// ============================================================================
1085
1086/// Future for sending a transaction.
1087pub struct TransactionSend {
1088    builder: TransactionBuilder,
1089}
1090
1091impl TransactionSend {
1092    /// Set the execution wait level.
1093    pub fn wait_until(mut self, status: TxExecutionStatus) -> Self {
1094        self.builder.wait_until = status;
1095        self
1096    }
1097}
1098
1099impl IntoFuture for TransactionSend {
1100    type Output = Result<FinalExecutionOutcome, Error>;
1101    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1102
1103    fn into_future(self) -> Self::IntoFuture {
1104        Box::pin(async move {
1105            let builder = self.builder;
1106
1107            if builder.actions.is_empty() {
1108                return Err(Error::InvalidTransaction(
1109                    "Transaction must have at least one action".to_string(),
1110                ));
1111            }
1112
1113            let signer = builder
1114                .signer_override
1115                .as_ref()
1116                .or(builder.signer.as_ref())
1117                .ok_or(Error::NoSigner)?;
1118
1119            let signer_id = signer.account_id().clone();
1120
1121            // Retry loop for InvalidNonceError
1122            let max_nonce_retries = builder.max_nonce_retries;
1123            let network = builder.rpc.url().to_string();
1124            let mut last_error: Option<Error> = None;
1125            let mut last_ak_nonce: Option<u64> = None;
1126
1127            for attempt in 0..max_nonce_retries {
1128                // Get a signing key atomically for this attempt
1129                let key = signer.key();
1130                let public_key = key.public_key().clone();
1131                let public_key_str = public_key.to_string();
1132
1133                // Get nonce from manager (fetches from blockchain on first call, then increments locally)
1134                let rpc = builder.rpc.clone();
1135                let signer_id_clone = signer_id.clone();
1136                let public_key_clone = public_key.clone();
1137
1138                let nonce = if let Some(ak_nonce) = last_ak_nonce.take() {
1139                    // Use the ak_nonce from the error directly - avoids refetching
1140                    nonce_manager().update_and_get_next(
1141                        &network,
1142                        signer_id.as_ref(),
1143                        &public_key_str,
1144                        ak_nonce,
1145                    )
1146                } else {
1147                    nonce_manager()
1148                        .get_next_nonce(&network, signer_id.as_ref(), &public_key_str, || async {
1149                            let access_key = rpc
1150                                .view_access_key(
1151                                    &signer_id_clone,
1152                                    &public_key_clone,
1153                                    BlockReference::Finality(Finality::Optimistic),
1154                                )
1155                                .await?;
1156                            Ok(access_key.nonce)
1157                        })
1158                        .await?
1159                };
1160
1161                // Get recent block hash (use finalized for stability)
1162                let block = builder
1163                    .rpc
1164                    .block(BlockReference::Finality(Finality::Final))
1165                    .await?;
1166
1167                // Build transaction
1168                let tx = Transaction::new(
1169                    signer_id.clone(),
1170                    public_key.clone(),
1171                    nonce,
1172                    builder.receiver_id.clone(),
1173                    block.header.hash,
1174                    builder.actions.clone(),
1175                );
1176
1177                // Sign with the key
1178                let signature = match key.sign(tx.get_hash().as_bytes()).await {
1179                    Ok(sig) => sig,
1180                    Err(e) => return Err(Error::Signing(e)),
1181                };
1182                let signed_tx = crate::types::SignedTransaction {
1183                    transaction: tx,
1184                    signature,
1185                };
1186
1187                // Send
1188                match builder.rpc.send_tx(&signed_tx, builder.wait_until).await {
1189                    Ok(response) => {
1190                        let outcome = response.outcome.ok_or_else(|| {
1191                            Error::InvalidTransaction(format!(
1192                                "Transaction {} submitted with wait_until={:?} but no execution \
1193                                 outcome was returned. Use rpc().send_tx() for fire-and-forget \
1194                                 submission.",
1195                                response.transaction_hash, builder.wait_until,
1196                            ))
1197                        })?;
1198                        if outcome.is_failure() {
1199                            return Err(Error::TransactionFailed(
1200                                outcome.failure_message().unwrap_or_default(),
1201                            ));
1202                        }
1203                        return Ok(outcome);
1204                    }
1205                    Err(RpcError::InvalidNonce { tx_nonce, ak_nonce })
1206                        if attempt < max_nonce_retries - 1 =>
1207                    {
1208                        // Store ak_nonce for next iteration to avoid refetching
1209                        last_ak_nonce = Some(ak_nonce);
1210                        last_error =
1211                            Some(Error::Rpc(RpcError::InvalidNonce { tx_nonce, ak_nonce }));
1212                        continue;
1213                    }
1214                    Err(e) => return Err(Error::Rpc(e)),
1215                }
1216            }
1217
1218            Err(last_error.unwrap_or_else(|| {
1219                Error::InvalidTransaction("Unknown error during transaction send".to_string())
1220            }))
1221        })
1222    }
1223}
1224
1225impl IntoFuture for TransactionBuilder {
1226    type Output = Result<FinalExecutionOutcome, Error>;
1227    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1228
1229    fn into_future(self) -> Self::IntoFuture {
1230        self.send().into_future()
1231    }
1232}