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