hopr_chain_actions/
node.rs

1//! This module contains the [NodeActions] trait defining action which relate to HOPR node itself.
2//!
3//! An implementation of this trait is added to [ChainActions] which realizes the redemption
4//! operations via [ActionQueue](crate::action_queue::ActionQueue).
5//!
6//! There are 3 functions that can be used to redeem tickets in the [NodeActions] trait:
7//! - [withdraw](NodeActions::withdraw)
8//! - [announce](NodeActions::announce)
9//! - [register_safe_by_node](NodeActions::register_safe_by_node)
10//!
11//! All necessary pre-requisites are checked by the implementation before the respective [Action] is submitted
12//! to the [ActionQueue](crate::action_queue::ActionQueue).
13use async_trait::async_trait;
14use hopr_chain_types::actions::Action;
15use hopr_crypto_types::keypairs::OffchainKeypair;
16use hopr_crypto_types::prelude::Keypair;
17use hopr_db_sql::accounts::HoprDbAccountOperations;
18use hopr_internal_types::prelude::*;
19use hopr_primitive_types::prelude::*;
20use multiaddr::Multiaddr;
21use tracing::info;
22
23use crate::action_queue::PendingAction;
24use crate::errors::{
25    ChainActionsError::{AlreadyAnnounced, InvalidArguments},
26    Result,
27};
28use crate::ChainActions;
29
30/// Contains all on-chain calls specific to HOPR node itself.
31#[async_trait]
32pub trait NodeActions {
33    /// Withdraws the specified `amount` of tokens to the given `recipient`.
34    async fn withdraw(&self, recipient: Address, amount: Balance) -> Result<PendingAction>;
35
36    /// Announces node on-chain with key binding.
37    /// The operation should also check if such announcement has not been already made on-chain.
38    async fn announce(&self, multiaddrs: &[Multiaddr], offchain_key: &OffchainKeypair) -> Result<PendingAction>;
39
40    /// Registers the safe address with the node
41    async fn register_safe_by_node(&self, safe_address: Address) -> Result<PendingAction>;
42}
43
44#[async_trait]
45impl<Db> NodeActions for ChainActions<Db>
46where
47    Db: HoprDbAccountOperations + Clone + Send + Sync + std::fmt::Debug,
48{
49    #[tracing::instrument(level = "debug", skip(self))]
50    async fn withdraw(&self, recipient: Address, amount: Balance) -> Result<PendingAction> {
51        if amount.eq(&amount.of_same("0")) {
52            return Err(InvalidArguments("cannot withdraw zero amount".into()));
53        }
54
55        // TODO: should we check native/token balance here before withdrawing ?
56
57        info!(%amount, %recipient, "initiating withdrawal");
58        self.tx_sender.send(Action::Withdraw(recipient, amount)).await
59    }
60
61    #[tracing::instrument(level = "debug", skip(self))]
62    async fn announce(&self, multiaddrs: &[Multiaddr], offchain_key: &OffchainKeypair) -> Result<PendingAction> {
63        // TODO: allow announcing all addresses once that option is supported
64        let announcement_data = AnnouncementData::new(
65            multiaddrs[0].clone(),
66            Some(KeyBinding::new(self.self_address(), offchain_key)),
67        )?;
68
69        if !self.db.get_accounts(None, true).await?.into_iter().any(|account| {
70            account.public_key.eq(offchain_key.public())
71                && account
72                    .get_multiaddr()
73                    .is_some_and(|ma| decapsulate_multiaddress(ma).eq(announcement_data.multiaddress()))
74        }) {
75            info!(%announcement_data, "initiating announcement");
76            self.tx_sender.send(Action::Announce(announcement_data)).await
77        } else {
78            Err(AlreadyAnnounced)
79        }
80    }
81
82    #[tracing::instrument(level = "debug", skip(self))]
83    async fn register_safe_by_node(&self, safe_address: Address) -> Result<PendingAction> {
84        info!(%safe_address, "initiating safe address registration");
85        self.tx_sender.send(Action::RegisterSafe(safe_address)).await
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::action_queue::{ActionQueue, MockTransactionExecutor};
92    use crate::action_state::MockActionState;
93    use crate::errors::ChainActionsError;
94    use crate::node::NodeActions;
95    use crate::ChainActions;
96    use futures::FutureExt;
97    use hex_literal::hex;
98    use hopr_chain_types::actions::Action;
99    use hopr_chain_types::chain_events::{ChainEventType, SignificantChainEvent};
100    use hopr_crypto_random::random_bytes;
101    use hopr_crypto_types::prelude::*;
102    use hopr_db_sql::accounts::HoprDbAccountOperations;
103    use hopr_db_sql::db::HoprDb;
104    use hopr_db_sql::{api::info::DomainSeparator, info::HoprDbInfoOperations};
105    use hopr_internal_types::prelude::*;
106    use hopr_primitive_types::prelude::*;
107    use multiaddr::Multiaddr;
108    use std::str::FromStr;
109
110    lazy_static::lazy_static! {
111        static ref ALICE_KP: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
112        static ref BOB_KP: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
113        static ref ALICE: Address = ALICE_KP.public().to_address();
114        static ref BOB: Address = BOB_KP.public().to_address();
115        static ref ALICE_OFFCHAIN: OffchainKeypair = OffchainKeypair::from_secret(&hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e")).expect("lazy static keypair should be constructible");
116    }
117
118    #[async_std::test]
119    async fn test_announce() -> anyhow::Result<()> {
120        let random_hash = Hash::from(random_bytes::<{ Hash::SIZE }>());
121        let announce_multiaddr = Multiaddr::from_str("/ip4/1.2.3.4/tcp/9009")?;
122
123        let db = HoprDb::new_in_memory(ALICE_KP.clone()).await?;
124        db.set_domain_separator(None, DomainSeparator::Channel, Default::default())
125            .await?;
126
127        let ma = announce_multiaddr.clone();
128        let pubkey_clone = ALICE_OFFCHAIN.public().clone();
129        let mut tx_exec = MockTransactionExecutor::new();
130        tx_exec
131            .expect_announce()
132            .once()
133            .withf(move |ad| {
134                let kb = ad.key_binding.clone().expect("key binding must be present");
135                ma.eq(ad.multiaddress()) && kb.packet_key == pubkey_clone && kb.chain_key == *ALICE
136            })
137            .returning(move |_| Ok(random_hash));
138
139        let ma = announce_multiaddr.clone();
140        let pk = ALICE_OFFCHAIN.public().clone();
141        let mut indexer_action_tracker = MockActionState::new();
142        indexer_action_tracker
143            .expect_register_expectation()
144            .once()
145            .returning(move |_| {
146                Ok(futures::future::ok(SignificantChainEvent {
147                    tx_hash: random_hash,
148                    event_type: ChainEventType::Announcement {
149                        peer: pk.into(),
150                        multiaddresses: vec![ma.clone()],
151                        address: *ALICE,
152                    },
153                })
154                .boxed())
155            });
156
157        let tx_queue = ActionQueue::new(db.clone(), indexer_action_tracker, tx_exec, Default::default());
158        let tx_sender = tx_queue.new_sender();
159        async_std::task::spawn(async move {
160            tx_queue.start().await;
161        });
162
163        let actions = ChainActions::new(&ALICE_KP, db.clone(), tx_sender.clone());
164        let tx_res = actions.announce(&[announce_multiaddr], &ALICE_OFFCHAIN).await?.await?;
165
166        assert_eq!(tx_res.tx_hash, random_hash, "tx hashes must be equal");
167        assert!(matches!(tx_res.action, Action::Announce(_)), "must be announce action");
168        assert!(
169            matches!(tx_res.event, Some(ChainEventType::Announcement { .. })),
170            "must correspond to announcement chain event"
171        );
172
173        Ok(())
174    }
175
176    #[async_std::test]
177    async fn test_announce_should_not_allow_reannouncing_with_same_multiaddress() -> anyhow::Result<()> {
178        let announce_multiaddr = Multiaddr::from_str("/ip4/1.2.3.4/tcp/9009")?;
179
180        let db = HoprDb::new_in_memory(ALICE_KP.clone()).await?;
181        db.set_domain_separator(None, DomainSeparator::Channel, Default::default())
182            .await?;
183
184        db.insert_account(
185            None,
186            AccountEntry::new(
187                *ALICE_OFFCHAIN.public(),
188                *ALICE,
189                AccountType::Announced {
190                    multiaddr: announce_multiaddr.clone(),
191                    updated_block: 0,
192                },
193            ),
194        )
195        .await?;
196
197        let tx_queue = ActionQueue::new(
198            db.clone(),
199            MockActionState::new(),
200            MockTransactionExecutor::new(),
201            Default::default(),
202        );
203        let tx_sender = tx_queue.new_sender();
204
205        let actions = ChainActions::new(&ALICE_KP, db.clone(), tx_sender.clone());
206
207        let res = actions.announce(&[announce_multiaddr], &*ALICE_OFFCHAIN).await;
208        assert!(
209            matches!(res, Err(ChainActionsError::AlreadyAnnounced)),
210            "must not be able to re-announce with same address"
211        );
212
213        Ok(())
214    }
215
216    #[async_std::test]
217    async fn test_withdraw() -> anyhow::Result<()> {
218        let stake = Balance::new(10_u32, BalanceType::HOPR);
219        let random_hash = Hash::from(random_bytes::<{ Hash::SIZE }>());
220
221        let db = HoprDb::new_in_memory(ALICE_KP.clone()).await?;
222        db.set_domain_separator(None, DomainSeparator::Channel, Default::default())
223            .await?;
224
225        let mut tx_exec = MockTransactionExecutor::new();
226        tx_exec
227            .expect_withdraw()
228            .times(1)
229            .withf(move |dst, balance| *BOB == *dst && stake.eq(balance))
230            .returning(move |_, _| Ok(random_hash));
231
232        let mut indexer_action_tracker = MockActionState::new();
233        indexer_action_tracker.expect_register_expectation().never();
234
235        let tx_queue = ActionQueue::new(db.clone(), indexer_action_tracker, tx_exec, Default::default());
236        let tx_sender = tx_queue.new_sender();
237        async_std::task::spawn(async move {
238            tx_queue.start().await;
239        });
240
241        let actions = ChainActions::new(&ALICE_KP, db.clone(), tx_sender.clone());
242
243        let tx_res = actions.withdraw(*BOB, stake).await?.await?;
244
245        assert_eq!(tx_res.tx_hash, random_hash, "tx hashes must be equal");
246        assert!(
247            matches!(tx_res.action, Action::Withdraw(_, _)),
248            "must be withdraw action"
249        );
250        assert!(
251            tx_res.event.is_none(),
252            "withdraw tx must not connect to any chain event"
253        );
254
255        Ok(())
256    }
257
258    #[async_std::test]
259    async fn test_should_not_withdraw_zero_amount() -> anyhow::Result<()> {
260        let db = HoprDb::new_in_memory(ALICE_KP.clone()).await?;
261        db.set_domain_separator(None, DomainSeparator::Channel, Default::default())
262            .await?;
263
264        let tx_queue = ActionQueue::new(
265            db.clone(),
266            MockActionState::new(),
267            MockTransactionExecutor::new(),
268            Default::default(),
269        );
270        let actions = ChainActions::new(&ALICE_KP, db.clone(), tx_queue.new_sender());
271
272        assert!(
273            matches!(
274                actions
275                    .withdraw(*BOB, Balance::zero(BalanceType::HOPR))
276                    .await
277                    .err()
278                    .expect("must be error"),
279                ChainActionsError::InvalidArguments(_)
280            ),
281            "should not allow to withdraw 0"
282        );
283
284        Ok(())
285    }
286}