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