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::from_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::fmt;
32use std::future::{Future, IntoFuture};
33use std::pin::Pin;
34use std::sync::{Arc, OnceLock};
35
36use tracing::Instrument;
37
38use crate::error::{Error, RpcError};
39use crate::types::{
40    AccountId, Action, BlockReference, CryptoHash, DelegateAction, DeterministicAccountStateInit,
41    FinalExecutionOutcome, Finality, Gas, GlobalContractIdentifier, GlobalContractRef, IntoGas,
42    IntoNearToken, NearToken, NonDelegateAction, PublicKey, PublishMode, SignedDelegateAction,
43    SignedTransaction, Transaction, TryIntoAccountId, WaitLevel,
44};
45
46use super::nonce_manager::NonceManager;
47use super::rpc::RpcClient;
48use super::signer::Signer;
49
50/// Global nonce manager shared across all TransactionBuilder instances.
51/// This is an implementation detail - not exposed to users.
52fn nonce_manager() -> &'static NonceManager {
53    static NONCE_MANAGER: OnceLock<NonceManager> = OnceLock::new();
54    NONCE_MANAGER.get_or_init(NonceManager::new)
55}
56
57/// Produce a comma-separated summary of action types for tracing spans.
58///
59/// Function calls include the method name, e.g. `"create_account,transfer,function_call(init)"`.
60fn actions_summary(actions: &[Action]) -> String {
61    use std::fmt::Write;
62    let mut out = String::new();
63    for (i, a) in actions.iter().enumerate() {
64        if i > 0 {
65            out.push(',');
66        }
67        match a {
68            Action::CreateAccount(_) => out.push_str("create_account"),
69            Action::DeployContract(_) => out.push_str("deploy_contract"),
70            Action::FunctionCall(fc) => write!(out, "function_call({})", fc.method_name).unwrap(),
71            Action::Transfer(_) => out.push_str("transfer"),
72            Action::Stake(_) => out.push_str("stake"),
73            Action::AddKey(_) => out.push_str("add_key"),
74            Action::DeleteKey(_) => out.push_str("delete_key"),
75            Action::DeleteAccount(_) => out.push_str("delete_account"),
76            Action::Delegate(_) => out.push_str("delegate"),
77            Action::DeployGlobalContract(_) => out.push_str("deploy_global_contract"),
78            Action::UseGlobalContract(_) => out.push_str("use_global_contract"),
79            Action::DeterministicStateInit(_) => out.push_str("deterministic_state_init"),
80            Action::TransferToGasKey(_) => out.push_str("transfer_to_gas_key"),
81            Action::WithdrawFromGasKey(_) => out.push_str("withdraw_from_gas_key"),
82        }
83    }
84    out
85}
86
87/// Record function-call span fields from the action list.
88///
89/// When the actions contain exactly one function call, records `method`, `gas`,
90/// and `deposit` on the current span. Multiple function calls emit a debug event
91/// per call instead, since span fields can't repeat.
92fn record_function_call_fields(actions: &[Action]) {
93    let function_calls: Vec<_> = actions
94        .iter()
95        .filter_map(|a| match a {
96            Action::FunctionCall(fc) => Some(fc),
97            _ => None,
98        })
99        .collect();
100
101    match function_calls.as_slice() {
102        [fc] => {
103            let span = tracing::Span::current();
104            span.record("method", fc.method_name.as_str());
105            span.record("gas", tracing::field::display(&fc.gas));
106            span.record("deposit", tracing::field::display(&fc.deposit));
107        }
108        multiple if !multiple.is_empty() => {
109            for fc in multiple {
110                tracing::debug!(
111                    method = %fc.method_name,
112                    gas = %fc.gas,
113                    deposit = %fc.deposit,
114                    "function_call action"
115                );
116            }
117        }
118        _ => {}
119    }
120}
121
122// ============================================================================
123// Delegate Action Types
124// ============================================================================
125
126/// Options for creating a delegate action (meta-transaction).
127#[derive(Clone, Debug, Default)]
128pub struct DelegateOptions {
129    /// Explicit block height at which the delegate action expires.
130    /// If omitted, uses the current block height plus `block_height_offset`.
131    pub max_block_height: Option<u64>,
132
133    /// Number of blocks after the current height when the delegate action should expire.
134    /// Defaults to 200 blocks if neither this nor `max_block_height` is provided.
135    pub block_height_offset: Option<u64>,
136
137    /// Override nonce to use for the delegate action. If omitted, fetches
138    /// from the access key and uses nonce + 1.
139    pub nonce: Option<u64>,
140}
141
142impl DelegateOptions {
143    /// Create options with a specific block height offset.
144    pub fn with_offset(offset: u64) -> Self {
145        Self {
146            block_height_offset: Some(offset),
147            ..Default::default()
148        }
149    }
150
151    /// Create options with a specific max block height.
152    pub fn with_max_height(height: u64) -> Self {
153        Self {
154            max_block_height: Some(height),
155            ..Default::default()
156        }
157    }
158}
159
160/// Result of creating a delegate action.
161///
162/// Contains the signed delegate action plus a pre-encoded payload for transport.
163#[derive(Clone, Debug)]
164pub struct DelegateResult {
165    /// The fully signed delegate action.
166    pub signed_delegate_action: SignedDelegateAction,
167    /// Base64-encoded payload for HTTP/JSON transport.
168    pub payload: String,
169}
170
171impl DelegateResult {
172    /// Get the raw bytes of the signed delegate action.
173    pub fn to_bytes(&self) -> Vec<u8> {
174        self.signed_delegate_action.to_bytes()
175    }
176
177    /// Get the sender account ID.
178    pub fn sender_id(&self) -> &AccountId {
179        self.signed_delegate_action.sender_id()
180    }
181
182    /// Get the receiver account ID.
183    pub fn receiver_id(&self) -> &AccountId {
184        self.signed_delegate_action.receiver_id()
185    }
186}
187
188// ============================================================================
189// TransactionBuilder
190// ============================================================================
191
192/// Builder for constructing multi-action transactions.
193///
194/// Created via [`crate::Near::transaction`]. Supports chaining multiple actions
195/// into a single atomic transaction.
196///
197/// # Example
198///
199/// ```rust,no_run
200/// # use near_kit::*;
201/// # async fn example() -> Result<(), near_kit::Error> {
202/// let near = Near::testnet()
203///     .credentials("ed25519:...", "alice.testnet")?
204///     .build();
205///
206/// // Single action
207/// near.transaction("bob.testnet")
208///     .transfer(NearToken::from_near(1))
209///     .send()
210///     .await?;
211///
212/// // Multiple actions (atomic)
213/// let key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse()?;
214/// near.transaction("new.alice.testnet")
215///     .create_account()
216///     .transfer(NearToken::from_near(5))
217///     .add_full_access_key(key)
218///     .send()
219///     .await?;
220/// # Ok(())
221/// # }
222/// ```
223pub struct TransactionBuilder {
224    rpc: Arc<RpcClient>,
225    signer: Option<Arc<dyn Signer>>,
226    receiver_id: AccountId,
227    actions: Vec<Action>,
228    signer_override: Option<Arc<dyn Signer>>,
229    max_nonce_retries: u32,
230}
231
232impl fmt::Debug for TransactionBuilder {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("TransactionBuilder")
235            .field(
236                "signer_id",
237                &self
238                    .signer_override
239                    .as_ref()
240                    .or(self.signer.as_ref())
241                    .map(|s| s.account_id()),
242            )
243            .field("receiver_id", &self.receiver_id)
244            .field("action_count", &self.actions.len())
245            .field("max_nonce_retries", &self.max_nonce_retries)
246            .finish()
247    }
248}
249
250impl TransactionBuilder {
251    pub(crate) fn new(
252        rpc: Arc<RpcClient>,
253        signer: Option<Arc<dyn Signer>>,
254        receiver_id: AccountId,
255        max_nonce_retries: u32,
256    ) -> Self {
257        Self {
258            rpc,
259            signer,
260            receiver_id,
261            actions: Vec::new(),
262            signer_override: None,
263            max_nonce_retries,
264        }
265    }
266
267    // ========================================================================
268    // Action methods
269    // ========================================================================
270
271    /// Add a create account action.
272    ///
273    /// Creates a new sub-account. Must be followed by `transfer` and `add_key`
274    /// to properly initialize the account.
275    pub fn create_account(mut self) -> Self {
276        self.actions.push(Action::create_account());
277        self
278    }
279
280    /// Add a transfer action.
281    ///
282    /// Transfers NEAR tokens to the receiver account.
283    ///
284    /// # Example
285    ///
286    /// ```rust,no_run
287    /// # use near_kit::*;
288    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
289    /// near.transaction("bob.testnet")
290    ///     .transfer(NearToken::from_near(1))
291    ///     .send()
292    ///     .await?;
293    /// # Ok(())
294    /// # }
295    /// ```
296    ///
297    /// # Panics
298    ///
299    /// Panics if the amount string cannot be parsed.
300    pub fn transfer(mut self, amount: impl IntoNearToken) -> Self {
301        let amount = amount
302            .into_near_token()
303            .expect("invalid transfer amount - use NearToken::from_str() for user input");
304        self.actions.push(Action::transfer(amount));
305        self
306    }
307
308    /// Add a deploy contract action.
309    ///
310    /// Deploys WASM code to the receiver account.
311    pub fn deploy(mut self, code: impl Into<Vec<u8>>) -> Self {
312        self.actions.push(Action::deploy_contract(code.into()));
313        self
314    }
315
316    /// Add a function call action.
317    ///
318    /// Returns a [`CallBuilder`] for configuring the call with args, gas, and deposit.
319    ///
320    /// # Example
321    ///
322    /// ```rust,no_run
323    /// # use near_kit::*;
324    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
325    /// near.transaction("contract.testnet")
326    ///     .call("set_greeting")
327    ///         .args(serde_json::json!({ "greeting": "Hello" }))
328    ///         .gas(Gas::from_tgas(10))
329    ///         .deposit(NearToken::ZERO)
330    ///     .call("another_method")
331    ///         .args(serde_json::json!({ "value": 42 }))
332    ///     .send()
333    ///     .await?;
334    /// # Ok(())
335    /// # }
336    /// ```
337    pub fn call(self, method: &str) -> CallBuilder {
338        CallBuilder::new(self, method.to_string())
339    }
340
341    /// Add a full access key to the account.
342    pub fn add_full_access_key(mut self, public_key: PublicKey) -> Self {
343        self.actions.push(Action::add_full_access_key(public_key));
344        self
345    }
346
347    /// Add a function call access key to the account.
348    ///
349    /// # Arguments
350    ///
351    /// * `public_key` - The public key to add
352    /// * `receiver_id` - The contract this key can call
353    /// * `method_names` - Methods this key can call (empty = all methods)
354    /// * `allowance` - Maximum amount this key can spend (None = unlimited)
355    pub fn add_function_call_key(
356        mut self,
357        public_key: PublicKey,
358        receiver_id: impl TryIntoAccountId,
359        method_names: Vec<String>,
360        allowance: Option<NearToken>,
361    ) -> Self {
362        let receiver_id = receiver_id
363            .try_into_account_id()
364            .expect("invalid account ID");
365        self.actions.push(Action::add_function_call_key(
366            public_key,
367            receiver_id,
368            method_names,
369            allowance,
370        ));
371        self
372    }
373
374    /// Delete an access key from the account.
375    pub fn delete_key(mut self, public_key: PublicKey) -> Self {
376        self.actions.push(Action::delete_key(public_key));
377        self
378    }
379
380    /// Delete the account and transfer remaining balance to beneficiary.
381    pub fn delete_account(mut self, beneficiary_id: impl TryIntoAccountId) -> Self {
382        let beneficiary_id = beneficiary_id
383            .try_into_account_id()
384            .expect("invalid account ID");
385        self.actions.push(Action::delete_account(beneficiary_id));
386        self
387    }
388
389    /// Add a stake action.
390    ///
391    /// # Panics
392    ///
393    /// Panics if the amount string cannot be parsed.
394    pub fn stake(mut self, amount: impl IntoNearToken, public_key: PublicKey) -> Self {
395        let amount = amount
396            .into_near_token()
397            .expect("invalid stake amount - use NearToken::from_str() for user input");
398        self.actions.push(Action::stake(amount, public_key));
399        self
400    }
401
402    /// Add a signed delegate action to this transaction (for relayers).
403    ///
404    /// This is used by relayers to wrap a user's signed delegate action
405    /// and submit it to the blockchain, paying for the gas on behalf of the user.
406    ///
407    /// # Example
408    ///
409    /// ```rust,no_run
410    /// # use near_kit::*;
411    /// # async fn example(relayer: Near, payload: &str) -> Result<(), near_kit::Error> {
412    /// // Relayer receives base64 payload from user
413    /// let signed_delegate = SignedDelegateAction::from_base64(payload)?;
414    ///
415    /// // Relayer submits it, paying the gas
416    /// let result = relayer
417    ///     .transaction(signed_delegate.sender_id())
418    ///     .signed_delegate_action(signed_delegate)
419    ///     .send()
420    ///     .await?;
421    /// # Ok(())
422    /// # }
423    /// ```
424    pub fn signed_delegate_action(mut self, signed_delegate: SignedDelegateAction) -> Self {
425        // Set receiver_id to the sender of the delegate action (the original user)
426        self.receiver_id = signed_delegate.sender_id().clone();
427        self.actions.push(Action::delegate(signed_delegate));
428        self
429    }
430
431    // ========================================================================
432    // Meta-transactions (Delegate Actions)
433    // ========================================================================
434
435    /// Build and sign a delegate action for meta-transactions (NEP-366).
436    ///
437    /// This allows the user to sign a set of actions off-chain, which can then
438    /// be submitted by a relayer who pays the gas fees. The user's signature
439    /// authorizes the actions, but they don't need to hold NEAR for gas.
440    ///
441    /// # Example
442    ///
443    /// ```rust,no_run
444    /// # use near_kit::*;
445    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
446    /// // User builds and signs a delegate action
447    /// let result = near
448    ///     .transaction("contract.testnet")
449    ///     .call("add_message")
450    ///         .args(serde_json::json!({ "text": "Hello!" }))
451    ///         .gas(Gas::from_tgas(30))
452    ///     .delegate(Default::default())
453    ///     .await?;
454    ///
455    /// // Send payload to relayer via HTTP
456    /// println!("Payload to send: {}", result.payload);
457    /// # Ok(())
458    /// # }
459    /// ```
460    pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, Error> {
461        if self.actions.is_empty() {
462            return Err(Error::InvalidTransaction(
463                "Delegate action requires at least one action".to_string(),
464            ));
465        }
466
467        // Verify no nested delegates
468        for action in &self.actions {
469            if matches!(action, Action::Delegate(_)) {
470                return Err(Error::InvalidTransaction(
471                    "Delegate actions cannot contain nested signed delegate actions".to_string(),
472                ));
473            }
474        }
475
476        // Get the signer
477        let signer = self
478            .signer_override
479            .as_ref()
480            .or(self.signer.as_ref())
481            .ok_or(Error::NoSigner)?;
482
483        let sender_id = signer.account_id().clone();
484
485        // Get a signing key atomically
486        let key = signer.key();
487        let public_key = key.public_key().clone();
488
489        // Get nonce
490        let nonce = if let Some(n) = options.nonce {
491            n
492        } else {
493            let access_key = self
494                .rpc
495                .view_access_key(
496                    &sender_id,
497                    &public_key,
498                    BlockReference::Finality(Finality::Optimistic),
499                )
500                .await?;
501            access_key.nonce + 1
502        };
503
504        // Get max block height
505        let max_block_height = if let Some(h) = options.max_block_height {
506            h
507        } else {
508            let status = self.rpc.status().await?;
509            let offset = options.block_height_offset.unwrap_or(200);
510            status.sync_info.latest_block_height + offset
511        };
512
513        // Convert actions to NonDelegateAction
514        let delegate_actions: Vec<NonDelegateAction> = self
515            .actions
516            .into_iter()
517            .filter_map(NonDelegateAction::from_action)
518            .collect();
519
520        // Create delegate action
521        let delegate_action = DelegateAction {
522            sender_id,
523            receiver_id: self.receiver_id,
524            actions: delegate_actions,
525            nonce,
526            max_block_height,
527            public_key: public_key.clone(),
528        };
529
530        // Sign the delegate action
531        let hash = delegate_action.get_hash();
532        let signature = key.sign(hash.as_bytes()).await?;
533
534        // Create signed delegate action
535        let signed_delegate_action = delegate_action.sign(signature);
536        let payload = signed_delegate_action.to_base64();
537
538        Ok(DelegateResult {
539            signed_delegate_action,
540            payload,
541        })
542    }
543
544    // ========================================================================
545    // Global Contract Actions
546    // ========================================================================
547
548    /// Publish a contract to the global registry.
549    ///
550    /// Global contracts are deployed once and can be referenced by multiple accounts,
551    /// saving storage costs. Two modes are available via [`PublishMode`]:
552    ///
553    /// - [`PublishMode::Updatable`]: the contract is identified by the publisher's
554    ///   account and can be updated by publishing new code from the same account.
555    /// - [`PublishMode::Immutable`]: the contract is identified by its code hash and
556    ///   cannot be updated once published.
557    ///
558    /// # Example
559    ///
560    /// ```rust,no_run
561    /// # use near_kit::*;
562    /// # async fn example(near: Near) -> Result<(), Box<dyn std::error::Error>> {
563    /// let wasm_code = std::fs::read("contract.wasm")?;
564    ///
565    /// // Publish updatable contract (identified by your account)
566    /// near.transaction("alice.testnet")
567    ///     .publish(wasm_code.clone(), PublishMode::Updatable)
568    ///     .send()
569    ///     .await?;
570    ///
571    /// // Publish immutable contract (identified by its hash)
572    /// near.transaction("alice.testnet")
573    ///     .publish(wasm_code, PublishMode::Immutable)
574    ///     .send()
575    ///     .await?;
576    /// # Ok(())
577    /// # }
578    /// ```
579    pub fn publish(mut self, code: impl Into<Vec<u8>>, mode: PublishMode) -> Self {
580        self.actions.push(Action::publish(code.into(), mode));
581        self
582    }
583
584    /// Deploy a contract from the global registry.
585    ///
586    /// Accepts any [`GlobalContractRef`] (such as a [`CryptoHash`] or an account ID
587    /// string/[`AccountId`]) to reference a previously published contract.
588    ///
589    /// # Example
590    ///
591    /// ```rust,no_run
592    /// # use near_kit::*;
593    /// # async fn example(near: Near, code_hash: CryptoHash) -> Result<(), near_kit::Error> {
594    /// near.transaction("alice.testnet")
595    ///     .deploy_from(code_hash)
596    ///     .send()
597    ///     .await?;
598    /// # Ok(())
599    /// # }
600    /// ```
601    pub fn deploy_from(mut self, contract_ref: impl GlobalContractRef) -> Self {
602        let identifier = contract_ref.into_identifier();
603        self.actions.push(match identifier {
604            GlobalContractIdentifier::CodeHash(hash) => Action::deploy_from_hash(hash),
605            GlobalContractIdentifier::AccountId(id) => Action::deploy_from_account(id),
606        });
607        self
608    }
609
610    /// Create a NEP-616 deterministic state init action.
611    ///
612    /// The receiver_id is automatically set to the deterministically derived account ID:
613    /// `"0s" + hex(keccak256(borsh(state_init))[12..32])`
614    ///
615    /// # Example
616    ///
617    /// ```rust,no_run
618    /// # use near_kit::*;
619    /// # async fn example(near: Near, code_hash: CryptoHash) -> Result<(), near_kit::Error> {
620    /// let si = DeterministicAccountStateInit::by_hash(code_hash, Default::default());
621    /// let outcome = near.transaction("alice.testnet")
622    ///     .state_init(si, NearToken::from_near(1))
623    ///     .send()
624    ///     .await?;
625    /// # Ok(())
626    /// # }
627    /// ```
628    ///
629    /// # Panics
630    ///
631    /// Panics if the deposit amount string cannot be parsed.
632    pub fn state_init(
633        mut self,
634        state_init: DeterministicAccountStateInit,
635        deposit: impl IntoNearToken,
636    ) -> Self {
637        let deposit = deposit
638            .into_near_token()
639            .expect("invalid deposit amount - use NearToken::from_str() for user input");
640
641        self.receiver_id = state_init.derive_account_id();
642        self.actions.push(Action::state_init(state_init, deposit));
643        self
644    }
645
646    /// Add a pre-built action to the transaction.
647    ///
648    /// This is the most flexible way to add actions, since it accepts any
649    /// [`Action`] variant directly. It's especially useful when you want to
650    /// build function call actions independently and attach them later, or
651    /// when working with action types that don't have dedicated builder
652    /// methods.
653    ///
654    /// # Example
655    ///
656    /// ```rust,no_run
657    /// # use near_kit::*;
658    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
659    /// let action = Action::function_call(
660    ///     "transfer",
661    ///     serde_json::to_vec(&serde_json::json!({ "receiver": "bob.testnet" }))?,
662    ///     Gas::from_tgas(30),
663    ///     NearToken::ZERO,
664    /// );
665    ///
666    /// near.transaction("contract.testnet")
667    ///     .add_action(action)
668    ///     .send()
669    ///     .await?;
670    /// # Ok(())
671    /// # }
672    /// ```
673    pub fn add_action(mut self, action: impl Into<Action>) -> Self {
674        self.actions.push(action.into());
675        self
676    }
677
678    // ========================================================================
679    // Configuration methods
680    // ========================================================================
681
682    /// Override the signer for this transaction.
683    pub fn sign_with(mut self, signer: impl Signer + 'static) -> Self {
684        self.signer_override = Some(Arc::new(signer));
685        self
686    }
687
688    /// Set the execution wait level and prepare to send.
689    ///
690    /// This is a shorthand for `.send().wait_until(level)`.
691    /// The return type changes based on the wait level — see [`TransactionSend::wait_until`].
692    pub fn wait_until<W: crate::types::WaitLevel>(self, level: W) -> TransactionSend<W> {
693        self.send().wait_until(level)
694    }
695
696    /// Override the number of nonce retries for this transaction on `InvalidNonce`
697    /// errors. `0` means no retries (send once), `1` means one retry, etc.
698    pub fn max_nonce_retries(mut self, retries: u32) -> Self {
699        self.max_nonce_retries = retries;
700        self
701    }
702
703    // ========================================================================
704    // Execution
705    // ========================================================================
706
707    /// Build the unsigned transaction without signing.
708    ///
709    /// Resolves the nonce and block hash from the network, then returns the
710    /// unsigned [`Transaction`]. Use this for external signing workflows
711    /// (hardware wallets, MPC, HSM) where you need the transaction hash
712    /// before signing.
713    ///
714    /// The returned [`Transaction`] provides [`get_hash()`](Transaction::get_hash)
715    /// for the bytes to sign, and [`complete()`](Transaction::complete) to attach
716    /// an externally-produced signature.
717    ///
718    /// # Example
719    ///
720    /// ```rust,no_run
721    /// # use near_kit::*;
722    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
723    /// let unsigned = near.transaction("bob.testnet")
724    ///     .transfer(NearToken::from_near(1))
725    ///     .build()
726    ///     .await?;
727    ///
728    /// // Get the hash that needs signing
729    /// let hash = unsigned.get_hash();
730    ///
731    /// // Sign externally (Ledger, MPC, HSM, etc.)
732    /// // let sig_bytes = device.sign(hash.as_bytes())?;
733    /// // let signature = Signature::from_parts(KeyType::ED25519, &sig_bytes)?;
734    ///
735    /// // Complete and submit
736    /// // let signed = unsigned.complete(signature);
737    /// // near.send(&signed).await?;
738    /// # Ok(())
739    /// # }
740    /// ```
741    pub async fn build(self) -> Result<Transaction, Error> {
742        if self.actions.is_empty() {
743            return Err(Error::InvalidTransaction(
744                "Transaction must have at least one action".to_string(),
745            ));
746        }
747
748        let signer = self
749            .signer_override
750            .or(self.signer)
751            .ok_or(Error::NoSigner)?;
752
753        let signer_id = signer.account_id().clone();
754        let action_count = self.actions.len();
755
756        let span = tracing::info_span!(
757            "build_transaction",
758            sender = %signer_id,
759            receiver = %self.receiver_id,
760            action_count,
761            actions = %actions_summary(&self.actions),
762            method = tracing::field::Empty,
763            gas = tracing::field::Empty,
764            deposit = tracing::field::Empty,
765        );
766
767        let actions = self.actions;
768        async move {
769            record_function_call_fields(&actions);
770
771            // Use public_key() directly to avoid side effects from key() —
772            // e.g. RotatingSigner advances its rotation counter on key().
773            let public_key = signer.public_key().clone();
774
775            let access_key = self
776                .rpc
777                .view_access_key(
778                    &signer_id,
779                    &public_key,
780                    BlockReference::Finality(Finality::Final),
781                )
782                .await?;
783            let block_hash = access_key.block_hash;
784
785            let network = self.rpc.url().to_string();
786            let nonce = nonce_manager().next(
787                network,
788                signer_id.clone(),
789                public_key.clone(),
790                access_key.nonce,
791            );
792
793            let tx = Transaction::new(
794                signer_id,
795                public_key,
796                nonce,
797                self.receiver_id,
798                block_hash,
799                actions,
800            );
801
802            tracing::debug!(tx_hash = %tx.get_hash(), nonce, "Transaction built (unsigned)");
803
804            Ok(tx)
805        }
806        .instrument(span)
807        .await
808    }
809
810    /// Build an unsigned transaction offline without network access or a signer.
811    ///
812    /// Use this for fully air-gapped workflows where you provide all
813    /// transaction metadata manually.
814    ///
815    /// # Arguments
816    ///
817    /// * `signer_id` - The account that will sign and pay for the transaction
818    /// * `public_key` - The public key of the signer
819    /// * `block_hash` - A recent block hash (transaction expires ~24h after this block)
820    /// * `nonce` - The next nonce for the signing key (current nonce + 1)
821    ///
822    /// # Example
823    ///
824    /// ```rust,no_run
825    /// # use near_kit::*;
826    /// # fn example(near: Near) -> Result<(), near_kit::Error> {
827    /// let block_hash = CryptoHash::ZERO;
828    /// let public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse().unwrap();
829    ///
830    /// let unsigned = near.transaction("bob.testnet")
831    ///     .transfer(NearToken::from_near(1))
832    ///     .build_offline("alice.testnet", public_key, block_hash, 12345)?;
833    ///
834    /// let hash = unsigned.get_hash();
835    /// // Sign hash externally, then call unsigned.complete(signature)
836    /// # Ok(())
837    /// # }
838    /// ```
839    pub fn build_offline(
840        self,
841        signer_id: impl TryIntoAccountId,
842        public_key: PublicKey,
843        block_hash: CryptoHash,
844        nonce: u64,
845    ) -> Result<Transaction, Error> {
846        if self.actions.is_empty() {
847            return Err(Error::InvalidTransaction(
848                "Transaction must have at least one action".to_string(),
849            ));
850        }
851
852        let signer_id: AccountId = signer_id.try_into_account_id()?;
853
854        Ok(Transaction::new(
855            signer_id,
856            public_key,
857            nonce,
858            self.receiver_id,
859            block_hash,
860            self.actions,
861        ))
862    }
863
864    /// Sign the transaction without sending it.
865    ///
866    /// Returns a `SignedTransaction` that can be inspected or sent later.
867    ///
868    /// # Example
869    ///
870    /// ```rust,no_run
871    /// # use near_kit::*;
872    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
873    /// let signed = near.transaction("bob.testnet")
874    ///     .transfer(NearToken::from_near(1))
875    ///     .sign()
876    ///     .await?;
877    ///
878    /// // Inspect the transaction
879    /// println!("Hash: {}", signed.transaction.get_hash());
880    /// println!("Actions: {:?}", signed.transaction.actions);
881    ///
882    /// // Send it later
883    /// let outcome = near.send(&signed).await?;
884    /// # Ok(())
885    /// # }
886    /// ```
887    pub async fn sign(self) -> Result<SignedTransaction, Error> {
888        if self.actions.is_empty() {
889            return Err(Error::InvalidTransaction(
890                "Transaction must have at least one action".to_string(),
891            ));
892        }
893
894        let signer = self
895            .signer_override
896            .or(self.signer)
897            .ok_or(Error::NoSigner)?;
898
899        let signer_id = signer.account_id().clone();
900        let action_count = self.actions.len();
901
902        let span = tracing::info_span!(
903            "sign_transaction",
904            sender = %signer_id,
905            receiver = %self.receiver_id,
906            action_count,
907            actions = %actions_summary(&self.actions),
908            method = tracing::field::Empty,
909            gas = tracing::field::Empty,
910            deposit = tracing::field::Empty,
911        );
912
913        let actions = self.actions;
914        async move {
915            record_function_call_fields(&actions);
916
917            // Get a signing key atomically. For RotatingSigner, this claims the next
918            // key in rotation. The key contains both the public key and signing capability.
919            let key = signer.key();
920            let public_key = key.public_key().clone();
921
922            // Single view_access_key call provides both nonce and block_hash.
923            // Uses Finality::Final for block hash stability.
924            let access_key = self
925                .rpc
926                .view_access_key(
927                    &signer_id,
928                    &public_key,
929                    BlockReference::Finality(Finality::Final),
930                )
931                .await?;
932            let block_hash = access_key.block_hash;
933
934            let network = self.rpc.url().to_string();
935            let nonce = nonce_manager().next(
936                network,
937                signer_id.clone(),
938                public_key.clone(),
939                access_key.nonce,
940            );
941
942            // Build transaction
943            let tx = Transaction::new(
944                signer_id,
945                public_key,
946                nonce,
947                self.receiver_id,
948                block_hash,
949                actions,
950            );
951
952            // Sign with the key
953            let tx_hash = tx.get_hash();
954            let signature = key.sign(tx_hash.as_bytes()).await?;
955
956            tracing::debug!(tx_hash = %tx_hash, nonce, "Transaction signed");
957
958            Ok(SignedTransaction {
959                transaction: tx,
960                signature,
961            })
962        }
963        .instrument(span)
964        .await
965    }
966
967    /// Sign the transaction offline without network access.
968    ///
969    /// This is useful for air-gapped signing workflows where you need to
970    /// provide the block hash and nonce manually (obtained from a separate
971    /// online machine).
972    ///
973    /// # Arguments
974    ///
975    /// * `block_hash` - A recent block hash (transaction expires ~24h after this block)
976    /// * `nonce` - The next nonce for the signing key (current nonce + 1)
977    ///
978    /// # Example
979    ///
980    /// ```rust,ignore
981    /// # use near_kit::*;
982    /// // On online machine: get block hash and nonce
983    /// // let block = near.rpc().block(BlockReference::latest()).await?;
984    /// // let access_key = near.rpc().view_access_key(...).await?;
985    ///
986    /// // On offline machine: sign with pre-fetched values
987    /// let block_hash: CryptoHash = "11111111111111111111111111111111".parse().unwrap();
988    /// let nonce = 12345u64;
989    ///
990    /// let signed = near.transaction("bob.testnet")
991    ///     .transfer(NearToken::from_near(1))
992    ///     .sign_offline(block_hash, nonce)
993    ///     .await?;
994    ///
995    /// // Transport signed_tx.to_base64() back to online machine
996    /// ```
997    pub async fn sign_offline(
998        self,
999        block_hash: CryptoHash,
1000        nonce: u64,
1001    ) -> Result<SignedTransaction, Error> {
1002        if self.actions.is_empty() {
1003            return Err(Error::InvalidTransaction(
1004                "Transaction must have at least one action".to_string(),
1005            ));
1006        }
1007
1008        let signer = self
1009            .signer_override
1010            .or(self.signer)
1011            .ok_or(Error::NoSigner)?;
1012
1013        let signer_id = signer.account_id().clone();
1014
1015        // Get a signing key atomically
1016        let key = signer.key();
1017        let public_key = key.public_key().clone();
1018
1019        // Build transaction with provided block_hash and nonce
1020        let tx = Transaction::new(
1021            signer_id,
1022            public_key,
1023            nonce,
1024            self.receiver_id,
1025            block_hash,
1026            self.actions,
1027        );
1028
1029        // Sign
1030        let signature = key.sign(tx.get_hash().as_bytes()).await?;
1031
1032        Ok(SignedTransaction {
1033            transaction: tx,
1034            signature,
1035        })
1036    }
1037
1038    /// Send the transaction.
1039    ///
1040    /// Returns a [`TransactionSend`] that defaults to [`crate::types::ExecutedOptimistic`] wait level.
1041    /// Chain `.wait_until(...)` to change the wait level before awaiting.
1042    pub fn send(self) -> TransactionSend {
1043        TransactionSend {
1044            builder: self,
1045            _marker: std::marker::PhantomData,
1046        }
1047    }
1048}
1049
1050// ============================================================================
1051// FunctionCall
1052// ============================================================================
1053
1054/// A standalone function call configuration, decoupled from any transaction.
1055///
1056/// Use this when you need to pre-build calls and compose them into a transaction
1057/// later. This is especially useful for dynamic transaction composition (e.g. in
1058/// a loop) or for batching typed contract calls into a single transaction.
1059///
1060/// Note: `FunctionCall` does not capture a receiver/contract account. The call
1061/// will execute against whichever `receiver_id` is set on the transaction it's
1062/// added to.
1063///
1064/// # Examples
1065///
1066/// ```rust,no_run
1067/// # use near_kit::*;
1068/// # async fn example(near: Near) -> Result<(), near_kit::Error> {
1069/// // Pre-build calls independently
1070/// let init = FunctionCall::new("init")
1071///     .args(serde_json::json!({"owner": "alice.testnet"}))
1072///     .gas(Gas::from_tgas(50));
1073///
1074/// let notify = FunctionCall::new("notify")
1075///     .args(serde_json::json!({"msg": "done"}));
1076///
1077/// // Compose into a single atomic transaction
1078/// near.transaction("contract.testnet")
1079///     .add_action(init)
1080///     .add_action(notify)
1081///     .send()
1082///     .await?;
1083/// # Ok(())
1084/// # }
1085/// ```
1086///
1087/// ```rust,no_run
1088/// # use near_kit::*;
1089/// # async fn example(near: Near) -> Result<(), near_kit::Error> {
1090/// // Dynamic composition in a loop
1091/// let calls = vec![
1092///     FunctionCall::new("method_a").args(serde_json::json!({"x": 1})),
1093///     FunctionCall::new("method_b").args(serde_json::json!({"y": 2})),
1094/// ];
1095///
1096/// let mut tx = near.transaction("contract.testnet");
1097/// for call in calls {
1098///     tx = tx.add_action(call);
1099/// }
1100/// tx.send().await?;
1101/// # Ok(())
1102/// # }
1103/// ```
1104pub struct FunctionCall {
1105    method: String,
1106    args: Vec<u8>,
1107    gas: Gas,
1108    deposit: NearToken,
1109}
1110
1111impl fmt::Debug for FunctionCall {
1112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1113        f.debug_struct("FunctionCall")
1114            .field("method", &self.method)
1115            .field("args_len", &self.args.len())
1116            .field("gas", &self.gas)
1117            .field("deposit", &self.deposit)
1118            .finish()
1119    }
1120}
1121
1122impl FunctionCall {
1123    /// Create a new function call for the given method name.
1124    pub fn new(method: impl Into<String>) -> Self {
1125        Self {
1126            method: method.into(),
1127            args: Vec::new(),
1128            gas: Gas::from_tgas(30),
1129            deposit: NearToken::ZERO,
1130        }
1131    }
1132
1133    /// Set JSON arguments.
1134    pub fn args(mut self, args: impl serde::Serialize) -> Self {
1135        self.args = serde_json::to_vec(&args).unwrap_or_default();
1136        self
1137    }
1138
1139    /// Set raw byte arguments.
1140    pub fn args_raw(mut self, args: Vec<u8>) -> Self {
1141        self.args = args;
1142        self
1143    }
1144
1145    /// Set Borsh-encoded arguments.
1146    pub fn args_borsh(mut self, args: impl borsh::BorshSerialize) -> Self {
1147        self.args = borsh::to_vec(&args).unwrap_or_default();
1148        self
1149    }
1150
1151    /// Set gas limit.
1152    ///
1153    /// Defaults to 30 TGas if not set.
1154    ///
1155    /// # Panics
1156    ///
1157    /// Panics if the gas string cannot be parsed. Use [`Gas`]'s `FromStr` impl
1158    /// for fallible parsing of user input.
1159    pub fn gas(mut self, gas: impl IntoGas) -> Self {
1160        self.gas = gas
1161            .into_gas()
1162            .expect("invalid gas format - use Gas::from_str() for user input");
1163        self
1164    }
1165
1166    /// Set attached deposit.
1167    ///
1168    /// Defaults to zero if not set.
1169    ///
1170    /// # Panics
1171    ///
1172    /// Panics if the amount string cannot be parsed. Use [`NearToken`]'s `FromStr`
1173    /// impl for fallible parsing of user input.
1174    pub fn deposit(mut self, amount: impl IntoNearToken) -> Self {
1175        self.deposit = amount
1176            .into_near_token()
1177            .expect("invalid deposit amount - use NearToken::from_str() for user input");
1178        self
1179    }
1180}
1181
1182impl From<FunctionCall> for Action {
1183    fn from(call: FunctionCall) -> Self {
1184        Action::function_call(call.method, call.args, call.gas, call.deposit)
1185    }
1186}
1187
1188// ============================================================================
1189// CallBuilder
1190// ============================================================================
1191
1192/// Builder for configuring a function call within a transaction.
1193///
1194/// Created via [`TransactionBuilder::call`]. Allows setting args, gas, and deposit
1195/// before continuing to chain more actions or sending.
1196pub struct CallBuilder {
1197    builder: TransactionBuilder,
1198    call: FunctionCall,
1199}
1200
1201impl fmt::Debug for CallBuilder {
1202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1203        f.debug_struct("CallBuilder")
1204            .field("call", &self.call)
1205            .field("builder", &self.builder)
1206            .finish()
1207    }
1208}
1209
1210impl CallBuilder {
1211    fn new(builder: TransactionBuilder, method: String) -> Self {
1212        Self {
1213            builder,
1214            call: FunctionCall::new(method),
1215        }
1216    }
1217
1218    /// Set JSON arguments.
1219    pub fn args<A: serde::Serialize>(mut self, args: A) -> Self {
1220        self.call = self.call.args(args);
1221        self
1222    }
1223
1224    /// Set raw byte arguments.
1225    pub fn args_raw(mut self, args: Vec<u8>) -> Self {
1226        self.call = self.call.args_raw(args);
1227        self
1228    }
1229
1230    /// Set Borsh-encoded arguments.
1231    pub fn args_borsh<A: borsh::BorshSerialize>(mut self, args: A) -> Self {
1232        self.call = self.call.args_borsh(args);
1233        self
1234    }
1235
1236    /// Set gas limit.
1237    ///
1238    /// # Example
1239    ///
1240    /// ```rust,no_run
1241    /// # use near_kit::*;
1242    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
1243    /// near.transaction("contract.testnet")
1244    ///     .call("method")
1245    ///         .gas(Gas::from_tgas(50))
1246    ///     .send()
1247    ///     .await?;
1248    /// # Ok(())
1249    /// # }
1250    /// ```
1251    ///
1252    /// # Panics
1253    ///
1254    /// Panics if the gas string cannot be parsed. Use [`Gas`]'s `FromStr` impl
1255    /// for fallible parsing of user input.
1256    pub fn gas(mut self, gas: impl IntoGas) -> Self {
1257        self.call = self.call.gas(gas);
1258        self
1259    }
1260
1261    /// Set attached deposit.
1262    ///
1263    /// # Example
1264    ///
1265    /// ```rust,no_run
1266    /// # use near_kit::*;
1267    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
1268    /// near.transaction("contract.testnet")
1269    ///     .call("method")
1270    ///         .deposit(NearToken::from_near(1))
1271    ///     .send()
1272    ///     .await?;
1273    /// # Ok(())
1274    /// # }
1275    /// ```
1276    ///
1277    /// # Panics
1278    ///
1279    /// Panics if the amount string cannot be parsed. Use [`NearToken`]'s `FromStr`
1280    /// impl for fallible parsing of user input.
1281    pub fn deposit(mut self, amount: impl IntoNearToken) -> Self {
1282        self.call = self.call.deposit(amount);
1283        self
1284    }
1285
1286    /// Convert this call into a standalone [`Action`], discarding the
1287    /// underlying transaction builder.
1288    ///
1289    /// This is useful for extracting a typed contract call so it can be
1290    /// composed into a different transaction.
1291    ///
1292    /// # Example
1293    ///
1294    /// ```rust,no_run
1295    /// # use near_kit::*;
1296    /// # async fn example(near: Near) -> Result<(), near_kit::Error> {
1297    /// // Extract actions from the fluent builder
1298    /// let action = near.transaction("contract.testnet")
1299    ///     .call("method")
1300    ///     .args(serde_json::json!({"key": "value"}))
1301    ///     .gas(Gas::from_tgas(50))
1302    ///     .into_action();
1303    ///
1304    /// // Compose into a different transaction
1305    /// near.transaction("contract.testnet")
1306    ///     .add_action(action)
1307    ///     .send()
1308    ///     .await?;
1309    /// # Ok(())
1310    /// # }
1311    /// ```
1312    ///
1313    /// # Panics
1314    ///
1315    /// Panics if the underlying transaction builder already has accumulated
1316    /// actions, since those would be silently dropped. Use [`finish`](Self::finish)
1317    /// instead when chaining multiple actions on the same transaction.
1318    pub fn into_action(self) -> Action {
1319        assert!(
1320            self.builder.actions.is_empty(),
1321            "into_action() discards {} previously accumulated action(s) — \
1322             use .finish() to keep them in the transaction",
1323            self.builder.actions.len(),
1324        );
1325        self.call.into()
1326    }
1327
1328    /// Finish this call and return to the transaction builder.
1329    ///
1330    /// This is useful when you need to conditionally add actions to a
1331    /// transaction, since it gives back the [`TransactionBuilder`] so you can
1332    /// branch on runtime state before starting the next action.
1333    pub fn finish(self) -> TransactionBuilder {
1334        self.builder.add_action(self.call)
1335    }
1336
1337    // ========================================================================
1338    // Chaining methods (delegate to TransactionBuilder after finishing)
1339    // ========================================================================
1340
1341    /// Add a pre-built action to the transaction.
1342    ///
1343    /// Finishes this function call, then adds the given action.
1344    /// See [`TransactionBuilder::add_action`] for details.
1345    pub fn add_action(self, action: impl Into<Action>) -> TransactionBuilder {
1346        self.finish().add_action(action)
1347    }
1348
1349    /// Add another function call.
1350    pub fn call(self, method: &str) -> CallBuilder {
1351        self.finish().call(method)
1352    }
1353
1354    /// Add a create account action.
1355    pub fn create_account(self) -> TransactionBuilder {
1356        self.finish().create_account()
1357    }
1358
1359    /// Add a transfer action.
1360    pub fn transfer(self, amount: impl IntoNearToken) -> TransactionBuilder {
1361        self.finish().transfer(amount)
1362    }
1363
1364    /// Add a deploy contract action.
1365    pub fn deploy(self, code: impl Into<Vec<u8>>) -> TransactionBuilder {
1366        self.finish().deploy(code)
1367    }
1368
1369    /// Add a full access key.
1370    pub fn add_full_access_key(self, public_key: PublicKey) -> TransactionBuilder {
1371        self.finish().add_full_access_key(public_key)
1372    }
1373
1374    /// Add a function call access key.
1375    pub fn add_function_call_key(
1376        self,
1377        public_key: PublicKey,
1378        receiver_id: impl TryIntoAccountId,
1379        method_names: Vec<String>,
1380        allowance: Option<NearToken>,
1381    ) -> TransactionBuilder {
1382        self.finish()
1383            .add_function_call_key(public_key, receiver_id, method_names, allowance)
1384    }
1385
1386    /// Delete an access key.
1387    pub fn delete_key(self, public_key: PublicKey) -> TransactionBuilder {
1388        self.finish().delete_key(public_key)
1389    }
1390
1391    /// Delete the account.
1392    pub fn delete_account(self, beneficiary_id: impl TryIntoAccountId) -> TransactionBuilder {
1393        self.finish().delete_account(beneficiary_id)
1394    }
1395
1396    /// Add a stake action.
1397    pub fn stake(self, amount: impl IntoNearToken, public_key: PublicKey) -> TransactionBuilder {
1398        self.finish().stake(amount, public_key)
1399    }
1400
1401    /// Publish a contract to the global registry.
1402    pub fn publish(self, code: impl Into<Vec<u8>>, mode: PublishMode) -> TransactionBuilder {
1403        self.finish().publish(code, mode)
1404    }
1405
1406    /// Deploy a contract from the global registry.
1407    pub fn deploy_from(self, contract_ref: impl GlobalContractRef) -> TransactionBuilder {
1408        self.finish().deploy_from(contract_ref)
1409    }
1410
1411    /// Create a NEP-616 deterministic state init action.
1412    pub fn state_init(
1413        self,
1414        state_init: DeterministicAccountStateInit,
1415        deposit: impl IntoNearToken,
1416    ) -> TransactionBuilder {
1417        self.finish().state_init(state_init, deposit)
1418    }
1419
1420    /// Override the signer.
1421    pub fn sign_with(self, signer: impl Signer + 'static) -> TransactionBuilder {
1422        self.finish().sign_with(signer)
1423    }
1424
1425    /// Set the execution wait level.
1426    pub fn wait_until<W: WaitLevel>(self, level: W) -> TransactionSend<W> {
1427        self.finish().wait_until(level)
1428    }
1429
1430    /// Override the number of nonce retries for this transaction on `InvalidNonce`
1431    /// errors. `0` means no retries (send once), `1` means one retry, etc.
1432    pub fn max_nonce_retries(self, retries: u32) -> TransactionBuilder {
1433        self.finish().max_nonce_retries(retries)
1434    }
1435
1436    /// Build and sign a delegate action for meta-transactions (NEP-366).
1437    ///
1438    /// This finishes the current function call and then creates a delegate action.
1439    pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, crate::Error> {
1440        self.finish().delegate(options).await
1441    }
1442
1443    /// Sign the transaction offline without network access.
1444    ///
1445    /// See [`TransactionBuilder::sign_offline`] for details.
1446    pub async fn sign_offline(
1447        self,
1448        block_hash: CryptoHash,
1449        nonce: u64,
1450    ) -> Result<SignedTransaction, Error> {
1451        self.finish().sign_offline(block_hash, nonce).await
1452    }
1453
1454    /// Sign the transaction without sending it.
1455    ///
1456    /// See [`TransactionBuilder::sign`] for details.
1457    pub async fn sign(self) -> Result<SignedTransaction, Error> {
1458        self.finish().sign().await
1459    }
1460
1461    /// Send the transaction.
1462    pub fn send(self) -> TransactionSend {
1463        self.finish().send()
1464    }
1465}
1466
1467impl IntoFuture for CallBuilder {
1468    type Output = Result<FinalExecutionOutcome, Error>;
1469    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1470
1471    fn into_future(self) -> Self::IntoFuture {
1472        self.send().into_future()
1473    }
1474}
1475
1476// ============================================================================
1477// TransactionSend
1478// ============================================================================
1479
1480/// Future for sending a transaction.
1481///
1482/// The type parameter `W` determines the wait level and the return type:
1483/// - Executed levels ([`crate::types::ExecutedOptimistic`], [`crate::types::Executed`],
1484///   [`crate::types::Final`]) → [`FinalExecutionOutcome`]
1485/// - Non-executed levels ([`crate::types::Submitted`], [`crate::types::Included`],
1486///   [`crate::types::IncludedFinal`]) → [`crate::types::SendTxResponse`]
1487pub struct TransactionSend<W: WaitLevel = crate::types::ExecutedOptimistic> {
1488    builder: TransactionBuilder,
1489    _marker: std::marker::PhantomData<W>,
1490}
1491
1492impl<W: WaitLevel> TransactionSend<W> {
1493    /// Change the execution wait level.
1494    ///
1495    /// The return type changes based on the wait level:
1496    ///
1497    /// ```rust,no_run
1498    /// # use near_kit::*;
1499    /// # async fn example(near: &Near) -> Result<(), Error> {
1500    /// // Executed levels return FinalExecutionOutcome
1501    /// let outcome = near.transfer("bob.testnet", NearToken::from_near(1))
1502    ///     .send()
1503    ///     .wait_until(Final)
1504    ///     .await?;
1505    ///
1506    /// // Non-executed levels return SendTxResponse
1507    /// let response = near.transfer("bob.testnet", NearToken::from_near(1))
1508    ///     .send()
1509    ///     .wait_until(Included)
1510    ///     .await?;
1511    /// # Ok(())
1512    /// # }
1513    /// ```
1514    pub fn wait_until<W2: WaitLevel>(self, _level: W2) -> TransactionSend<W2> {
1515        TransactionSend {
1516            builder: self.builder,
1517            _marker: std::marker::PhantomData,
1518        }
1519    }
1520
1521    /// Override the number of nonce retries for this transaction on `InvalidNonce`
1522    /// errors. `0` means no retries (send once), `1` means one retry, etc.
1523    pub fn max_nonce_retries(mut self, retries: u32) -> Self {
1524        self.builder.max_nonce_retries = retries;
1525        self
1526    }
1527}
1528
1529impl<W: WaitLevel> IntoFuture for TransactionSend<W> {
1530    type Output = Result<W::Response, Error>;
1531    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1532
1533    fn into_future(self) -> Self::IntoFuture {
1534        Box::pin(async move {
1535            let builder = self.builder;
1536
1537            if builder.actions.is_empty() {
1538                return Err(Error::InvalidTransaction(
1539                    "Transaction must have at least one action".to_string(),
1540                ));
1541            }
1542
1543            let signer = builder
1544                .signer_override
1545                .as_ref()
1546                .or(builder.signer.as_ref())
1547                .ok_or(Error::NoSigner)?;
1548
1549            let signer_id = signer.account_id().clone();
1550
1551            let span = tracing::info_span!(
1552                "send_transaction",
1553                sender = %signer_id,
1554                receiver = %builder.receiver_id,
1555                action_count = builder.actions.len(),
1556                actions = %actions_summary(&builder.actions),
1557                method = tracing::field::Empty,
1558                gas = tracing::field::Empty,
1559                deposit = tracing::field::Empty,
1560            );
1561
1562            async move {
1563                record_function_call_fields(&builder.actions);
1564
1565                // Retry loop for transient InvalidTxErrors (nonce conflicts, expired block hash)
1566                let max_nonce_retries = builder.max_nonce_retries;
1567                let wait_until = W::status();
1568                let network = builder.rpc.url().to_string();
1569                let mut last_error: Option<Error> = None;
1570                let mut last_ak_nonce: Option<u64> = None;
1571
1572                for attempt in 0..=max_nonce_retries {
1573                    // Get a signing key atomically for this attempt
1574                    let key = signer.key();
1575                    let public_key = key.public_key().clone();
1576
1577                    // Single view_access_key call provides both nonce and block_hash.
1578                    // Uses Finality::Final for block hash stability.
1579                    let access_key = builder
1580                        .rpc
1581                        .view_access_key(
1582                            &signer_id,
1583                            &public_key,
1584                            BlockReference::Finality(Finality::Final),
1585                        )
1586                        .await?;
1587                    let block_hash = access_key.block_hash;
1588
1589                    // Resolve nonce: prefer ak_nonce from a prior InvalidNonce
1590                    // error (more recent than the view_access_key result), then
1591                    // fall back to the chain nonce. The nonce manager takes
1592                    // max(cached, provided) so stale values are harmless.
1593                    let nonce = nonce_manager().next(
1594                        network.clone(),
1595                        signer_id.clone(),
1596                        public_key.clone(),
1597                        last_ak_nonce.take().unwrap_or(access_key.nonce),
1598                    );
1599
1600                    // Build transaction
1601                    let tx = Transaction::new(
1602                        signer_id.clone(),
1603                        public_key.clone(),
1604                        nonce,
1605                        builder.receiver_id.clone(),
1606                        block_hash,
1607                        builder.actions.clone(),
1608                    );
1609
1610                    // Sign with the key
1611                    let signature = match key.sign(tx.get_hash().as_bytes()).await {
1612                        Ok(sig) => sig,
1613                        Err(e) => return Err(Error::Signing(e)),
1614                    };
1615                    let signed_tx = crate::types::SignedTransaction {
1616                        transaction: tx,
1617                        signature,
1618                    };
1619
1620                    // Send
1621                    match builder.rpc.send_tx(&signed_tx, wait_until).await {
1622                        Ok(response) => {
1623                            // W::convert handles the response appropriately:
1624                            // - Executed levels: extract outcome, check for InvalidTxError
1625                            // - Non-executed levels: build SendTxResponse with hash + sender
1626                            return W::convert(response, &signer_id);
1627                        }
1628                        Err(RpcError::InvalidTx(
1629                            crate::types::InvalidTxError::InvalidNonce { tx_nonce, ak_nonce },
1630                        )) if attempt < max_nonce_retries => {
1631                            tracing::warn!(
1632                                tx_nonce = tx_nonce,
1633                                ak_nonce = ak_nonce,
1634                                attempt = attempt + 1,
1635                                "Invalid nonce, retrying"
1636                            );
1637                            // Store ak_nonce for next iteration to avoid refetching
1638                            last_ak_nonce = Some(ak_nonce);
1639                            last_error = Some(Error::InvalidTx(Box::new(
1640                                crate::types::InvalidTxError::InvalidNonce { tx_nonce, ak_nonce },
1641                            )));
1642                            continue;
1643                        }
1644                        Err(RpcError::InvalidTx(crate::types::InvalidTxError::Expired))
1645                            if attempt + 1 < max_nonce_retries =>
1646                        {
1647                            tracing::warn!(
1648                                attempt = attempt + 1,
1649                                "Transaction expired (stale block hash), retrying with fresh block hash"
1650                            );
1651                            // Expired tx was rejected before nonce consumption.
1652                            // No cache invalidation needed: the next iteration calls
1653                            // view_access_key which provides a fresh nonce, and
1654                            // next() uses max(cached, chain) so stale cache is harmless.
1655                            last_error = Some(Error::InvalidTx(Box::new(
1656                                crate::types::InvalidTxError::Expired,
1657                            )));
1658                            continue;
1659                        }
1660                        Err(e) => {
1661                            tracing::error!(error = %e, "Transaction send failed");
1662                            return Err(e.into());
1663                        }
1664                    }
1665                }
1666
1667                Err(last_error.unwrap_or_else(|| {
1668                    Error::InvalidTransaction("Unknown error during transaction send".to_string())
1669                }))
1670            }
1671            .instrument(span)
1672            .await
1673        })
1674    }
1675}
1676
1677impl IntoFuture for TransactionBuilder {
1678    type Output = Result<FinalExecutionOutcome, Error>;
1679    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1680
1681    fn into_future(self) -> Self::IntoFuture {
1682        self.send().into_future()
1683    }
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688    use super::*;
1689
1690    /// Create a TransactionBuilder for unit tests (no real network needed).
1691    fn test_builder() -> TransactionBuilder {
1692        let rpc = Arc::new(RpcClient::new("https://rpc.testnet.near.org"));
1693        let receiver: AccountId = "contract.testnet".parse().unwrap();
1694        TransactionBuilder::new(rpc, None, receiver, 0)
1695    }
1696
1697    #[test]
1698    fn add_action_appends_to_transaction() {
1699        let action = Action::function_call(
1700            "do_something",
1701            serde_json::to_vec(&serde_json::json!({ "key": "value" })).unwrap(),
1702            Gas::from_tgas(30),
1703            NearToken::ZERO,
1704        );
1705
1706        let builder = test_builder().add_action(action);
1707        assert_eq!(builder.actions.len(), 1);
1708    }
1709
1710    #[test]
1711    fn add_action_chains_with_other_actions() {
1712        let call_action =
1713            Action::function_call("init", Vec::new(), Gas::from_tgas(10), NearToken::ZERO);
1714
1715        let builder = test_builder()
1716            .create_account()
1717            .transfer(NearToken::from_near(5))
1718            .add_action(call_action);
1719
1720        assert_eq!(builder.actions.len(), 3);
1721    }
1722
1723    #[test]
1724    fn add_action_works_after_call_builder() {
1725        let extra_action = Action::transfer(NearToken::from_near(1));
1726
1727        let builder = test_builder()
1728            .call("setup")
1729            .args(serde_json::json!({ "admin": "alice.testnet" }))
1730            .gas(Gas::from_tgas(50))
1731            .add_action(extra_action);
1732
1733        // Should have two actions: the function call from CallBuilder + the transfer
1734        assert_eq!(builder.actions.len(), 2);
1735    }
1736
1737    // FunctionCall tests
1738
1739    #[test]
1740    fn function_call_into_action() {
1741        let call = FunctionCall::new("init")
1742            .args(serde_json::json!({"owner": "alice.testnet"}))
1743            .gas(Gas::from_tgas(50))
1744            .deposit(NearToken::from_near(1));
1745
1746        let action: Action = call.into();
1747        match &action {
1748            Action::FunctionCall(fc) => {
1749                assert_eq!(fc.method_name, "init");
1750                assert_eq!(
1751                    fc.args,
1752                    serde_json::to_vec(&serde_json::json!({"owner": "alice.testnet"})).unwrap()
1753                );
1754                assert_eq!(fc.gas, Gas::from_tgas(50));
1755                assert_eq!(fc.deposit, NearToken::from_near(1));
1756            }
1757            other => panic!("expected FunctionCall, got {:?}", other),
1758        }
1759    }
1760
1761    #[test]
1762    fn function_call_defaults() {
1763        let call = FunctionCall::new("method");
1764        let action: Action = call.into();
1765        match &action {
1766            Action::FunctionCall(fc) => {
1767                assert_eq!(fc.method_name, "method");
1768                assert!(fc.args.is_empty());
1769                assert_eq!(fc.gas, Gas::from_tgas(30));
1770                assert_eq!(fc.deposit, NearToken::ZERO);
1771            }
1772            other => panic!("expected FunctionCall, got {:?}", other),
1773        }
1774    }
1775
1776    #[test]
1777    fn function_call_compose_into_transaction() {
1778        let init = FunctionCall::new("init")
1779            .args(serde_json::json!({"owner": "alice.testnet"}))
1780            .gas(Gas::from_tgas(50));
1781
1782        let notify = FunctionCall::new("notify").args(serde_json::json!({"msg": "done"}));
1783
1784        let builder = test_builder()
1785            .deploy(vec![0u8])
1786            .add_action(init)
1787            .add_action(notify);
1788
1789        assert_eq!(builder.actions.len(), 3);
1790    }
1791
1792    #[test]
1793    fn function_call_dynamic_loop_composition() {
1794        let methods = vec!["step1", "step2", "step3"];
1795
1796        let mut tx = test_builder();
1797        for method in methods {
1798            tx = tx.add_action(FunctionCall::new(method));
1799        }
1800
1801        assert_eq!(tx.actions.len(), 3);
1802    }
1803
1804    #[test]
1805    fn call_builder_into_action() {
1806        let action = test_builder()
1807            .call("setup")
1808            .args(serde_json::json!({"admin": "alice.testnet"}))
1809            .gas(Gas::from_tgas(50))
1810            .deposit(NearToken::from_near(1))
1811            .into_action();
1812
1813        match &action {
1814            Action::FunctionCall(fc) => {
1815                assert_eq!(fc.method_name, "setup");
1816                assert_eq!(fc.gas, Gas::from_tgas(50));
1817                assert_eq!(fc.deposit, NearToken::from_near(1));
1818            }
1819            other => panic!("expected FunctionCall, got {:?}", other),
1820        }
1821    }
1822
1823    #[test]
1824    fn call_builder_into_action_compose() {
1825        let action1 = test_builder()
1826            .call("method_a")
1827            .gas(Gas::from_tgas(50))
1828            .into_action();
1829
1830        let action2 = test_builder()
1831            .call("method_b")
1832            .deposit(NearToken::from_near(1))
1833            .into_action();
1834
1835        let builder = test_builder().add_action(action1).add_action(action2);
1836
1837        assert_eq!(builder.actions.len(), 2);
1838    }
1839}