@2bad/walt 1.0.1 → 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,12 +39,13 @@ 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
43
46
  - [Transactions](examples/transactions.md): history, details and reading a transaction
44
47
  - [Errors](examples/errors.md): `WaltRequestError`, `WaltAuthError` and what is retried
48
+ - [Command line](examples/cli.md): the read-only `walt` command for balances, history, prices and orders
45
49
 
46
50
  A runnable script is in [examples/portfolio.ts](examples/portfolio.ts).
47
51
 
@@ -55,11 +59,18 @@ type TelegramTokenSource = {
55
59
  disconnect(): Promise<void>
56
60
  }
57
61
 
62
+ new RefreshTokenSource(options: RefreshTokenSourceOptions)
63
+
64
+ type RefreshTokenSource = {
65
+ token(rejected?: string): Promise<string>
66
+ }
67
+
58
68
  new WaltClient(options: WaltClientOptions)
59
69
 
60
70
  type WaltClient = {
61
71
  accounts(): Promise<AccountsResponse>
62
72
  portfolio(): Promise<Portfolio>
73
+ balances(fiatCurrency: FiatCurrency): Promise<Balances>
63
74
  pnl(): Promise<PnlResponse>
64
75
  portfolioHistory(dateFrom: Date | string, resolution: '1d' | '1m'): Promise<PortfolioHistory>
65
76
  frozenBalances<T = unknown>(): Promise<T[]>
@@ -74,6 +85,7 @@ type WaltClient = {
74
85
  charts(currency: string, fiatCurrency: FiatCurrency): Promise<Charts>
75
86
  priceForFiatAtTime(crypto: string, fiat: FiatCurrency, amount: number | string, time: Date | string | number): Promise<PriceAtTime>
76
87
  localCurrency(): Promise<FiatCurrency>
88
+ candles(requests: CandleRequest[]): Promise<CandleSeries[]>
77
89
 
78
90
  availableExchanges(): Promise<AvailableExchanges>
79
91
  quote(fromCurrency: string, toCurrency: string, fromAmount: number): Promise<ExchangeQuote>
@@ -85,6 +97,11 @@ type WaltClient = {
85
97
  limitOrderBounds(baseCurrency: string): Promise<LimitOrderBounds>
86
98
  limitOrderTokens(): Promise<LimitOrderToken[]>
87
99
 
100
+ perpContracts(contractId?: number): Promise<PerpContract[]>
101
+ perpSystemStatus(): Promise<PerpSystemStatus>
102
+ perpPositions(options?: { statuses?: PerpPositionStatus[]; contractId?: number }): Promise<PerpPosition[]>
103
+ perpOrders(options?: { contractId?: number; statuses?: PerpOrderStatus[]; limit?: number }): Promise<PerpOrdersPage>
104
+
88
105
  transactions(limit?: number, cursor?: string): Promise<TransactionsPage>
89
106
  accountTransactions(currency: string, limit?: number, cursor?: string): Promise<TransactionsPage>
90
107
  allTransactions(limit?: number): AsyncGenerator<Transaction>
@@ -0,0 +1,23 @@
1
+ export type Balance = {
2
+ /** `REGULAR` for spot, `HOLD_LIMIT` for USDT reserved by open limit orders, `EARN_V2` for staked funds. */
3
+ account_type: string;
4
+ currency: string;
5
+ asset_currency: string;
6
+ total_balance: string;
7
+ available_balance: string;
8
+ balance_hold: string;
9
+ is_always_on_main: boolean;
10
+ category: string;
11
+ rate_to_fiat: string;
12
+ rate_to_usd: string;
13
+ available_balance_fiat_amount: string;
14
+ available_balance_usd_amount: string;
15
+ };
16
+ export type Balances = {
17
+ accounts: Balance[];
18
+ fiat_currency: string;
19
+ /** Spot, staked and limit order holds together. */
20
+ overall_available_balance_fiat_amount: string;
21
+ /** Also `true` when the perps account has no open position. */
22
+ has_open_perp_position: boolean;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ export type CandleRequest = {
2
+ /** `<currency>/USD/SPOT`, or `<base>/USD/PERP_TRADE` for a perps contract. */
3
+ ticker: string;
4
+ /** Accepted: `1m`, `5m`, `15m`, `1h`, `4h`, `1d`, `1w`, `1mo`. */
5
+ resolution: string;
6
+ /** At least 1. The whole batch may ask for 3000 candles at most. */
7
+ limit: number;
8
+ };
9
+ export type Candle = {
10
+ o: string;
11
+ h: string;
12
+ l: string;
13
+ c: string;
14
+ v: string;
15
+ };
16
+ export type CandleSeries = {
17
+ ticker: string;
18
+ resolution: string;
19
+ /** Opening time of the first candle, in ms. */
20
+ start_ts: number;
21
+ candles: Candle[];
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ export type PerpContract = {
2
+ contract_id: number;
3
+ contract_name: string;
4
+ base_asset: string;
5
+ quote_asset: string;
6
+ venue_status: string;
7
+ is_active: boolean;
8
+ price_decimals: number;
9
+ size_decimals: number;
10
+ tick_size: string;
11
+ step_size: string;
12
+ min_size_base: string;
13
+ max_size_base: string;
14
+ max_size_quote: string;
15
+ min_size_quote: string;
16
+ max_leverage: number;
17
+ maker_fee_pct: string;
18
+ taker_fee_pct: string;
19
+ base_taker_fee_pct: string;
20
+ taker_fee_subscription_id: number;
21
+ price_slippage: string;
22
+ tpsl_buffer_pct: string;
23
+ funding_rate_hourly: string;
24
+ next_funding_at: string;
25
+ open_interest_usd: string;
26
+ volume_24h_usd: string;
27
+ trading_hours_state: unknown;
28
+ market_data: {
29
+ server_time: string;
30
+ /** `fresh`, or `stale` with both prices `null`. */
31
+ status: string;
32
+ mark_price: string | null;
33
+ index_price: string | null;
34
+ source_timestamp: string;
35
+ expires_at: string;
36
+ };
37
+ /** Fractions in basis points: `1000` is 10%, so 10x leverage. */
38
+ margin: {
39
+ default_initial_margin_fraction: number;
40
+ min_initial_margin_fraction: number;
41
+ maintenance_margin_fraction: number;
42
+ closeout_margin_fraction: number;
43
+ };
44
+ };
45
+ export type PerpSystemStatus = {
46
+ actions: {
47
+ open_enabled: boolean;
48
+ cancel_enabled: boolean;
49
+ close_enabled: boolean;
50
+ add_margin_enabled: boolean;
51
+ remove_margin_enabled: boolean;
52
+ auto_close_enabled: boolean;
53
+ };
54
+ exchange_status: {
55
+ exchange_partner_tech_name: string;
56
+ operational_state: string;
57
+ is_trading_halted: boolean;
58
+ message: unknown;
59
+ current_status: unknown;
60
+ upcoming_maintenance: unknown;
61
+ last_changed_at: string;
62
+ last_checked_at: string;
63
+ };
64
+ };
65
+ export type PerpOrdersPage = {
66
+ orders: PerpOrder[];
67
+ next_cursor: unknown;
68
+ /** `sort_ts` of the last order on the page, `null` on an empty page. */
69
+ last_sort_ts: string | null;
70
+ };
71
+ export type PerpPositionStatus = 'open' | 'closed';
72
+ export type PerpOrderStatus = 'pending' | 'processing' | 'processed' | 'filled' | 'partially_filled' | 'failed' | 'canceled';
73
+ type PerpPositionCore = {
74
+ contract_id: number;
75
+ contract_name: string;
76
+ contract_base_asset: string;
77
+ contract_quote_asset: string;
78
+ /** Seen: `long`, `short`. */
79
+ side: string;
80
+ /** Seen: `closed`. */
81
+ status: string;
82
+ leverage: number;
83
+ /** Seen: `isolated`. */
84
+ margin_type: string;
85
+ value_base: string;
86
+ margin_quote: string;
87
+ avg_entry_price: string;
88
+ liquidation_price: string;
89
+ funding_paid_quote: string;
90
+ created_at: string;
91
+ updated_at: string;
92
+ synced_at: string;
93
+ };
94
+ export type PerpPosition = PerpPositionCore & {
95
+ trading_agent_code: string;
96
+ /** Priced at the current mark, closed positions included, so it is not the P/L a closed position realised. */
97
+ valuation: {
98
+ server_time: string;
99
+ status: string;
100
+ mark_price: string;
101
+ index_price: string;
102
+ source_timestamp: string;
103
+ expires_at: string;
104
+ unrealized_pnl_quote: string;
105
+ equity_quote: string;
106
+ };
107
+ auto_close: unknown;
108
+ };
109
+ export type PerpOrder = {
110
+ order_id: string;
111
+ contract_id: number;
112
+ contract_name: string;
113
+ contract_base_asset: string;
114
+ contract_quote_asset: string;
115
+ side: string;
116
+ /** Seen: `market`. */
117
+ type: string;
118
+ /** Seen: `open`, `close`, `stop_loss`, `take_profit`. */
119
+ category: string;
120
+ status: string;
121
+ error_code: unknown;
122
+ leverage: number;
123
+ base_size: string;
124
+ base_price: string;
125
+ accounting_base_amount: string;
126
+ accounting_quote_amount: string;
127
+ filled_price: string | null;
128
+ filled_base_amount: string | null;
129
+ filled_quote_amount: string | null;
130
+ pnl_quote: string | null;
131
+ pnl_quote_charged: unknown;
132
+ roi: string | null;
133
+ trigger_price_quote: string | null;
134
+ limit_price_quote: string | null;
135
+ /** Unix seconds. */
136
+ valid_to: number | null;
137
+ liquidation_fee: unknown;
138
+ liquidation_fee_charged: unknown;
139
+ integrator_fee: string | null;
140
+ integrator_fee_charged: string | null;
141
+ venue_fee: string | null;
142
+ total_fee: string | null;
143
+ total_fee_charged: string | null;
144
+ margin_returned_amount: string | null;
145
+ created_at: string;
146
+ updated_at: string;
147
+ filled_at: string | null;
148
+ canceled_at: unknown;
149
+ sort_ts: string;
150
+ position: PerpPositionCore & {
151
+ fee_quote: unknown;
152
+ close_price: string;
153
+ };
154
+ };
155
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ // source/bin/quiet-localstorage.ts
2
+ const nodeOptions = (process.env["NODE_OPTIONS"] ?? "").split(/\s+/);
3
+ if (![...process.execArgv, ...nodeOptions].some((arg) => arg.startsWith("--localstorage-file"))) Object.defineProperty(globalThis, "localStorage", {
4
+ value: void 0,
5
+ configurable: true,
6
+ writable: true
7
+ });
8
+ export {};
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import "./quiet-localstorage.js";
3
+ import { COMMANDS, UsageError, run } from "../cli/run.js";
4
+ import { WaltClient } from "../client.js";
5
+ import { TelegramTokenSource } from "../telegram/auth.js";
6
+ import { WaltAuthError, WaltRequestError } from "../transport.js";
7
+ import { TelegramClient } from "teleproto";
8
+ import { StringSession } from "teleproto/sessions/index.js";
9
+ import { readFileSync } from "node:fs";
10
+ import { stdin, stdout } from "node:process";
11
+ import { createInterface } from "node:readline/promises";
12
+ import { parseArgs } from "node:util";
13
+ // source/bin/walt.ts
14
+ /**
15
+ * Signs in to Telegram interactively and prints the session string to save as `TG_SESSION`.
16
+ *
17
+ * @returns The process exit code.
18
+ */
19
+ const login = async () => {
20
+ const rl = createInterface({
21
+ input: stdin,
22
+ output: stdout
23
+ });
24
+ const ask = (question) => rl.question(question);
25
+ const apiId = Number(process.env["TG_API_ID"] || await ask("API id (from https://my.telegram.org): "));
26
+ if (!Number.isSafeInteger(apiId) || apiId <= 0) {
27
+ rl.close();
28
+ console.error("The API id must be the number shown at https://my.telegram.org");
29
+ return 1;
30
+ }
31
+ const apiHash = process.env["TG_API_HASH"] || await ask("API hash: ");
32
+ const client = new TelegramClient(new StringSession(""), apiId, apiHash, { connectionRetries: 3 });
33
+ await client.start({
34
+ phoneNumber: () => ask("Phone (+country...): "),
35
+ phoneCode: () => ask("Code you received: "),
36
+ password: () => ask("2FA password (blank if none): "),
37
+ onError: (error) => console.error(error)
38
+ });
39
+ rl.close();
40
+ console.log("\nLogged in. Save this as TG_SESSION (keep it secret):\n");
41
+ console.log(client.session.save());
42
+ await client.disconnect();
43
+ return 0;
44
+ };
45
+ const USAGE = `Usage: walt <command> [options]
46
+
47
+ ${COMMANDS}
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
50
+
51
+ Options:
52
+ --json print JSON instead of a table
53
+ --fiat CODE currency for values (default USD)
54
+ --env-file PATH read TG_API_ID, TG_API_HASH and TG_SESSION from a file
55
+ -h, --help show this help
56
+ -v, --version show the version
57
+
58
+ Needs TG_API_ID and TG_API_HASH from https://my.telegram.org and TG_SESSION from \`walt login\`.
59
+ Every command is read-only.`;
60
+ const fail = (message, code) => {
61
+ console.error(`walt: ${message}`);
62
+ process.exit(code);
63
+ };
64
+ const parse = () => {
65
+ try {
66
+ return parseArgs({
67
+ allowPositionals: true,
68
+ options: {
69
+ json: { type: "boolean" },
70
+ fiat: { type: "string" },
71
+ "env-file": { type: "string" },
72
+ asset: { type: "string" },
73
+ limit: { type: "string" },
74
+ from: { type: "string" },
75
+ monthly: { type: "boolean" },
76
+ all: { type: "boolean" },
77
+ closed: { type: "boolean" },
78
+ help: {
79
+ type: "boolean",
80
+ short: "h"
81
+ },
82
+ version: {
83
+ type: "boolean",
84
+ short: "v"
85
+ }
86
+ }
87
+ });
88
+ } catch (error) {
89
+ return fail(`${error.message}\nRun \`walt --help\` for usage.`, 2);
90
+ }
91
+ };
92
+ const { values, positionals } = parse();
93
+ const [command, ...args] = positionals;
94
+ if (values.version) {
95
+ const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
96
+ console.log(manifest.version);
97
+ process.exit(0);
98
+ }
99
+ if (values.help || command === void 0) {
100
+ console.log(USAGE);
101
+ process.exit(command === void 0 && !values.help ? 2 : 0);
102
+ }
103
+ if (values["env-file"] !== void 0) try {
104
+ process.loadEnvFile(values["env-file"]);
105
+ } catch (error) {
106
+ fail(`cannot read ${values["env-file"]}: ${error.message}`, 2);
107
+ }
108
+ if (command === "login") process.exit(await login());
109
+ const session = process.env["TG_SESSION"];
110
+ const apiId = Number(process.env["TG_API_ID"]);
111
+ const apiHash = process.env["TG_API_HASH"];
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;
115
+ const source = new TelegramTokenSource({
116
+ session: session ?? "",
117
+ apiId,
118
+ apiHash: apiHash ?? "",
119
+ ...command === "token" && { storage: {
120
+ load: async () => null,
121
+ save: async (state) => {
122
+ issued = state;
123
+ }
124
+ } }
125
+ });
126
+ const client = new WaltClient({ tokenProvider: (rejected) => source.token(rejected) });
127
+ let exitCode = 0;
128
+ try {
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));
133
+ } catch (error) {
134
+ if (error instanceof UsageError) {
135
+ console.error(`walt: ${error.message}\nRun \`walt --help\` for usage.`);
136
+ exitCode = 2;
137
+ } else if (error instanceof WaltRequestError) {
138
+ const code = error.body?.code;
139
+ console.error(`walt: ${error.message}${typeof code === "string" ? ` (${code})` : ""}`);
140
+ exitCode = 1;
141
+ } else if (error instanceof WaltAuthError) {
142
+ console.error(`walt: sign-in failed: ${error.message}`);
143
+ exitCode = 1;
144
+ } else {
145
+ console.error(`walt: ${error instanceof Error ? error.message : String(error)}`);
146
+ exitCode = 1;
147
+ }
148
+ } finally {
149
+ await source.disconnect();
150
+ }
151
+ process.exit(exitCode);
152
+ export {};
@@ -0,0 +1,22 @@
1
+ import { WaltClient } from "../client.js";
2
+ export type CliOptions = {
3
+ json?: boolean;
4
+ fiat?: string;
5
+ asset?: string;
6
+ limit?: string;
7
+ from?: string;
8
+ monthly?: boolean;
9
+ all?: boolean;
10
+ closed?: boolean;
11
+ };
12
+ export declare class UsageError extends Error {}
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
+ /**
15
+ * Runs one read-only command and returns what to print.
16
+ *
17
+ * @param client - A signed-in client.
18
+ * @param command - The command name, such as `balances`.
19
+ * @param args - Positional arguments after the command.
20
+ * @param options - Parsed flags.
21
+ */
22
+ export declare const run: (client: WaltClient, command: string, args: string[], options?: CliOptions) => Promise<string>;