use std::collections::BTreeMap;
use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use crate::error::{Error, RpcError};
use crate::types::{
AccountId, Action, BlockReference, CryptoHash, DelegateAction, DeterministicAccountStateInit,
DeterministicAccountStateInitV1, FinalExecutionOutcome, Finality, Gas,
GlobalContractIdentifier, IntoGas, IntoNearToken, NearToken, NonDelegateAction, PublicKey,
SignedDelegateAction, SignedTransaction, Transaction, TxExecutionStatus,
};
use super::nonce_manager::NonceManager;
use super::rpc::RpcClient;
use super::signer::Signer;
fn nonce_manager() -> &'static NonceManager {
static NONCE_MANAGER: OnceLock<NonceManager> = OnceLock::new();
NONCE_MANAGER.get_or_init(NonceManager::new)
}
#[derive(Clone, Debug, Default)]
pub struct DelegateOptions {
pub max_block_height: Option<u64>,
pub block_height_offset: Option<u64>,
pub nonce: Option<u64>,
}
impl DelegateOptions {
pub fn with_offset(offset: u64) -> Self {
Self {
block_height_offset: Some(offset),
..Default::default()
}
}
pub fn with_max_height(height: u64) -> Self {
Self {
max_block_height: Some(height),
..Default::default()
}
}
}
#[derive(Clone, Debug)]
pub struct DelegateResult {
pub signed_delegate_action: SignedDelegateAction,
pub payload: String,
}
impl DelegateResult {
pub fn to_bytes(&self) -> Vec<u8> {
self.signed_delegate_action.to_bytes()
}
pub fn sender_id(&self) -> &AccountId {
self.signed_delegate_action.sender_id()
}
pub fn receiver_id(&self) -> &AccountId {
self.signed_delegate_action.receiver_id()
}
}
pub struct TransactionBuilder {
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
receiver_id: AccountId,
actions: Vec<Action>,
signer_override: Option<Arc<dyn Signer>>,
wait_until: TxExecutionStatus,
max_nonce_retries: u32,
}
impl TransactionBuilder {
pub(crate) fn new(
rpc: Arc<RpcClient>,
signer: Option<Arc<dyn Signer>>,
receiver_id: AccountId,
max_nonce_retries: u32,
) -> Self {
Self {
rpc,
signer,
receiver_id,
actions: Vec::new(),
signer_override: None,
wait_until: TxExecutionStatus::ExecutedOptimistic,
max_nonce_retries,
}
}
pub fn create_account(mut self) -> Self {
self.actions.push(Action::create_account());
self
}
pub fn transfer(mut self, amount: impl IntoNearToken) -> Self {
let amount = amount
.into_near_token()
.expect("invalid transfer amount - use NearToken::from_str() for user input");
self.actions.push(Action::transfer(amount));
self
}
pub fn deploy(mut self, code: impl Into<Vec<u8>>) -> Self {
self.actions.push(Action::deploy_contract(code.into()));
self
}
pub fn call(self, method: &str) -> CallBuilder {
CallBuilder::new(self, method.to_string())
}
pub fn add_full_access_key(mut self, public_key: PublicKey) -> Self {
self.actions.push(Action::add_full_access_key(public_key));
self
}
pub fn add_function_call_key(
mut self,
public_key: PublicKey,
receiver_id: impl AsRef<str>,
method_names: Vec<String>,
allowance: Option<NearToken>,
) -> Self {
let receiver_id = AccountId::parse_lenient(receiver_id);
self.actions.push(Action::add_function_call_key(
public_key,
receiver_id,
method_names,
allowance,
));
self
}
pub fn delete_key(mut self, public_key: PublicKey) -> Self {
self.actions.push(Action::delete_key(public_key));
self
}
pub fn delete_account(mut self, beneficiary_id: impl AsRef<str>) -> Self {
let beneficiary_id = AccountId::parse_lenient(beneficiary_id);
self.actions.push(Action::delete_account(beneficiary_id));
self
}
pub fn stake(mut self, amount: impl IntoNearToken, public_key: PublicKey) -> Self {
let amount = amount
.into_near_token()
.expect("invalid stake amount - use NearToken::from_str() for user input");
self.actions.push(Action::stake(amount, public_key));
self
}
pub fn signed_delegate_action(mut self, signed_delegate: SignedDelegateAction) -> Self {
self.receiver_id = signed_delegate.sender_id().clone();
self.actions.push(Action::delegate(signed_delegate));
self
}
pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, Error> {
if self.actions.is_empty() {
return Err(Error::InvalidTransaction(
"Delegate action requires at least one action".to_string(),
));
}
for action in &self.actions {
if matches!(action, Action::Delegate(_)) {
return Err(Error::InvalidTransaction(
"Delegate actions cannot contain nested signed delegate actions".to_string(),
));
}
}
let signer = self
.signer_override
.as_ref()
.or(self.signer.as_ref())
.ok_or(Error::NoSigner)?;
let sender_id = signer.account_id().clone();
let key = signer.key();
let public_key = key.public_key().clone();
let nonce = if let Some(n) = options.nonce {
n
} else {
let access_key = self
.rpc
.view_access_key(
&sender_id,
&public_key,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
access_key.nonce + 1
};
let max_block_height = if let Some(h) = options.max_block_height {
h
} else {
let status = self.rpc.status().await?;
let offset = options.block_height_offset.unwrap_or(200);
status.sync_info.latest_block_height + offset
};
let delegate_actions: Vec<NonDelegateAction> = self
.actions
.into_iter()
.filter_map(NonDelegateAction::from_action)
.collect();
let delegate_action = DelegateAction {
sender_id,
receiver_id: self.receiver_id,
actions: delegate_actions,
nonce,
max_block_height,
public_key: public_key.clone(),
};
let hash = delegate_action.get_hash();
let signature = key.sign(hash.as_bytes()).await?;
let signed_delegate_action = delegate_action.sign(signature);
let payload = signed_delegate_action.to_base64();
Ok(DelegateResult {
signed_delegate_action,
payload,
})
}
pub fn publish_contract(mut self, code: impl Into<Vec<u8>>, by_hash: bool) -> Self {
self.actions
.push(Action::publish_contract(code.into(), by_hash));
self
}
pub fn deploy_from_hash(mut self, code_hash: CryptoHash) -> Self {
self.actions.push(Action::deploy_from_hash(code_hash));
self
}
pub fn deploy_from_publisher(mut self, publisher_id: impl AsRef<str>) -> Self {
let publisher_id = AccountId::parse_lenient(publisher_id);
self.actions.push(Action::deploy_from_account(publisher_id));
self
}
pub fn state_init_by_hash(
mut self,
code_hash: CryptoHash,
data: BTreeMap<Vec<u8>, Vec<u8>>,
deposit: impl IntoNearToken,
) -> Self {
let deposit = deposit
.into_near_token()
.expect("invalid deposit amount - use NearToken::from_str() for user input");
let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::CodeHash(code_hash),
data: data.clone(),
});
self.receiver_id = state_init.derive_account_id();
self.actions
.push(Action::state_init_by_hash(code_hash, data, deposit));
self
}
pub fn state_init_by_publisher(
mut self,
publisher_id: impl AsRef<str>,
data: BTreeMap<Vec<u8>, Vec<u8>>,
deposit: impl IntoNearToken,
) -> Self {
let publisher_id = AccountId::parse_lenient(publisher_id);
let deposit = deposit
.into_near_token()
.expect("invalid deposit amount - use NearToken::from_str() for user input");
let state_init = DeterministicAccountStateInit::V1(DeterministicAccountStateInitV1 {
code: GlobalContractIdentifier::AccountId(publisher_id.clone()),
data: data.clone(),
});
self.receiver_id = state_init.derive_account_id();
self.actions
.push(Action::state_init_by_account(publisher_id, data, deposit));
self
}
pub fn sign_with(mut self, signer: impl Signer + 'static) -> Self {
self.signer_override = Some(Arc::new(signer));
self
}
pub fn wait_until(mut self, status: TxExecutionStatus) -> Self {
self.wait_until = status;
self
}
pub async fn sign(self) -> Result<SignedTransaction, Error> {
if self.actions.is_empty() {
return Err(Error::InvalidTransaction(
"Transaction must have at least one action".to_string(),
));
}
let signer = self
.signer_override
.or(self.signer)
.ok_or(Error::NoSigner)?;
let signer_id = signer.account_id().clone();
let key = signer.key();
let public_key = key.public_key().clone();
let public_key_str = public_key.to_string();
let rpc = self.rpc.clone();
let network = rpc.url().to_string();
let signer_id_clone = signer_id.clone();
let public_key_clone = public_key.clone();
let nonce = nonce_manager()
.get_next_nonce(&network, signer_id.as_ref(), &public_key_str, || async {
let access_key = rpc
.view_access_key(
&signer_id_clone,
&public_key_clone,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
Ok(access_key.nonce)
})
.await?;
let block = self
.rpc
.block(BlockReference::Finality(Finality::Final))
.await?;
let tx = Transaction::new(
signer_id,
public_key,
nonce,
self.receiver_id,
block.header.hash,
self.actions,
);
let signature = key.sign(tx.get_hash().as_bytes()).await?;
Ok(SignedTransaction {
transaction: tx,
signature,
})
}
pub async fn sign_offline(
self,
block_hash: CryptoHash,
nonce: u64,
) -> Result<SignedTransaction, Error> {
if self.actions.is_empty() {
return Err(Error::InvalidTransaction(
"Transaction must have at least one action".to_string(),
));
}
let signer = self
.signer_override
.or(self.signer)
.ok_or(Error::NoSigner)?;
let signer_id = signer.account_id().clone();
let key = signer.key();
let public_key = key.public_key().clone();
let tx = Transaction::new(
signer_id,
public_key,
nonce,
self.receiver_id,
block_hash,
self.actions,
);
let signature = key.sign(tx.get_hash().as_bytes()).await?;
Ok(SignedTransaction {
transaction: tx,
signature,
})
}
pub fn send(self) -> TransactionSend {
TransactionSend { builder: self }
}
fn push_action(&mut self, action: Action) {
self.actions.push(action);
}
}
pub struct CallBuilder {
builder: TransactionBuilder,
method: String,
args: Vec<u8>,
gas: Gas,
deposit: NearToken,
}
impl CallBuilder {
fn new(builder: TransactionBuilder, method: String) -> Self {
Self {
builder,
method,
args: Vec::new(),
gas: Gas::DEFAULT,
deposit: NearToken::ZERO,
}
}
pub fn args<A: serde::Serialize>(mut self, args: A) -> Self {
self.args = serde_json::to_vec(&args).unwrap_or_default();
self
}
pub fn args_raw(mut self, args: Vec<u8>) -> Self {
self.args = args;
self
}
pub fn args_borsh<A: borsh::BorshSerialize>(mut self, args: A) -> Self {
self.args = borsh::to_vec(&args).unwrap_or_default();
self
}
pub fn gas(mut self, gas: impl IntoGas) -> Self {
self.gas = gas
.into_gas()
.expect("invalid gas format - use Gas::from_str() for user input");
self
}
pub fn deposit(mut self, amount: impl IntoNearToken) -> Self {
self.deposit = amount
.into_near_token()
.expect("invalid deposit amount - use NearToken::from_str() for user input");
self
}
fn finish(self) -> TransactionBuilder {
let mut builder = self.builder;
builder.push_action(Action::function_call(
self.method,
self.args,
self.gas,
self.deposit,
));
builder
}
pub fn call(self, method: &str) -> CallBuilder {
self.finish().call(method)
}
pub fn create_account(self) -> TransactionBuilder {
self.finish().create_account()
}
pub fn transfer(self, amount: impl IntoNearToken) -> TransactionBuilder {
self.finish().transfer(amount)
}
pub fn deploy(self, code: impl Into<Vec<u8>>) -> TransactionBuilder {
self.finish().deploy(code)
}
pub fn add_full_access_key(self, public_key: PublicKey) -> TransactionBuilder {
self.finish().add_full_access_key(public_key)
}
pub fn add_function_call_key(
self,
public_key: PublicKey,
receiver_id: impl AsRef<str>,
method_names: Vec<String>,
allowance: Option<NearToken>,
) -> TransactionBuilder {
self.finish()
.add_function_call_key(public_key, receiver_id, method_names, allowance)
}
pub fn delete_key(self, public_key: PublicKey) -> TransactionBuilder {
self.finish().delete_key(public_key)
}
pub fn delete_account(self, beneficiary_id: impl AsRef<str>) -> TransactionBuilder {
self.finish().delete_account(beneficiary_id)
}
pub fn stake(self, amount: impl IntoNearToken, public_key: PublicKey) -> TransactionBuilder {
self.finish().stake(amount, public_key)
}
pub fn publish_contract(self, code: impl Into<Vec<u8>>, by_hash: bool) -> TransactionBuilder {
self.finish().publish_contract(code, by_hash)
}
pub fn deploy_from_hash(self, code_hash: CryptoHash) -> TransactionBuilder {
self.finish().deploy_from_hash(code_hash)
}
pub fn deploy_from_publisher(self, publisher_id: impl AsRef<str>) -> TransactionBuilder {
self.finish().deploy_from_publisher(publisher_id)
}
pub fn state_init_by_hash(
self,
code_hash: CryptoHash,
data: BTreeMap<Vec<u8>, Vec<u8>>,
deposit: impl IntoNearToken,
) -> TransactionBuilder {
self.finish().state_init_by_hash(code_hash, data, deposit)
}
pub fn state_init_by_publisher(
self,
publisher_id: impl AsRef<str>,
data: BTreeMap<Vec<u8>, Vec<u8>>,
deposit: impl IntoNearToken,
) -> TransactionBuilder {
self.finish()
.state_init_by_publisher(publisher_id, data, deposit)
}
pub fn sign_with(self, signer: impl Signer + 'static) -> TransactionBuilder {
self.finish().sign_with(signer)
}
pub fn wait_until(self, status: TxExecutionStatus) -> TransactionBuilder {
self.finish().wait_until(status)
}
pub async fn delegate(self, options: DelegateOptions) -> Result<DelegateResult, crate::Error> {
self.finish().delegate(options).await
}
pub async fn sign_offline(
self,
block_hash: CryptoHash,
nonce: u64,
) -> Result<SignedTransaction, Error> {
self.finish().sign_offline(block_hash, nonce).await
}
pub async fn sign(self) -> Result<SignedTransaction, Error> {
self.finish().sign().await
}
pub fn send(self) -> TransactionSend {
self.finish().send()
}
}
impl IntoFuture for CallBuilder {
type Output = Result<FinalExecutionOutcome, Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
self.send().into_future()
}
}
pub struct TransactionSend {
builder: TransactionBuilder,
}
impl TransactionSend {
pub fn wait_until(mut self, status: TxExecutionStatus) -> Self {
self.builder.wait_until = status;
self
}
}
impl IntoFuture for TransactionSend {
type Output = Result<FinalExecutionOutcome, Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let builder = self.builder;
if builder.actions.is_empty() {
return Err(Error::InvalidTransaction(
"Transaction must have at least one action".to_string(),
));
}
let signer = builder
.signer_override
.as_ref()
.or(builder.signer.as_ref())
.ok_or(Error::NoSigner)?;
let signer_id = signer.account_id().clone();
let max_nonce_retries = builder.max_nonce_retries;
let network = builder.rpc.url().to_string();
let mut last_error: Option<Error> = None;
let mut last_ak_nonce: Option<u64> = None;
for attempt in 0..max_nonce_retries {
let key = signer.key();
let public_key = key.public_key().clone();
let public_key_str = public_key.to_string();
let rpc = builder.rpc.clone();
let signer_id_clone = signer_id.clone();
let public_key_clone = public_key.clone();
let nonce = if let Some(ak_nonce) = last_ak_nonce.take() {
nonce_manager().update_and_get_next(
&network,
signer_id.as_ref(),
&public_key_str,
ak_nonce,
)
} else {
nonce_manager()
.get_next_nonce(&network, signer_id.as_ref(), &public_key_str, || async {
let access_key = rpc
.view_access_key(
&signer_id_clone,
&public_key_clone,
BlockReference::Finality(Finality::Optimistic),
)
.await?;
Ok(access_key.nonce)
})
.await?
};
let block = builder
.rpc
.block(BlockReference::Finality(Finality::Final))
.await?;
let tx = Transaction::new(
signer_id.clone(),
public_key.clone(),
nonce,
builder.receiver_id.clone(),
block.header.hash,
builder.actions.clone(),
);
let signature = match key.sign(tx.get_hash().as_bytes()).await {
Ok(sig) => sig,
Err(e) => return Err(Error::Signing(e)),
};
let signed_tx = crate::types::SignedTransaction {
transaction: tx,
signature,
};
match builder.rpc.send_tx(&signed_tx, builder.wait_until).await {
Ok(response) => {
let outcome = response.outcome.ok_or_else(|| {
Error::InvalidTransaction(format!(
"Transaction {} submitted with wait_until={:?} but no execution \
outcome was returned. Use rpc().send_tx() for fire-and-forget \
submission.",
response.transaction_hash, builder.wait_until,
))
})?;
if outcome.is_failure() {
return Err(Error::TransactionFailed(
outcome.failure_message().unwrap_or_default(),
));
}
return Ok(outcome);
}
Err(RpcError::InvalidNonce { tx_nonce, ak_nonce })
if attempt < max_nonce_retries - 1 =>
{
last_ak_nonce = Some(ak_nonce);
last_error =
Some(Error::Rpc(RpcError::InvalidNonce { tx_nonce, ak_nonce }));
continue;
}
Err(e) => return Err(Error::Rpc(e)),
}
}
Err(last_error.unwrap_or_else(|| {
Error::InvalidTransaction("Unknown error during transaction send".to_string())
}))
})
}
}
impl IntoFuture for TransactionBuilder {
type Output = Result<FinalExecutionOutcome, Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
self.send().into_future()
}
}