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