steam_trading/
lib.rs

1//! Steam trade manager is the module that allows you to automate trade offers, by extending `SteamAuthenticator`.
2//!
3//! It inherently needs `SteamAuthenticator` as a dependency, since we need cookies from Steam Community and Steam Store
4//! to be able to create and accept trade offers, along with mobile confirmations.
5//!
6//! **IT IS VERY IMPORTANT THAT STEAM GUARD IS ENABLED ON THE ACCOUNT BEING USED, WITH MOBILE CONFIRMATIONS.**
7//!
8//! Currently, `SteamAuthenticator` is "stateless", in comparison of alternatives such as Node.js.
9//! This means that it does not need to stay up running and react to events.
10//!
11//! But this also means that you will need to keep track of trades and polling yourself, but it won't be much work,
12//! since there are convenience functions for almost every need.
13//!
14//! Perhaps the event based trading experience will be an extension someday, but for now this works fine.
15//!
16//! Compiles on stable Rust.
17
18#![allow(dead_code)]
19// #![warn(missing_docs, missing_doc_code_examples)]
20#![deny(
21    missing_debug_implementations,
22    missing_copy_implementations,
23    trivial_casts,
24    trivial_numeric_casts,
25    unsafe_code,
26    unused_import_braces,
27    unused_qualifications
28)]
29
30use std::str::FromStr;
31use std::sync::Arc;
32use std::time::Duration;
33
34use const_format::concatcp;
35pub use errors::ConfirmationError;
36pub use errors::InternalError;
37pub use errors::OfferError;
38pub use errors::TradeError;
39pub use errors::TradelinkError;
40use futures::stream::FuturesOrdered;
41use futures::StreamExt;
42use futures::TryFutureExt;
43use futures_timer::Delay;
44use serde::de::DeserializeOwned;
45use steam_language_gen::generated::enums::ETradeOfferState;
46use steam_mobile::user::PresentMaFile;
47use steam_mobile::Authenticated;
48use steam_mobile::ConfirmationAction;
49use steam_mobile::HeaderMap;
50use steam_mobile::Method;
51use steam_mobile::SteamAuthenticator;
52use steam_mobile::STEAM_COMMUNITY_HOST;
53use steamid_parser::SteamID;
54use tappet::response_types::GetTradeHistoryResponse;
55use tappet::response_types::GetTradeOffersResponse;
56use tappet::response_types::TradeHistory_Trade;
57use tappet::response_types::TradeOffer_Trade;
58use tappet::ExecutorResponse;
59use tappet::SteamAPI;
60use tracing::debug;
61pub use types::asset_collection::AssetCollection;
62pub use types::trade_link::Tradelink;
63pub use types::trade_offer::TradeOffer;
64
65use crate::additional_checks::check_steam_guard_error;
66use crate::api_extensions::FilterBy;
67use crate::api_extensions::HasAssets;
68use crate::errors::error_from_strmessage;
69use crate::errors::tradeoffer_error_from_eresult;
70use crate::errors::TradeError::GeneralError;
71use crate::types::sessionid::HasSessionID;
72use crate::types::trade_offer_web::TradeOfferAcceptRequest;
73use crate::types::trade_offer_web::TradeOfferCancelResponse;
74use crate::types::trade_offer_web::TradeOfferCommonParameters;
75use crate::types::trade_offer_web::TradeOfferCreateRequest;
76use crate::types::trade_offer_web::TradeOfferCreateResponse;
77use crate::types::trade_offer_web::TradeOfferGenericErrorResponse;
78use crate::types::trade_offer_web::TradeOfferGenericRequest;
79use crate::types::trade_offer_web::TradeOfferParams;
80use crate::types::TradeKind;
81
82mod additional_checks;
83pub mod api_extensions;
84mod errors;
85#[cfg(feature = "time")]
86pub mod time;
87mod types;
88
89const TRADEOFFER_BASE: &str = "https://steamcommunity.com/tradeoffer/";
90const TRADEOFFER_NEW_URL: &str = concatcp!(TRADEOFFER_BASE, "new/send");
91
92/// This is decided upon various factors, mainly stability of Steam servers when dealing with huge
93/// trade offers.
94///
95/// Consider this when creating trade websites.
96pub const TRADE_MAX_ITEMS: u8 = u8::MAX;
97
98/// Max trade offers to a single account.
99pub const TRADE_MAX_TRADES_PER_SINGLE_USER: u8 = 5;
100
101/// Max total sent trade offers.
102pub const TRADE_MAX_ONGOING_TRADES: u8 = 30;
103
104/// Standard delay, in milliseconds
105const STANDARD_DELAY: u64 = 1000;
106
107const MAX_HISTORICAL_CUTOFF: u32 = u32::MAX;
108
109pub(crate) type SteamCompleteAuthenticator = SteamAuthenticator<Authenticated, PresentMaFile>;
110
111#[derive(Debug)]
112pub struct SteamTradeManager<'a> {
113    authenticator: &'a SteamCompleteAuthenticator,
114    api_client: SteamAPI,
115}
116
117impl<'a> SteamTradeManager<'a> {
118    /// Returns a new `[SteamTradeManager]`.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if API Key is not cached by `authenticator`.
123    pub fn new(
124        authenticator: &'a SteamAuthenticator<Authenticated, PresentMaFile>,
125    ) -> Result<SteamTradeManager<'a>, TradeError> {
126        let api_key = authenticator
127            .api_key()
128            .ok_or_else(|| GeneralError("Can't build without an API Key cached.".to_string()))?;
129
130        Ok(Self {
131            authenticator,
132            api_client: SteamAPI::new(api_key),
133        })
134    }
135
136    /// Checks whether the user of `tradelink` has recently activated his mobile SteamGuard.
137    pub async fn check_steam_guard_recently_activated(&self, tradelink: Tradelink) -> Result<(), TradeError> {
138        let Tradelink { partner_id, token, .. } = tradelink;
139
140        check_steam_guard_error(self.authenticator, partner_id, &*token)
141            .err_into()
142            .await
143    }
144
145    /// Call to GetTradeOffers endpoint.
146    ///
147    /// Convenience function that fetches information about active trades for the current logged in account.
148    pub async fn get_trade_offers(
149        &self,
150        sent: bool,
151        received: bool,
152        active_only: bool,
153    ) -> Result<GetTradeOffersResponse, TradeError> {
154        self.api_client
155            .get()
156            .IEconService()
157            .GetTradeOffers(
158                sent,
159                received,
160                MAX_HISTORICAL_CUTOFF,
161                Some(active_only),
162                None,
163                None,
164                None,
165            )
166            .execute_with_response()
167            .err_into()
168            .await
169    }
170
171    /// Returns `[GetTradeHistoryResponse]` after a call to `GetTradeHistory` endpoint.
172    /// `max_trades` If not set will default to a maximum of 500 trade offers.
173    ///
174    /// Contains Information about completed trades and recovery of the new asset ids that were generated after the
175    /// trade.
176    async fn get_trade_offers_history(
177        &self,
178        max_trades: Option<u32>,
179        include_failed: bool,
180    ) -> Result<GetTradeHistoryResponse, TradeError> {
181        let max_trades = max_trades.unwrap_or(500);
182
183        self.api_client
184            .get()
185            .IEconService()
186            .GetTradeHistory(max_trades, include_failed, false, None, None, None, None, None)
187            .execute_with_response()
188            .err_into()
189            .await
190    }
191
192    /// Returns a single trade offer.
193    pub async fn get_tradeoffer_by_id(&self, tradeoffer_id: u64) -> Result<Vec<TradeOffer_Trade>, TradeError> {
194        self.get_trade_offers(true, true, true)
195            .map_ok(|tradeoffers| tradeoffers.filter_by(|offer| offer.tradeofferid == tradeoffer_id))
196            .await
197    }
198
199    /// Returns the new asset ids for a trade of `tradeid`.
200    ///
201    /// Convenience function that internally calls `get_trade_offers_history` but filters it directly.
202    pub async fn get_new_assetids(&self, tradeid: i64) -> Result<Vec<i64>, TradeError> {
203        let found_trade: TradeHistory_Trade = self
204            .get_trade_offers_history(None, false)
205            .map_ok(|tradeoffers| tradeoffers.filter_by(|trade| trade.tradeid == tradeid))
206            .await?
207            .swap_remove(0);
208
209        Ok(found_trade
210            .every_asset()
211            .into_iter()
212            .map(|traded_asset| traded_asset.new_assetid)
213            .collect::<Vec<_>>())
214    }
215
216    /// Convenience function to auto decline *received* offers.
217    ///
218    /// Helps to keep the trade offers log clean of the total trade offer limit, if there is one.
219    pub async fn decline_received_offers(&self) -> Result<(), TradeError> {
220        let mut deny_offers_fut = FuturesOrdered::new();
221
222        let active_received_offers: Vec<TradeOffer_Trade> = self
223            .get_trade_offers(true, true, true)
224            .map_ok(|tradeoffers| {
225                tradeoffers.filter_by(|offer| offer.state == ETradeOfferState::Active && !offer.is_our_offer)
226            })
227            .await?;
228
229        let total = active_received_offers.len();
230        active_received_offers
231            .into_iter()
232            .map(|x| x.tradeofferid)
233            .for_each(|x| {
234                deny_offers_fut.push_back(
235                    self.deny_offer(x)
236                        .map_ok(|_| Delay::new(Duration::from_millis(STANDARD_DELAY))),
237                );
238            });
239
240        while let Some(result) = deny_offers_fut.next().await {
241            match result {
242                Ok(_) => {}
243                Err(e) => return Err(e),
244            }
245        }
246
247        debug!("Successfully denied a total of {} received offers.", total);
248
249        Ok(())
250    }
251
252    /// Creates a new trade offer, and confirms it with the inner [`SteamAuthenticator`].
253    ///
254    /// Returns the `tradeoffer_id` on success and if the confirmation was not found but the trade created.
255    pub async fn create_offer_and_confirm(&self, tradeoffer: TradeOffer) -> Result<u64, TradeError> {
256        let tradeoffer_id = self.create_offer(tradeoffer).await?;
257        Delay::new(Duration::from_millis(STANDARD_DELAY));
258        self.accept_offer(tradeoffer_id).await?;
259        Ok(tradeoffer_id)
260    }
261
262    /// Creates a new trade offer and return its `tradeoffer_id`.
263    pub async fn create_offer(&self, tradeoffer: TradeOffer) -> Result<u64, TradeError> {
264        self.request::<TradeOfferCreateResponse>(TradeKind::Create(tradeoffer), None)
265            .map_ok(|c| {
266                c.tradeofferid
267                    .and_then(|id| u64::from_str(&id).ok())
268                    .expect("Safe to unwrap.")
269            })
270            .await
271    }
272
273    /// Accepts a trade offer made to this account and confirms it with inner [SteamAuthenticator]
274    ///
275    /// **Note: This is irreversable, be extra careful when accepting any trade offer.**
276    pub async fn accept_offer(&self, tradeoffer_id: u64) -> Result<(), TradeError> {
277        let resp: TradeOfferCreateResponse = self.request(TradeKind::Accept, Some(tradeoffer_id)).await?;
278
279        if resp.needs_mobile_confirmation.is_none() && !resp.needs_mobile_confirmation.unwrap() {
280            return Ok(());
281        }
282
283        let confirmation = self
284            .authenticator
285            .fetch_confirmations()
286            .inspect_ok(|_| debug!("Confirmations fetched successfully."))
287            .await?
288            .into_iter()
289            .find(|c| c.has_trade_offer_id(tradeoffer_id))
290            .ok_or_else(|| TradeError::from(ConfirmationError::NotFound))?;
291
292        self.authenticator
293            .process_confirmations(ConfirmationAction::Accept, std::iter::once(confirmation))
294            .err_into()
295            .await
296    }
297
298    /// Denies a trade offer sent to this account.
299    ///
300    /// # Errors
301    ///
302    /// Will error if couldn't deny the trade offer.
303    pub async fn deny_offer(&self, tradeoffer_id: u64) -> Result<(), TradeError> {
304        self.request::<TradeOfferCancelResponse>(TradeKind::Decline, Some(tradeoffer_id))
305            .await
306            .map(|_| ())
307    }
308
309    /// Cancel a trade offer sent by this account.
310    ///
311    /// # Errors
312    ///
313    /// Will error if couldn't cancel the tradeoffer.
314    pub async fn cancel_offer(&self, tradeoffer_id: u64) -> Result<(), TradeError> {
315        self.request::<TradeOfferCancelResponse>(TradeKind::Cancel, Some(tradeoffer_id))
316            .await
317            .map(|_| ())
318    }
319
320    /// Check current session health, injects SessionID cookie, and send the request.
321    async fn request<OUTPUT>(&self, operation: TradeKind, tradeoffer_id: Option<u64>) -> Result<OUTPUT, TradeError>
322    where
323        OUTPUT: DeserializeOwned + Send + Sync,
324    {
325        let tradeoffer_endpoint = operation.endpoint(tradeoffer_id);
326
327        let mut header: Option<HeaderMap> = None;
328        let mut partner_id_and_token = None;
329
330        match &operation {
331            TradeKind::Create(offer) => {
332                header.replace(HeaderMap::new());
333                header
334                    .as_mut()
335                    .unwrap()
336                    .insert("Referer", (TRADEOFFER_BASE.to_owned() + "new").parse().unwrap());
337
338                partner_id_and_token = Some((
339                    offer.their_tradelink.partner_id.clone(),
340                    offer.their_tradelink.token.clone(),
341                ));
342            }
343            TradeKind::Accept => {
344                header.replace(HeaderMap::new());
345                header.as_mut().unwrap().insert(
346                    "Referer",
347                    format!("{}{}/", TRADEOFFER_BASE, tradeoffer_id.unwrap())
348                        .parse()
349                        .unwrap(),
350                );
351            }
352            _ => {}
353        };
354
355        let mut request: Box<dyn HasSessionID> = match operation {
356            TradeKind::Accept => {
357                let partner_id = self
358                    .get_tradeoffer_by_id(tradeoffer_id.unwrap())
359                    .await?
360                    .first()
361                    .map(|c| SteamID::from_steam3(c.tradeofferid as u32, None, None))
362                    .map(|steamid| steamid.to_steam64())
363                    .ok_or(OfferError::NoMatch)?;
364
365                let trade_request_data = TradeOfferAcceptRequest {
366                    common: TradeOfferCommonParameters {
367                        their_steamid: partner_id,
368                        ..Default::default()
369                    },
370                    tradeofferid: tradeoffer_id.unwrap(),
371                    ..Default::default()
372                };
373
374                debug!("{:#}", serde_json::to_string_pretty(&trade_request_data).unwrap());
375                Box::new(trade_request_data)
376            }
377
378            TradeKind::Cancel | TradeKind::Decline => Box::<TradeOfferGenericRequest>::default(),
379            TradeKind::Create(offer) => Box::new(Self::prepare_offer(offer)?),
380        };
381
382        // TODO: Check if session is ok, then inject cookie
383        let session_id_cookie = self
384            .authenticator
385            .dump_cookie(STEAM_COMMUNITY_HOST, "sessionid")
386            .ok_or_else(|| {
387                GeneralError("Somehow you don't have a sessionid cookie. You need to login first.".to_string())
388            })?;
389
390        request.set_sessionid(session_id_cookie);
391        let request: Arc<dyn HasSessionID> = Arc::from(request);
392
393        let response = self
394            .authenticator
395            .request_custom_endpoint(tradeoffer_endpoint, Method::POST, header, Some(request))
396            .err_into::<InternalError>()
397            .await?;
398        let response_text = response.text().err_into::<InternalError>().await?;
399
400        match serde_json::from_str::<OUTPUT>(&response_text) {
401            Ok(response) => Ok(response),
402            Err(_) => {
403                // try to match into a generic message
404                if let Ok(resp) = serde_json::from_str::<TradeOfferGenericErrorResponse>(&response_text) {
405                    if resp.error_message.is_some() {
406                        let err_msg = resp.error_message.unwrap();
407                        Err(error_from_strmessage(&*err_msg).unwrap().into())
408                    } else if resp.eresult.is_some() {
409                        let eresult = resp.eresult.unwrap();
410                        Err(tradeoffer_error_from_eresult(eresult).into())
411                    } else {
412                        tracing::error!("Unable to understand Steam Response. Please report it as bug.");
413                        Err(OfferError::GeneralFailure(format!(
414                            "Steam Response: {}\nThis is a bug, please report it.",
415                            response_text
416                        ))
417                        .into())
418                    }
419                } else {
420                    if let Some((steamid, token)) = partner_id_and_token {
421                        check_steam_guard_error(self.authenticator, steamid, &token).await?;
422                    }
423
424                    tracing::error!(
425                        "Failure to deserialize a valid response Steam Offer response. Maybe Steam Servers are \
426                         offline."
427                    );
428                    Err(OfferError::GeneralFailure(format!("Steam Response: {}", response_text)).into())
429                }
430            }
431        }
432    }
433
434    /// Checks that the tradeoffer is valid, and process it, getting the trade token and steamid3, into a
435    /// `TradeOfferCreateRequest`, ready to send it.
436    fn prepare_offer(tradeoffer: TradeOffer) -> Result<TradeOfferCreateRequest, TradeError> {
437        TradeOffer::validate(&tradeoffer.my_assets, &tradeoffer.their_assets)?;
438
439        let tradelink = tradeoffer.their_tradelink.clone();
440
441        let their_steamid64 = tradelink.partner_id.to_steam64();
442        let trade_offer_params = TradeOfferParams {
443            trade_offer_access_token: tradelink.token,
444        };
445
446        Ok(TradeOfferCreateRequest::new(
447            their_steamid64,
448            tradeoffer,
449            trade_offer_params,
450        ))
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn get_tradeoffer_url_with_token() -> &'static str {
459        "https://steamcommunity.com/tradeoffer/new/?partner=79925588&token=Ob27qXzn"
460    }
461
462    fn get_tradeoffer_url_without_token() -> &'static str {
463        "https://steamcommunity.com/tradeoffer/new/?partner=79925588"
464    }
465
466    #[allow(clippy::too_many_lines)]
467    fn sample_trade_history_response() -> GetTradeHistoryResponse {
468        let response = r#"{
469  "response": {
470    "more": true,
471    "trades": [
472      {
473        "tradeid": "3622543526924228084",
474        "steamid_other": "76561198040191316",
475        "time_init": 1603998438,
476        "status": 3,
477        "assets_given": [
478          {
479            "appid": 730,
480            "contextid": "2",
481            "assetid": "15319724006",
482            "amount": "1",
483            "classid": "3035569977",
484            "instanceid": "302028390",
485            "new_assetid": "19793871926",
486            "new_contextid": "2"
487          }
488        ]
489      },
490      {
491        "tradeid": "3151905948742966439",
492        "steamid_other": "76561198040191316",
493        "time_init": 1594190957,
494        "status": 3,
495        "assets_received": [
496          {
497            "appid": 730,
498            "contextid": "2",
499            "assetid": "17300115678",
500            "amount": "1",
501            "classid": "1989330488",
502            "instanceid": "302028390",
503            "new_assetid": "19034292089",
504            "new_contextid": "2"
505          }
506        ]
507      },
508      {
509        "tradeid": "3151905948742946486",
510        "steamid_other": "76561198040191316",
511        "time_init": 1594190486,
512        "status": 3,
513        "assets_received": [
514          {
515            "appid": 730,
516            "contextid": "2",
517            "assetid": "17341684309",
518            "amount": "1",
519            "classid": "1989279043",
520            "instanceid": "302028390",
521            "new_assetid": "19034259977",
522            "new_contextid": "2"
523          }
524        ]
525      },
526      {
527        "tradeid": "3151905948734426645",
528        "steamid_other": "76561198017653157",
529        "time_init": 1593990409,
530        "status": 3,
531        "assets_received": [
532          {
533            "appid": 730,
534            "contextid": "2",
535            "assetid": "8246208960",
536            "amount": "1",
537            "classid": "310776668",
538            "instanceid": "302028390",
539            "new_assetid": "19019879428",
540            "new_contextid": "2"
541          },
542          {
543            "appid": 730,
544            "contextid": "2",
545            "assetid": "11589364986",
546            "amount": "1",
547            "classid": "469467368",
548            "instanceid": "302028390",
549            "new_assetid": "19019879441",
550            "new_contextid": "2"
551          }
552        ]
553      },
554      {
555        "tradeid": "2816382071757670028",
556        "steamid_other": "76561198040191316",
557        "time_init": 1587519425,
558        "status": 3,
559        "assets_received": [
560          {
561            "appid": 730,
562            "contextid": "2",
563            "assetid": "17921552800",
564            "amount": "1",
565            "classid": "1989286992",
566            "instanceid": "302028390",
567            "new_assetid": "18426035472",
568            "new_contextid": "2"
569          }
570        ]
571      },
572      {
573        "tradeid": "2289455842905057389",
574        "steamid_other": "76561198994791561",
575        "time_init": 1582942255,
576        "time_escrow_end": 1584238255,
577        "status": 3,
578        "assets_given": [
579          {
580            "appid": 730,
581            "contextid": "2",
582            "assetid": "16832065568",
583            "amount": "1",
584            "classid": "1989312177",
585            "instanceid": "302028390",
586            "new_assetid": "18074934023",
587            "new_contextid": "2"
588          }
589        ]
590      },
591      {
592        "tradeid": "2022547174628342555",
593        "steamid_other": "76561197966598809",
594        "time_init": 1515645117,
595        "status": 3,
596        "assets_received": [
597          {
598            "appid": 730,
599            "contextid": "2",
600            "assetid": "4345999",
601            "amount": "1",
602            "classid": "310777161",
603            "instanceid": "188530139",
604            "new_assetid": "13327873664",
605            "new_contextid": "2"
606          }
607        ]
608      },
609      {
610        "tradeid": "2022547174628335361",
611        "steamid_other": "76561197976600825",
612        "time_init": 1515644947,
613        "status": 3,
614        "assets_received": [
615          {
616            "appid": 730,
617            "contextid": "2",
618            "assetid": "12792180950",
619            "amount": "1",
620            "classid": "2521767801",
621            "instanceid": "0",
622            "new_assetid": "13327860916",
623            "new_contextid": "2"
624          },
625          {
626            "appid": 447820,
627            "contextid": "2",
628            "assetid": "1667881814169014779",
629            "amount": "1",
630            "classid": "2219693199",
631            "instanceid": "0",
632            "new_assetid": "1827766562939536102",
633            "new_contextid": "2"
634          }
635        ]
636      },
637      {
638        "tradeid": "2022547174624411155",
639        "steamid_other": "76561197971392179",
640        "time_init": 1515552781,
641        "status": 3,
642        "assets_received": [
643          {
644            "appid": 730,
645            "contextid": "2",
646            "assetid": "13213233361",
647            "amount": "1",
648            "classid": "2521767801",
649            "instanceid": "0",
650            "new_assetid": "13314519275",
651            "new_contextid": "2"
652          },
653          {
654            "appid": 447820,
655            "contextid": "2",
656            "assetid": "2364813217118906230",
657            "amount": "1",
658            "classid": "2219693201",
659            "instanceid": "0",
660            "new_assetid": "1827766492051349432",
661            "new_contextid": "2"
662          },
663          {
664            "appid": 578080,
665            "contextid": "2",
666            "assetid": "1807498550407081772",
667            "amount": "1",
668            "classid": "2451623575",
669            "instanceid": "0",
670            "new_assetid": "1827766492051349437",
671            "new_contextid": "2"
672          }
673        ]
674      },
675      {
676        "tradeid": "1640843092290105607",
677        "steamid_other": "76561197998993178",
678        "time_init": 1492806587,
679        "status": 3,
680        "assets_given": [
681          {
682            "appid": 730,
683            "contextid": "2",
684            "assetid": "4063307518",
685            "amount": "1",
686            "classid": "310779465",
687            "instanceid": "188530139",
688            "new_assetid": "9937692380",
689            "new_contextid": "2"
690          }
691        ]
692      }
693    ]
694  }
695}
696"#;
697        serde_json::from_str::<GetTradeHistoryResponse>(&response).unwrap()
698    }
699
700    #[test]
701    fn new_assets() {
702        let raw_response = sample_trade_history_response();
703        let filtered = raw_response.filter_by(|x| x.tradeid == 3622543526924228084).remove(0);
704        let asset = filtered.every_asset().remove(0);
705        assert_eq!(asset.assetid, 15319724006);
706        assert_eq!(asset.new_assetid, 19793871926);
707    }
708
709    #[cfg(feature = "time")]
710    #[test]
711    fn estimate_time() {
712        use crate::time::estimate_tradelock_end;
713        use crate::time::ONE_WEEK_SECONDS;
714
715        let raw_response = sample_trade_history_response();
716        let filtered_trade = raw_response.filter_by(|x| x.tradeid == 3622543526924228084).remove(0);
717        let trade_completed_time = filtered_trade.time_init;
718        assert_eq!(
719            estimate_tradelock_end(trade_completed_time, ONE_WEEK_SECONDS).timestamp(),
720            1604649600
721        );
722    }
723}