@2bad/walt 2.0.0 → 2.1.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/README.md CHANGED
@@ -21,6 +21,9 @@ npm install @2bad/walt
21
21
 
22
22
  ESM only. Requires Node.js 26 or later.
23
23
 
24
+ `@2bad/walt/client` exports everything except `TelegramTokenSource`.
25
+ It imports no Node.js API and no Telegram library, for runtimes such as Cloudflare Workers.
26
+
24
27
  ## Quick start
25
28
 
26
29
  ```ts
@@ -36,7 +39,7 @@ await source.disconnect()
36
39
 
37
40
  ## Examples
38
41
 
39
- - [Connecting](examples/connecting.md): Telegram credentials, token storage, retries and pacing
42
+ - [Connecting](examples/connecting.md): Telegram credentials, token storage, refreshing without Telegram, retries and pacing
40
43
  - [Balances](examples/balances.md): accounts, portfolio, profit and loss, staked funds
41
44
  - [Prices](examples/prices.md): coin rates, charts and past prices
42
45
  - [Exchange](examples/exchange.md): available pairs, quotes, exchanges and limit orders
@@ -56,6 +59,12 @@ type TelegramTokenSource = {
56
59
  disconnect(): Promise<void>
57
60
  }
58
61
 
62
+ new RefreshTokenSource(options: RefreshTokenSourceOptions)
63
+
64
+ type RefreshTokenSource = {
65
+ token(rejected?: string): Promise<string>
66
+ }
67
+
59
68
  new WaltClient(options: WaltClientOptions)
60
69
 
61
70
  type WaltClient = {
@@ -1,8 +1,9 @@
1
1
  export type CandleRequest = {
2
2
  /** `<currency>/USD/SPOT`, or `<base>/USD/PERP_TRADE` for a perps contract. */
3
3
  ticker: string;
4
- /** Seen: `5m`, `15m`. */
4
+ /** Accepted: `1m`, `5m`, `15m`, `1h`, `4h`, `1d`, `1w`, `1mo`. */
5
5
  resolution: string;
6
+ /** At least 1. The whole batch may ask for 3000 candles at most. */
6
7
  limit: number;
7
8
  };
8
9
  export type Candle = {
package/build/bin/walt.js CHANGED
@@ -46,6 +46,7 @@ const USAGE = `Usage: walt <command> [options]
46
46
 
47
47
  ${COMMANDS}
48
48
  login sign in to Telegram and print TG_SESSION
49
+ token sign in as a new device and print its token state as JSON
49
50
 
50
51
  Options:
51
52
  --json print JSON instead of a table
@@ -109,15 +110,26 @@ const session = process.env["TG_SESSION"];
109
110
  const apiId = Number(process.env["TG_API_ID"]);
110
111
  const apiHash = process.env["TG_API_HASH"];
111
112
  if (!session || !apiId || !apiHash) fail("set TG_API_ID, TG_API_HASH and TG_SESSION first (run `walt login` for the session)", 2);
113
+ if (command === "token") console.log = console.error;
114
+ let issued = null;
112
115
  const source = new TelegramTokenSource({
113
116
  session: session ?? "",
114
117
  apiId,
115
- apiHash: apiHash ?? ""
118
+ apiHash: apiHash ?? "",
119
+ ...command === "token" && { storage: {
120
+ load: async () => null,
121
+ save: async (state) => {
122
+ issued = state;
123
+ }
124
+ } }
116
125
  });
117
126
  const client = new WaltClient({ tokenProvider: (rejected) => source.token(rejected) });
118
127
  let exitCode = 0;
