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