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