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}