Skip to main content

dash_sdk/platform/transition/
top_up_address.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use super::address_inputs::collect_address_infos_from_proof;
4use super::broadcast::BroadcastStateTransition;
5use super::put_settings::PutSettings;
6use super::validation::ensure_valid_state_transition_structure;
7use crate::{Error, Sdk};
8use dpp::address_funds::{AddressFundsFeeStrategy, PlatformAddress};
9use dpp::dashcore::PrivateKey;
10use dpp::errors::consensus::basic::state_transition::TransitionNoOutputsError;
11use dpp::fee::Credits;
12use dpp::identity::signer::Signer;
13use dpp::prelude::{AddressNonce, AssetLockProof, UserFeeIncrease};
14use dpp::state_transition::address_funding_from_asset_lock_transition::methods::AddressFundingFromAssetLockTransitionMethodsV0;
15use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition;
16use dpp::state_transition::proof_result::StateTransitionProofResult;
17use dpp::state_transition::StateTransition;
18use dpp::ProtocolError;
19use drive_proof_verifier::types::AddressInfos;
20
21/// Trait for topping up Platform addresses using various funding sources.
22#[async_trait::async_trait]
23pub trait TopUpAddress<S: Signer<PlatformAddress>> {
24    /// Tops up addresses using a raw private key for the asset-lock proof.
25    ///
26    /// Returns proof-backed [`AddressInfos`] for the funded addresses.
27    ///
28    /// Prefer [`Self::top_up_with_signers`] when the asset-lock private
29    /// key lives outside Rust (Swift / hardware wallet / HSM): the
30    /// `_with_signers` variant routes asset-lock signing through an
31    /// external [`dpp::key_wallet::signer::Signer`] so no raw private
32    /// key crosses the FFI boundary.
33    async fn top_up(
34        &self,
35        sdk: &Sdk,
36        asset_lock_proof: AssetLockProof,
37        asset_lock_private_key: PrivateKey,
38        fee_strategy: AddressFundsFeeStrategy,
39        signer: &S,
40        settings: Option<PutSettings>,
41    ) -> Result<AddressInfos, Error>;
42
43    /// Top up addresses with an external asset-lock signer.
44    ///
45    /// `signer` (the trait's `S: Signer<PlatformAddress>`) signs each
46    /// per-input `AddressWitness`; `asset_lock_signer` produces the
47    /// outer state-transition ECDSA signature for the key at
48    /// `asset_lock_proof_path` — atomically deriving, signing, and
49    /// zeroising inside the signer's trust boundary. This is the
50    /// signing path used by hosts that hold their private keys outside
51    /// Rust (the iOS Swift SDK, hardware wallets, remote signers).
52    ///
53    /// `settings.user_fee_increase` is threaded straight through to
54    /// the transition builder. It both affects fee accounting AND
55    /// changes the ST's signable bytes, which the upstream CL-height
56    /// retry path in `platform-wallet` relies on to bypass
57    /// Tenderdash's invalid-tx hash cache
58    /// (`keep-invalid-txs-in-cache = true` in dashmate's
59    /// mainnet/testnet templates). `None` / unset = unaltered fees.
60    #[cfg(feature = "core_key_wallet")]
61    #[allow(clippy::too_many_arguments)]
62    async fn top_up_with_signers<AS>(
63        &self,
64        sdk: &Sdk,
65        asset_lock_proof: AssetLockProof,
66        asset_lock_proof_path: &dpp::key_wallet::bip32::DerivationPath,
67        fee_strategy: AddressFundsFeeStrategy,
68        signer: &S,
69        asset_lock_signer: &AS,
70        settings: Option<PutSettings>,
71    ) -> Result<AddressInfos, Error>
72    where
73        AS: dpp::key_wallet::signer::Signer + Send + Sync;
74}
75
76pub type AddressWithBalance = (PlatformAddress, Option<Credits>);
77pub type AddressesWithBalances = BTreeMap<PlatformAddress, Option<Credits>>;
78
79#[async_trait::async_trait]
80impl<S: Signer<PlatformAddress>> TopUpAddress<S> for AddressWithBalance
81where
82    BTreeMap<PlatformAddress, Option<Credits>>: TopUpAddress<S>,
83{
84    async fn top_up(
85        &self,
86        sdk: &Sdk,
87        asset_lock_proof: AssetLockProof,
88        asset_lock_private_key: PrivateKey,
89        fee_strategy: AddressFundsFeeStrategy,
90        signer: &S,
91        settings: Option<PutSettings>,
92    ) -> Result<AddressInfos, Error> {
93        BTreeMap::from([(self.0, self.1)])
94            .top_up(
95                sdk,
96                asset_lock_proof,
97                asset_lock_private_key,
98                fee_strategy,
99                signer,
100                settings,
101            )
102            .await
103    }
104
105    #[cfg(feature = "core_key_wallet")]
106    #[allow(clippy::too_many_arguments)]
107    async fn top_up_with_signers<AS>(
108        &self,
109        sdk: &Sdk,
110        asset_lock_proof: AssetLockProof,
111        asset_lock_proof_path: &dpp::key_wallet::bip32::DerivationPath,
112        fee_strategy: AddressFundsFeeStrategy,
113        signer: &S,
114        asset_lock_signer: &AS,
115        settings: Option<PutSettings>,
116    ) -> Result<AddressInfos, Error>
117    where
118        AS: dpp::key_wallet::signer::Signer + Send + Sync,
119    {
120        BTreeMap::from([(self.0, self.1)])
121            .top_up_with_signers(
122                sdk,
123                asset_lock_proof,
124                asset_lock_proof_path,
125                fee_strategy,
126                signer,
127                asset_lock_signer,
128                settings,
129            )
130            .await
131    }
132}
133
134#[async_trait::async_trait]
135impl<S: Signer<PlatformAddress>> TopUpAddress<S> for AddressesWithBalances {
136    async fn top_up(
137        &self,
138        sdk: &Sdk,
139        asset_lock_proof: AssetLockProof,
140        asset_lock_private_key: PrivateKey,
141        fee_strategy: AddressFundsFeeStrategy,
142        signer: &S,
143        settings: Option<PutSettings>,
144    ) -> Result<AddressInfos, Error> {
145        if self.is_empty() {
146            return Err(Error::from(TransitionNoOutputsError::new()));
147        }
148
149        let user_fee_increase = settings
150            .as_ref()
151            .and_then(|settings| settings.user_fee_increase)
152            .unwrap_or_default();
153
154        let state_transition = create_address_funding_from_asset_lock_transition(
155            asset_lock_proof,
156            asset_lock_private_key.inner.as_ref(),
157            BTreeMap::new(),
158            self.clone(),
159            fee_strategy,
160            signer,
161            user_fee_increase,
162            sdk,
163        )
164        .await?;
165
166        broadcast_and_collect_address_infos(self, state_transition, sdk, settings).await
167    }
168
169    #[cfg(feature = "core_key_wallet")]
170    #[allow(clippy::too_many_arguments)]
171    async fn top_up_with_signers<AS>(
172        &self,
173        sdk: &Sdk,
174        asset_lock_proof: AssetLockProof,
175        asset_lock_proof_path: &dpp::key_wallet::bip32::DerivationPath,
176        fee_strategy: AddressFundsFeeStrategy,
177        signer: &S,
178        asset_lock_signer: &AS,
179        settings: Option<PutSettings>,
180    ) -> Result<AddressInfos, Error>
181    where
182        AS: dpp::key_wallet::signer::Signer + Send + Sync,
183    {
184        if self.is_empty() {
185            return Err(Error::from(TransitionNoOutputsError::new()));
186        }
187
188        // Pull `user_fee_increase` from settings *before* the
189        // broadcast call. The upstream CL-height retry path
190        // (`platform-wallet::wallet::asset_lock::orchestration::submit_with_cl_height_retry`)
191        // bumps this value between attempts to change the ST's
192        // signable bytes — if we silently dropped it here, retries
193        // would hash identically and get cached out by Tenderdash.
194        let user_fee_increase = settings
195            .as_ref()
196            .and_then(|settings| settings.user_fee_increase)
197            .unwrap_or_default();
198
199        let state_transition =
200            AddressFundingFromAssetLockTransition::try_from_asset_lock_with_signers::<S, AS>(
201                asset_lock_proof,
202                asset_lock_proof_path,
203                BTreeMap::new(),
204                self.clone(),
205                fee_strategy,
206                signer,
207                asset_lock_signer,
208                user_fee_increase,
209                sdk.version(),
210            )
211            .await?;
212
213        broadcast_and_collect_address_infos(self, state_transition, sdk, settings).await
214    }
215}
216
217/// Broadcast the address-funding ST and convert the proof into the
218/// `AddressInfos` map. Shared between the legacy private-key path and
219/// the new signer-pair path — both flows want the same proof-shape
220/// guarantee and the same expected-addresses cross-check.
221async fn broadcast_and_collect_address_infos(
222    expected: &AddressesWithBalances,
223    state_transition: StateTransition,
224    sdk: &Sdk,
225    settings: Option<PutSettings>,
226) -> Result<AddressInfos, Error> {
227    ensure_valid_state_transition_structure(&state_transition, sdk.version())?;
228    let st_result = state_transition
229        .broadcast_and_wait::<StateTransitionProofResult>(sdk, settings)
230        .await?;
231    match st_result {
232        StateTransitionProofResult::VerifiedAddressInfos(address_infos) => {
233            let expected_addresses = expected
234                .keys()
235                .copied()
236                .collect::<BTreeSet<PlatformAddress>>();
237            collect_address_infos_from_proof(address_infos, &expected_addresses)
238        }
239        other => Err(Error::InvalidProvedResponse(format!(
240            "address info proof was expected for {:?}, but received {:?}",
241            state_transition, other
242        ))),
243    }
244}
245
246#[allow(clippy::too_many_arguments)]
247async fn create_address_funding_from_asset_lock_transition<S: Signer<PlatformAddress>>(
248    asset_lock_proof: AssetLockProof,
249    asset_lock_private_key: &[u8],
250    inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
251    outputs: BTreeMap<PlatformAddress, Option<Credits>>,
252    fee_strategy: AddressFundsFeeStrategy,
253    signer: &S,
254    user_fee_increase: UserFeeIncrease,
255    sdk: &Sdk,
256) -> Result<StateTransition, ProtocolError> {
257    AddressFundingFromAssetLockTransition::try_from_asset_lock_with_signer_and_private_key(
258        asset_lock_proof,
259        asset_lock_private_key,
260        inputs,
261        outputs,
262        fee_strategy,
263        signer,
264        user_fee_increase,
265        sdk.version(),
266    )
267    .await
268}