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