1use crate::wasm_client_storage::WasmClientStorage;
2use crate::wasm_key_locker::WasmKeyLockerManager;
3use crate::wasm_transport::WasmTransportFactory;
4use crate::wasm_try;
5use hashiverse_lib::client::args::Args;
6use hashiverse_lib::client::hashiverse_client::HashiverseClient;
7use hashiverse_lib::client::key_locker::key_locker::KeyLockerManager;
8use hashiverse_lib::tools::buckets::{BucketLocation, BucketType};
9use hashiverse_lib::tools::time::TimeMillis;
10use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
11use hashiverse_lib::tools::pow_generator::pow_generator::PowGenerator;
12use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
13use hashiverse_lib::tools::runtime_services::RuntimeServices;
14use hashiverse_lib::tools::types::Id;
15use log::warn;
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use anyhow::anyhow;
19use tsify::Tsify;
20use wasm_bindgen::prelude::*;
21use wasm_bindgen::JsValue;
22use bytes::Bytes;
23use hashiverse_lib::protocol::posting::encoded_post::EncodedPostV1;
24
25#[wasm_bindgen]
26pub struct HashiverseClientWasm {
28 logged_in: bool,
29 hashiverse_client: HashiverseClient,
30}
31
32#[wasm_bindgen]
33impl HashiverseClientWasm {
34 async fn create_from_xxx(logged_in: bool, key_locker: Arc<dyn hashiverse_lib::client::key_locker::key_locker::KeyLocker>) -> anyhow::Result<Self> {
35 let time_provider: Arc<dyn hashiverse_lib::tools::time_provider::time_provider::TimeProvider> = Arc::new(RealTimeProvider::default());
36 let transport_factory: Arc<dyn hashiverse_lib::transport::transport::TransportFactory> = Arc::new(WasmTransportFactory::default());
37 let client_storage = WasmClientStorage::new().await?;
38 let pow_generator: Arc<dyn PowGenerator> = match crate::get_wasm_parallel_pow_generator() {
39 Some(g) => g as Arc<dyn PowGenerator>,
40 None => {
41 warn!("No native PoW generator available, falling back to SingleThreadedPowGenerator");
42 Arc::new(SingleThreadedPowGenerator::new())
43 }
44 };
45 let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
46 let hashiverse_client = HashiverseClient::new(runtime_services, client_storage, key_locker, Args::new()).await?;
47 Ok(Self { logged_in, hashiverse_client })
48 }
49
50 #[wasm_bindgen]
51 pub async fn create_from_keyphrase(key_phrase: String) -> Result<Self, JsValue> {
52 wasm_try!({
53 let logged_in = !key_phrase.is_empty();
54 let key_locker_manager = WasmKeyLockerManager::new().await?;
55 let key_locker = key_locker_manager.create(key_phrase).await?;
56 Self::create_from_xxx(logged_in, key_locker).await?
57 })
58 }
59
60 #[wasm_bindgen]
61 pub async fn create_from_stored_key(client_id_hex: String) -> Result<Self, JsValue> {
62 wasm_try!({
63 let key_locker_manager = WasmKeyLockerManager::new().await?;
64 let key_locker = key_locker_manager.switch(client_id_hex).await?;
65 Self::create_from_xxx(true, key_locker).await?
66 })
67 }
68
69 #[wasm_bindgen]
70 pub fn logged_in(&self) -> bool {
71 self.logged_in
72 }
73
74 #[wasm_bindgen]
75 pub async fn list_stored_key_ids_v1(&self) -> Result<Vec<String>, JsValue> {
76 wasm_try!({
77 let key_locker_manager = WasmKeyLockerManager::new().await?;
78 key_locker_manager.list().await?
79 })
80 }
81
82 #[wasm_bindgen]
83 pub async fn delete_stored_key_v1(&self, key_public: String) -> Result<(), JsValue> {
84 wasm_try!({
85 let key_locker_manager = WasmKeyLockerManager::new().await?;
86 key_locker_manager.delete(key_public).await?;
87 })
88 }
89
90 #[wasm_bindgen]
91 pub async fn delete_all_stored_keys_v1(&self) -> Result<(), JsValue> {
92 wasm_try!({
93 let key_locker_manager = WasmKeyLockerManager::new().await?;
94 key_locker_manager.reset().await?;
95 })
96 }
97
98 #[wasm_bindgen]
99 pub fn get_client_id(&self) -> String {
100 self.hashiverse_client.client_id().id_hex()
101 }
102
103 #[wasm_bindgen]
104 pub async fn client_storage_reset(&self) -> Result<(), JsValue> {
105 wasm_try!({
106 self.hashiverse_client.client_storage_reset().await?;
107 })
108 }
109
110 #[wasm_bindgen]
111 pub async fn post_v1(&self, post: &str) -> Result<Post, JsValue> {
112 self.post_v2(post, true).await
113 }
114
115 #[wasm_bindgen]
119 pub async fn post_v2(&self, post: &str, wait_for_all_submissions: bool) -> Result<Post, JsValue> {
120 wasm_try!({
121 let (commit_tokens, (encoded_post, raw_bytes)) = self.hashiverse_client.submit_post_with_wait(post, wait_for_all_submissions).await?;
122 let bucket_location = &commit_tokens[0].bucket_location;
123 let client_id = encoded_post.header.client_id()?;
124 let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
125 Post {
126 post_id: encoded_post.post_id.to_hex_str(),
127 time_millis: encoded_post.header.time_millis.0,
128 client_id: client_id.id_hex(),
129 bucket_location: bucket_location.to_html_attr(),
130 post: encoded_post.post,
131 encoded_post_header_hex,
132 healed: false,
133 }
134 })
135 }
136
137 fn meta_post_manager(&self) -> &hashiverse_lib::client::meta_post::meta_post_manager::MetaPostManager {
138 self.hashiverse_client.meta_post_manager()
139 }
140
141 pub async fn set_bio(&self, nickname: String, status: String, selfie: String, avatar: String) -> Result<(), JsValue> {
142 wasm_try!({
143 self.meta_post_manager().set_bio(nickname, status, selfie, avatar).await?;
144 })
145 }
146
147 #[wasm_bindgen]
148 pub async fn submit_feedback_v1(&self, bucket_location: String, post_id: String, feedback_type: u8) -> Result<(), JsValue> {
149 wasm_try!({
150 let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
151 let post_id = Id::from_hex_str(&post_id)?;
152 self.hashiverse_client.submit_feedback(bucket_location, post_id, feedback_type).await?;
153 })
154 }
155
156 #[wasm_bindgen]
157 pub async fn get_post_v1(&self, bucket_location: String, post_id: String) -> Result<Post, JsValue> {
159 wasm_try!({
160 let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
161 let post_id = Id::from_hex_str(&post_id)?;
162 let (bucket_location, post, raw_bytes, healed) = self.hashiverse_client.get_post(bucket_location, &post_id).await?;
163 let client_id = post.header.client_id()?;
164 let encoded_post_header_hex = hex::encode(EncodedPostV1::bytes_without_body(raw_bytes)?);
165 Post {
166 post_id: post.post_id.to_hex_str(),
167 time_millis: post.header.time_millis.0,
168 client_id: client_id.id_hex(),
169 bucket_location: bucket_location.to_html_attr(),
170 post: post.post,
171 encoded_post_header_hex,
172 healed,
173 }
174 })
175 }
176
177 #[wasm_bindgen]
178 pub async fn get_post_feedbacks_v1(&self, bucket_location: String, post_id: String) -> Result<Vec<u32>, JsValue> {
182 wasm_try!({
183 let bucket_location = BucketLocation::from_html_attr(&bucket_location)?;
184 let post_id = Id::from_hex_str(&post_id)?;
185 let post_feedbacks = self.hashiverse_client.get_post_feedbacks(bucket_location, post_id).await?;
186 post_feedbacks.iter().map(|&feedback| feedback.min(u32::MAX as u64) as u32).collect()
187 })
188 }
189
190 #[wasm_bindgen]
191 pub async fn get_bio(&self, id: String) -> Result<Bio, JsValue> {
192 wasm_try!({
193 let meta_post_public = self.meta_post_manager().get_meta_post_public(Id::from_hex_str(&id)?).await?;
194 match meta_post_public {
195 Some(meta_post_public) => Bio {
196 client_id: id,
197 nickname: meta_post_public.nickname.value.unwrap_or_default(),
198 status: meta_post_public.status.value.unwrap_or_default(),
199 selfie: meta_post_public.selfie.value.unwrap_or_default(),
200 avatar: meta_post_public.avatar.value.unwrap_or_default(),
201 },
202 None => Bio {
203 client_id: id,
204 nickname: "".to_string(),
205 status: "".to_string(),
206 selfie: "".to_string(),
207 avatar: "".to_string(),
208 },
209 }
210 })
211 }
212
213 #[wasm_bindgen]
214 pub async fn get_all_bios(&self) -> Result<Vec<Bio>, JsValue> {
215 wasm_try!({
216 let meta_post_publics = self.meta_post_manager().get_all_meta_post_publics().await?;
217 meta_post_publics.into_iter()
218 .map(|(client_id, meta_post_public)| Bio {
219 client_id,
220 nickname: meta_post_public.nickname.value.unwrap_or_default(),
221 status: meta_post_public.status.value.unwrap_or_default(),
222 selfie: meta_post_public.selfie.value.unwrap_or_default(),
223 avatar: meta_post_public.avatar.value.unwrap_or_default(),
224 })
225 .collect()
226 })
227 }
228
229 #[wasm_bindgen]
230 pub async fn get_all_known_peers_v1(&self) -> Result<Vec<PeerInfoV1>, JsValue> {
231 wasm_try!({
232 self.hashiverse_client.get_all_known_peers().await
233 .into_iter()
234 .map(|peer| PeerInfoV1 {
235 peer_id_hex: peer.id.to_hex_str(),
236 address: peer.address,
237 version: peer.version,
238 timestamp_millis: peer.timestamp.0,
239 pow_initial: peer.pow_initial.pow.0,
240 pow_current_day: peer.pow_current_day.pow.0,
241 pow_current_month: peer.pow_current_month.pow.0,
242 })
243 .collect::<Vec<_>>()
244 })
245 }
246
247 #[wasm_bindgen]
248 pub async fn get_peer_stats_v1(&self, peer_id_hex: String) -> Result<JsValue, JsValue> {
249 wasm_try!({
250 let peer_id = Id::from_hex_str(&peer_id_hex)?;
251 let doc = self.hashiverse_client.fetch_peer_stats(&peer_id).await?;
252 let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
253 doc.serialize(&serializer).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?
254 })
255 }
256
257 #[wasm_bindgen]
258 pub async fn get_active_pow_jobs_v1(&self) -> Result<Vec<PowJobStatusV1>, JsValue> {
259 wasm_try!({
260 self.hashiverse_client.active_pow_jobs()
261 .into_iter()
262 .map(|job| PowJobStatusV1 {
263 label: job.label,
264 pow_min: job.pow_min.0,
265 best_pow_so_far: job.best_pow_so_far.0,
266 })
267 .collect::<Vec<_>>()
268 })
269 }
270
271 #[wasm_bindgen]
274 pub async fn is_pow_busy_v1(&self, within_millis: u32) -> Result<bool, JsValue> {
275 wasm_try!({ self.hashiverse_client.is_pow_busy(within_millis as i64) })
276 }
277
278 fn post_process_timeline_posts(&self, encoded_posts: Vec<(BucketLocation, EncodedPostV1, Bytes, bool)>, oldest_processed_time_millis: TimeMillis) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
279 let response = SingleTimelineGetMoreV1Response {
280 oldest_processed_time_millis: if oldest_processed_time_millis == TimeMillis::MAX { None } else { Some(oldest_processed_time_millis.0) },
281 posts: encoded_posts
282 .into_iter()
283 .filter_map(|(bucket_location, post, raw_bytes, healed)| {
284 let client_id = match post.header.client_id() {
285 Ok(client_id) => client_id,
286 Err(e) => {
287 warn!("Skipping post with bad client_id in header: {}", e);
288 return None;
289 }
290 };
291 let encoded_post_header_hex = match EncodedPostV1::bytes_without_body(raw_bytes) {
292 Ok(header_bytes) => hex::encode(header_bytes),
293 Err(e) => {
294 warn!("Skipping post with bad header bytes: {}", e);
295 return None;
296 }
297 };
298 Some(Post {
299 post_id: post.post_id.to_hex_str(),
300 time_millis: post.header.time_millis.0,
301 client_id: client_id.id_hex(),
302 bucket_location: bucket_location.to_html_attr(),
303 post: post.post,
304 encoded_post_header_hex,
305 healed,
306 })
307 })
308 .collect(),
309 };
310
311 Ok(response)
312 }
313
314
315 #[wasm_bindgen]
316 pub async fn single_timeline_reset(&self) -> Result<(), JsValue> {
317 wasm_try!({
318 self.hashiverse_client.single_timeline_reset().await?;
319 })
320 }
321
322 async fn single_timeline_get_more(&self, bucket_type: BucketType, base_id: &Id) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
323 let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.single_timeline_get_more(bucket_type, base_id).await?;
324 self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
325 }
326
327 #[wasm_bindgen]
328 pub async fn single_timeline_get_more_me_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
329 wasm_try!({
330 let id = self.hashiverse_client.client_id().id;
331 self.single_timeline_get_more(BucketType::User, &id).await?
332 })
333 }
334
335 #[wasm_bindgen]
336 pub async fn single_timeline_get_more_me_mentioned_v1(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
337 wasm_try!({
338 let id = self.hashiverse_client.client_id().id;
339 self.single_timeline_get_more(BucketType::Mention, &id).await?
340 })
341 }
342
343 #[wasm_bindgen]
344 pub async fn single_timeline_get_more_hashtag_v1(&self, hashtag: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
345 wasm_try!({
346 let id = Id::from_hashtag_str(&hashtag)?;
347 self.single_timeline_get_more(BucketType::Hashtag, &id).await?
348 })
349 }
350
351 #[wasm_bindgen]
352 pub async fn single_timeline_get_more_user_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
353 wasm_try!({
354 let id = Id::from_hex_str(&client_id_hex)?;
355 self.single_timeline_get_more(BucketType::User, &id).await?
356 })
357 }
358
359 #[wasm_bindgen]
360 pub async fn single_timeline_get_more_user_mentioned_v1(&self, client_id_hex: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
361 wasm_try!({
362 let id = Id::from_hex_str(&client_id_hex)?;
363 self.single_timeline_get_more(BucketType::Mention, &id).await?
364 })
365 }
366
367 #[wasm_bindgen]
368 pub async fn single_timeline_get_more_reply_to_post_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
369 wasm_try!({
370 let id = Id::from_hex_str(&post_id)?;
371 self.single_timeline_get_more(BucketType::ReplyToPost, &id).await?
372 })
373 }
374
375 #[wasm_bindgen]
376 pub async fn single_timeline_get_more_sequel_v1(&self, post_id: String) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
377 wasm_try!({
378 let id = Id::from_hex_str(&post_id)?;
379 self.single_timeline_get_more(BucketType::Sequel, &id).await?
380 })
381 }
382
383 #[wasm_bindgen]
384 pub async fn multiple_timeline_reset(&self) -> Result<(), JsValue> {
385 wasm_try!({
386 self.hashiverse_client.multiple_timeline_reset().await?;
387 })
388 }
389
390 async fn multiple_timeline_get_more(&self, bucket_type: BucketType, base_ids: &Vec<Id>) -> anyhow::Result<SingleTimelineGetMoreV1Response> {
391 let (encoded_posts, oldest_processed_time_millis) = self.hashiverse_client.multiple_timeline_get_more(bucket_type, base_ids).await?;
392 self.post_process_timeline_posts(encoded_posts, oldest_processed_time_millis)
393 }
394
395 #[wasm_bindgen]
396 pub async fn multiple_timeline_get_more_followed_users(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
397 wasm_try!({
398 let ids = self.meta_post_manager().get_followed_client_ids().await?;
399 self.multiple_timeline_get_more(BucketType::User, &ids).await?
400 })
401 }
402
403 #[wasm_bindgen]
404 pub async fn get_followed_client_ids_v1(&self) -> Result<Vec<String>, JsValue> {
405 wasm_try!({
406 let ids = self.meta_post_manager().get_followed_client_ids().await?;
407 ids.into_iter().map(|id| id.to_hex_str()).collect()
408 })
409 }
410
411 #[wasm_bindgen]
412 pub async fn set_followed_client_ids_v1(&self, client_ids: JsValue) -> Result<(), JsValue> {
413 wasm_try!({
414 let client_id_strs: Vec<String> = serde_wasm_bindgen::from_value(client_ids).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
415 let ids = client_id_strs.iter().map(|s| Id::from_hex_str(s)).collect::<anyhow::Result<Vec<_>>>()?;
416 self.meta_post_manager().set_followed_client_ids(ids).await?;
417 })
418 }
419
420 #[wasm_bindgen]
421 pub async fn set_followed_client_id_v1(&self, client_id: String, is_followed: bool) -> Result<(), JsValue> {
422 wasm_try!({
423 let id = Id::from_hex_str(&client_id)?;
424 self.meta_post_manager().set_followed_client_id(id, is_followed).await?;
425 })
426 }
427
428 #[wasm_bindgen]
429 pub async fn multiple_timeline_get_more_followed_hashtags(&self) -> Result<SingleTimelineGetMoreV1Response, JsValue> {
430 wasm_try!({
431 let hashtags = self.meta_post_manager().get_followed_hashtags().await?;
432 let ids = hashtags.iter().map(|h| Id::from_hashtag_str(h)).collect::<anyhow::Result<Vec<_>>>()?;
433 self.multiple_timeline_get_more(BucketType::Hashtag, &ids).await?
434 })
435 }
436
437 #[wasm_bindgen]
438 pub async fn get_followed_hashtags_v1(&self) -> Result<Vec<String>, JsValue> {
439 wasm_try!({
440 self.meta_post_manager().get_followed_hashtags().await?
441 })
442 }
443
444 #[wasm_bindgen]
445 pub async fn set_followed_hashtags_v1(&self, hashtags: JsValue) -> Result<(), JsValue> {
446 wasm_try!({
447 let hashtags: Vec<String> = serde_wasm_bindgen::from_value(hashtags).map_err(|e| anyhow!("serde_wasm_bindgen::from_value error: {}", e))?;
448 self.meta_post_manager().set_followed_hashtags(hashtags).await?;
449 })
450 }
451
452 #[wasm_bindgen]
453 pub async fn set_followed_hashtag_v1(&self, hashtag: String, is_followed: bool) -> Result<(), JsValue> {
454 wasm_try!({
455 self.meta_post_manager().set_followed_hashtag(hashtag, is_followed).await?;
456 })
457 }
458
459 #[wasm_bindgen]
464 pub async fn submit_meta_post_v1(&self) -> Result<(), JsValue> {
465 wasm_try!({
466 self.hashiverse_client.submit_meta_post().await?;
467 })
468 }
469
470 #[wasm_bindgen]
471 pub async fn ensure_meta_post_in_current_bucket_v1(&self) -> Result<(), JsValue> {
472 wasm_try!({
473 self.hashiverse_client.ensure_meta_post_in_current_bucket().await?;
474 })
475 }
476
477 #[wasm_bindgen]
482 pub async fn get_content_thresholds_v1(&self) -> Result<JsValue, JsValue> {
483 wasm_try!({
484 let thresholds = self.meta_post_manager().get_content_thresholds().await?;
485 let thresholds_js: std::collections::HashMap<String, u32> = thresholds.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
489 let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
490 thresholds_js.serialize(&serializer).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?
491 })
492 }
493
494 #[wasm_bindgen]
495 pub async fn set_content_thresholds_v1(&self, thresholds: JsValue) -> Result<(), JsValue> {
496 wasm_try!({
497 let thresholds_str: std::collections::HashMap<String, u32> = serde_wasm_bindgen::from_value(thresholds).map_err(|e| anyhow!("serde_wasm_bindgen error: {}", e))?;
498 let thresholds: std::collections::HashMap<u8, u32> = thresholds_str.into_iter()
499 .map(|(k, v)| Ok((k.parse::<u8>().map_err(|e| anyhow!("invalid feedback_type key: {}", e))?, v)))
500 .collect::<anyhow::Result<_>>()?;
501 self.meta_post_manager().set_content_thresholds(thresholds).await?;
502 })
503 }
504
505 #[wasm_bindgen]
510 pub async fn get_skip_warnings_for_followed_v1(&self) -> Result<bool, JsValue> {
511 wasm_try!({
512 self.meta_post_manager().get_skip_warnings_for_followed().await?
513 })
514 }
515
516 #[wasm_bindgen]
517 pub async fn set_skip_warnings_for_followed_v1(&self, value: bool) -> Result<(), JsValue> {
518 wasm_try!({
519 self.meta_post_manager().set_skip_warnings_for_followed(value).await?;
520 })
521 }
522
523 #[wasm_bindgen]
524 pub async fn fetch_url_preview_v1(&self, url: String) -> Result<UrlPreview, JsValue> {
525 wasm_try!({
526 let preview = self.hashiverse_client.fetch_url_preview(&url).await?;
527 UrlPreview {
528 url: preview.url,
529 title: preview.title,
530 description: preview.description,
531 image_url: preview.image_url,
532 }
533 })
534 }
535
536 #[wasm_bindgen]
537 pub async fn fetch_trending_hashtags_v1(&self, limit: u16) -> Result<TrendingHashtagsFetchResponse, JsValue> {
538 wasm_try!({
539 let response = self.hashiverse_client.fetch_trending_hashtags(limit).await?;
540 TrendingHashtagsFetchResponse {
541 trending_hashtags: response.trending_hashtags.into_iter().map(|entry| TrendingHashtag {
542 hashtag: entry.hashtag,
543 count: entry.count,
544 }).collect(),
545 }
546 })
547 }
548}
549
550#[derive(Tsify, Serialize, Deserialize)]
551#[tsify(into_wasm_abi)]
552pub struct SingleTimelineGetMoreV1Response {
553 pub posts: Vec<Post>,
554 pub oldest_processed_time_millis: Option<i64>,
555}
556
557#[derive(Tsify, Serialize, Deserialize)]
558#[tsify(into_wasm_abi)]
559pub struct Post {
560 pub post_id: String,
561 pub time_millis: i64,
562 pub client_id: String,
563 pub bucket_location: String,
564 pub post: String,
565 pub encoded_post_header_hex: String, pub healed: bool, }
568
569#[derive(Tsify, Serialize, Deserialize)]
570#[tsify(into_wasm_abi)]
571pub struct Bio {
572 pub client_id: String,
573 pub nickname: String,
574 pub status: String,
575 pub selfie: String,
576 pub avatar: String,
577}
578
579#[derive(Tsify, Serialize, Deserialize)]
580#[tsify(into_wasm_abi)]
581pub struct UrlPreview {
582 pub url: String,
583 pub title: String,
584 pub description: String,
585 pub image_url: String,
586}
587
588#[derive(Tsify, Serialize, Deserialize)]
589#[tsify(into_wasm_abi)]
590pub struct TrendingHashtag {
591 pub hashtag: String,
592 pub count: u64,
593}
594
595#[derive(Tsify, Serialize, Deserialize)]
596#[tsify(into_wasm_abi)]
597pub struct TrendingHashtagsFetchResponse {
598 pub trending_hashtags: Vec<TrendingHashtag>,
599}
600
601#[derive(Tsify, Serialize, Deserialize)]
602#[tsify(into_wasm_abi)]
603pub struct PeerInfoV1 {
604 pub peer_id_hex: String,
605 pub address: String,
606 pub version: String,
607 pub timestamp_millis: i64,
608 pub pow_initial: u8,
609 pub pow_current_day: u8,
610 pub pow_current_month: u8,
611}
612
613#[derive(Tsify, Serialize, Deserialize)]
614#[tsify(into_wasm_abi)]
615pub struct PowJobStatusV1 {
616 pub label: String,
617 pub pow_min: u8,
618 pub best_pow_so_far: u8,
619}