119
128
  try {
120
- console.log(await run(client, command, args, values));
129
+ if (command === "token") {
130
+ await source.token();
131
+ stdout.write(`${JSON.stringify(issued)}\n`);
132
+ } else console.log(await run(client, command, args, values));
121
133
  } catch (error) {
122
134
  if (error instanceof UsageError) {
123
135
  console.error(`walt: ${error.message}\nRun \`walt --help\` for usage.`);
@@ -10,7 +10,7 @@ export type CliOptions = {
10
10
  closed?: boolean;
11
11
  };
12
12
  export declare class UsageError extends Error {}
13
- export declare const COMMANDS = "Commands:\n balances every asset: spot, staked and held by limit orders\n history [--monthly] balance, deposits, withdrawals and P/L per day or month\n [--from DATE] start date, YYYY-MM-DD (default: 30 days or 12 months back)\n transactions latest transactions\n [--asset SYM] only this asset\n [--limit N] how many (default 20)\n price SYM... current price and change over 24h, 7d and 30d\n quote FROM TO AMOUNT what an exchange would give right now, without making it\n orders [--all] open limit orders, or every order with --all\n perps [--closed] open perpetual positions, or closed ones\n earn [--all] your earn stakes, or every active campaign with --all";
13
+ export declare const COMMANDS = "Commands:\n balances [--all] every asset: spot, staked and held by limit orders,\n and with --all the ones worth less than 0.01\n history [--monthly] balance, deposits, withdrawals and P/L per day or month\n [--from DATE] start date, YYYY-MM-DD (default: 30 days or 12 months back)\n transactions latest transactions\n [--asset SYM] only this asset\n [--limit N] how many (default 20)\n price SYM... current price and change over 24h, 7d and 30d\n quote FROM TO AMOUNT what an exchange would give right now, without making it\n orders [--all] open limit orders, or every order with --all\n perps [--closed] open perpetual positions, or closed ones\n earn [--all] your earn stakes, or every active campaign with --all";
14
14
  /**
15
15
  * Runs one read-only command and returns what to print.
16
16
  *
package/build/cli/run.js CHANGED
@@ -2,7 +2,8 @@ import { WaltRequestError } from "../transport.js";
2
2
  // source/cli/run.ts
3
3
  var UsageError = class extends Error {};
4
4
  const COMMANDS = `Commands:
5
- balances every asset: spot, staked and held by limit orders
5
+ balances [--all] every asset: spot, staked and held by limit orders,
6
+ and with --all the ones worth less than 0.01
6
7
  history [--monthly] balance, deposits, withdrawals and P/L per day or month
7
8
  [--from DATE] start date, YYYY-MM-DD (default: 30 days or 12 months back)
8
9
  transactions latest transactions
@@ -22,7 +23,7 @@ const trimmed = (value) => {
22
23
  if (value === null || value === void 0 || value === "" || !Number.isFinite(parsed)) return "-";
23
24
  return String(Number(parsed.toFixed(8)));
24
25
  };
25
- const significant = (value) => String(Number(value.toPrecision(8)));
26
+ const significant = (value) => Number.isFinite(value) ? String(Number(value.toPrecision(8))) : "-";
26
27
  const percent = (value) => Number.isFinite(value) ? `${value >= 0 ? "+" : ""}${(value * 100).toFixed(2)}%` : "-";
27
28
  const change = (now, then) => then === void 0 || then === 0 ? "-" : percent(now / then - 1);
28
29
  const minute = (timestamp) => timestamp.slice(0, 16).replace("T", " ");
@@ -38,13 +39,15 @@ const positiveInteger = (value, fallback) => {
38
39
  if (!Number.isSafeInteger(parsed) || parsed < 1) throw new UsageError(`--limit must be a whole number, got ${value}`);
39
40
  return parsed;
40
41
  };
41
- const balances = async (client, fiat) => {
42
+ const balances = async (client, fiat, options) => {
42
43
  const data = await client.balances(fiat);
43
44
  const total = Number(data.overall_available_balance_fiat_amount);
44
- const rows = data.accounts.filter((account) => Number(account.total_balance) > 0).map((account) => ({
45
+ const held = data.accounts.filter((account) => Number(account.total_balance) > 0).map((account) => ({
45
46
  account,
46
47
  value: Number(account.available_balance_fiat_amount)
47
- })).sort((a, b) => b.value - a.value).map(({ account, value }) => ({
48
+ }));
49
+ const shown = options.all ? held : held.filter(({ value }) => !(value < .005));
50
+ const rows = shown.sort((a, b) => b.value - a.value).map(({ account, value }) => ({
48
51
  asset: account.currency,
49
52
  kind: ACCOUNT_KINDS[account.account_type] ?? account.account_type.toLowerCase(),
50
53
  amount: trimmed(account.total_balance),
@@ -78,7 +81,7 @@ const balances = async (client, fiat) => {
78
81
  }
79
82
  ],
80
83
  rows,
81
- footer: [`total ${total.toFixed(2)} ${data.fiat_currency}`]
84
+ footer: [`total ${total.toFixed(2)} ${data.fiat_currency}`, ...shown.length < held.length ? [`${held.length - shown.length} worth less than 0.01 not shown, --all lists them`] : []]
82
85
  };
83
86
  };
84
87
  const history = async (client, options) => {
@@ -168,7 +171,7 @@ const transactions = async (client, options) => {
168
171
  time: minute(row.created_at),
169
172
  type: row.type,
170
173
  gateway: row.gateway,
171
- amount: row.amount,
174
+ amount: trimmed(row.amount),
172
175
  asset: row.currency,
173
176
  status: row.status
174
177
  }))
@@ -336,7 +339,7 @@ const orders = async (client, options) => {
336
339
  fee: order.fee,
337
340
  status: order.status
338
341
  })),
339
- footer: [`${held.toFixed(2)} held by open orders`]
342
+ footer: [`${held.toFixed(2)} USDT held by open orders`]
340
343
  };
341
344
  };
342
345
  const perps = async (client, options) => {
@@ -397,13 +400,13 @@ const perps = async (client, options) => {
397
400
  contract: position.contract_name,
398
401
  side: position.side,
399
402
  leverage: `${position.leverage}x`,
400
- size: position.value_base,
401
- entry: position.avg_entry_price,
403
+ size: trimmed(position.value_base),
404
+ entry: significant(Number(position.avg_entry_price)),
402
405
  margin: fixed(position.margin_quote),
403
406
  opened: minute(position.created_at),
404
407
  closed: minute(position.updated_at),
405
- mark: position.valuation.mark_price,
406
- liquidation: fixed(position.liquidation_price, 4),
408
+ mark: significant(Number(position.valuation.mark_price)),
409
+ liquidation: significant(Number(position.liquidation_price)),
407
410
  pnl: fixed(position.valuation.unrealized_pnl_quote)
408
411
  }))
409
412
  };
@@ -476,7 +479,7 @@ const run = async (client, command, args, options = {}) => {
476
479
  let report;
477
480
  switch (command) {
478
481
  case "balances":
479
- report = await balances(client, fiat);
482
+ report = await balances(client, fiat, options);
480
483
  break;
481
484
  case "history":
482
485
  report = await history(client, options);
@@ -0,0 +1,13 @@
1
+ import { Account, AccountAction, AccountActions, AccountAddress, AccountsResponse, ChartPoint, Charts, Coin, CoinDetails, FiatCurrency, PriceAtTime, TradingHoursPeriod, TradingHoursState } from "./api/accounts.js";
2
+ import { PnlPosition, PnlResponse, PnlTotals, PortfolioFlows, PortfolioHistory, PortfolioPeriod } from "./api/analytics.js";
3
+ import { Balance, Balances } from "./api/balances.js";
4
+ import { Candle, CandleRequest, CandleSeries } from "./api/candles.js";
5
+ import { EarnAccessPolicy, EarnAccount, EarnAmount, EarnBonus, EarnCampaign, EarnCampaignsResponse, EarnDepegInfo, EarnInterestRate, EarnPayoutPool, EarnRate, EarnTransaction, EarnTransactionsPage } from "./api/earn.js";
6
+ import { AvailableExchanges, Exchange, ExchangeQuote, ExchangeRequest } from "./api/exchange.js";
7
+ import { LimitOrder, LimitOrderBounds, LimitOrderQuote, LimitOrderToken } from "./api/limit-orders.js";
8
+ import { PerpContract, PerpOrder, PerpOrderStatus, PerpOrdersPage, PerpPosition, PerpPositionStatus, PerpSystemStatus } from "./api/perps.js";
9
+ import { NormalizedTransaction, Transaction, TransactionDetails, TransactionGateway, TransactionLeg, TransactionsPage, normalizeTransaction } from "./api/transactions.js";
10
+ import { Portfolio, PortfolioEntry, WaltClient, WaltClientOptions } from "./client.js";
11
+ import { RefreshTokenSource, RefreshTokenSourceOptions, TokenState, TokenStorage } from "./refresh.js";
12
+ import { WaltAuthError, WaltRequestError } from "./transport.js";
13
+ export { type Account, type AccountAction, type AccountActions, type AccountAddress, type AccountsResponse, type AvailableExchanges, type Balance, type Balances, type Candle, type CandleRequest, type CandleSeries, type ChartPoint, type Charts, type Coin, type CoinDetails, type EarnAccessPolicy, type EarnAccount, type EarnAmount, type EarnBonus, type EarnCampaign, type EarnCampaignsResponse, type EarnDepegInfo, type EarnInterestRate, type EarnPayoutPool, type EarnRate, type EarnTransaction, type EarnTransactionsPage, type Exchange, type ExchangeQuote, type ExchangeRequest, type FiatCurrency, type LimitOrder, type LimitOrderBounds, type LimitOrderQuote, type LimitOrderToken, type NormalizedTransaction, type PerpContract, type PerpOrder, type PerpOrderStatus, type PerpOrdersPage, type PerpPosition, type PerpPositionStatus, type PerpSystemStatus, type PnlPosition, type PnlResponse, type PnlTotals, type Portfolio, type PortfolioEntry, type PortfolioFlows, type PortfolioHistory, type PortfolioPeriod, type PriceAtTime, RefreshTokenSource, type RefreshTokenSourceOptions, type TokenState, type TokenStorage, type TradingHoursPeriod, type TradingHoursState, type Transaction, type TransactionDetails, type TransactionGateway, type TransactionLeg, type TransactionsPage, WaltAuthError, WaltClient, type WaltClientOptions, WaltRequestError, normalizeTransaction };
package/build/core.js ADDED
@@ -0,0 +1,5 @@
1
+ import { normalizeTransaction } from "./api/transactions.js";
2
+ import { WaltClient } from "./client.js";
3
+ import { RefreshTokenSource } from "./refresh.js";
4
+ import { WaltAuthError, WaltRequestError } from "./transport.js";
5
+ export { RefreshTokenSource, WaltAuthError, WaltClient, WaltRequestError, normalizeTransaction };
package/build/index.d.ts CHANGED
@@ -8,6 +8,7 @@ import { LimitOrder, LimitOrderBounds, LimitOrderQuote, LimitOrderToken } from "
8
8
  import { PerpContract, PerpOrder, PerpOrderStatus, PerpOrdersPage, PerpPosition, PerpPositionStatus, PerpSystemStatus } from "./api/perps.js";
9
9
  import { NormalizedTransaction, Transaction, TransactionDetails, TransactionGateway, TransactionLeg, TransactionsPage, normalizeTransaction } from "./api/transactions.js";
10
10
  import { Portfolio, PortfolioEntry, WaltClient, WaltClientOptions } from "./client.js";
11
- import { TelegramTokenSource, TelegramTokenSourceOptions, TokenState, TokenStorage } from "./telegram/auth.js";
11
+ import { RefreshTokenSource, RefreshTokenSourceOptions, TokenState, TokenStorage } from "./refresh.js";
12
12
  import { WaltAuthError, WaltRequestError } from "./transport.js";
13
- export { type Account, type AccountAction, type AccountActions, type AccountAddress, type AccountsResponse, type AvailableExchanges, type Balance, type Balances, type Candle, type CandleRequest, type CandleSeries, type ChartPoint, type Charts, type Coin, type CoinDetails, type EarnAccessPolicy, type EarnAccount, type EarnAmount, type EarnBonus, type EarnCampaign, type EarnCampaignsResponse, type EarnDepegInfo, type EarnInterestRate, type EarnPayoutPool, type EarnRate, type EarnTransaction, type EarnTransactionsPage, type Exchange, type ExchangeQuote, type ExchangeRequest, type FiatCurrency, type LimitOrder, type LimitOrderBounds, type LimitOrderQuote, type LimitOrderToken, type NormalizedTransaction, type PerpContract, type PerpOrder, type PerpOrderStatus, type PerpOrdersPage, type PerpPosition, type PerpPositionStatus, type PerpSystemStatus, type PnlPosition, type PnlResponse, type PnlTotals, type Portfolio, type PortfolioEntry, type PortfolioFlows, type PortfolioHistory, type PortfolioPeriod, type PriceAtTime, TelegramTokenSource, type TelegramTokenSourceOptions, type TokenState, type TokenStorage, type TradingHoursPeriod, type TradingHoursState, type Transaction, type TransactionDetails, type TransactionGateway, type TransactionLeg, type TransactionsPage, WaltAuthError, WaltClient, type WaltClientOptions, WaltRequestError, normalizeTransaction };
13
+ import { TelegramTokenSource, TelegramTokenSourceOptions } from "./telegram/auth.js";
14
+ export { type Account, type AccountAction, type AccountActions, type AccountAddress, type AccountsResponse, type AvailableExchanges, type Balance, type Balances, type Candle, type CandleRequest, type CandleSeries, type ChartPoint, type Charts, type Coin, type CoinDetails, type EarnAccessPolicy, type EarnAccount, type EarnAmount, type EarnBonus, type EarnCampaign, type EarnCampaignsResponse, type EarnDepegInfo, type EarnInterestRate, type EarnPayoutPool, type EarnRate, type EarnTransaction, type EarnTransactionsPage, type Exchange, type ExchangeQuote, type ExchangeRequest, type FiatCurrency, type LimitOrder, type LimitOrderBounds, type LimitOrderQuote, type LimitOrderToken, type NormalizedTransaction, type PerpContract, type PerpOrder, type PerpOrderStatus, type PerpOrdersPage, type PerpPosition, type PerpPositionStatus, type PerpSystemStatus, type PnlPosition, type PnlResponse, type PnlTotals, type Portfolio, type PortfolioEntry, type PortfolioFlows, type PortfolioHistory, type PortfolioPeriod, type PriceAtTime, RefreshTokenSource, type RefreshTokenSourceOptions, TelegramTokenSource, type TelegramTokenSourceOptions, type TokenState, type TokenStorage, type TradingHoursPeriod, type TradingHoursState, type Transaction, type TransactionDetails, type TransactionGateway, type TransactionLeg, type TransactionsPage, WaltAuthError, WaltClient, type WaltClientOptions, WaltRequestError, normalizeTransaction };
package/build/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { normalizeTransaction } from "./api/transactions.js";
1
+ import { WaltAuthError, WaltRequestError } from "./transport.js";
2
2
  import { WaltClient } from "./client.js";
3
+ import { normalizeTransaction } from "./api/transactions.js";
4
+ import { RefreshTokenSource } from "./refresh.js";
3
5
  import { TelegramTokenSource } from "./telegram/auth.js";
4
- import { WaltAuthError, WaltRequestError } from "./transport.js";
5
- export { TelegramTokenSource, WaltAuthError, WaltClient, WaltRequestError, normalizeTransaction };
6
+ export { RefreshTokenSource, TelegramTokenSource, WaltAuthError, WaltClient, WaltRequestError, normalizeTransaction };
@@ -0,0 +1,60 @@
1
+ export type TokenState = {
2
+ jwt: string;
3
+ refreshToken: string;
4
+ sessionId: string;
5
+ expiresAt: number;
6
+ deviceSerial: string;
7
+ };
8
+ export type TokenStorage = {
9
+ load: () => Promise<TokenState | null>;
10
+ save: (state: TokenState) => Promise<void>;
11
+ };
12
+ export type RefreshTokenSourceOptions = {
13
+ /** Where to start. Without it the state comes from `storage.load()`. */
14
+ state?: TokenState;
15
+ /** `save` receives the new state after every renewal, rotated refresh token included. */
16
+ storage?: TokenStorage | undefined;
17
+ /** Origin of the web app, sent as `origin` and `referer`. */
18
+ baseUrl?: string | undefined;
19
+ /** Where the refresh requests go. */
20
+ authUrl?: string | undefined;
21
+ fetch?: typeof fetch | undefined;
22
+ };
23
+ export type AuthResponse = {
24
+ jwt: string;
25
+ refresh_token: string;
26
+ session_id: string;
27
+ access_token_expires_at: string;
28
+ access_token_expires_in: number;
29
+ };
30
+ /**
31
+ * Renews the access token from a stored refresh token over HTTPS, without Telegram.
32
+ * Throws `WaltAuthError` once the refresh token is rejected; a new token state is then needed.
33
+ */
34
+ export declare class RefreshTokenSource {
35
+ protected readonly baseUrl: string;
36
+ protected readonly storage: TokenStorage | undefined;
37
+ protected state: TokenState | null;
38
+ private readonly authUrl;
39
+ private readonly fetchImpl;
40
+ private restored;
41
+ private pending;
42
+ constructor(options: RefreshTokenSourceOptions);
43
+ /**
44
+ * @param rejected - A token the API answered with 401. If it is the cached one, it is renewed even before expiry.
45
+ */
46
+ token(rejected?: string): Promise<string>;
47
+ private fresh;
48
+ private resolve;
49
+ private restore;
50
+ /**
51
+ * Called when there is no refresh token to use, or the server rejected it.
52
+ *
53
+ * @param cause - The rejection, if there was one.
54
+ * @returns The new jwt.
55
+ */
56
+ protected signIn(cause?: unknown): Promise<string>;
57
+ private refresh;
58
+ protected post(path: string, body: Record<string, unknown>): Promise<AuthResponse>;
59
+ protected store(data: AuthResponse, deviceSerial: string): Promise<string>;
60
+ }
@@ -0,0 +1,130 @@
1
+ import { BROWSER_HEADERS } from "./browser.js";
2
+ import { WaltAuthError } from "./transport.js";
3
+ // source/refresh.ts
4
+ const BASE_URL = "https://walletbot.me";
5
+ const AUTH_URL = "https://alectryon.walletbot.me";
6
+ const EXPIRY_SKEW_MS = 3e4;
7
+ var AuthEndpointError = class extends Error {
8
+ status;
9
+ constructor(message, status) {
10
+ super(message);
11
+ this.status = status;
12
+ }
13
+ };
14
+ const isRejection = (error) => error instanceof AuthEndpointError && error.status >= 400 && error.status < 500 && error.status !== 429;
15
+ /**
16
+ * Renews the access token from a stored refresh token over HTTPS, without Telegram.
17
+ * Throws `WaltAuthError` once the refresh token is rejected; a new token state is then needed.
18
+ */
19
+ var RefreshTokenSource = class {
20
+ baseUrl;
21
+ storage;
22
+ state;
23
+ authUrl;
24
+ fetchImpl;
25
+ restored;
26
+ pending = null;
27
+ constructor(options) {
28
+ this.state = options.state ?? null;
29
+ this.storage = options.storage;
30
+ this.restored = options.state !== void 0;
31
+ this.baseUrl = options.baseUrl ?? BASE_URL;
32
+ this.authUrl = options.authUrl ?? AUTH_URL;
33
+ this.fetchImpl = options.fetch ?? fetch;
34
+ }
35
+ /**
36
+ * @param rejected - A token the API answered with 401. If it is the cached one, it is renewed even before expiry.
37
+ */
38
+ async token(rejected) {
39
+ if (this.state && rejected !== void 0 && rejected === this.state.jwt) this.state = {
40
+ ...this.state,
41
+ expiresAt: 0
42
+ };
43
+ if (this.state && this.fresh(this.state)) return this.state.jwt;
44
+ this.pending ??= this.resolve();
45
+ try {
46
+ return await this.pending;
47
+ } finally {
48
+ this.pending = null;
49
+ }
50
+ }
51
+ fresh(state) {
52
+ return Date.now() < state.expiresAt - EXPIRY_SKEW_MS;
53
+ }
54
+ async resolve() {
55
+ await this.restore();
56
+ const state = this.state;
57
+ if (state && this.fresh(state)) return state.jwt;
58
+ if (state?.refreshToken) try {
59
+ return await this.refresh(state);
60
+ } catch (error) {
61
+ if (!isRejection(error)) throw error;
62
+ this.state = {
63
+ ...state,
64
+ refreshToken: ""
65
+ };
66
+ return this.signIn(error);
67
+ }
68
+ return this.signIn();
69
+ }
70
+ async restore() {
71
+ if (this.restored || !this.storage) return;
72
+ this.restored = true;
73
+ this.state = await this.storage.load();
74
+ }
75
+ /**
76
+ * Called when there is no refresh token to use, or the server rejected it.
77
+ *
78
+ * @param cause - The rejection, if there was one.
79
+ * @returns The new jwt.
80
+ */
81
+ signIn(cause) {
82
+ const message = this.state ? "refresh token rejected, a new token state is needed" : "no token state to refresh";
83
+ return Promise.reject(new WaltAuthError(message, { cause }));
84
+ }
85
+ async refresh(state) {
86
+ const data = await this.post("/alectryon/public-api/auth-refresh", {
87
+ session_id: state.sessionId,
88
+ refresh_token: state.refreshToken,
89
+ device_serial: state.deviceSerial
90
+ });
91
+ return this.store(data, state.deviceSerial);
92
+ }
93
+ async post(path, body) {
94
+ const response = await this.fetchImpl(`${this.authUrl}${path}`, {
95
+ method: "POST",
96
+ headers: {
97
+ ...BROWSER_HEADERS,
98
+ accept: "*/*",
99
+ "sec-fetch-site": new URL(this.authUrl).origin === new URL(this.baseUrl).origin ? "same-origin" : "same-site",
100
+ "content-type": "application/json",
101
+ origin: this.baseUrl,
102
+ referer: `${this.baseUrl}/`,
103
+ "x-wallet-tz-name": Intl.DateTimeFormat().resolvedOptions().timeZone,
104
+ "x-wallet-tz-offset": String(-(/* @__PURE__ */ new Date()).getTimezoneOffset())
105
+ },
106
+ body: JSON.stringify(body)
107
+ });
108
+ if (!response.ok) throw new AuthEndpointError(`${path} failed (${response.status}): ${await response.text()}`, response.status);
109
+ return await response.json();
110
+ }
111
+ async store(data, deviceSerial) {
112
+ let expiresAt;
113
+ if (Number.isFinite(data.access_token_expires_in)) expiresAt = Date.now() + data.access_token_expires_in * 1e3;
114
+ else {
115
+ const parsed = Date.parse(data.access_token_expires_at);
116
+ expiresAt = Number.isNaN(parsed) ? Date.now() + 3e5 : parsed;
117
+ }
118
+ const state = {
119
+ jwt: data.jwt,
120
+ refreshToken: data.refresh_token,
121
+ sessionId: data.session_id,
122
+ expiresAt,
123
+ deviceSerial
124
+ };
125
+ this.state = state;
126
+ await this.storage?.save(state);
127
+ return state.jwt;
128
+ }
129
+ };
130
+ export { RefreshTokenSource };
@@ -1,14 +1,4 @@
1
- export type TokenState = {
2
- jwt: string;
3
- refreshToken: string;
4
- sessionId: string;
5
- expiresAt: number;
6
- deviceSerial: string;
7
- };
8
- export type TokenStorage = {
9
- load: () => Promise<TokenState | null>;
10
- save: (state: TokenState) => Promise<void>;
11
- };
1
+ import { RefreshTokenSource, TokenStorage } from "../refresh.js";
12
2
  export type TelegramTokenSourceOptions = {
13
3
  session: string;
14
4
  apiId: number;
@@ -24,34 +14,15 @@ export type TelegramTokenSourceOptions = {
24
14
  storage?: TokenStorage | false;
25
15
  maxFloodWaitMs?: number;
26
16
  };
27
- export declare class TelegramTokenSource {
17
+ /** Signs in over Telegram, then renews like `RefreshTokenSource`, signing in again when the refresh token is rejected. */
18
+ export declare class TelegramTokenSource extends RefreshTokenSource {
28
19
  private readonly client;
29
20
  private readonly bot;
30
- private readonly baseUrl;
31
- private readonly authUrl;
32
- private readonly fetchImpl;
33
- private readonly storage;
34
21
  private readonly maxFloodWaitMs;
35
- private deviceSerial;
36
- private deviceSerialKnown;
37
- private jwt;
38
- private refreshToken;
39
- private sessionId;
40
- private expiresAt;
41
- private restored;
42
- private pending;
22
+ private readonly knownDeviceSerial;
23
+ private readonly newDeviceSerial;
43
24
  constructor(options: TelegramTokenSourceOptions);
44
- /**
45
- * @param rejected - A token the API answered with 401. If it is the cached one, it is renewed even before expiry.
46
- */
47
- token(rejected?: string): Promise<string>;
48
- private fresh;
49
- private resolve;
50
- private restore;
51
- private authenticate;
25
+ protected signIn(): Promise<string>;
52
26
  private requestInitData;
53
- private refresh;
54
- private post;
55
- private store;
56
27
  disconnect(): Promise<void>;
57
28
  }
@@ -1,97 +1,40 @@
1
- import { BROWSER_HEADERS } from "../browser.js";
1
+ import { RefreshTokenSource } from "../refresh.js";
2
2
  import { defaultTokenPath, fileTokenStorage } from "./file-storage.js";
3
3
  import { Api, TelegramClient, errors } from "teleproto";
4
4
  import { StringSession } from "teleproto/sessions/index.js";
5
5
  // source/telegram/auth.ts
6
- const BASE_URL = "https://walletbot.me";
7
- const AUTH_URL = "https://alectryon.walletbot.me";
8
- const EXPIRY_SKEW_MS = 3e4;
9
6
  const MAX_FLOOD_WAIT_MS = 6e4;
10
7
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
- var AuthEndpointError = class extends Error {
12
- status;
13
- constructor(message, status) {
14
- super(message);
15
- this.status = status;
16
- }
17
- };
18
- var TelegramTokenSource = class {
8
+ /** Signs in over Telegram, then renews like `RefreshTokenSource`, signing in again when the refresh token is rejected. */
9
+ var TelegramTokenSource = class extends RefreshTokenSource {
19
10
  client;
20
11
  bot;
21
- baseUrl;
22
- authUrl;
23
- fetchImpl;
24
- storage;
25
12
  maxFloodWaitMs;
26
- deviceSerial;
27
- deviceSerialKnown;
28
- jwt = null;
29
- refreshToken = null;
30
- sessionId = null;
31
- expiresAt = 0;
32
- restored = false;
33
- pending = null;
13
+ knownDeviceSerial;
14
+ newDeviceSerial = `weba:${crypto.randomUUID()}`;
34
15
  constructor(options) {
16
+ super({
17
+ storage: options.storage === false ? void 0 : options.storage ?? fileTokenStorage(defaultTokenPath(options.session)),
18
+ baseUrl: options.baseUrl,
19
+ authUrl: options.authUrl,
20
+ fetch: options.fetch
21
+ });
35
22
  this.client = new TelegramClient(new StringSession(options.session), options.apiId, options.apiHash, { connectionRetries: 3 });
36
23
  this.bot = options.bot ?? "walt";
37
- this.deviceSerial = options.deviceSerial ?? `weba:${crypto.randomUUID()}`;
38
- this.deviceSerialKnown = options.deviceSerial !== void 0;
39
- this.baseUrl = options.baseUrl ?? BASE_URL;
40
- this.authUrl = options.authUrl ?? AUTH_URL;
41
- this.fetchImpl = options.fetch ?? fetch;
42
- this.storage = options.storage === false ? void 0 : options.storage ?? fileTokenStorage(defaultTokenPath(options.session));
24
+ this.knownDeviceSerial = options.deviceSerial;
43
25
  this.maxFloodWaitMs = options.maxFloodWaitMs ?? MAX_FLOOD_WAIT_MS;
44
26
  }
45
- /**
46
- * @param rejected - A token the API answered with 401. If it is the cached one, it is renewed even before expiry.
47
- */
48
- async token(rejected) {
49
- if (rejected !== void 0 && rejected === this.jwt) this.expiresAt = 0;
50
- if (this.fresh()) return this.jwt;
51
- this.pending ??= this.resolve();
52
- try {
53
- return await this.pending;
54
- } finally {
55
- this.pending = null;
56
- }
57
- }
58
- fresh() {
59
- return this.jwt !== null && Date.now() < this.expiresAt - EXPIRY_SKEW_MS;
60
- }
61
- async resolve() {
62
- await this.restore();
63
- if (this.fresh()) return this.jwt;
64
- if (this.refreshToken) try {
65
- return await this.refresh();
66
- } catch (error) {
67
- if (!(error instanceof AuthEndpointError && error.status >= 400 && error.status < 500 && error.status !== 429)) throw error;
68
- this.refreshToken = null;
69
- }
70
- return this.authenticate();
71
- }
72
- async restore() {
73
- if (this.restored || !this.storage) return;
74
- this.restored = true;
75
- const state = await this.storage.load();
76
- if (!state) return;
77
- this.jwt = state.jwt;
78
- this.refreshToken = state.refreshToken;
79
- this.sessionId = state.sessionId;
80
- this.expiresAt = state.expiresAt;
81
- this.deviceSerial = state.deviceSerial;
82
- this.deviceSerialKnown = true;
83
- }
84
- async authenticate() {
27
+ async signIn() {
85
28
  const initData = await this.requestInitData();
86
- const serial = this.deviceSerialKnown ? { device_serial: this.deviceSerial } : { device_serial_fallback: this.deviceSerial };
87
- const token = await this.store(await this.post("/alectryon/public-api/auth", {
29
+ const known = this.state?.deviceSerial ?? this.knownDeviceSerial;
30
+ const serial = known === void 0 ? { device_serial_fallback: this.newDeviceSerial } : { device_serial: known };
31
+ const data = await this.post("/alectryon/public-api/auth", {
88
32
  web_view_init_data_raw: initData,
89
33
  ep: "menu",
90
34
  device_serial_multi_domain_backup_supported: true,
91
35
  ...serial
92
- }));
93
- this.deviceSerialKnown = true;
94
- return token;
36
+ });
37
+ return this.store(data, known ?? this.newDeviceSerial);
95
38
  }
96
39
  async requestInitData(retryAfterFlood = true) {
97
40
  if (!this.client.connected) await this.client.connect();
@@ -114,49 +57,6 @@ var TelegramTokenSource = class {
114
57
  if (!initData) throw new Error("no tgWebAppData in web view url");
115
58
  return initData;
116
59
  }
117
- async refresh() {
118
- return this.store(await this.post("/alectryon/public-api/auth-refresh", {
119
- session_id: this.sessionId,
120
- refresh_token: this.refreshToken,
121
- device_serial: this.deviceSerial
122
- }));
123
- }
124
- async post(path, body) {
125
- const response = await this.fetchImpl(`${this.authUrl}${path}`, {
126
- method: "POST",
127
- headers: {
128
- ...BROWSER_HEADERS,
129
- accept: "*/*",
130
- "sec-fetch-site": new URL(this.authUrl).origin === new URL(this.baseUrl).origin ? "same-origin" : "same-site",
131
- "content-type": "application/json",
132
- origin: this.baseUrl,
133
- referer: `${this.baseUrl}/`,
134
- "x-wallet-tz-name": Intl.DateTimeFormat().resolvedOptions().timeZone,
135
- "x-wallet-tz-offset": String(-(/* @__PURE__ */ new Date()).getTimezoneOffset())
136
- },
137
- body: JSON.stringify(body)
138
- });
139
- if (!response.ok) throw new AuthEndpointError(`${path} failed (${response.status}): ${await response.text()}`, response.status);
140
- return await response.json();
141
- }
142
- async store(data) {
143
- this.jwt = data.jwt;
144
- this.refreshToken = data.refresh_token;
145
- this.sessionId = data.session_id;
146
- if (Number.isFinite(data.access_token_expires_in)) this.expiresAt = Date.now() + data.access_token_expires_in * 1e3;
147
- else {
148
- const parsed = Date.parse(data.access_token_expires_at);
149
- this.expiresAt = Number.isNaN(parsed) ? Date.now() + 3e5 : parsed;
150
- }
151
- await this.storage?.save({
152
- jwt: this.jwt,
153
- refreshToken: this.refreshToken,
154
- sessionId: this.sessionId,
155
- expiresAt: this.expiresAt,
156
- deviceSerial: this.deviceSerial
157
- });
158
- return this.jwt;
159
- }
160
60
  async disconnect() {
161
61
  await this.client.disconnect();
162
62
  }
@@ -1,3 +1,3 @@
1
- import { TokenStorage } from "./auth.js";
1
+ import { TokenStorage } from "../refresh.js";
2
2
  export declare const defaultTokenPath: (session: string) => string;
3
3
  export declare const fileTokenStorage: (path: string) => TokenStorage;
@@ -1,5 +1,4 @@
1
1
  import { BROWSER_HEADERS, SENTRY_PUBLIC_KEY, SENTRY_RELEASE } from "./browser.js";
2
- import { randomBytes } from "node:crypto";
3
2
  // source/transport.ts
4
3
  var WaltAuthError = class extends Error {};
5
4
  var WaltRequestError = class extends Error {
@@ -29,7 +28,7 @@ const BEARER_SERVICES = [
29
28
  "/kandelabr/"
30
29
  ];
31
30
  const MAX_RETRY_DELAY_MS = 3e4;
32
- const hex = (bytes) => randomBytes(bytes).toString("hex");
31
+ const hex = (bytes) => Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
33
32
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
34
33
  const parseBody = (text) => {
35
34
  try {
@@ -44,8 +43,9 @@ const retryAfterOf = (response) => {
44
43
  };
45
44
  const telegramUserIdOf = (token) => {
46
45
  try {
47
- const payload = token.split(".")[1] ?? "";
48
- const id = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).web_view_init_data?.user?.id;
46
+ const payload = (token.split(".")[1] ?? "").replaceAll("-", "+").replaceAll("_", "/");
47
+ const bytes = Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
48
+ const id = JSON.parse(new TextDecoder().decode(bytes)).web_view_init_data?.user?.id;
49
49
  return typeof id === "number" ? id : null;
50
50
  } catch {
51
51
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2bad/walt",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Client for the Walt (ex. Wallet) API in Telegram to track a crypto portfolio and exchange between assets",
5
5
  "keywords": [
6
6
  "telegram",
@@ -33,6 +33,12 @@
33
33
  "types": "./build/index.d.ts",
34
34
  "default": "./build/index.js"
35
35
  }
36
+ },
37
+ "./client": {
38
+ "import": {
39
+ "types": "./build/core.d.ts",
40
+ "default": "./build/core.js"
41
+ }
36
42
  }
37
43
  },
38
44
  "bin": {
@@ -42,28 +48,28 @@
42
48
  "build"
43
49
  ],
44
50
  "dependencies": {
45
- "teleproto": "1.232.0"
51
+ "teleproto": "1.232.1"
46
52
  },
47
53
  "devDependencies": {
48
54
  "@2bad/axiom": "2.1.0",
49
55
  "@2bad/tsconfig": "4.0.0",
50
56
  "@arethetypeswrong/cli": "0.18.5",
51
57
  "@types/node": "26.6.6",
52
- "@vitest/coverage-v8": "5.0.5",
58
+ "@vitest/coverage-v8": "5.1.0",
53
59
  "npm-run-all2": "9.0.4",
54
- "oxfmt": "0.75.0",
55
- "oxlint": "1.90.0",
60
+ "oxfmt": "0.76.0",
61
+ "oxlint": "1.91.0",
56
62
  "publint": "0.3.25",
57
- "tsdown": "0.23.4",
63
+ "tsdown": "0.23.5",
58
64
  "typescript": "7.0.2",
59
- "vitest": "5.0.5"
65
+ "vitest": "5.1.0"
60
66
  },
61
67
  "engines": {
62
68
  "node": ">=26"
63
69
  },
64
70
  "volta": {
65
71
  "node": "26.13.0",
66
- "pnpm": "12.16.1"
72
+ "pnpm": "12.18.0"
67
73
  },
68
74
  "scripts": {
69
75
  "build": "tsdown",