Skip to main content

thruput_signing/
default_wallet.rs

1//! Native `DefaultWallet` transactions for pubkey accounts.
2//!
3//! Deployment atomically transfers an explicit amount, installs attached code,
4//! and optionally initializes that code. Callers must select a runtime supporting
5//! this deployment ABI; these helpers do not discover runtime capabilities.
6//! Legacy deployed wallets have their own transaction format.
7
8use thruput_xdr::contract::{Contract, ContractEntrypointID};
9use thruput_xdr::transaction::Transaction;
10use thruput_xdr::types::{ContractAddress, SHA256Hash};
11use xdr_common::VecM;
12
13use crate::{
14    BuildCallTransaction, BuildDeployTransaction, PubkeyAccount, SigningError,
15    build_call_transaction, build_deploy_transaction, wallet_contract_hash,
16};
17
18/// Canonical `DefaultWallet.execute` selector.
19pub const DEFAULT_WALLET_EXECUTE_ENTRYPOINT: ContractEntrypointID =
20    ContractEntrypointID::new(0xc3ed_b11f_dd6d_5d58_u64.to_be_bytes());
21/// Canonical `DefaultWallet.deploy` selector.
22pub const DEFAULT_WALLET_DEPLOY_ENTRYPOINT: ContractEntrypointID =
23    ContractEntrypointID::new(0x6be0_cb0f_6de0_917a_u64.to_be_bytes());
24
25/// Optional initialization of the newly deployed contract, called by the wallet.
26/// A failed invocation rolls back funding and deployment; return bytes are discarded.
27#[derive(Debug, Clone, Copy)]
28pub struct WalletInitCall<'a> {
29    /// Initializer method on the new contract.
30    pub entrypoint: ContractEntrypointID,
31    /// Gas budget for the initializer invocation.
32    pub gas_limit: u64,
33    /// Encoded initializer arguments.
34    pub calldata: &'a [u8],
35}
36
37/// One child invocation wrapped in the native wallet's `execute` call.
38#[derive(Debug, Clone, Copy)]
39pub struct BuildDefaultWalletCallTransaction<'a> {
40    /// Determines both the root address and its pubkey authentication claim.
41    pub account: &'a PubkeyAccount,
42    pub target_address: ContractAddress,
43    pub target_entrypoint: ContractEntrypointID,
44    pub target_calldata: &'a [u8],
45    pub target_gas_limit: u64,
46    pub gas_limit: u64,
47    pub block_expiry_height: i64,
48    /// Transaction uniquifier, independent of the pubkey account nonce.
49    pub nonce: u64,
50}
51
52/// Build one native child invocation without submitting or signing it.
53pub fn build_default_wallet_call_transaction(
54    args: BuildDefaultWalletCallTransaction<'_>,
55) -> Result<Transaction, SigningError> {
56    let call = (
57        args.target_address,
58        args.target_entrypoint,
59        args.target_gas_limit,
60        args.target_calldata,
61    );
62    let calldata = xdrs::cxs::encode(&[call].as_slice())
63        .map_err(|error| SigningError::XdrEncode(error.to_string()))?;
64    Ok(build_call_transaction(BuildCallTransaction {
65        invoked_address: args.account.address(),
66        entrypoint: DEFAULT_WALLET_EXECUTE_ENTRYPOINT,
67        calldata,
68        gas_limit: args.gas_limit,
69        block_expiry_height: args.block_expiry_height,
70        nonce: args.nonce,
71        auth_claim: args.account.auth_claim(),
72    }))
73}
74
75/// Code deployed either from this transaction or from an earlier registration.
76#[derive(Debug, Clone)]
77pub enum DeploymentCode {
78    /// Register this binary and deploy it in the same transaction.
79    Attached(Contract),
80    /// Deploy a binary that is already registered on the network.
81    Registered(SHA256Hash),
82}
83
84/// Native deployment with explicit code identity.
85#[derive(Debug, Clone)]
86pub struct BuildDefaultWalletDeployTransaction<'a> {
87    pub account: &'a PubkeyAccount,
88    /// Stable 32-byte deployment salt, separate from the transaction uniquifier.
89    pub deployment_nonce: SHA256Hash,
90    pub code: DeploymentCode,
91    /// Exact raw native token amount to transfer. Zero skips the transfer.
92    pub amount: u128,
93    /// Initialization at the newly deployed address within the same root frame.
94    pub initializer: Option<WalletInitCall<'a>>,
95    pub gas_limit: u64,
96    pub block_expiry_height: i64,
97    pub nonce: u64,
98}
99
100/// Build an atomic deployment with optional funding and initialization.
101/// Zero skips funding. The initializer targets only the newly deployed contract.
102/// This performs no submission or capability discovery and has no multi-transaction fallback.
103pub fn build_default_wallet_deploy_transaction(
104    args: BuildDefaultWalletDeployTransaction<'_>,
105) -> Result<Transaction, SigningError> {
106    let (hash, contracts_to_deploy) = match args.code {
107        DeploymentCode::Attached(contract) => {
108            (wallet_contract_hash(&contract)?, VecM(vec![contract]))
109        }
110        DeploymentCode::Registered(hash) => (hash, VecM(vec![])),
111    };
112    let initializer = args
113        .initializer
114        .map(|call| (call.entrypoint, call.gas_limit, call.calldata));
115    let calldata = xdrs::cxs::encode(&(args.deployment_nonce, hash, args.amount, initializer))
116        .map_err(|error| SigningError::XdrEncode(error.to_string()))?;
117    Ok(build_deploy_transaction(BuildDeployTransaction {
118        invoked_address: args.account.address(),
119        entrypoint: DEFAULT_WALLET_DEPLOY_ENTRYPOINT,
120        calldata,
121        gas_limit: args.gas_limit,
122        block_expiry_height: args.block_expiry_height,
123        nonce: args.nonce,
124        contracts_to_deploy,
125        auth_claim: args.account.auth_claim(),
126    }))
127}
128
129const DEFAULT_WALLET_RETURN_DECODE_LIMIT: usize = 1024;
130
131/// Decode a successful native `execute` result for a single child call.
132/// Pass return data only after checking the root receipt's success status.
133/// Accepts at most 1 KiB, including collection framing. This is a local decoding
134/// limit; the chain's return-buffer limit is configured independently.
135pub fn decode_default_wallet_call_return(data: &[u8]) -> Result<Vec<u8>, SigningError> {
136    if data.len() > DEFAULT_WALLET_RETURN_DECODE_LIMIT {
137        return Err(SigningError::XdrDecode(
138            "native wallet return exceeds decoder buffer limit".into(),
139        ));
140    }
141    let mut returns: Vec<Vec<u8>> =
142        xdrs::cxs::decode(data).map_err(|error| SigningError::XdrDecode(error.to_string()))?;
143    if returns.len() != 1 {
144        return Err(SigningError::UnexpectedWalletReturnCount(returns.len()));
145    }
146    Ok(returns.remove(0))
147}