1use 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#[async_trait]
32pub trait NodeActions {
33 async fn withdraw(&self, recipient: Address, amount: Balance) -> Result<PendingAction>;
35
36 async fn announce(&self, multiaddrs: &[Multiaddr], offchain_key: &OffchainKeypair) -> Result<PendingAction>;
39
40 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 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 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}