@assetpay/steam-mobile 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -68,6 +68,7 @@ let EConfirmationType = /* @__PURE__ */ function(EConfirmationType) {
68
68
  EConfirmationType[EConfirmationType["FeatureOptOut"] = 4] = "FeatureOptOut";
69
69
  EConfirmationType[EConfirmationType["PhoneNumberChange"] = 5] = "PhoneNumberChange";
70
70
  EConfirmationType[EConfirmationType["AccountRecovery"] = 6] = "AccountRecovery";
71
+ EConfirmationType[EConfirmationType["BuyOrder"] = 12] = "BuyOrder";
71
72
  return EConfirmationType;
72
73
  }({});
73
74
  let EOfferFilter = /* @__PURE__ */ function(EOfferFilter) {
@@ -306,6 +307,13 @@ var RateLimitError = class extends SteamError {
306
307
  this.unlockAt = Date.now() + this.retryAfterMs;
307
308
  }
308
309
  };
310
+ var ThrottledError = class extends SteamError {
311
+ statusCode = 403;
312
+ constructor(message = "Throttled (403)", options) {
313
+ super(message, options);
314
+ this.name = "ThrottledError";
315
+ }
316
+ };
309
317
  var EscrowError = class extends SteamError {
310
318
  escrowDays;
311
319
  constructor(escrowDays, message) {
@@ -350,6 +358,12 @@ var ConfirmationError = class extends SteamError {
350
358
  this.name = "ConfirmationError";
351
359
  }
352
360
  };
361
+ var ConfirmationOutcomeUnknownError = class extends ConfirmationError {
362
+ constructor(message = "Confirmation response unreadable; the outcome is unknown") {
363
+ super(message);
364
+ this.name = "ConfirmationOutcomeUnknownError";
365
+ }
366
+ };
353
367
  var FamilyViewError = class extends SteamError {
354
368
  constructor(message = "Family View Restricted") {
355
369
  super(message);
@@ -362,6 +376,74 @@ var PrivateInventoryError = class extends SteamError {
362
376
  this.name = "PrivateInventoryError";
363
377
  }
364
378
  };
379
+ var MarketConfirmationLimitError = class extends SteamError {
380
+ reason;
381
+ constructor(message = "Too many listings pending confirmation", options) {
382
+ super(message, options);
383
+ this.name = "MarketConfirmationLimitError";
384
+ this.reason = options?.reason ?? "unknown";
385
+ }
386
+ };
387
+ var MarketItemUnavailableError = class extends SteamError {
388
+ constructor(message, options) {
389
+ super(message, options);
390
+ this.name = "MarketItemUnavailableError";
391
+ }
392
+ };
393
+ var MarketIneligibleError = class extends SteamError {
394
+ constructor(message = "Steam redirected to the market eligibility check — this account cannot use the market") {
395
+ super(message);
396
+ this.name = "MarketIneligibleError";
397
+ }
398
+ };
399
+ var CurrencyMismatchError = class extends SteamError {
400
+ expected;
401
+ actual;
402
+ constructor(expected, actual, context) {
403
+ super(`Currency mismatch in ${context}: expected ${expected}, got ${actual ?? "unknown"}`);
404
+ this.name = "CurrencyMismatchError";
405
+ this.expected = expected;
406
+ this.actual = actual;
407
+ }
408
+ };
409
+ var WalletCurrencyUnknownError = class extends SteamError {
410
+ constructor(context) {
411
+ super(`Wallet currency unknown for ${context} — seed walletCurrency or call getWalletDetails() first`);
412
+ this.name = "WalletCurrencyUnknownError";
413
+ }
414
+ };
415
+ var BuyOrderError = class extends SteamError {
416
+ stage;
417
+ code;
418
+ mayExist;
419
+ orderExists;
420
+ confirmationId;
421
+ constructor(message, options) {
422
+ super(message, {
423
+ ...options.code !== void 0 ? { eresult: options.code } : {},
424
+ body: options.body,
425
+ cause: options.cause
426
+ });
427
+ this.name = "BuyOrderError";
428
+ this.stage = options.stage;
429
+ this.code = options.code;
430
+ this.orderExists = options.orderExists ?? false;
431
+ this.mayExist = options.mayExist || this.orderExists;
432
+ this.confirmationId = options.confirmationId;
433
+ }
434
+ };
435
+ var WalletBalanceLimitError = class extends SteamError {
436
+ constructor(message = "Listing would exceed the maximum wallet balance", options) {
437
+ super(message, options);
438
+ this.name = "WalletBalanceLimitError";
439
+ }
440
+ };
441
+ var MarketBlockedError = class extends SteamError {
442
+ constructor(message = "This account is currently unable to use the Community Market", options) {
443
+ super(message, options);
444
+ this.name = "MarketBlockedError";
445
+ }
446
+ };
365
447
  var LoginError = class extends SteamError {
366
448
  extendedErrorMessage;
367
449
  isTransient;
@@ -883,6 +965,12 @@ const IMPIT_BROWSER = {
883
965
  }
884
966
  };
885
967
  const BROWSER_ACCEPT$1 = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
968
+ function requestControl(control) {
969
+ const out = {};
970
+ if (control?.signal) out.signal = control.signal;
971
+ if (control?.timeoutMs !== void 0) out.timeoutMs = control.timeoutMs;
972
+ return out;
973
+ }
886
974
  function clean(input) {
887
975
  if (!input) return void 0;
888
976
  const out = {};
@@ -926,7 +1014,8 @@ var HttpClient = class {
926
1014
  const res = await this.perform(method, url, opts);
927
1015
  const location = firstHeader(res.headers.location);
928
1016
  if (res.statusCode >= 300 && res.statusCode < 400 && location?.includes("/market/eligibilitycheck")) {
929
- await this.perform("GET", location, opts.signal ? { signal: opts.signal } : {});
1017
+ await this.perform("GET", location, requestControl(opts));
1018
+ if (opts.noEligibilityReplay) return res;
930
1019
  return this.perform(method, url, opts);
931
1020
  }
932
1021
  return res;
@@ -1177,7 +1266,7 @@ function httpError(res, rateLimitWindowMs) {
1177
1266
  if (res.statusCode >= 300 && res.statusCode <= 399) {
1178
1267
  const loc = locationHeader(res);
1179
1268
  if (loc.includes("/login")) return new SteamSessionExpiredError();
1180
- if (loc.includes("eligibilitycheck")) return new SteamError("Steam redirected to the market eligibility check — this account is limited / not trade-eligible");
1269
+ if (loc.includes("eligibilitycheck")) return new MarketIneligibleError("Steam redirected to the market eligibility check — this account is limited / not trade-eligible");
1181
1270
  }
1182
1271
  return new HttpStatusError(res.statusCode, void 0, res.body);
1183
1272
  }
@@ -1292,7 +1381,7 @@ function queryParams(url) {
1292
1381
  for (const [k, v] of new URL(url).searchParams) out[k] = v;
1293
1382
  return out;
1294
1383
  }
1295
- function decodeEntities(s) {
1384
+ function decodeEntities$1(s) {
1296
1385
  return s.replace(/&quot;/g, "\"").replace(/&#0?39;/g, "'").replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
1297
1386
  }
1298
1387
  function parseOpenidForm(html) {
@@ -1303,14 +1392,14 @@ function parseOpenidForm(html) {
1303
1392
  for (const tag of (form[2] ?? "").match(/<input\b[^>]*>/gi) ?? []) {
1304
1393
  const name = tag.match(/\bname=["']([^"']*)["']/i)?.[1];
1305
1394
  if (!name) continue;
1306
- const value = decodeEntities(tag.match(/\bvalue=["']([^"']*)["']/i)?.[1] ?? "");
1395
+ const value = decodeEntities$1(tag.match(/\bvalue=["']([^"']*)["']/i)?.[1] ?? "");
1307
1396
  fields.push({
1308
1397
  name,
1309
1398
  value
1310
1399
  });
1311
1400
  }
1312
1401
  return {
1313
- action: new URL(decodeEntities(action), STEAM_OPENID_URL).toString(),
1402
+ action: new URL(decodeEntities$1(action), STEAM_OPENID_URL).toString(),
1314
1403
  fields
1315
1404
  };
1316
1405
  }
@@ -1472,6 +1561,16 @@ var CommunityNamespace = class {
1472
1561
  input: { steamid: id }
1473
1562
  })).response?.player_level ?? 0;
1474
1563
  }
1564
+ async getTwoFactorStatus() {
1565
+ const body = await this.api.call({
1566
+ httpMethod: "POST",
1567
+ iface: "ITwoFactorService",
1568
+ method: "QueryStatus",
1569
+ input: { steamid: this.session.steamID.getSteamID64() }
1570
+ });
1571
+ if (!body.response) throw new SteamError("Malformed two-factor status response", { body });
1572
+ return body.response;
1573
+ }
1475
1574
  async ensureApiKey(domain = "assetpay.gg") {
1476
1575
  await this.session.getAccessToken();
1477
1576
  const res = await this.http.get(`${URLS.community}/dev/apikey?l=english`, { responseType: "text" });
@@ -1571,8 +1670,11 @@ function xmlValue(xml, tag) {
1571
1670
  }
1572
1671
  //#endregion
1573
1672
  //#region src/http/queryTime.ts
1574
- async function queryServerTimeOffset(http) {
1575
- const res = await http.post(`${URLS.api}/ITwoFactorService/QueryTime/v1/`, { responseType: "json" });
1673
+ async function queryServerTimeOffset(http, control) {
1674
+ const res = await http.post(`${URLS.api}/ITwoFactorService/QueryTime/v1/`, {
1675
+ ...requestControl(control),
1676
+ responseType: "json"
1677
+ });
1576
1678
  if (res.statusCode !== 200) throw httpError(res);
1577
1679
  const serverTime = Number(res.body?.response?.server_time);
1578
1680
  if (!serverTime) throw new SteamError("Failed to query Steam server time");
@@ -1602,9 +1704,9 @@ var ConfirmationManager = class {
1602
1704
  const android = getDeviceID(this.steamID.getSteamID64());
1603
1705
  return this.profile.mobileClient === "ios" ? android.replace(/^android:/, "").toUpperCase() : android;
1604
1706
  }
1605
- async getConfirmations(time, key) {
1707
+ async getConfirmations(time, key, control) {
1606
1708
  const { tag, k } = splitKey(key, "conf");
1607
- const body = await this.getlist(k, time, tag);
1709
+ const body = await this.getlist(k, time, tag, control);
1608
1710
  if (!body?.success) {
1609
1711
  if (body?.needauth) throw new SteamSessionExpiredError();
1610
1712
  throw new ConfirmationError(String(body?.message ?? body?.detail ?? "Failed to get confirmation list"));
@@ -1622,41 +1724,58 @@ var ConfirmationManager = class {
1622
1724
  icon: String(c.icon ?? "")
1623
1725
  }));
1624
1726
  }
1625
- async respondToConfirmation(confID, confKey, time, key, accept) {
1727
+ async respondToConfirmation(confID, confKey, time, key, accept, control) {
1728
+ const ids = Array.isArray(confID) ? confID : [confID];
1729
+ const keys = Array.isArray(confKey) ? confKey : [confKey];
1730
+ if (ids.length !== keys.length) throw new ConfirmationError("confID and confKey must have the same length");
1626
1731
  const { tag, k } = splitKey(key, accept ? "allow" : "cancel");
1627
1732
  const op = accept ? "allow" : "cancel";
1628
1733
  const res = await this.http.post(`${URLS.community}/mobileconf/multiajaxop`, {
1734
+ ...requestControl(control),
1629
1735
  responseType: "json",
1630
1736
  searchParams: {
1631
1737
  ...this.confParams(k, time, tag),
1632
1738
  op
1633
1739
  },
1634
- multipart: [{
1740
+ multipart: ids.flatMap((id, i) => [{
1635
1741
  name: "cid[]",
1636
- value: confID
1742
+ value: id
1637
1743
  }, {
1638
1744
  name: "ck[]",
1639
- value: confKey
1640
- }]
1745
+ value: keys[i]
1746
+ }])
1641
1747
  });
1642
1748
  if (res.statusCode !== 200) throw httpError(res);
1643
1749
  if (res.body?.success) return;
1750
+ if (res.body === void 0 || res.body === null) throw new ConfirmationOutcomeUnknownError();
1644
1751
  throw new ConfirmationError(String(res.body?.message ?? "Could not act on confirmation"));
1645
1752
  }
1646
- async getPending() {
1753
+ async getPending(control) {
1647
1754
  const secret = this.requireSecret();
1648
- const listTime = time(await this.getTimeOffset());
1755
+ const listTime = time(await this.getTimeOffset(control));
1649
1756
  const listKey = getConfirmationKey(secret, listTime, "list");
1650
1757
  return this.getConfirmations(listTime, {
1651
1758
  tag: "list",
1652
1759
  key: listKey
1653
- });
1760
+ }, control);
1654
1761
  }
1655
1762
  acceptConfirmation(confID, nonce) {
1656
- return this.actOnConfirmation(confID, nonce, true);
1763
+ return this.actOnConfirmations([{
1764
+ id: confID,
1765
+ key: nonce
1766
+ }], true);
1657
1767
  }
1658
1768
  rejectConfirmation(confID, nonce) {
1659
- return this.actOnConfirmation(confID, nonce, false);
1769
+ return this.actOnConfirmations([{
1770
+ id: confID,
1771
+ key: nonce
1772
+ }], false);
1773
+ }
1774
+ acceptConfirmations(confs, control) {
1775
+ return this.actOnConfirmations(confs, true, control);
1776
+ }
1777
+ rejectConfirmations(confs, control) {
1778
+ return this.actOnConfirmations(confs, false, control);
1660
1779
  }
1661
1780
  async acceptAll() {
1662
1781
  const pending = await this.getPending();
@@ -1668,16 +1787,17 @@ var ConfirmationManager = class {
1668
1787
  if (!conf) throw new ConfirmationError(`Could not find confirmation for object ${objectID}`);
1669
1788
  await this.acceptConfirmation(conf.id, conf.key);
1670
1789
  }
1671
- async actOnConfirmation(confID, nonce, accept) {
1790
+ async actOnConfirmations(confs, accept, control) {
1791
+ if (confs.length === 0) return;
1672
1792
  const secret = this.requireSecret();
1673
- const offset = await this.getTimeOffset();
1793
+ const offset = await this.getTimeOffset(control);
1674
1794
  const time = this.nextConfTime(offset);
1675
1795
  const tag = accept ? "accept" : "cancel";
1676
1796
  const key = getConfirmationKey(secret, time, tag);
1677
- await this.respondToConfirmation(confID, nonce, time, {
1797
+ await this.respondToConfirmation(confs.map((c) => c.id), confs.map((c) => c.key), time, {
1678
1798
  tag,
1679
1799
  key
1680
- }, accept);
1800
+ }, accept, control);
1681
1801
  }
1682
1802
  requireSecret() {
1683
1803
  if (!this.identitySecret) throw new ConfirmationError("identitySecret is required to respond to confirmations");
@@ -1691,9 +1811,9 @@ var ConfirmationManager = class {
1691
1811
  if (this.usedConfTimes.length > 60) this.usedConfTimes.splice(0, this.usedConfTimes.length - 60);
1692
1812
  return time$1;
1693
1813
  }
1694
- async getTimeOffset() {
1814
+ async getTimeOffset(control) {
1695
1815
  if (this.timeOffset !== void 0 && Date.now() - this.timeOffsetAt < TIME_OFFSET_TTL_MS) return this.timeOffset;
1696
- const offset = await queryServerTimeOffset(this.http);
1816
+ const offset = await queryServerTimeOffset(this.http, control);
1697
1817
  this.timeOffset = offset;
1698
1818
  this.timeOffsetAt = Date.now();
1699
1819
  return offset;
@@ -1708,8 +1828,9 @@ var ConfirmationManager = class {
1708
1828
  tag
1709
1829
  };
1710
1830
  }
1711
- async getlist(key, time, tag) {
1831
+ async getlist(key, time, tag, control) {
1712
1832
  const res = await this.http.get(`${URLS.community}/mobileconf/getlist`, {
1833
+ ...requestControl(control),
1713
1834
  responseType: "json",
1714
1835
  searchParams: this.confParams(key, time, tag)
1715
1836
  });
@@ -1779,6 +1900,7 @@ var WebApiClient = class {
1779
1900
  console.log(`[http] ${httpMethod} ${url}?${qs.toString()}`);
1780
1901
  }
1781
1902
  const res = await this.http.request(httpMethod, url, {
1903
+ ...requestControl(params),
1782
1904
  responseType: "json",
1783
1905
  ...httpMethod === "GET" ? { searchParams: payload } : { form: payload }
1784
1906
  });
@@ -1877,6 +1999,900 @@ var SteamWebApi = class {
1877
1999
  return response;
1878
2000
  }
1879
2001
  };
2002
+ //#endregion
2003
+ //#region src/market/fees.ts
2004
+ const DEFAULT_MARKET_FEE_INFO = Object.freeze({
2005
+ walletFee: true,
2006
+ walletFeePercent: .05,
2007
+ walletFeeMinimum: 1,
2008
+ walletFeeBase: 0,
2009
+ publisherFeePercent: .1
2010
+ });
2011
+ function assertCents(name, value) {
2012
+ if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative integer number of cents, got ${value}`);
2013
+ }
2014
+ const MAX_FEE_FRACTION = .5;
2015
+ function assertFeeInfo(info) {
2016
+ const fraction = (v) => Number.isFinite(v) && v >= 0 && v < MAX_FEE_FRACTION;
2017
+ const cents = (v) => Number.isSafeInteger(v) && v >= 0;
2018
+ if (typeof info?.walletFee !== "boolean" || !fraction(info.walletFeePercent) || !fraction(info.publisherFeePercent) || !cents(info.walletFeeMinimum) || !cents(info.walletFeeBase)) throw new RangeError(`invalid market fee info: ${JSON.stringify(info)}`);
2019
+ }
2020
+ function feeInfoFromWalletInfo(raw, publisherFeePercent) {
2021
+ const num = (v) => typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
2022
+ const info = {
2023
+ walletFee: num(raw.wallet_fee) === 1,
2024
+ walletFeePercent: num(raw.wallet_fee_percent),
2025
+ walletFeeMinimum: num(raw.wallet_fee_minimum),
2026
+ walletFeeBase: num(raw.wallet_fee_base),
2027
+ publisherFeePercent: publisherFeePercent ?? num(raw.wallet_publisher_fee_percent_default)
2028
+ };
2029
+ if (num(raw.wallet_fee) !== 0 && num(raw.wallet_fee) !== 1) return null;
2030
+ try {
2031
+ assertFeeInfo(info);
2032
+ } catch {
2033
+ return null;
2034
+ }
2035
+ return info;
2036
+ }
2037
+ function buyerPaysForSellerReceives(receivedAmount, info) {
2038
+ assertCents("receivedAmount", receivedAmount);
2039
+ assertFeeInfo(info);
2040
+ return amountToSend(receivedAmount, info);
2041
+ }
2042
+ function feesForBuyerPays(amount, info) {
2043
+ assertCents("amount", amount);
2044
+ assertFeeInfo(info);
2045
+ if (!info.walletFee) return {
2046
+ amount,
2047
+ steamFee: 0,
2048
+ publisherFee: 0,
2049
+ fees: 0
2050
+ };
2051
+ let iterations = 0;
2052
+ let estimate = Math.trunc((amount - Math.trunc(info.walletFeeBase)) / (info.walletFeePercent + info.publisherFeePercent + 1));
2053
+ let everUndershot = false;
2054
+ let fees = amountToSend(estimate, info);
2055
+ while (fees.amount !== amount && iterations < 10) {
2056
+ if (fees.amount > amount) {
2057
+ if (everUndershot) {
2058
+ fees = amountToSend(estimate - 1, info);
2059
+ fees.steamFee += amount - fees.amount;
2060
+ fees.fees += amount - fees.amount;
2061
+ fees.amount = amount;
2062
+ break;
2063
+ }
2064
+ estimate--;
2065
+ } else {
2066
+ everUndershot = true;
2067
+ estimate++;
2068
+ }
2069
+ fees = amountToSend(estimate, info);
2070
+ iterations++;
2071
+ }
2072
+ return fees;
2073
+ }
2074
+ function sellerReceivesForBuyerPays(amount, info) {
2075
+ return Math.max(0, amount - feesForBuyerPays(amount, info).fees);
2076
+ }
2077
+ function landingBuyerPays(targetBuyerPays, info) {
2078
+ const receives = sellerReceivesForBuyerPays(targetBuyerPays, info);
2079
+ return receives === 0 ? 0 : buyerPaysForSellerReceives(receives, info).amount;
2080
+ }
2081
+ function amountToSend(receivedAmount, info) {
2082
+ if (!info.walletFee) return {
2083
+ amount: receivedAmount,
2084
+ steamFee: 0,
2085
+ publisherFee: 0,
2086
+ fees: 0
2087
+ };
2088
+ const steamFee = Math.trunc(Math.floor(Math.max(receivedAmount * info.walletFeePercent, info.walletFeeMinimum) + Math.trunc(info.walletFeeBase)));
2089
+ const publisherFee = Math.trunc(Math.floor(info.publisherFeePercent > 0 ? Math.max(receivedAmount * info.publisherFeePercent, 1) : 0));
2090
+ return {
2091
+ amount: Math.trunc(receivedAmount + steamFee + publisherFee),
2092
+ steamFee,
2093
+ publisherFee,
2094
+ fees: steamFee + publisherFee
2095
+ };
2096
+ }
2097
+ //#endregion
2098
+ //#region src/market/prices.ts
2099
+ function getPriceValueAsInt(strAmount) {
2100
+ if (!strAmount) return 0;
2101
+ let amount = String(strAmount);
2102
+ amount = amount.replace(/,/g, ".");
2103
+ amount = amount.replace(".--", ".00");
2104
+ amount = amount.replace(/[^\d.]/g, "");
2105
+ if (amount.indexOf(".") !== -1) {
2106
+ const segments = amount.split(".");
2107
+ const last = segments[segments.length - 1] ?? "";
2108
+ if (!Number.isNaN(Number(last)) && last.length === 3 && segments[segments.length - 2] !== "0") amount = segments.join("");
2109
+ else amount = `${segments.slice(0, -1).join("")}.${last}`;
2110
+ }
2111
+ const cents = Number.parseFloat(amount) * 100;
2112
+ return Math.max(Math.floor(Number.isNaN(cents) ? 0 : cents + 1e-6), 0);
2113
+ }
2114
+ //#endregion
2115
+ //#region src/market/history.ts
2116
+ function parseMarketHistory(body) {
2117
+ const history = {
2118
+ events: [],
2119
+ sales: [],
2120
+ purchases: [],
2121
+ listingEvents: [],
2122
+ malformed: [],
2123
+ totalCount: body.total_count
2124
+ };
2125
+ if (!body.results_html) return history;
2126
+ const hovers = body.hovers ?? "";
2127
+ const assets = body.assets ?? {};
2128
+ const html = body.results_html.replace(/<!--[\s\S]*?-->/g, "");
2129
+ for (const row of findByClass(html, "market_listing_row", "market_recent_listing_row")) {
2130
+ const dates = findByClass(row.inner, "market_listing_listed_date");
2131
+ const itemName = textOf(row.inner, "market_listing_item_name");
2132
+ const gameName = textOf(row.inner, "market_listing_game_name");
2133
+ const actedOn = dates[0] ? elementText(dates[0]).trim() : "";
2134
+ const listedOn = dates[1] ? elementText(dates[1]).trim() : "";
2135
+ const displayPrice = textOf(row.inner, "market_listing_price").trim();
2136
+ const rowId = attrValue(row.attrs, "id") ?? "";
2137
+ const priceInCents = getPriceValueAsInt(displayPrice);
2138
+ const gainOrLoss = textOf(row.inner, "market_listing_gainorloss").trim();
2139
+ const type = gainOrLoss === "-" ? "sale" : gainOrLoss === "+" ? "purchase" : "listing_event";
2140
+ const idMatch = rowId.match(/^history_row_(\d+)_(\d+)$/);
2141
+ if (!idMatch) continue;
2142
+ if (priceInCents === 0 && (displayPrice || type !== "listing_event")) {
2143
+ history.malformed.push({
2144
+ historyId: rowId,
2145
+ displayPrice,
2146
+ gainOrLoss,
2147
+ reason: displayPrice ? "zero_price" : "missing_price"
2148
+ });
2149
+ continue;
2150
+ }
2151
+ const event = {
2152
+ historyId: rowId,
2153
+ listingid: idMatch[1],
2154
+ eventid: idMatch[2],
2155
+ itemName,
2156
+ gameName,
2157
+ listedOn,
2158
+ actedOn,
2159
+ displayPrice,
2160
+ priceInCents,
2161
+ type,
2162
+ marketName: null,
2163
+ appID: null,
2164
+ contextID: null,
2165
+ assetID: null,
2166
+ classID: null,
2167
+ instanceID: null,
2168
+ unOwnedContextID: null,
2169
+ unOwnedID: null
2170
+ };
2171
+ if (type !== "listing_event") attachAsset(event, rowId, hovers, assets);
2172
+ if (type === "sale") {
2173
+ const sale = {
2174
+ ...event,
2175
+ type,
2176
+ receivedAmount: priceInCents
2177
+ };
2178
+ history.sales.push(sale);
2179
+ history.events.push(sale);
2180
+ } else if (type === "purchase") {
2181
+ const purchase = {
2182
+ ...event,
2183
+ type,
2184
+ paidAmount: priceInCents
2185
+ };
2186
+ history.purchases.push(purchase);
2187
+ history.events.push(purchase);
2188
+ } else {
2189
+ const listingEvent = {
2190
+ ...event,
2191
+ type
2192
+ };
2193
+ history.listingEvents.push(listingEvent);
2194
+ history.events.push(listingEvent);
2195
+ }
2196
+ }
2197
+ return history;
2198
+ }
2199
+ function attachAsset(event, rowId, hovers, assets) {
2200
+ try {
2201
+ const args = hovers.split(`${rowId}_name`)[1].split(")")[0].split(",");
2202
+ event.appID = Number.parseInt(args[1], 10);
2203
+ event.contextID = args[2].split("'")[1] ?? null;
2204
+ event.assetID = args[3].split("'")[1] ?? null;
2205
+ if (event.contextID === null || event.assetID === null) return;
2206
+ const asset = assets[String(event.appID)]?.[event.contextID]?.[event.assetID];
2207
+ if (asset) {
2208
+ event.classID = asset.classid ?? null;
2209
+ event.instanceID = asset.instanceid ?? null;
2210
+ event.unOwnedContextID = asset.unowned_contextid ?? null;
2211
+ event.unOwnedID = asset.unowned_id ?? null;
2212
+ event.marketName = asset.market_hash_name ?? null;
2213
+ }
2214
+ } catch {}
2215
+ }
2216
+ const TAG = /<([a-zA-Z][\w:-]*)((?:"[^"]*"|'[^']*'|[^'">])*)>/g;
2217
+ const VOID_TAGS = /* @__PURE__ */ new Set([
2218
+ "area",
2219
+ "base",
2220
+ "br",
2221
+ "col",
2222
+ "embed",
2223
+ "hr",
2224
+ "img",
2225
+ "input",
2226
+ "link",
2227
+ "meta",
2228
+ "source",
2229
+ "track",
2230
+ "wbr"
2231
+ ]);
2232
+ function findByClass(html, ...classes) {
2233
+ const found = [];
2234
+ for (const m of html.matchAll(TAG)) {
2235
+ const attrs = m[2] ?? "";
2236
+ const tokens = (attrValue(attrs, "class") ?? "").split(/\s+/);
2237
+ if (!classes.every((c) => tokens.includes(c))) continue;
2238
+ const tag = m[1].toLowerCase();
2239
+ const start = m.index + m[0].length;
2240
+ const empty = VOID_TAGS.has(tag) || attrs.trimEnd().endsWith("/");
2241
+ found.push({
2242
+ attrs,
2243
+ inner: empty ? "" : html.slice(start, closeIndex(html, tag, start))
2244
+ });
2245
+ }
2246
+ return found;
2247
+ }
2248
+ function closeIndex(html, tag, from) {
2249
+ const re = new RegExp(`<(/?)${tag}\\b((?:"[^"]*"|'[^']*'|[^'">])*)>`, "gi");
2250
+ re.lastIndex = from;
2251
+ let depth = 1;
2252
+ for (let m = re.exec(html); m; m = re.exec(html)) if (m[1]) {
2253
+ if (--depth === 0) return m.index;
2254
+ } else if (!(m[2] ?? "").trimEnd().endsWith("/")) depth++;
2255
+ return html.length;
2256
+ }
2257
+ const ATTR = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
2258
+ function attrValue(attrs, name) {
2259
+ for (const m of attrs.matchAll(ATTR)) if (m[1].toLowerCase() === name) return decodeEntities(m[2] ?? m[3] ?? m[4] ?? "");
2260
+ }
2261
+ function textOf(html, className) {
2262
+ return findByClass(html, className).map(elementText).join("");
2263
+ }
2264
+ function elementText(el) {
2265
+ return decodeEntities(el.inner.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, ""));
2266
+ }
2267
+ const NAMED_ENTITIES = {
2268
+ amp: "&",
2269
+ lt: "<",
2270
+ gt: ">",
2271
+ quot: "\"",
2272
+ apos: "'",
2273
+ nbsp: "\xA0"
2274
+ };
2275
+ function decodeEntities(s) {
2276
+ return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, ref) => {
2277
+ if (ref[0] === "#") {
2278
+ const code = ref[1] === "x" || ref[1] === "X" ? Number.parseInt(ref.slice(2), 16) : Number.parseInt(ref.slice(1), 10);
2279
+ return code <= 1114111 ? String.fromCodePoint(code) : whole;
2280
+ }
2281
+ return NAMED_ENTITIES[ref.toLowerCase()] ?? whole;
2282
+ });
2283
+ }
2284
+ function priceTextCurrency(text) {
2285
+ const t = text.trim();
2286
+ if (t.includes("€")) return t.includes("$") ? void 0 : 3;
2287
+ const dollar = t.indexOf("$");
2288
+ if (dollar === -1 || t.indexOf("$", dollar + 1) !== -1) return void 0;
2289
+ if (/[A-Za-z]$/.test(t.slice(0, dollar)) || /^[A-Za-z]/.test(t.slice(dollar + 1))) return;
2290
+ return 1;
2291
+ }
2292
+ //#endregion
2293
+ //#region src/market/MarketNamespace.ts
2294
+ const LISTINGS_PAGE_SIZE = 100;
2295
+ const MARKET_REFERER = `${URLS.community}/market/`;
2296
+ const LISTING_CURRENCY_BASE = 2e3;
2297
+ const BUY_ORDER_CONFIRMATION_LOOKUPS = [
2298
+ 0,
2299
+ 1e3,
2300
+ 2500
2301
+ ];
2302
+ const BUY_ORDER_NOT_PLACED_CODES = /* @__PURE__ */ new Set([
2303
+ 15,
2304
+ 25,
2305
+ 107
2306
+ ]);
2307
+ const UNKNOWN_OUTCOME_RE = /hearing back|double check/i;
2308
+ const DUPLICATE_ORDER_CODE = 29;
2309
+ var MarketNamespace = class {
2310
+ http;
2311
+ session;
2312
+ confirmations;
2313
+ api;
2314
+ currency;
2315
+ currencyVerified = false;
2316
+ constructor(http, session, confirmations, api) {
2317
+ this.http = http;
2318
+ this.session = session;
2319
+ this.confirmations = confirmations;
2320
+ this.api = api;
2321
+ }
2322
+ get walletCurrency() {
2323
+ return this.currency;
2324
+ }
2325
+ get walletCurrencyVerified() {
2326
+ return this.currencyVerified;
2327
+ }
2328
+ seedWalletCurrency(currency) {
2329
+ if (!Number.isInteger(currency) || currency <= 0) throw new RangeError(`walletCurrency must be a positive integer, got ${currency}`);
2330
+ if (this.currency !== void 0 && this.currency !== currency) throw new CurrencyMismatchError(this.currency, currency, "seedWalletCurrency");
2331
+ this.currency = currency;
2332
+ }
2333
+ async getWalletDetails(options = {}) {
2334
+ const body = await this.api.call({
2335
+ httpMethod: "POST",
2336
+ iface: "IUserAccountService",
2337
+ method: "GetClientWalletDetails",
2338
+ input: {
2339
+ include_balance_in_usd: 1,
2340
+ include_formatted_balance: 1
2341
+ },
2342
+ ...requestControl(options)
2343
+ });
2344
+ if (!body.response) throw new SteamError("Malformed wallet details response", { body });
2345
+ const currency = body.response.currency_code;
2346
+ if (body.response.has_wallet === true && typeof currency === "number" && currency > 0) this.learnCurrency(currency, "wallet details");
2347
+ return body.response;
2348
+ }
2349
+ async getMarketWalletInfo(options = {}) {
2350
+ await this.session.getAccessToken();
2351
+ const res = await this.http.get(`${URLS.community}/market/`, requestControl(options));
2352
+ if (res.statusCode !== 200) throw httpError(res);
2353
+ if (res.body.match(/g_steamID\s*=\s*"(\d+)"/)?.[1] !== this.session.steamID.getSteamID64()) throw new SteamSessionExpiredError("market page was not rendered for this account");
2354
+ const match = res.body.match(/g_rgWalletInfo\s*=\s*(\{[^;]*?\});/);
2355
+ if (!match?.[1]) throw new SteamError("g_rgWalletInfo not found on the market page");
2356
+ let info;
2357
+ try {
2358
+ info = JSON.parse(match[1]);
2359
+ } catch {
2360
+ throw new SteamError("g_rgWalletInfo on the market page is not valid JSON");
2361
+ }
2362
+ const currency = Number(info.wallet_currency);
2363
+ if (this.currency !== void 0 && currency !== this.currency) throw new CurrencyMismatchError(this.currency, currency, "g_rgWalletInfo");
2364
+ return info;
2365
+ }
2366
+ async getMyListings(options = {}) {
2367
+ const currency = this.readCurrency(options.currency, "mylistings");
2368
+ await this.session.getAccessToken();
2369
+ const result = {
2370
+ listings: [],
2371
+ listings_to_confirm: [],
2372
+ listings_on_hold: [],
2373
+ buy_orders: [],
2374
+ num_active_listings: 0,
2375
+ complete: false
2376
+ };
2377
+ const seen = /* @__PURE__ */ new Set();
2378
+ let firstTotal;
2379
+ let totalMoved = false;
2380
+ let buyOrdersMissing = false;
2381
+ let start = 0;
2382
+ for (;;) {
2383
+ const res = await this.http.get(`${URLS.community}/market/mylistings/`, {
2384
+ ...requestControl(options),
2385
+ responseType: "json",
2386
+ searchParams: {
2387
+ norender: 1,
2388
+ currency: currency ?? void 0,
2389
+ start,
2390
+ count: LISTINGS_PAGE_SIZE
2391
+ }
2392
+ });
2393
+ if (res.statusCode !== 200) throw httpError(res);
2394
+ const body = res.body;
2395
+ if (!body?.success) throw new SteamError("Steam returned success=false for /market/mylistings", { body });
2396
+ if (body.num_active_listings !== void 0) {
2397
+ result.num_active_listings = body.num_active_listings;
2398
+ if (firstTotal === void 0) firstTotal = body.num_active_listings;
2399
+ else if (body.num_active_listings !== firstTotal) totalMoved = true;
2400
+ }
2401
+ if (start === 0) {
2402
+ if (Array.isArray(body.buy_orders)) result.buy_orders = body.buy_orders;
2403
+ else buyOrdersMissing = true;
2404
+ if (Array.isArray(body.listings_to_confirm)) result.listings_to_confirm = body.listings_to_confirm;
2405
+ if (Array.isArray(body.listings_on_hold)) result.listings_on_hold = body.listings_on_hold;
2406
+ }
2407
+ const page = body.listings ?? body.results?.listings ?? [];
2408
+ for (const listing of page) {
2409
+ if (seen.has(listing.listingid)) continue;
2410
+ seen.add(listing.listingid);
2411
+ result.listings.push(listing);
2412
+ }
2413
+ start += page.length;
2414
+ if (start >= result.num_active_listings || page.length === 0) break;
2415
+ }
2416
+ result.complete = !totalMoved && !buyOrdersMissing && result.listings.length === result.num_active_listings;
2417
+ if (currency !== null) {
2418
+ for (const order of result.buy_orders) {
2419
+ const actual = Number(order.wallet_currency);
2420
+ if (actual !== currency) throw new CurrencyMismatchError(currency, actual, "buy orders");
2421
+ }
2422
+ const all = [
2423
+ ...result.listings,
2424
+ ...result.listings_to_confirm,
2425
+ ...result.listings_on_hold
2426
+ ];
2427
+ for (const listing of all) assertListingCurrency(listing, currency);
2428
+ if (result.buy_orders.length > 0 || all.length > 0) this.confirmCurrency(currency);
2429
+ }
2430
+ return result;
2431
+ }
2432
+ async getMyHistory(options = {}) {
2433
+ const currency = this.readCurrency(options.currency, "myhistory");
2434
+ await this.session.getAccessToken();
2435
+ const res = await this.http.get(`${URLS.community}/market/myhistory`, {
2436
+ ...requestControl(options),
2437
+ responseType: "json",
2438
+ searchParams: {
2439
+ count: options.count ?? 500,
2440
+ start: options.start
2441
+ }
2442
+ });
2443
+ if (res.statusCode !== 200) throw httpError(res);
2444
+ if (!res.body?.success) throw new SteamError("Steam returned success=false for /market/myhistory", { body: res.body });
2445
+ const history = parseMarketHistory(res.body);
2446
+ if (currency !== null && PINNED_PRICE_FORMATS.has(currency)) {
2447
+ const other = new Set(history.events.filter((e) => e.displayPrice && priceTextCurrency(e.displayPrice) !== currency).map((e) => e.historyId));
2448
+ if (other.size > 0) {
2449
+ for (const e of history.events) {
2450
+ if (!other.has(e.historyId)) continue;
2451
+ history.malformed.push({
2452
+ historyId: e.historyId,
2453
+ displayPrice: e.displayPrice,
2454
+ gainOrLoss: e.type === "sale" ? "-" : e.type === "purchase" ? "+" : "",
2455
+ reason: "other_currency"
2456
+ });
2457
+ }
2458
+ const keep = (rows) => rows.filter((r) => !other.has(r.historyId));
2459
+ history.events = keep(history.events);
2460
+ history.sales = keep(history.sales);
2461
+ history.purchases = keep(history.purchases);
2462
+ history.listingEvents = keep(history.listingEvents);
2463
+ }
2464
+ }
2465
+ return history;
2466
+ }
2467
+ async getOrderbook(appid, marketHashName, options = {}) {
2468
+ const expected = this.readCurrency(options.expectedCurrency, "orderbook");
2469
+ await this.session.getAccessToken();
2470
+ const qp = encodeURIComponent(JSON.stringify([appid, marketHashName]));
2471
+ const res = await this.http.get(`${URLS.community}/market/orderbook?q=Load&qp=${qp}`, {
2472
+ ...requestControl(options),
2473
+ responseType: "json"
2474
+ });
2475
+ if (res.statusCode === 403) throw new ThrottledError(void 0, { body: res.body });
2476
+ if (res.statusCode !== 200) throw httpError(res);
2477
+ const body = res.body;
2478
+ const payload = body && body.success === void 0 && body.data ? body.data : body;
2479
+ if (!payload?.success || !payload.data) throw new SteamError(`Bad orderbook response for ${marketHashName}`, { body });
2480
+ const data = payload.data;
2481
+ if (expected !== null) {
2482
+ if (data.eCurrency !== expected) throw new CurrencyMismatchError(expected, data.eCurrency, `orderbook of ${marketHashName}`);
2483
+ this.confirmCurrency(expected);
2484
+ }
2485
+ return data;
2486
+ }
2487
+ async getPriceHistory(appid, marketHashName, options = {}) {
2488
+ const expected = this.readCurrency(options.expectedCurrency, "pricehistory");
2489
+ await this.session.getAccessToken();
2490
+ const res = await this.http.get(`${URLS.community}/market/pricehistory?appid=${appid}&market_hash_name=${encodeURIComponent(marketHashName)}`, {
2491
+ ...requestControl(options),
2492
+ responseType: "json"
2493
+ });
2494
+ if (res.statusCode !== 200) throw httpError(res);
2495
+ if (!res.body?.success) throw new SteamError(`Steam returned success=false for pricehistory of ${marketHashName}`, { body: res.body });
2496
+ if (expected !== null && PINNED_PRICE_FORMATS.has(expected)) {
2497
+ const actual = priceHistoryCurrency(res.body.price_prefix, res.body.price_suffix);
2498
+ if (actual !== expected) throw new CurrencyMismatchError(expected, actual, `pricehistory of ${marketHashName}`);
2499
+ }
2500
+ return Array.isArray(res.body.prices) ? res.body.prices : [];
2501
+ }
2502
+ async getMarketItemDetails(appid, marketHashName, options = {}) {
2503
+ await this.session.getAccessToken();
2504
+ const query = encodeURIComponent(`"${marketHashName}"`);
2505
+ const res = await this.http.get(`${URLS.community}/market/search/render/?query=${query}&start=0&count=10&search_descriptions=0&sort_column=quantity&sort_dir=desc&appid=${appid}&norender=1`, {
2506
+ ...requestControl(options),
2507
+ responseType: "json"
2508
+ });
2509
+ if (res.statusCode !== 200) throw httpError(res);
2510
+ const body = res.body;
2511
+ if (!body || typeof body !== "object" || body.success === false) throw new SteamError("Malformed or unsuccessful market search response", { body });
2512
+ return (Array.isArray(body.results) ? body.results : []).find((r) => r.hash_name === marketHashName) ?? null;
2513
+ }
2514
+ async sellItem(options) {
2515
+ this.writeCurrency(options.currency, "sellitem");
2516
+ assertPositiveCents("price", options.price);
2517
+ const amount = options.amount ?? 1;
2518
+ assertPositiveCents("amount", amount);
2519
+ await this.session.getAccessToken();
2520
+ const sessionid = await this.http.getSessionId();
2521
+ const res = await this.http.post(`${URLS.community}/market/sellitem/`, {
2522
+ ...requestControl(options),
2523
+ noEligibilityReplay: true,
2524
+ responseType: "json",
2525
+ form: {
2526
+ sessionid,
2527
+ appid: options.appid,
2528
+ contextid: options.contextid ?? "2",
2529
+ assetid: options.assetid,
2530
+ amount,
2531
+ price: options.price
2532
+ },
2533
+ headers: { Referer: MARKET_REFERER }
2534
+ });
2535
+ const body = res.body;
2536
+ if (res.statusCode === 200 && (body?.success === true || body?.success === 1)) return body;
2537
+ if (res.statusCode === 429 || res.statusCode === 401) throw httpError(res);
2538
+ if (body?.message) throw sellItemError(body.message, body);
2539
+ if (res.statusCode !== 200) throw httpError(res);
2540
+ throw new SteamError("Unknown error listing item", {
2541
+ ...eresultOf(body),
2542
+ body
2543
+ });
2544
+ }
2545
+ async cancelListing(listingId, options = {}) {
2546
+ await this.session.getAccessToken();
2547
+ const sessionid = await this.http.getSessionId();
2548
+ const res = await this.http.post(`${URLS.community}/market/removelisting/${listingId}`, {
2549
+ ...requestControl(options),
2550
+ noEligibilityReplay: true,
2551
+ form: { sessionid },
2552
+ headers: { Referer: MARKET_REFERER }
2553
+ });
2554
+ if (res.statusCode === 200 || res.statusCode === 302 && redirectsToMarket(res)) return;
2555
+ throw httpError(res);
2556
+ }
2557
+ async createBuyOrder(options) {
2558
+ const currency = this.writeCurrency(options.currency, "createbuyorder");
2559
+ assertPositiveCents("quantity", options.quantity);
2560
+ assertPositiveCents("priceTotal", options.priceTotal);
2561
+ if (options.priceTotal % options.quantity !== 0) throw new RangeError("priceTotal must be the per-unit price times quantity");
2562
+ await this.session.getAccessToken();
2563
+ const sessionid = await this.http.getSessionId();
2564
+ const referer = `${URLS.community}/market/listings/${options.appid}/${encodeURIComponent(options.marketHashName)}`;
2565
+ const form = {
2566
+ sessionid,
2567
+ currency,
2568
+ appid: options.appid,
2569
+ market_hash_name: options.marketHashName,
2570
+ price_total: options.priceTotal,
2571
+ quantity: options.quantity,
2572
+ confirmation: 0
2573
+ };
2574
+ let first;
2575
+ try {
2576
+ first = await this.postBuyOrder(form, referer, options);
2577
+ } catch (err) {
2578
+ if (isNotPlacedError(err)) throw err;
2579
+ throw new BuyOrderError("createbuyorder request failed", {
2580
+ stage: "create",
2581
+ mayExist: true,
2582
+ cause: err
2583
+ });
2584
+ }
2585
+ if (first.buyOrderId) return first.buyOrderId;
2586
+ const confirmationId = first.confirmationId;
2587
+ if (!confirmationId) throw buyOrderRefusal(first, "create");
2588
+ await this.acceptBuyOrderConfirmation(confirmationId, options);
2589
+ let final;
2590
+ try {
2591
+ final = await this.postBuyOrder({
2592
+ ...form,
2593
+ confirmation: confirmationId
2594
+ }, referer, options);
2595
+ } catch (err) {
2596
+ throw new BuyOrderError("createbuyorder finalize request failed", {
2597
+ stage: "finalize",
2598
+ mayExist: true,
2599
+ confirmationId,
2600
+ cause: err
2601
+ });
2602
+ }
2603
+ if (final.buyOrderId) return final.buyOrderId;
2604
+ throw buyOrderRefusal(final, "finalize", confirmationId);
2605
+ }
2606
+ async cancelBuyOrder(buyOrderId, options = {}) {
2607
+ await this.session.getAccessToken();
2608
+ const sessionid = await this.http.getSessionId();
2609
+ const res = await this.http.post(`${URLS.community}/market/cancelbuyorder/`, {
2610
+ ...requestControl(options),
2611
+ noEligibilityReplay: true,
2612
+ responseType: "json",
2613
+ form: {
2614
+ sessionid,
2615
+ buy_orderid: buyOrderId
2616
+ },
2617
+ headers: { Referer: MARKET_REFERER }
2618
+ });
2619
+ if (res.statusCode !== 200) throw httpError(res);
2620
+ const body = res.body ?? {};
2621
+ if (body.success === 1 || body.success === true) return body;
2622
+ throw new SteamError(`cancelbuyorder refused for ${buyOrderId}`, {
2623
+ ...eresultOf(body),
2624
+ body
2625
+ });
2626
+ }
2627
+ async confirmListingIds(listingIds, options = {}) {
2628
+ const wanted = new Set(listingIds.map(String));
2629
+ if (wanted.size === 0) return {
2630
+ confirmed: [],
2631
+ missing: []
2632
+ };
2633
+ await this.session.getAccessToken();
2634
+ const confirmed = (await this.confirmations.getPending(options)).filter((c) => c.type === 3 && wanted.has(c.creator));
2635
+ await this.confirmations.acceptConfirmations(confirmed, options);
2636
+ const found = new Set(confirmed.map((c) => c.creator));
2637
+ return {
2638
+ confirmed,
2639
+ missing: [...wanted].filter((id) => !found.has(id))
2640
+ };
2641
+ }
2642
+ async confirmListings(expected, options = {}) {
2643
+ if (Object.values(expected).reduce((sum, n) => sum + n, 0) === 0) return {
2644
+ confirmed: [],
2645
+ skipped: []
2646
+ };
2647
+ await this.session.getAccessToken();
2648
+ const listings = (await this.confirmations.getPending(options)).filter((c) => c.type === 3).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
2649
+ const remaining = { ...expected };
2650
+ const confirmed = [];
2651
+ const skipped = [];
2652
+ for (const conf of listings) {
2653
+ const name = (remaining[conf.sending] ?? 0) > 0 ? conf.sending : Object.keys(remaining).find((n) => (remaining[n] ?? 0) > 0 && conf.sending !== "" && n.includes(conf.sending));
2654
+ if (name === void 0) {
2655
+ skipped.push(conf);
2656
+ continue;
2657
+ }
2658
+ confirmed.push(conf);
2659
+ remaining[name] = (remaining[name] ?? 0) - 1;
2660
+ }
2661
+ await this.confirmations.acceptConfirmations(confirmed, options);
2662
+ return {
2663
+ confirmed,
2664
+ skipped
2665
+ };
2666
+ }
2667
+ async rejectListings(types = [3], options = {}) {
2668
+ await this.session.getAccessToken();
2669
+ const keep = new Set((options.keepCreators ?? []).map(String));
2670
+ const matched = (await this.confirmations.getPending(options)).filter((c) => types.includes(c.type) && !keep.has(c.creator));
2671
+ await this.confirmations.rejectConfirmations(matched, options);
2672
+ return matched;
2673
+ }
2674
+ async acceptBuyOrderConfirmation(confirmationId, options) {
2675
+ let conf;
2676
+ try {
2677
+ for (const delay of BUY_ORDER_CONFIRMATION_LOOKUPS) {
2678
+ if (delay > 0) await sleep(delay, options.signal);
2679
+ conf = (await this.confirmations.getPending(options)).find((c) => c.creator === confirmationId && c.type === 12);
2680
+ if (conf) break;
2681
+ }
2682
+ } catch (err) {
2683
+ throw new BuyOrderError("could not read the buy order's mobile confirmation", {
2684
+ stage: "confirm",
2685
+ mayExist: false,
2686
+ confirmationId,
2687
+ cause: err
2688
+ });
2689
+ }
2690
+ if (!conf) throw new BuyOrderError(`no mobile confirmation found for buy order ${confirmationId}`, {
2691
+ stage: "confirm",
2692
+ mayExist: false,
2693
+ confirmationId
2694
+ });
2695
+ try {
2696
+ await this.confirmations.acceptConfirmations([conf], options);
2697
+ } catch (err) {
2698
+ throw new BuyOrderError("accepting the buy order's mobile confirmation failed", {
2699
+ stage: "confirm",
2700
+ mayExist: !(err instanceof ConfirmationError && !(err instanceof ConfirmationOutcomeUnknownError)),
2701
+ confirmationId,
2702
+ cause: err
2703
+ });
2704
+ }
2705
+ }
2706
+ async postBuyOrder(form, referer, control) {
2707
+ const res = await this.http.post(`${URLS.community}/market/createbuyorder/`, {
2708
+ ...requestControl(control),
2709
+ noEligibilityReplay: true,
2710
+ form,
2711
+ headers: { Referer: referer }
2712
+ });
2713
+ if (res.statusCode === 429 || res.statusCode === 401) throw httpError(res);
2714
+ const raw = typeof res.body === "string" ? res.body : "";
2715
+ const body = safeJson(raw);
2716
+ const ok = body?.success === 1 || body?.success === true || body?.success === void 0 && body?.wallet_info?.success === 1;
2717
+ return {
2718
+ res,
2719
+ body,
2720
+ buyOrderId: res.statusCode === 200 && ok ? rawIdField(raw, "buy_orderid") ?? void 0 : void 0,
2721
+ confirmationId: rawIdField(raw, "confirmation_id") ?? void 0
2722
+ };
2723
+ }
2724
+ readCurrency(explicit, context) {
2725
+ if (explicit === null) return null;
2726
+ const known = this.currency;
2727
+ if (explicit !== void 0) {
2728
+ if (known !== void 0 && explicit !== known) throw new CurrencyMismatchError(known, explicit, context);
2729
+ return explicit;
2730
+ }
2731
+ if (known === void 0) throw new WalletCurrencyUnknownError(context);
2732
+ return known;
2733
+ }
2734
+ writeCurrency(explicit, context) {
2735
+ const known = this.currency;
2736
+ if (known === void 0 || !this.currencyVerified) throw new WalletCurrencyUnknownError(context);
2737
+ if (explicit !== void 0 && explicit !== known) throw new CurrencyMismatchError(known, explicit, context);
2738
+ return known;
2739
+ }
2740
+ learnCurrency(currency, context) {
2741
+ if (this.currency !== void 0 && this.currency !== currency) throw new CurrencyMismatchError(this.currency, currency, context);
2742
+ this.currency = currency;
2743
+ this.currencyVerified = true;
2744
+ }
2745
+ confirmCurrency(currency) {
2746
+ if (this.currency === currency) this.currencyVerified = true;
2747
+ }
2748
+ };
2749
+ function summarizeOrderbook(data) {
2750
+ const sells = depth(data.rgCompactSellOrders, "asc");
2751
+ const buys = depth(data.rgCompactBuyOrders, "desc");
2752
+ return {
2753
+ currency: data.eCurrency,
2754
+ lowestAsk: level(data.amtMinSellOrder, sells),
2755
+ askVolumeAtLowest: sells[1] ?? 0,
2756
+ secondLowestAsk: sells.length >= 4 ? sells[2] ?? null : null,
2757
+ highestBid: level(data.amtMaxBuyOrder, buys),
2758
+ bidVolumeAtHighest: buys[1] ?? 0,
2759
+ secondHighestBid: buys.length >= 4 ? buys[2] ?? null : null
2760
+ };
2761
+ }
2762
+ function wholeCents(v) {
2763
+ if (typeof v === "number") return Number.isSafeInteger(v) ? v : null;
2764
+ if (typeof v === "string" && /^\d+$/.test(v)) {
2765
+ const n = Number(v);
2766
+ return Number.isSafeInteger(n) ? n : null;
2767
+ }
2768
+ return null;
2769
+ }
2770
+ function depth(raw, order) {
2771
+ if (raw === void 0 || raw === null) return [];
2772
+ if (!Array.isArray(raw) || raw.length % 2 !== 0) throw new SteamError("Malformed orderbook depth", { body: raw });
2773
+ const out = [];
2774
+ for (const v of raw) {
2775
+ const n = wholeCents(v);
2776
+ if (n === null || n <= 0) throw new SteamError("Malformed orderbook depth", { body: raw });
2777
+ out.push(n);
2778
+ }
2779
+ for (let i = 2; i < out.length; i += 2) {
2780
+ const prev = out[i - 2];
2781
+ const cur = out[i];
2782
+ if (order === "asc" ? cur <= prev : cur >= prev) throw new SteamError("Orderbook depth is not strictly ordered", { body: raw });
2783
+ }
2784
+ return out;
2785
+ }
2786
+ function level(best, levels) {
2787
+ if (best === null || best === void 0) {
2788
+ if (levels.length > 0) throw new SteamError("Orderbook has depth but no best level");
2789
+ return null;
2790
+ }
2791
+ const value = wholeCents(best);
2792
+ if (value === null || value <= 0) throw new SteamError(`Malformed orderbook best level ${best}`);
2793
+ if (levels.length === 0 || levels[0] !== value) throw new SteamError(`Orderbook best level ${value} disagrees with its depth ${levels[0]}`);
2794
+ return value;
2795
+ }
2796
+ const PINNED_PRICE_FORMATS = /* @__PURE__ */ new Set([1, 3]);
2797
+ function priceHistoryCurrency(prefix, suffix) {
2798
+ const p = typeof prefix === "string" ? prefix.trim() : "";
2799
+ const s = typeof suffix === "string" ? suffix.trim() : "";
2800
+ if (p === "$" && s === "") return 1;
2801
+ if (p === "" && s === "€") return 3;
2802
+ }
2803
+ function assertListingCurrency(listing, currency) {
2804
+ const raw = listing.currencyid;
2805
+ const actual = raw === void 0 || raw === null ? void 0 : Number(raw) - LISTING_CURRENCY_BASE;
2806
+ if (actual !== currency) throw new CurrencyMismatchError(currency, actual, `listing ${listing.listingid}`);
2807
+ }
2808
+ function assertPositiveCents(name, value) {
2809
+ if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${name} must be a positive integer, got ${value}`);
2810
+ }
2811
+ function isNotPlacedError(err) {
2812
+ return err instanceof RateLimitError || err instanceof SteamSessionExpiredError || err instanceof MarketIneligibleError;
2813
+ }
2814
+ function redirectsToMarket(res) {
2815
+ const location = res.headers.location;
2816
+ if (typeof location !== "string" || !URL.canParse(location, URLS.community)) return false;
2817
+ const { pathname } = new URL(location, URLS.community);
2818
+ return pathname === "/market/" || pathname === "/market";
2819
+ }
2820
+ function eresultOf(body) {
2821
+ const success = body?.success;
2822
+ return typeof success === "number" && success !== 1 ? { eresult: success } : {};
2823
+ }
2824
+ function sellItemError(message, body) {
2825
+ const options = {
2826
+ ...eresultOf(body),
2827
+ body
2828
+ };
2829
+ const lower = message.toLowerCase();
2830
+ if (lower.includes("pending confirmation")) {
2831
+ const reason = lower.includes("already have a listing") ? "item_pending" : lower.includes("too many") ? "too_many_pending" : "unknown";
2832
+ return new MarketConfirmationLimitError(message, {
2833
+ ...options,
2834
+ reason
2835
+ });
2836
+ }
2837
+ if (lower.includes("maximum wallet balance")) return new WalletBalanceLimitError(message, options);
2838
+ if (lower.includes("unable to use the community market")) return new MarketBlockedError(message, options);
2839
+ if (lower.includes("no longer in your inventory") || lower.includes("not allowed to be traded")) return new MarketItemUnavailableError(message, options);
2840
+ return new SteamError(message, options);
2841
+ }
2842
+ function buyOrderRefusal(reply, stage, confirmationId) {
2843
+ const { res, body } = reply;
2844
+ const code = typeof body?.success === "number" && body.success !== 1 ? body.success : void 0;
2845
+ const message = typeof body?.message === "string" ? body.message : void 0;
2846
+ if (code === 15 || message?.toLowerCase().includes("unable to use the community market")) return new MarketBlockedError(message, {
2847
+ ...code !== void 0 ? { eresult: code } : {},
2848
+ body
2849
+ });
2850
+ if (code === void 0 && message === void 0 && stage === "create") {
2851
+ if (![200, 406].includes(res.statusCode)) {
2852
+ const transport = httpError(res);
2853
+ if (isNotPlacedError(transport)) return transport;
2854
+ }
2855
+ }
2856
+ const unknownOutcome = message !== void 0 && UNKNOWN_OUTCOME_RE.test(message);
2857
+ const notPlaced = code !== void 0 && BUY_ORDER_NOT_PLACED_CODES.has(code) && !unknownOutcome;
2858
+ const orderExists = code === DUPLICATE_ORDER_CODE || message !== void 0 && /already have an active buy order/i.test(message);
2859
+ return new BuyOrderError(message ?? `createbuyorder ${stage} failed (HTTP ${res.statusCode})`, {
2860
+ stage,
2861
+ mayExist: !notPlaced,
2862
+ orderExists,
2863
+ ...code !== void 0 ? { code } : {},
2864
+ ...confirmationId !== void 0 ? { confirmationId } : {},
2865
+ body: body ?? res.body
2866
+ });
2867
+ }
2868
+ function safeJson(text) {
2869
+ if (!text) return void 0;
2870
+ try {
2871
+ return JSON.parse(text);
2872
+ } catch {
2873
+ return;
2874
+ }
2875
+ }
2876
+ function rawIdField(raw, field) {
2877
+ return raw.match(new RegExp(`"${field}"\\s*:\\s*"?(\\d+)"?`))?.[1] ?? null;
2878
+ }
2879
+ function sleep(ms, signal) {
2880
+ return new Promise((resolve, reject) => {
2881
+ if (signal?.aborted) {
2882
+ reject(signal.reason);
2883
+ return;
2884
+ }
2885
+ const timer = setTimeout(() => {
2886
+ signal?.removeEventListener("abort", onAbort);
2887
+ resolve();
2888
+ }, ms);
2889
+ const onAbort = () => {
2890
+ clearTimeout(timer);
2891
+ reject(signal?.reason);
2892
+ };
2893
+ signal?.addEventListener("abort", onAbort, { once: true });
2894
+ });
2895
+ }
1880
2896
  /**
1881
2897
  * Describes the message CEconItemPreviewDataBlock.
1882
2898
  * Use `create(CEconItemPreviewDataBlockSchema)` to create a new message.
@@ -2961,6 +3977,7 @@ var SteamMobile = class extends EventEmitter {
2961
3977
  confirmations;
2962
3978
  trade;
2963
3979
  community;
3980
+ market;
2964
3981
  identitySecret;
2965
3982
  polling;
2966
3983
  proxy;
@@ -2981,6 +3998,8 @@ var SteamMobile = class extends EventEmitter {
2981
3998
  this.confirmations = new ConfirmationManager(this.http, this.session.steamID, options.identitySecret, profile);
2982
3999
  this.trade = new TradeNamespace(this.api, this.http, this.session, this.confirmations);
2983
4000
  this.community = new CommunityNamespace(this.http, this.session, this.confirmations, this.api);
4001
+ this.market = new MarketNamespace(this.http, this.session, this.confirmations, this.api);
4002
+ if (options.walletCurrency !== void 0) this.market.seedWalletCurrency(options.walletCurrency);
2984
4003
  this.session.on("refreshToken", (token) => this.emit("refreshToken", token));
2985
4004
  this.session.on("sessionExpired", (error) => this.emit("sessionExpired", error));
2986
4005
  this.session.on("debug", (message) => this.emit("debug", message));
@@ -3040,6 +4059,6 @@ var SteamMobile = class extends EventEmitter {
3040
4059
  }
3041
4060
  };
3042
4061
  //#endregion
3043
- export { ANDROID_PROFILE, AccessTokenError, AuthClient, CommunityNamespace, ConfirmationError, ConfirmationManager, CredentialSession, DEFAULT_CONTEXTID, DEFAULT_POLL_FULL_UPDATE_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_POLL_MAX_AGE_MS, DEFAULT_RATE_LIMIT_RETRY_MS, EAuthSessionGuardType, EAuthTokenPlatformType, EAuthTokenRevokeAction, EConfirmationMethod, EConfirmationType, EOfferFilter, EResult, ESessionPersistence, ETokenRenewalType, ETradeOfferState, ETradeStatus, EscrowError, FamilyViewError, HttpClient, HttpStatusError, IOS_PROFILE, ItemServerUnavailableError, LANG, LoginError, NewDeviceError, NoMobileAuthenticatorError, OfferLimitError, OpenIdError, Poller, PrivateInventoryError, ProxyError, RATE_LIMITS, RETRY_AFTER, RateLimitError, SessionManager, SteamError, SteamMobile, SteamSessionExpiredError, SteamWebApi, TERMINAL_AUTH_ERESULTS, TRANSIENT_ERESULTS, TargetCannotTradeError, TradeBanError, TradeNamespace, TradeOffer, URLS, WebApiClient, confirmOpenid, decodeJwt, decodePreviewToken, encodePreviewToken, getTradeHistory, getTradeOffersSummary, getTradeStatus, isTerminalAuthEResult, isTerminalState, isTransientEResult, loginWithCredentials, parseInventory, parseOpenidForm, parsePartnerInventory, resolveMobileProfile, resolveTarget, secondsUntilExpiry, steamOpenidLogin };
4062
+ export { ANDROID_PROFILE, AccessTokenError, AuthClient, BuyOrderError, CommunityNamespace, ConfirmationError, ConfirmationManager, ConfirmationOutcomeUnknownError, CredentialSession, CurrencyMismatchError, DEFAULT_CONTEXTID, DEFAULT_MARKET_FEE_INFO, DEFAULT_POLL_FULL_UPDATE_INTERVAL, DEFAULT_POLL_INTERVAL, DEFAULT_POLL_MAX_AGE_MS, DEFAULT_RATE_LIMIT_RETRY_MS, EAuthSessionGuardType, EAuthTokenPlatformType, EAuthTokenRevokeAction, EConfirmationMethod, EConfirmationType, EOfferFilter, EResult, ESessionPersistence, ETokenRenewalType, ETradeOfferState, ETradeStatus, EscrowError, FamilyViewError, HttpClient, HttpStatusError, IOS_PROFILE, ItemServerUnavailableError, LANG, LoginError, MarketBlockedError, MarketConfirmationLimitError, MarketIneligibleError, MarketItemUnavailableError, MarketNamespace, NewDeviceError, NoMobileAuthenticatorError, OfferLimitError, OpenIdError, Poller, PrivateInventoryError, ProxyError, RATE_LIMITS, RETRY_AFTER, RateLimitError, SessionManager, SteamError, SteamMobile, SteamSessionExpiredError, SteamWebApi, TERMINAL_AUTH_ERESULTS, TRANSIENT_ERESULTS, TargetCannotTradeError, ThrottledError, TradeBanError, TradeNamespace, TradeOffer, URLS, WalletBalanceLimitError, WalletCurrencyUnknownError, WebApiClient, buyerPaysForSellerReceives, confirmOpenid, decodeJwt, decodePreviewToken, encodePreviewToken, feeInfoFromWalletInfo, feesForBuyerPays, getPriceValueAsInt, getTradeHistory, getTradeOffersSummary, getTradeStatus, isTerminalAuthEResult, isTerminalState, isTransientEResult, landingBuyerPays, loginWithCredentials, parseInventory, parseMarketHistory, parseOpenidForm, parsePartnerInventory, priceTextCurrency, resolveMobileProfile, resolveTarget, secondsUntilExpiry, sellerReceivesForBuyerPays, steamOpenidLogin, summarizeOrderbook };
3044
4063
 
3045
4064
  //# sourceMappingURL=index.mjs.map