@2bad/walt 1.0.1 → 2.0.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 +8 -0
- package/build/api/balances.d.ts +23 -0
- package/build/api/balances.js +1 -0
- package/build/api/candles.d.ts +21 -0
- package/build/api/candles.js +1 -0
- package/build/api/perps.d.ts +155 -0
- package/build/api/perps.js +1 -0
- package/build/bin/quiet-localstorage.js +8 -0
- package/build/bin/walt.d.ts +1 -0
- package/build/bin/walt.js +140 -0
- package/build/cli/run.d.ts +22 -0
- package/build/cli/run.js +507 -0
- package/build/client.d.ts +38 -0
- package/build/client.js +55 -0
- package/build/index.d.ts +4 -1
- package/build/transport.js +6 -1
- package/package.json +3 -3
- package/build/bin/login.js +0 -30
- /package/build/bin/{login.d.ts → quiet-localstorage.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ await source.disconnect()
|
|
|
42
42
|
- [Exchange](examples/exchange.md): available pairs, quotes, exchanges and limit orders
|
|
43
43
|
- [Transactions](examples/transactions.md): history, details and reading a transaction
|
|
44
44
|
- [Errors](examples/errors.md): `WaltRequestError`, `WaltAuthError` and what is retried
|
|
45
|
+
- [Command line](examples/cli.md): the read-only `walt` command for balances, history, prices and orders
|
|
45
46
|
|
|
46
47
|
A runnable script is in [examples/portfolio.ts](examples/portfolio.ts).
|
|
47
48
|
|
|
@@ -60,6 +61,7 @@ new WaltClient(options: WaltClientOptions)
|
|
|
60
61
|
type WaltClient = {
|
|
61
62
|
accounts(): Promise<AccountsResponse>
|
|
62
63
|
portfolio(): Promise<Portfolio>
|
|
64
|
+
balances(fiatCurrency: FiatCurrency): Promise<Balances>
|
|
63
65
|
pnl(): Promise<PnlResponse>
|
|
64
66
|
portfolioHistory(dateFrom: Date | string, resolution: '1d' | '1m'): Promise<PortfolioHistory>
|
|
65
67
|
frozenBalances<T = unknown>(): Promise<T[]>
|
|
@@ -74,6 +76,7 @@ type WaltClient = {
|
|
|
74
76
|
charts(currency: string, fiatCurrency: FiatCurrency): Promise<Charts>
|
|
75
77
|
priceForFiatAtTime(crypto: string, fiat: FiatCurrency, amount: number | string, time: Date | string | number): Promise<PriceAtTime>
|
|
76
78
|
localCurrency(): Promise<FiatCurrency>
|
|
79
|
+
candles(requests: CandleRequest[]): Promise<CandleSeries[]>
|
|
77
80
|
|
|
78
81
|
availableExchanges(): Promise<AvailableExchanges>
|
|
79
82
|
quote(fromCurrency: string, toCurrency: string, fromAmount: number): Promise<ExchangeQuote>
|
|
@@ -85,6 +88,11 @@ type WaltClient = {
|
|
|
85
88
|
limitOrderBounds(baseCurrency: string): Promise<LimitOrderBounds>
|
|
86
89
|
limitOrderTokens(): Promise<LimitOrderToken[]>
|
|
87
90
|
|
|
91
|
+
perpContracts(contractId?: number): Promise<PerpContract[]>
|
|
92
|
+
perpSystemStatus(): Promise<PerpSystemStatus>
|
|
93
|
+
perpPositions(options?: { statuses?: PerpPositionStatus[]; contractId?: number }): Promise<PerpPosition[]>
|
|
94
|
+
perpOrders(options?: { contractId?: number; statuses?: PerpOrderStatus[]; limit?: number }): Promise<PerpOrdersPage>
|
|
95
|
+
|
|
88
96
|
transactions(limit?: number, cursor?: string): Promise<TransactionsPage>
|
|
89
97
|
accountTransactions(currency: string, limit?: number, cursor?: string): Promise<TransactionsPage>
|
|
90
98
|
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,21 @@
|
|
|
1
|
+
export type CandleRequest = {
|
|
2
|
+
/** `<currency>/USD/SPOT`, or `<base>/USD/PERP_TRADE` for a perps contract. */
|
|
3
|
+
ticker: string;
|
|
4
|
+
/** Seen: `5m`, `15m`. */
|
|
5
|
+
resolution: string;
|
|
6
|
+
limit: number;
|
|
7
|
+
};
|
|
8
|
+
export type Candle = {
|
|
9
|
+
o: string;
|
|
10
|
+
h: string;
|
|
11
|
+
l: string;
|
|
12
|
+
c: string;
|
|
13
|
+
v: string;
|
|
14
|
+
};
|
|
15
|
+
export type CandleSeries = {
|
|
16
|
+
ticker: string;
|
|
17
|
+
resolution: string;
|
|
18
|
+
/** Opening time of the first candle, in ms. */
|
|
19
|
+
start_ts: number;
|
|
20
|
+
candles: Candle[];
|
|
21
|
+
};
|
|
@@ -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,140 @@
|
|
|
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
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--json print JSON instead of a table
|
|
52
|
+
--fiat CODE currency for values (default USD)
|
|
53
|
+
--env-file PATH read TG_API_ID, TG_API_HASH and TG_SESSION from a file
|
|
54
|
+
-h, --help show this help
|
|
55
|
+
-v, --version show the version
|
|
56
|
+
|
|
57
|
+
Needs TG_API_ID and TG_API_HASH from https://my.telegram.org and TG_SESSION from \`walt login\`.
|
|
58
|
+
Every command is read-only.`;
|
|
59
|
+
const fail = (message, code) => {
|
|
60
|
+
console.error(`walt: ${message}`);
|
|
61
|
+
process.exit(code);
|
|
62
|
+
};
|
|
63
|
+
const parse = () => {
|
|
64
|
+
try {
|
|
65
|
+
return parseArgs({
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
options: {
|
|
68
|
+
json: { type: "boolean" },
|
|
69
|
+
fiat: { type: "string" },
|
|
70
|
+
"env-file": { type: "string" },
|
|
71
|
+
asset: { type: "string" },
|
|
72
|
+
limit: { type: "string" },
|
|
73
|
+
from: { type: "string" },
|
|
74
|
+
monthly: { type: "boolean" },
|
|
75
|
+
all: { type: "boolean" },
|
|
76
|
+
closed: { type: "boolean" },
|
|
77
|
+
help: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
short: "h"
|
|
80
|
+
},
|
|
81
|
+
version: {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
short: "v"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return fail(`${error.message}\nRun \`walt --help\` for usage.`, 2);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const { values, positionals } = parse();
|
|
92
|
+
const [command, ...args] = positionals;
|
|
93
|
+
if (values.version) {
|
|
94
|
+
const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
95
|
+
console.log(manifest.version);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
if (values.help || command === void 0) {
|
|
99
|
+
console.log(USAGE);
|
|
100
|
+
process.exit(command === void 0 && !values.help ? 2 : 0);
|
|
101
|
+
}
|
|
102
|
+
if (values["env-file"] !== void 0) try {
|
|
103
|
+
process.loadEnvFile(values["env-file"]);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
fail(`cannot read ${values["env-file"]}: ${error.message}`, 2);
|
|
106
|
+
}
|
|
107
|
+
if (command === "login") process.exit(await login());
|
|
108
|
+
const session = process.env["TG_SESSION"];
|
|
109
|
+
const apiId = Number(process.env["TG_API_ID"]);
|
|
110
|
+
const apiHash = process.env["TG_API_HASH"];
|
|
111
|
+
if (!session || !apiId || !apiHash) fail("set TG_API_ID, TG_API_HASH and TG_SESSION first (run `walt login` for the session)", 2);
|
|
112
|
+
const source = new TelegramTokenSource({
|
|
113
|
+
session: session ?? "",
|
|
114
|
+
apiId,
|
|
115
|
+
apiHash: apiHash ?? ""
|
|
116
|
+
});
|
|
117
|
+
const client = new WaltClient({ tokenProvider: (rejected) => source.token(rejected) });
|
|
118
|
+
let exitCode = 0;
|
|
119
|
+
try {
|
|
120
|
+
console.log(await run(client, command, args, values));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof UsageError) {
|
|
123
|
+
console.error(`walt: ${error.message}\nRun \`walt --help\` for usage.`);
|
|
124
|
+
exitCode = 2;
|
|
125
|
+
} else if (error instanceof WaltRequestError) {
|
|
126
|
+
const code = error.body?.code;
|
|
127
|
+
console.error(`walt: ${error.message}${typeof code === "string" ? ` (${code})` : ""}`);
|
|
128
|
+
exitCode = 1;
|
|
129
|
+
} else if (error instanceof WaltAuthError) {
|
|
130
|
+
console.error(`walt: sign-in failed: ${error.message}`);
|
|
131
|
+
exitCode = 1;
|
|
132
|
+
} else {
|
|
133
|
+
console.error(`walt: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
await source.disconnect();
|
|
138
|
+
}
|
|
139
|
+
process.exit(exitCode);
|
|
140
|
+
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 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";
|
|
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>;
|
package/build/cli/run.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { WaltRequestError } from "../transport.js";
|
|
2
|
+
// source/cli/run.ts
|
|
3
|
+
var UsageError = class extends Error {};
|
|
4
|
+
const COMMANDS = `Commands:
|
|
5
|
+
balances every asset: spot, staked and held by limit orders
|
|
6
|
+
history [--monthly] balance, deposits, withdrawals and P/L per day or month
|
|
7
|
+
[--from DATE] start date, YYYY-MM-DD (default: 30 days or 12 months back)
|
|
8
|
+
transactions latest transactions
|
|
9
|
+
[--asset SYM] only this asset
|
|
10
|
+
[--limit N] how many (default 20)
|
|
11
|
+
price SYM... current price and change over 24h, 7d and 30d
|
|
12
|
+
quote FROM TO AMOUNT what an exchange would give right now, without making it
|
|
13
|
+
orders [--all] open limit orders, or every order with --all
|
|
14
|
+
perps [--closed] open perpetual positions, or closed ones
|
|
15
|
+
earn [--all] your earn stakes, or every active campaign with --all`;
|
|
16
|
+
const fixed = (value, digits = 2) => {
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
return value === null || value === void 0 || value === "" || !Number.isFinite(parsed) ? "-" : parsed.toFixed(digits);
|
|
19
|
+
};
|
|
20
|
+
const trimmed = (value) => {
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
if (value === null || value === void 0 || value === "" || !Number.isFinite(parsed)) return "-";
|
|
23
|
+
return String(Number(parsed.toFixed(8)));
|
|
24
|
+
};
|
|
25
|
+
const significant = (value) => String(Number(value.toPrecision(8)));
|
|
26
|
+
const percent = (value) => Number.isFinite(value) ? `${value >= 0 ? "+" : ""}${(value * 100).toFixed(2)}%` : "-";
|
|
27
|
+
const change = (now, then) => then === void 0 || then === 0 ? "-" : percent(now / then - 1);
|
|
28
|
+
const minute = (timestamp) => timestamp.slice(0, 16).replace("T", " ");
|
|
29
|
+
const isoDate = (date) => date.toISOString().slice(0, 10);
|
|
30
|
+
const ACCOUNT_KINDS = {
|
|
31
|
+
REGULAR: "spot",
|
|
32
|
+
EARN_V2: "earn",
|
|
33
|
+
HOLD_LIMIT: "orders"
|
|
34
|
+
};
|
|
35
|
+
const positiveInteger = (value, fallback) => {
|
|
36
|
+
if (value === void 0) return fallback;
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new UsageError(`--limit must be a whole number, got ${value}`);
|
|
39
|
+
return parsed;
|
|
40
|
+
};
|
|
41
|
+
const balances = async (client, fiat) => {
|
|
42
|
+
const data = await client.balances(fiat);
|
|
43
|
+
const total = Number(data.overall_available_balance_fiat_amount);
|
|
44
|
+
const rows = data.accounts.filter((account) => Number(account.total_balance) > 0).map((account) => ({
|
|
45
|
+
account,
|
|
46
|
+
value: Number(account.available_balance_fiat_amount)
|
|
47
|
+
})).sort((a, b) => b.value - a.value).map(({ account, value }) => ({
|
|
48
|
+
asset: account.currency,
|
|
49
|
+
kind: ACCOUNT_KINDS[account.account_type] ?? account.account_type.toLowerCase(),
|
|
50
|
+
amount: trimmed(account.total_balance),
|
|
51
|
+
value: value.toFixed(2),
|
|
52
|
+
share: total > 0 ? `${(value / total * 100).toFixed(1)}%` : "-"
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
columns: [
|
|
56
|
+
{
|
|
57
|
+
key: "asset",
|
|
58
|
+
title: "ASSET"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "kind",
|
|
62
|
+
title: "KIND"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "amount",
|
|
66
|
+
title: "AMOUNT",
|
|
67
|
+
numeric: true
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "value",
|
|
71
|
+
title: `VALUE ${data.fiat_currency}`,
|
|
72
|
+
numeric: true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "share",
|
|
76
|
+
title: "SHARE",
|
|
77
|
+
numeric: true
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
rows,
|
|
81
|
+
footer: [`total ${total.toFixed(2)} ${data.fiat_currency}`]
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const history = async (client, options) => {
|
|
85
|
+
const start = /* @__PURE__ */ new Date();
|
|
86
|
+
if (options.monthly) start.setUTCMonth(start.getUTCMonth() - 11, 1);
|
|
87
|
+
else start.setUTCDate(start.getUTCDate() - 29);
|
|
88
|
+
const from = options.from ?? isoDate(start);
|
|
89
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(from)) throw new UsageError(`--from must be YYYY-MM-DD, got ${from}`);
|
|
90
|
+
const data = await client.portfolioHistory(from, options.monthly ? "1m" : "1d");
|
|
91
|
+
const period = (dateFrom) => options.monthly ? dateFrom.slice(0, 7) : dateFrom.slice(0, 10);
|
|
92
|
+
const rows = data.chart.data_points.map((point) => ({
|
|
93
|
+
period: period(point.date_from),
|
|
94
|
+
balance: fixed(point.close_amount_fiat),
|
|
95
|
+
deposits: fixed(point.deposits_details.total),
|
|
96
|
+
withdrawals: fixed(point.withdrawals_details.total),
|
|
97
|
+
pnl: fixed(point.total_pnl),
|
|
98
|
+
pnlPercent: point.total_pnl_percent === null ? "-" : `${fixed(point.total_pnl_percent)}%`
|
|
99
|
+
}));
|
|
100
|
+
const total = data.total_period_details;
|
|
101
|
+
return {
|
|
102
|
+
columns: [
|
|
103
|
+
{
|
|
104
|
+
key: "period",
|
|
105
|
+
title: options.monthly ? "MONTH" : "DAY"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "balance",
|
|
109
|
+
title: `BALANCE ${data.fiat_currency}`,
|
|
110
|
+
numeric: true
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "deposits",
|
|
114
|
+
title: "DEPOSITS",
|
|
115
|
+
numeric: true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "withdrawals",
|
|
119
|
+
title: "WITHDRAWALS",
|
|
120
|
+
numeric: true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "pnl",
|
|
124
|
+
title: "P/L",
|
|
125
|
+
numeric: true
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: "pnlPercent",
|
|
129
|
+
title: "P/L %",
|
|
130
|
+
numeric: true
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
rows,
|
|
134
|
+
footer: [`since ${from}: P/L ${fixed(total.total_pnl)} ${data.fiat_currency}` + (total.total_pnl_percent === null ? "" : ` (${fixed(total.total_pnl_percent)}%)`) + `, deposits ${fixed(total.deposits_details.total)}, withdrawals ${fixed(total.withdrawals_details.total)}`, "as reported by Walt"]
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const transactions = async (client, options) => {
|
|
138
|
+
const limit = positiveInteger(options.limit, 20);
|
|
139
|
+
return {
|
|
140
|
+
columns: [
|
|
141
|
+
{
|
|
142
|
+
key: "time",
|
|
143
|
+
title: "TIME (UTC)"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: "type",
|
|
147
|
+
title: "TYPE"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "gateway",
|
|
151
|
+
title: "VIA"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: "amount",
|
|
155
|
+
title: "AMOUNT",
|
|
156
|
+
numeric: true
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "asset",
|
|
160
|
+
title: "ASSET"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "status",
|
|
164
|
+
title: "STATUS"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
rows: (options.asset ? await client.accountTransactions(options.asset, limit) : await client.transactions(limit)).transactions.map((row) => ({
|
|
168
|
+
time: minute(row.created_at),
|
|
169
|
+
type: row.type,
|
|
170
|
+
gateway: row.gateway,
|
|
171
|
+
amount: row.amount,
|
|
172
|
+
asset: row.currency,
|
|
173
|
+
status: row.status
|
|
174
|
+
}))
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const price = async (client, symbols, fiat) => {
|
|
178
|
+
if (symbols.length === 0) throw new UsageError("price needs at least one symbol, such as `walt price BTC`");
|
|
179
|
+
const rows = await Promise.all(symbols.map(async (symbol) => {
|
|
180
|
+
const charts = await client.charts(symbol, fiat);
|
|
181
|
+
const rates = (points) => points.map((point) => Number(point.rate));
|
|
182
|
+
const day = rates(charts.day);
|
|
183
|
+
const last = day.at(-1) ?? rates(charts.month).at(-1);
|
|
184
|
+
if (last === void 0) return {
|
|
185
|
+
asset: symbol,
|
|
186
|
+
price: "-",
|
|
187
|
+
day: "-",
|
|
188
|
+
week: "-",
|
|
189
|
+
month: "-"
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
asset: symbol,
|
|
193
|
+
price: significant(last),
|
|
194
|
+
day: change(last, day[0]),
|
|
195
|
+
week: change(last, rates(charts.week)[0]),
|
|
196
|
+
month: change(last, rates(charts.month)[0])
|
|
197
|
+
};
|
|
198
|
+
}));
|
|
199
|
+
return {
|
|
200
|
+
columns: [
|
|
201
|
+
{
|
|
202
|
+
key: "asset",
|
|
203
|
+
title: "ASSET"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: "price",
|
|
207
|
+
title: `PRICE ${fiat}`,
|
|
208
|
+
numeric: true
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: "day",
|
|
212
|
+
title: "24H",
|
|
213
|
+
numeric: true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: "week",
|
|
217
|
+
title: "7D",
|
|
218
|
+
numeric: true
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: "month",
|
|
222
|
+
title: "30D",
|
|
223
|
+
numeric: true
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
rows
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
const quoteSizes = (amount) => {
|
|
230
|
+
const [whole = "", fraction = ""] = amount.split(".");
|
|
231
|
+
const sizes = [amount, ...[
|
|
232
|
+
6,
|
|
233
|
+
4,
|
|
234
|
+
2,
|
|
235
|
+
0
|
|
236
|
+
].filter((places) => places < fraction.length).map((places) => places === 0 ? whole : `${whole}.${fraction.slice(0, places)}`)];
|
|
237
|
+
return [...new Set(sizes)].filter((size) => Number(size) > 0);
|
|
238
|
+
};
|
|
239
|
+
const quote = async (client, args) => {
|
|
240
|
+
const [from, to, amount] = args;
|
|
241
|
+
if (!from || !to || amount === void 0 || !/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) throw new UsageError("usage: walt quote FROM TO AMOUNT, such as `walt quote USDT BTC 100`");
|
|
242
|
+
let result;
|
|
243
|
+
let size = amount;
|
|
244
|
+
let rejection;
|
|
245
|
+
for (const candidate of quoteSizes(amount)) try {
|
|
246
|
+
result = await client.quote(from, to, Number(candidate));
|
|
247
|
+
size = candidate;
|
|
248
|
+
break;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!(error instanceof WaltRequestError) || error.body?.code !== "invalid_amount") throw error;
|
|
251
|
+
rejection = error;
|
|
252
|
+
}
|
|
253
|
+
if (result === void 0) throw rejection;
|
|
254
|
+
const sale = result.to_currency === "USDT" && result.from_currency !== "USDT";
|
|
255
|
+
const price = result.from_amount <= 0 || result.to_amount <= 0 ? "-" : sale ? `${significant(result.to_amount / result.from_amount)} USDT/${result.from_currency}` : `${significant(result.from_amount / result.to_amount)} ${result.from_currency}/${result.to_currency}`;
|
|
256
|
+
const footer = [`order size ${result.min_amount} to ${result.max_amount} ${from}`, "fees included, nothing was exchanged"];
|
|
257
|
+
if (size !== amount) footer.unshift(`amount cut to ${size} ${from}, the most decimals it accepts`);
|
|
258
|
+
return {
|
|
259
|
+
columns: [
|
|
260
|
+
{
|
|
261
|
+
key: "pay",
|
|
262
|
+
title: "PAY",
|
|
263
|
+
numeric: true
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
key: "receive",
|
|
267
|
+
title: "RECEIVE",
|
|
268
|
+
numeric: true
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
key: "price",
|
|
272
|
+
title: "PRICE",
|
|
273
|
+
numeric: true
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
rows: [{
|
|
277
|
+
pay: `${result.from_amount} ${result.from_currency}`,
|
|
278
|
+
receive: `${result.to_amount} ${result.to_currency}`,
|
|
279
|
+
price
|
|
280
|
+
}],
|
|
281
|
+
footer
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const orders = async (client, options) => {
|
|
285
|
+
const list = await client.limitOrders(options.all ? { limit: 100 } : {
|
|
286
|
+
status: ["OPEN", "CANCELLING"],
|
|
287
|
+
limit: 100
|
|
288
|
+
});
|
|
289
|
+
const held = list.filter((order) => order.status === "OPEN").reduce((sum, order) => sum + Number(order.total), 0);
|
|
290
|
+
return {
|
|
291
|
+
columns: [
|
|
292
|
+
{
|
|
293
|
+
key: "created",
|
|
294
|
+
title: "CREATED (UTC)"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
key: "asset",
|
|
298
|
+
title: "ASSET"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: "side",
|
|
302
|
+
title: "SIDE"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "limit",
|
|
306
|
+
title: "LIMIT",
|
|
307
|
+
numeric: true
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
key: "quantity",
|
|
311
|
+
title: "QUANTITY",
|
|
312
|
+
numeric: true
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
key: "total",
|
|
316
|
+
title: "TOTAL",
|
|
317
|
+
numeric: true
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: "fee",
|
|
321
|
+
title: "FEE",
|
|
322
|
+
numeric: true
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
key: "status",
|
|
326
|
+
title: "STATUS"
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
rows: list.map((order) => ({
|
|
330
|
+
created: minute(order.created_at),
|
|
331
|
+
asset: order.base_currency,
|
|
332
|
+
side: order.side,
|
|
333
|
+
limit: order.price_limit,
|
|
334
|
+
quantity: order.quantity,
|
|
335
|
+
total: `${order.total} ${order.fee_currency}`,
|
|
336
|
+
fee: order.fee,
|
|
337
|
+
status: order.status
|
|
338
|
+
})),
|
|
339
|
+
footer: [`${held.toFixed(2)} held by open orders`]
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
const perps = async (client, options) => {
|
|
343
|
+
const positions = await client.perpPositions({ statuses: [options.closed ? "closed" : "open"] });
|
|
344
|
+
const columns = [
|
|
345
|
+
{
|
|
346
|
+
key: "contract",
|
|
347
|
+
title: "CONTRACT"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
key: "side",
|
|
351
|
+
title: "SIDE"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "leverage",
|
|
355
|
+
title: "LEV",
|
|
356
|
+
numeric: true
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
key: "size",
|
|
360
|
+
title: "SIZE",
|
|
361
|
+
numeric: true
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
key: "entry",
|
|
365
|
+
title: "ENTRY",
|
|
366
|
+
numeric: true
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
key: "margin",
|
|
370
|
+
title: "MARGIN",
|
|
371
|
+
numeric: true
|
|
372
|
+
}
|
|
373
|
+
];
|
|
374
|
+
if (options.closed) columns.push({
|
|
375
|
+
key: "opened",
|
|
376
|
+
title: "OPENED (UTC)"
|
|
377
|
+
}, {
|
|
378
|
+
key: "closed",
|
|
379
|
+
title: "CLOSED (UTC)"
|
|
380
|
+
});
|
|
381
|
+
else columns.push({
|
|
382
|
+
key: "mark",
|
|
383
|
+
title: "MARK",
|
|
384
|
+
numeric: true
|
|
385
|
+
}, {
|
|
386
|
+
key: "liquidation",
|
|
387
|
+
title: "LIQUIDATION",
|
|
388
|
+
numeric: true
|
|
389
|
+
}, {
|
|
390
|
+
key: "pnl",
|
|
391
|
+
title: "UNREALISED",
|
|
392
|
+
numeric: true
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
columns,
|
|
396
|
+
rows: positions.map((position) => ({
|
|
397
|
+
contract: position.contract_name,
|
|
398
|
+
side: position.side,
|
|
399
|
+
leverage: `${position.leverage}x`,
|
|
400
|
+
size: position.value_base,
|
|
401
|
+
entry: position.avg_entry_price,
|
|
402
|
+
margin: fixed(position.margin_quote),
|
|
403
|
+
opened: minute(position.created_at),
|
|
404
|
+
closed: minute(position.updated_at),
|
|
405
|
+
mark: position.valuation.mark_price,
|
|
406
|
+
liquidation: fixed(position.liquidation_price, 4),
|
|
407
|
+
pnl: fixed(position.valuation.unrealized_pnl_quote)
|
|
408
|
+
}))
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
const earn = async (client, options) => {
|
|
412
|
+
const { campaigns } = await client.earnCampaigns();
|
|
413
|
+
return {
|
|
414
|
+
columns: [
|
|
415
|
+
{
|
|
416
|
+
key: "campaign",
|
|
417
|
+
title: "CAMPAIGN"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
key: "asset",
|
|
421
|
+
title: "ASSET"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
key: "staked",
|
|
425
|
+
title: "STAKED",
|
|
426
|
+
numeric: true
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
key: "paid",
|
|
430
|
+
title: "PAID OUT",
|
|
431
|
+
numeric: true
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
key: "apy",
|
|
435
|
+
title: "APY",
|
|
436
|
+
numeric: true
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
key: "status",
|
|
440
|
+
title: "STATUS"
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
rows: campaigns.filter((campaign) => options.all ? campaign.status === "active" : campaign.account !== null && Number(campaign.account.balance) > 0).map((campaign) => ({
|
|
444
|
+
campaign: campaign.name,
|
|
445
|
+
asset: campaign.account?.currency ?? campaign.deposit_currency,
|
|
446
|
+
staked: trimmed(campaign.account?.balance),
|
|
447
|
+
paid: trimmed(campaign.account?.paid_out_amount),
|
|
448
|
+
apy: `${fixed(Number(campaign.current_apy) * 100)}%`,
|
|
449
|
+
status: campaign.status
|
|
450
|
+
}))
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
const render = (report) => {
|
|
454
|
+
if (report.rows.length === 0) return ["nothing to show", ...report.footer ?? []].join("\n");
|
|
455
|
+
const widths = report.columns.map((column) => Math.max(column.title.length, ...report.rows.map((row) => (row[column.key] ?? "").length)));
|
|
456
|
+
const line = (cells) => cells.map((cell, index) => {
|
|
457
|
+
const width = widths[index] ?? 0;
|
|
458
|
+
return report.columns[index]?.numeric ? cell.padStart(width) : cell.padEnd(width);
|
|
459
|
+
}).join(" ").trimEnd();
|
|
460
|
+
return [
|
|
461
|
+
line(report.columns.map((column) => column.title)),
|
|
462
|
+
...report.rows.map((row) => line(report.columns.map((column) => row[column.key] ?? ""))),
|
|
463
|
+
...report.footer ?? []
|
|
464
|
+
].join("\n");
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Runs one read-only command and returns what to print.
|
|
468
|
+
*
|
|
469
|
+
* @param client - A signed-in client.
|
|
470
|
+
* @param command - The command name, such as `balances`.
|
|
471
|
+
* @param args - Positional arguments after the command.
|
|
472
|
+
* @param options - Parsed flags.
|
|
473
|
+
*/
|
|
474
|
+
const run = async (client, command, args, options = {}) => {
|
|
475
|
+
const fiat = (options.fiat ?? "USD").toUpperCase();
|
|
476
|
+
let report;
|
|
477
|
+
switch (command) {
|
|
478
|
+
case "balances":
|
|
479
|
+
report = await balances(client, fiat);
|
|
480
|
+
break;
|
|
481
|
+
case "history":
|
|
482
|
+
report = await history(client, options);
|
|
483
|
+
break;
|
|
484
|
+
case "transactions":
|
|
485
|
+
report = await transactions(client, options);
|
|
486
|
+
break;
|
|
487
|
+
case "price":
|
|
488
|
+
report = await price(client, args, fiat);
|
|
489
|
+
break;
|
|
490
|
+
case "quote":
|
|
491
|
+
report = await quote(client, args);
|
|
492
|
+
break;
|
|
493
|
+
case "orders":
|
|
494
|
+
report = await orders(client, options);
|
|
495
|
+
break;
|
|
496
|
+
case "perps":
|
|
497
|
+
report = await perps(client, options);
|
|
498
|
+
break;
|
|
499
|
+
case "earn":
|
|
500
|
+
report = await earn(client, options);
|
|
501
|
+
break;
|
|
502
|
+
default: throw new UsageError(`unknown command: ${command}`);
|
|
503
|
+
}
|
|
504
|
+
if (!options.json) return render(report);
|
|
505
|
+
return JSON.stringify(report.rows.map((row) => Object.fromEntries(report.columns.map((column) => [column.key, row[column.key] ?? null]))), null, 2);
|
|
506
|
+
};
|
|
507
|
+
export { COMMANDS, UsageError, run };
|
package/build/client.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { AccountsResponse, Charts, Coin, CoinDetails, FiatCurrency, PriceAtTime } from "./api/accounts.js";
|
|
2
2
|
import { PnlResponse, PortfolioHistory } from "./api/analytics.js";
|
|
3
|
+
import { Balances } from "./api/balances.js";
|
|
4
|
+
import { CandleRequest, CandleSeries } from "./api/candles.js";
|
|
3
5
|
import { EarnCampaignsResponse, EarnRate, EarnTransaction, EarnTransactionsPage } from "./api/earn.js";
|
|
4
6
|
import { AvailableExchanges, Exchange, ExchangeQuote, ExchangeRequest } from "./api/exchange.js";
|
|
5
7
|
import { LimitOrder, LimitOrderBounds, LimitOrderQuote, LimitOrderToken } from "./api/limit-orders.js";
|
|
8
|
+
import { PerpContract, PerpOrderStatus, PerpOrdersPage, PerpPosition, PerpPositionStatus, PerpSystemStatus } from "./api/perps.js";
|
|
6
9
|
import { Transaction, TransactionDetails, TransactionGateway, TransactionsPage } from "./api/transactions.js";
|
|
7
10
|
export type WaltClientOptions = {
|
|
8
11
|
/** Called again with the rejected token after a 401, so the provider can renew it before its expiry. */
|
|
@@ -36,6 +39,12 @@ export declare class WaltClient {
|
|
|
36
39
|
private readonly maxConcurrent;
|
|
37
40
|
constructor(options: WaltClientOptions);
|
|
38
41
|
accounts(): Promise<AccountsResponse>;
|
|
42
|
+
/**
|
|
43
|
+
* Every balance with its account type, including USDT held by open limit orders, which `accounts()` omits.
|
|
44
|
+
*
|
|
45
|
+
* @param fiatCurrency - Currency of the fiat amounts and the overall total.
|
|
46
|
+
*/
|
|
47
|
+
balances(fiatCurrency: FiatCurrency): Promise<Balances>;
|
|
39
48
|
coins(fiatCurrency: FiatCurrency): Promise<Coin[]>;
|
|
40
49
|
localCurrency(): Promise<FiatCurrency>;
|
|
41
50
|
transactions(limit?: number, cursor?: string): Promise<TransactionsPage>;
|
|
@@ -115,6 +124,35 @@ export declare class WaltClient {
|
|
|
115
124
|
limitOrderBounds(baseCurrency: string): Promise<LimitOrderBounds>;
|
|
116
125
|
/** Every currency a limit order can buy. */
|
|
117
126
|
limitOrderTokens(): Promise<LimitOrderToken[]>;
|
|
127
|
+
/** @param contractId - Only this contract. */
|
|
128
|
+
perpContracts(contractId?: number): Promise<PerpContract[]>;
|
|
129
|
+
perpSystemStatus(): Promise<PerpSystemStatus>;
|
|
130
|
+
/**
|
|
131
|
+
* @param options - Filters, all optional.
|
|
132
|
+
* @param options.statuses - Only positions in these states. Without it, closed positions are included.
|
|
133
|
+
* @param options.contractId - Only this contract.
|
|
134
|
+
*/
|
|
135
|
+
perpPositions(options?: {
|
|
136
|
+
statuses?: PerpPositionStatus[];
|
|
137
|
+
contractId?: number;
|
|
138
|
+
}): Promise<PerpPosition[]>;
|
|
139
|
+
/**
|
|
140
|
+
* @param options - Filters, all optional.
|
|
141
|
+
* @param options.contractId - Only this contract.
|
|
142
|
+
* @param options.statuses - Only orders in these states.
|
|
143
|
+
* @param options.limit - Most orders to return.
|
|
144
|
+
*/
|
|
145
|
+
perpOrders(options?: {
|
|
146
|
+
contractId?: number;
|
|
147
|
+
statuses?: PerpOrderStatus[];
|
|
148
|
+
limit?: number;
|
|
149
|
+
}): Promise<PerpOrdersPage>;
|
|
150
|
+
/**
|
|
151
|
+
* OHLCV candles for several tickers in one request.
|
|
152
|
+
*
|
|
153
|
+
* @param requests - One per series wanted.
|
|
154
|
+
*/
|
|
155
|
+
candles(requests: CandleRequest[]): Promise<CandleSeries[]>;
|
|
118
156
|
coinDetails(currency: string, fiatCurrency: FiatCurrency): Promise<CoinDetails>;
|
|
119
157
|
charts(currency: string, fiatCurrency: FiatCurrency): Promise<Charts>;
|
|
120
158
|
priceForFiatAtTime(cryptoCurrency: string, localCurrency: FiatCurrency, amount: number | string, time: Date | string | number): Promise<PriceAtTime>;
|
package/build/client.js
CHANGED
|
@@ -32,6 +32,16 @@ var WaltClient = class {
|
|
|
32
32
|
accounts() {
|
|
33
33
|
return this.transport.get("/api/v1/accounts/");
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Every balance with its account type, including USDT held by open limit orders, which `accounts()` omits.
|
|
37
|
+
*
|
|
38
|
+
* @param fiatCurrency - Currency of the fiat amounts and the overall total.
|
|
39
|
+
*/
|
|
40
|
+
async balances(fiatCurrency) {
|
|
41
|
+
const query = new URLSearchParams({ fiat_currency: fiatCurrency });
|
|
42
|
+
const { data } = await this.transport.get(`/portmone/public-api/v1/user/accounts?${query.toString()}`);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
35
45
|
async coins(fiatCurrency) {
|
|
36
46
|
const { coins } = await this.transport.get(`/api/v1/coins/list/${fiatCurrency}/`);
|
|
37
47
|
return coins;
|
|
@@ -222,6 +232,51 @@ var WaltClient = class {
|
|
|
222
232
|
const { tokens } = await this.transport.get("/emporos/public-api/orders/limit/tokens");
|
|
223
233
|
return tokens;
|
|
224
234
|
}
|
|
235
|
+
/** @param contractId - Only this contract. */
|
|
236
|
+
async perpContracts(contractId) {
|
|
237
|
+
const query = contractId === void 0 ? "" : `?${new URLSearchParams({ contract_id: String(contractId) }).toString()}`;
|
|
238
|
+
const { contracts } = await this.transport.get(`/perps/public-api/v1/contracts/${query}`);
|
|
239
|
+
return contracts;
|
|
240
|
+
}
|
|
241
|
+
perpSystemStatus() {
|
|
242
|
+
return this.transport.get("/perps/public-api/v1/system-status/");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* @param options - Filters, all optional.
|
|
246
|
+
* @param options.statuses - Only positions in these states. Without it, closed positions are included.
|
|
247
|
+
* @param options.contractId - Only this contract.
|
|
248
|
+
*/
|
|
249
|
+
async perpPositions(options = {}) {
|
|
250
|
+
const query = new URLSearchParams();
|
|
251
|
+
if (options.contractId !== void 0) query.set("contract_id", String(options.contractId));
|
|
252
|
+
for (const status of options.statuses ?? []) query.append("statuses", status);
|
|
253
|
+
const search = query.size === 0 ? "" : `?${query.toString()}`;
|
|
254
|
+
const { positions } = await this.transport.get(`/perps/public-api/v1/positions/${search}`);
|
|
255
|
+
return positions;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* @param options - Filters, all optional.
|
|
259
|
+
* @param options.contractId - Only this contract.
|
|
260
|
+
* @param options.statuses - Only orders in these states.
|
|
261
|
+
* @param options.limit - Most orders to return.
|
|
262
|
+
*/
|
|
263
|
+
perpOrders(options = {}) {
|
|
264
|
+
const query = new URLSearchParams();
|
|
265
|
+
if (options.contractId !== void 0) query.set("contract_id", String(options.contractId));
|
|
266
|
+
if (options.limit !== void 0) query.set("limit", String(options.limit));
|
|
267
|
+
for (const status of options.statuses ?? []) query.append("statuses", status);
|
|
268
|
+
const search = query.size === 0 ? "" : `?${query.toString()}`;
|
|
269
|
+
return this.transport.get(`/perps/public-api/v2/orders/${search}`);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* OHLCV candles for several tickers in one request.
|
|
273
|
+
*
|
|
274
|
+
* @param requests - One per series wanted.
|
|
275
|
+
*/
|
|
276
|
+
async candles(requests) {
|
|
277
|
+
const { responses } = await this.transport.post("/kandelabr/public-api/v1/candles/batch", { requests });
|
|
278
|
+
return responses;
|
|
279
|
+
}
|
|
225
280
|
coinDetails(currency, fiatCurrency) {
|
|
226
281
|
return this.transport.get(`/api/v1/coins/details/${encodeURIComponent(currency)}/${encodeURIComponent(fiatCurrency)}/`);
|
|
227
282
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Account, AccountAction, AccountActions, AccountAddress, AccountsResponse, ChartPoint, Charts, Coin, CoinDetails, FiatCurrency, PriceAtTime, TradingHoursPeriod, TradingHoursState } from "./api/accounts.js";
|
|
2
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";
|
|
3
5
|
import { EarnAccessPolicy, EarnAccount, EarnAmount, EarnBonus, EarnCampaign, EarnCampaignsResponse, EarnDepegInfo, EarnInterestRate, EarnPayoutPool, EarnRate, EarnTransaction, EarnTransactionsPage } from "./api/earn.js";
|
|
4
6
|
import { AvailableExchanges, Exchange, ExchangeQuote, ExchangeRequest } from "./api/exchange.js";
|
|
5
7
|
import { LimitOrder, LimitOrderBounds, LimitOrderQuote, LimitOrderToken } from "./api/limit-orders.js";
|
|
8
|
+
import { PerpContract, PerpOrder, PerpOrderStatus, PerpOrdersPage, PerpPosition, PerpPositionStatus, PerpSystemStatus } from "./api/perps.js";
|
|
6
9
|
import { NormalizedTransaction, Transaction, TransactionDetails, TransactionGateway, TransactionLeg, TransactionsPage, normalizeTransaction } from "./api/transactions.js";
|
|
7
10
|
import { Portfolio, PortfolioEntry, WaltClient, WaltClientOptions } from "./client.js";
|
|
8
11
|
import { TelegramTokenSource, TelegramTokenSourceOptions, TokenState, TokenStorage } from "./telegram/auth.js";
|
|
9
12
|
import { WaltAuthError, WaltRequestError } from "./transport.js";
|
|
10
|
-
export { type Account, type AccountAction, type AccountActions, type AccountAddress, type AccountsResponse, type AvailableExchanges, 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 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
|
+
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 };
|
package/build/transport.js
CHANGED
|
@@ -23,6 +23,11 @@ const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
|
|
|
23
23
|
503,
|
|
24
24
|
504
|
|
25
25
|
]);
|
|
26
|
+
const BEARER_SERVICES = [
|
|
27
|
+
"/emporos/",
|
|
28
|
+
"/portmone/",
|
|
29
|
+
"/kandelabr/"
|
|
30
|
+
];
|
|
26
31
|
const MAX_RETRY_DELAY_MS = 3e4;
|
|
27
32
|
const hex = (bytes) => randomBytes(bytes).toString("hex");
|
|
28
33
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -121,7 +126,7 @@ var Transport = class {
|
|
|
121
126
|
const headers = {
|
|
122
127
|
...BROWSER_HEADERS,
|
|
123
128
|
accept: "application/json",
|
|
124
|
-
authorization: path.startsWith(
|
|
129
|
+
authorization: BEARER_SERVICES.some((prefix) => path.startsWith(prefix)) ? `Bearer ${jwt}` : jwt,
|
|
125
130
|
referer: `${this.baseUrl}/main?ep=menu${userId === null ? "" : `&tgWebAppUserId=${userId}`}`,
|
|
126
131
|
baggage: this.baggage,
|
|
127
132
|
"sentry-trace": `${this.traceId}-${hex(8)}-0`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2bad/walt",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"bin": {
|
|
39
|
-
"
|
|
39
|
+
"walt": "./build/bin/walt.js"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"build"
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"fix": "run-p fix:**",
|
|
75
75
|
"fix:code": "oxlint --fix ./source",
|
|
76
76
|
"fix:format": "oxfmt ./source ./examples",
|
|
77
|
-
"telegram:login": "node --env-file-if-exists=.env
|
|
77
|
+
"telegram:login": "run-s build && node --env-file-if-exists=.env build/bin/walt.js login",
|
|
78
78
|
"example": "run-s build && node --env-file-if-exists=.env examples/portfolio.ts",
|
|
79
79
|
"preversion": "run-s check test build",
|
|
80
80
|
"test": "vitest run",
|
package/build/bin/login.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { TelegramClient } from "teleproto";
|
|
3
|
-
import { StringSession } from "teleproto/sessions/index.js";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { stdin, stdout } from "node:process";
|
|
6
|
-
// source/bin/login.ts
|
|
7
|
-
const rl = createInterface({
|
|
8
|
-
input: stdin,
|
|
9
|
-
output: stdout
|
|
10
|
-
});
|
|
11
|
-
const ask = (question) => rl.question(question);
|
|
12
|
-
const apiId = Number(process.env["TG_API_ID"] || await ask("API id (from https://my.telegram.org): "));
|
|
13
|
-
if (!Number.isSafeInteger(apiId) || apiId <= 0) {
|
|
14
|
-
console.error("The API id must be the number shown at https://my.telegram.org");
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
const apiHash = process.env["TG_API_HASH"] || await ask("API hash: ");
|
|
18
|
-
const client = new TelegramClient(new StringSession(""), apiId, apiHash, { connectionRetries: 3 });
|
|
19
|
-
await client.start({
|
|
20
|
-
phoneNumber: () => ask("Phone (+country...): "),
|
|
21
|
-
phoneCode: () => ask("Code you received: "),
|
|
22
|
-
password: () => ask("2FA password (blank if none): "),
|
|
23
|
-
onError: (error) => console.error(error)
|
|
24
|
-
});
|
|
25
|
-
rl.close();
|
|
26
|
-
console.log("\nLogged in. Save this as TG_SESSION (keep it secret):\n");
|
|
27
|
-
console.log(client.session.save());
|
|
28
|
-
await client.disconnect();
|
|
29
|
-
process.exit(0);
|
|
30
|
-
export {};
|
|
File without changes
|