perp-cli 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-program.js +19 -2
- package/dist/commands/funds-onchain.d.ts +1 -1
- package/dist/commands/funds-onchain.js +9 -47
- package/dist/commands/funds.js +34 -48
- package/dist/commands/history.js +30 -9
- package/dist/commands/wallet.js +74 -155
- package/dist/exchanges/aster.d.ts +21 -0
- package/dist/exchanges/aster.js +117 -43
- package/dist/exchanges/ondo/adapter.d.ts +4 -18
- package/dist/exchanges/ondo/adapter.js +5 -29
- package/dist/exchanges/ondo/funds.d.ts +1 -28
- package/dist/exchanges/ondo/funds.js +2 -94
- package/dist/exchanges/pacifica.d.ts +5 -0
- package/dist/exchanges/pacifica.js +32 -15
- package/dist/exchanges/qfex/adapter.js +4 -1
- package/dist/exchanges/venues/ondo.d.ts +0 -5
- package/dist/exchanges/venues/ondo.js +2 -6
- package/dist/exchanges/venues/qfex.d.ts +11 -0
- package/dist/exchanges/venues/qfex.js +10 -2
- package/dist/mcp-server.js +2 -2
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +20 -3
- package/skills/perp-cli/references/agent-operations.md +2 -2
- package/skills/perp-cli/references/commands.md +7 -6
package/dist/commands/wallet.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
|
+
import { evmOnchainBalances, solanaOnchainBalances, stableTotal } from "../onchain-balances.js";
|
|
2
3
|
import { PerpError } from "../errors.js";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import bs58 from "bs58";
|
|
@@ -98,80 +99,60 @@ export function getWalletKeyByName(name) {
|
|
|
98
99
|
const store = loadStore();
|
|
99
100
|
return store.wallets[name]?.privateKey ?? null;
|
|
100
101
|
}
|
|
102
|
+
/** SOL + USDC / USDT on Solana (devnet: USDP). */
|
|
101
103
|
async function getSolanaBalances(address, isTestnet) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
return solanaOnchainBalances(address, isTestnet);
|
|
105
|
+
}
|
|
106
|
+
/** Gas token + USDC / USDT (USDG on Robinhood Chain) on every EVM chain perp-cli uses; `only` limits the chains. */
|
|
107
|
+
async function getEvmBalances(address, isTestnet, only) {
|
|
108
|
+
return evmOnchainBalances(address, isTestnet, only);
|
|
109
|
+
}
|
|
110
|
+
/** Human table: non-zero balances and failed reads (JSON keeps every row), then the stablecoin total. */
|
|
111
|
+
function balanceTable(balances) {
|
|
112
|
+
const shown = balances.filter(b => b.error !== null || Number(b.balance) !== 0);
|
|
113
|
+
const rows = shown.map((b) => [
|
|
114
|
+
b.chain,
|
|
115
|
+
chalk.white.bold(b.token),
|
|
116
|
+
b.balance === null ? chalk.red("read failed") : b.balance,
|
|
117
|
+
b.usdValue === null ? chalk.gray("-") : `$${formatUsd(b.usdValue)}`,
|
|
118
|
+
]);
|
|
119
|
+
const total = stableTotal(balances);
|
|
120
|
+
rows.push([chalk.gray("total"), chalk.white.bold("stablecoins"), "", `$${formatUsd(total.usd)}${total.errors.length > 0 ? chalk.red(" (some reads failed)") : ""}`]);
|
|
121
|
+
return makeTable(["Chain", "Token", "Balance", "USD Value"], rows);
|
|
122
|
+
}
|
|
123
|
+
async function balanceEntry(label, vm, address, testnet) {
|
|
124
|
+
const balances = vm === "solana" ? await getSolanaBalances(address, testnet) : await getEvmBalances(address, testnet);
|
|
125
|
+
const total = stableTotal(balances);
|
|
126
|
+
return { ...label, chain: vm, address, balances, stableUsd: total.usd, errors: total.errors };
|
|
127
|
+
}
|
|
128
|
+
/** Reads each distinct address once (several venues often share one key), in order. */
|
|
129
|
+
async function balanceEntries(items, testnet) {
|
|
130
|
+
const seen = new Map();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const it of items) {
|
|
133
|
+
const key = `${it.vm}:${it.address.toLowerCase()}`;
|
|
134
|
+
const prev = seen.get(key);
|
|
135
|
+
if (prev !== undefined) {
|
|
136
|
+
if (it.label.exchange !== undefined)
|
|
137
|
+
prev.exchange = prev.exchange === undefined ? it.label.exchange : `${prev.exchange},${it.label.exchange}`;
|
|
138
|
+
continue;
|
|
124
139
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
usdValue: `$${formatUsd(usdcBalance)}`,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
balances.push({ token: isTestnet ? "USDP" : "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd");
|
|
136
|
-
const json = (await res.json());
|
|
137
|
-
balances[0].usdValue = `$${formatUsd(solBalance * (json.solana?.usd ?? 0))}`;
|
|
140
|
+
const entry = await balanceEntry(it.label, it.vm, it.address, testnet);
|
|
141
|
+
seen.set(key, entry);
|
|
142
|
+
out.push(entry);
|
|
138
143
|
}
|
|
139
|
-
|
|
140
|
-
balances[0].usdValue = "-";
|
|
141
|
-
}
|
|
142
|
-
return balances;
|
|
144
|
+
return out;
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const ethBalance = Number(ethers.formatEther(ethBal));
|
|
153
|
-
balances.push({ token: "ETH", balance: ethBalance.toFixed(6), usdValue: "" });
|
|
154
|
-
const usdcAddr = isTestnet
|
|
155
|
-
? "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
|
|
156
|
-
: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
157
|
-
try {
|
|
158
|
-
const usdc = new ethers.Contract(usdcAddr, ["function balanceOf(address) view returns (uint256)"], provider);
|
|
159
|
-
const rawBal = await usdc.balanceOf(address);
|
|
160
|
-
const usdcBalance = Number(ethers.formatUnits(rawBal, 6));
|
|
161
|
-
balances.push({ token: "USDC", balance: usdcBalance.toFixed(2), usdValue: `$${formatUsd(usdcBalance)}` });
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
balances.push({ token: "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd");
|
|
168
|
-
const json = (await res.json());
|
|
169
|
-
balances[0].usdValue = `$${formatUsd(ethBalance * (json.ethereum?.usd ?? 0))}`;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
balances[0].usdValue = "-";
|
|
146
|
+
/** Exactly one JSON document (an array of entries), or one table per entry. */
|
|
147
|
+
function printBalanceEntries(entries, json) {
|
|
148
|
+
if (json)
|
|
149
|
+
return printJson(jsonOk(entries));
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const who = [e.wallet, e.exchange].filter(x => x !== undefined).join(" / ");
|
|
152
|
+
console.log(chalk.cyan.bold(`\n ${who} (${e.chain} ${e.address.slice(0, 8)}...)`));
|
|
153
|
+
console.log(balanceTable(e.balances));
|
|
173
154
|
}
|
|
174
|
-
|
|
155
|
+
console.log();
|
|
175
156
|
}
|
|
176
157
|
// ── Derive address from private key ──────────────────────────
|
|
177
158
|
async function deriveSolanaAddress(key) {
|
|
@@ -816,7 +797,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
816
797
|
// ── balance (by wallet name) ──
|
|
817
798
|
wallet
|
|
818
799
|
.command("balance [name]")
|
|
819
|
-
.description("
|
|
800
|
+
.description("On-chain balances for a saved wallet (or the active one): gas token + USDC / USDT on Arbitrum, Ethereum, Base, BNB Chain, Robinhood Chain (USDG) and Solana")
|
|
820
801
|
.option("--testnet", "Use testnet", false)
|
|
821
802
|
.action(async (name, opts) => {
|
|
822
803
|
const store = loadStore();
|
|
@@ -867,44 +848,25 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
867
848
|
if (activeEntries.length === 0) {
|
|
868
849
|
const settings = loadSettings();
|
|
869
850
|
if (settings.owsActiveWallet) {
|
|
851
|
+
// Only loading the OWS vault may fall through to the env path; balance reads report
|
|
852
|
+
// their own per-token errors and any other failure propagates.
|
|
853
|
+
let owsAccounts = null;
|
|
870
854
|
try {
|
|
871
855
|
const { loadOws } = await import("../signer/ows-loader.js");
|
|
872
|
-
|
|
873
|
-
const owsWallet = ows.getWallet(settings.owsActiveWallet);
|
|
874
|
-
const owsResults = [];
|
|
875
|
-
for (const acct of owsWallet.accounts) {
|
|
876
|
-
const isEvm = acct.chainId.startsWith("eip155:");
|
|
877
|
-
const isSol = acct.chainId.startsWith("solana:");
|
|
878
|
-
if (!isEvm && !isSol)
|
|
879
|
-
continue;
|
|
880
|
-
const address = acct.address;
|
|
881
|
-
const balances = isSol
|
|
882
|
-
? await getSolanaBalances(address, opts.testnet)
|
|
883
|
-
: await getEvmBalances(address, opts.testnet);
|
|
884
|
-
if (isJson()) {
|
|
885
|
-
owsResults.push({
|
|
886
|
-
wallet: settings.owsActiveWallet,
|
|
887
|
-
chain: isSol ? "solana" : "evm",
|
|
888
|
-
address,
|
|
889
|
-
balances,
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
console.log(chalk.cyan.bold(`\n ${settings.owsActiveWallet} (${isSol ? "solana" : "evm"} ${address.slice(0, 8)}...)`));
|
|
894
|
-
const rows = balances.map((b) => [
|
|
895
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
896
|
-
]);
|
|
897
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
if (isJson())
|
|
901
|
-
printJson(jsonOk(owsResults));
|
|
902
|
-
else
|
|
903
|
-
console.log();
|
|
904
|
-
return;
|
|
856
|
+
owsAccounts = loadOws().getWallet(settings.owsActiveWallet).accounts;
|
|
905
857
|
}
|
|
906
858
|
catch {
|
|
907
|
-
// OWS module unavailable or vault missing —
|
|
859
|
+
owsAccounts = null; // OWS module unavailable or vault missing — use the env path below
|
|
860
|
+
}
|
|
861
|
+
if (owsAccounts !== null) {
|
|
862
|
+
const results = [];
|
|
863
|
+
for (const acct of owsAccounts) {
|
|
864
|
+
const vm = acct.chainId.startsWith("solana:") ? "solana" : acct.chainId.startsWith("eip155:") ? "evm" : null;
|
|
865
|
+
if (vm === null)
|
|
866
|
+
continue;
|
|
867
|
+
results.push(await balanceEntry({ wallet: settings.owsActiveWallet }, vm, acct.address, opts.testnet));
|
|
868
|
+
}
|
|
869
|
+
return printBalanceEntries(results, isJson());
|
|
908
870
|
}
|
|
909
871
|
}
|
|
910
872
|
const stored = loadEnvFile();
|
|
@@ -928,54 +890,13 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
928
890
|
}
|
|
929
891
|
process.exit(1);
|
|
930
892
|
}
|
|
931
|
-
|
|
932
|
-
if (!isJson())
|
|
933
|
-
console.log(chalk.cyan.bold(`\n ${we.exchange} (${we.address.slice(0, 8)}...)`));
|
|
934
|
-
const balances = we.chain === "solana"
|
|
935
|
-
? await getSolanaBalances(we.address, opts.testnet)
|
|
936
|
-
: await getEvmBalances(we.address, opts.testnet);
|
|
937
|
-
if (isJson()) {
|
|
938
|
-
printJson(jsonOk({ exchange: we.exchange, address: we.address, balances }));
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
const rows = balances.map((b) => [
|
|
942
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
943
|
-
]);
|
|
944
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
945
|
-
}
|
|
946
|
-
console.log();
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
for (const ae of activeEntries) {
|
|
950
|
-
if (!isJson())
|
|
951
|
-
console.log(chalk.cyan.bold(`\n ${ae.name} (${ae.exchange})`));
|
|
952
|
-
const balances = ae.type === "solana"
|
|
953
|
-
? await getSolanaBalances(ae.address, opts.testnet)
|
|
954
|
-
: await getEvmBalances(ae.address, opts.testnet);
|
|
955
|
-
if (isJson()) {
|
|
956
|
-
printJson(jsonOk({ wallet: ae.name, exchange: ae.exchange, balances }));
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
const rows = balances.map((b) => [
|
|
960
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
961
|
-
]);
|
|
962
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
893
|
+
return printBalanceEntries(await balanceEntries(envEntries.map(we => ({ label: { exchange: we.exchange }, vm: we.chain, address: we.address })), opts.testnet), isJson());
|
|
963
894
|
}
|
|
964
|
-
|
|
965
|
-
|
|
895
|
+
return printBalanceEntries(await balanceEntries(activeEntries.map(ae => ({
|
|
896
|
+
label: { wallet: ae.name, exchange: ae.exchange }, vm: ae.type === "solana" ? "solana" : "evm", address: ae.address,
|
|
897
|
+
})), opts.testnet), isJson());
|
|
966
898
|
}
|
|
967
|
-
|
|
968
|
-
console.log(chalk.cyan(`\n Fetching balance for "${entry.name}" (${entry.address.slice(0, 8)}...)\n`));
|
|
969
|
-
const balances = entry.type === "solana"
|
|
970
|
-
? await getSolanaBalances(entry.address, opts.testnet)
|
|
971
|
-
: await getEvmBalances(entry.address, opts.testnet);
|
|
972
|
-
if (isJson())
|
|
973
|
-
return printJson(jsonOk({ wallet: entry.name, balances }));
|
|
974
|
-
const rows = balances.map((b) => [
|
|
975
|
-
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
976
|
-
]);
|
|
977
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
978
|
-
console.log();
|
|
899
|
+
return printBalanceEntries([await balanceEntry({ wallet: entry.name }, entry.type === "solana" ? "solana" : "evm", entry.address, opts.testnet)], isJson());
|
|
979
900
|
});
|
|
980
901
|
// ── direct address balance (kept for convenience) ──
|
|
981
902
|
wallet
|
|
@@ -988,8 +909,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
988
909
|
const balances = await getSolanaBalances(address, opts.testnet);
|
|
989
910
|
if (isJson())
|
|
990
911
|
return printJson(jsonOk(balances));
|
|
991
|
-
|
|
992
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
912
|
+
console.log(balanceTable(balances));
|
|
993
913
|
console.log(chalk.gray(`\n Network: ${opts.testnet ? "Devnet" : "Mainnet"}\n`));
|
|
994
914
|
});
|
|
995
915
|
wallet
|
|
@@ -999,11 +919,10 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
999
919
|
.action(async (address, opts) => {
|
|
1000
920
|
if (!isJson())
|
|
1001
921
|
console.log(chalk.cyan(`\n Fetching Arbitrum balances for ${address.slice(0, 8)}...${address.slice(-4)}\n`));
|
|
1002
|
-
const balances = await getEvmBalances(address, opts.testnet);
|
|
922
|
+
const balances = await getEvmBalances(address, opts.testnet, [opts.testnet ? "arbitrum-sepolia" : "arbitrum"]);
|
|
1003
923
|
if (isJson())
|
|
1004
924
|
return printJson(jsonOk(balances));
|
|
1005
|
-
|
|
1006
|
-
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
925
|
+
console.log(balanceTable(balances));
|
|
1007
926
|
console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
|
|
1008
927
|
});
|
|
1009
928
|
// ── migrate (legacy → OWS) ──
|
|
@@ -61,6 +61,8 @@ type ResolvedSigner = {
|
|
|
61
61
|
/** EVM address of the owner/master — used as `user` field in Aster requests */
|
|
62
62
|
userAddress: string;
|
|
63
63
|
};
|
|
64
|
+
/** Max 1000-row pages of 7-day commission income read for symbol discovery (20 000 fills). */
|
|
65
|
+
export declare const ASTER_INCOME_MAX_PAGES = 20;
|
|
64
66
|
export declare class AsterAdapter implements ExchangeAdapter {
|
|
65
67
|
readonly name = "aster";
|
|
66
68
|
readonly chain = "bnb";
|
|
@@ -207,7 +209,26 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
207
209
|
*/
|
|
208
210
|
invalidateOpenOrdersCache(): void;
|
|
209
211
|
getOpenOrders(): Promise<ExchangeOrder[]>;
|
|
212
|
+
/**
|
|
213
|
+
* Symbols with account activity: open positions, open orders, and every symbol with a
|
|
214
|
+
* COMMISSION income row in the last 7 days (the venue's default window). Aster's userTrades /
|
|
215
|
+
* allOrders both require `symbol`, so this is the per-symbol fan-out set. Income is read in
|
|
216
|
+
* pages of 1000 (the endpoint max; startTime / endTime are inclusive) walking back from now;
|
|
217
|
+
* more than ASTER_INCOME_MAX_PAGES pages → EXCHANGE_ERROR, never a partial set. Orders on a
|
|
218
|
+
* symbol that never filled, is not open and has no position cannot be found (allOrders needs it).
|
|
219
|
+
*/
|
|
220
|
+
private _tradedApiSymbols;
|
|
221
|
+
/**
|
|
222
|
+
* Recent orders across the symbols with account activity (see _tradedApiSymbols), newest first.
|
|
223
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
224
|
+
*/
|
|
210
225
|
getOrderHistory(limit?: number): Promise<ExchangeOrder[]>;
|
|
226
|
+
/**
|
|
227
|
+
* Recent fills. `GET /fapi/v3/userTrades` requires a symbol (asterdex/api-docs V3), so the
|
|
228
|
+
* symbols come from the open positions plus every symbol with a COMMISSION income row in the
|
|
229
|
+
* venue's default window (`GET /fapi/v3/income`: "the recent 7-day data" when no time is sent).
|
|
230
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
231
|
+
*/
|
|
211
232
|
getTradeHistory(limit?: number): Promise<ExchangeTrade[]>;
|
|
212
233
|
getFundingPayments(limit?: number): Promise<ExchangeFundingPayment[]>;
|
|
213
234
|
/**
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -152,6 +152,8 @@ export function requireAsterClientOrderId(id) {
|
|
|
152
152
|
return id;
|
|
153
153
|
}
|
|
154
154
|
// ── AsterAdapter ──────────────────────────────────────────────────────────────
|
|
155
|
+
/** Max 1000-row pages of 7-day commission income read for symbol discovery (20 000 fills). */
|
|
156
|
+
export const ASTER_INCOME_MAX_PAGES = 20;
|
|
155
157
|
export class AsterAdapter {
|
|
156
158
|
name = "aster";
|
|
157
159
|
chain = "bnb";
|
|
@@ -715,65 +717,137 @@ export class AsterAdapter {
|
|
|
715
717
|
this._ordersCache = { data: result, time: Date.now(), epoch: accountCacheEpoch() };
|
|
716
718
|
return result;
|
|
717
719
|
}
|
|
718
|
-
|
|
720
|
+
/**
|
|
721
|
+
* Symbols with account activity: open positions, open orders, and every symbol with a
|
|
722
|
+
* COMMISSION income row in the last 7 days (the venue's default window). Aster's userTrades /
|
|
723
|
+
* allOrders both require `symbol`, so this is the per-symbol fan-out set. Income is read in
|
|
724
|
+
* pages of 1000 (the endpoint max; startTime / endTime are inclusive) walking back from now;
|
|
725
|
+
* more than ASTER_INCOME_MAX_PAGES pages → EXCHANGE_ERROR, never a partial set. Orders on a
|
|
726
|
+
* symbol that never filled, is not open and has no position cannot be found (allOrders needs it).
|
|
727
|
+
*/
|
|
728
|
+
async _tradedApiSymbols(r) {
|
|
719
729
|
const positions = await this.getPositions();
|
|
720
730
|
const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
731
|
+
const open = await this._signedGetEip712("/fapi/v3/openOrders", {}, r);
|
|
732
|
+
if (!Array.isArray(open)) {
|
|
733
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster open orders is not a list (${JSON.stringify(open).slice(0, 120)})`, { exchange: "aster" });
|
|
734
|
+
}
|
|
735
|
+
for (const o of open) {
|
|
736
|
+
if (typeof o.symbol === "string" && o.symbol !== "")
|
|
737
|
+
apiSymbols.add(o.symbol);
|
|
738
|
+
}
|
|
739
|
+
const startTime = Date.now() - 7 * 86_400_000;
|
|
740
|
+
let endTime = Date.now();
|
|
741
|
+
for (let page = 0;; page++) {
|
|
742
|
+
if (page >= ASTER_INCOME_MAX_PAGES) {
|
|
743
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster commission history exceeds ${ASTER_INCOME_MAX_PAGES * 1000} rows in 7 days — the symbol list would be incomplete`, {
|
|
744
|
+
exchange: "aster", remediation: "Check fills per symbol in the Aster web app",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const income = await this._signedGetEip712("/fapi/v3/income", {
|
|
748
|
+
incomeType: "COMMISSION", startTime: String(startTime), endTime: String(endTime), limit: "1000",
|
|
749
|
+
}, r);
|
|
750
|
+
if (!Array.isArray(income)) {
|
|
751
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster income history is not a list (${JSON.stringify(income).slice(0, 120)})`, { exchange: "aster" });
|
|
752
|
+
}
|
|
753
|
+
let oldest = Infinity;
|
|
754
|
+
for (const row of income) {
|
|
755
|
+
if (typeof row.symbol === "string" && row.symbol !== "")
|
|
756
|
+
apiSymbols.add(row.symbol);
|
|
757
|
+
const t = Number(row.time);
|
|
758
|
+
if (!Number.isFinite(t)) {
|
|
759
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster income row without a numeric time: ${JSON.stringify(row).slice(0, 160)}`, { exchange: "aster" });
|
|
760
|
+
}
|
|
761
|
+
oldest = Math.min(oldest, t);
|
|
724
762
|
}
|
|
763
|
+
if (income.length < 1000 || oldest <= startTime)
|
|
764
|
+
break;
|
|
765
|
+
endTime = oldest - 1;
|
|
725
766
|
}
|
|
767
|
+
return apiSymbols;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Recent orders across the symbols with account activity (see _tradedApiSymbols), newest first.
|
|
771
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
772
|
+
*/
|
|
773
|
+
async getOrderHistory(limit = 30) {
|
|
726
774
|
const r = this._resolveSigner();
|
|
727
|
-
const
|
|
775
|
+
const apiSymbols = await this._tradedApiSymbols(r);
|
|
776
|
+
const field = (o, k) => {
|
|
777
|
+
const v = o[k];
|
|
778
|
+
if ((typeof v === "string" && v !== "") || typeof v === "number")
|
|
779
|
+
return String(v);
|
|
780
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row without a readable ${k}: ${JSON.stringify(o).slice(0, 160)}`, { exchange: "aster" });
|
|
781
|
+
};
|
|
782
|
+
const rows = [];
|
|
728
783
|
for (const sym of apiSymbols) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
symbol: this._fromApi(String(o.symbol ?? "")),
|
|
738
|
-
side: String(o.side).toLowerCase(),
|
|
739
|
-
price: String(o.price ?? "0"),
|
|
740
|
-
size: String(o.origQty ?? "0"),
|
|
741
|
-
filled: String(o.executedQty ?? "0"),
|
|
742
|
-
status: String(o.status ?? ""),
|
|
743
|
-
type: String(o.type ?? ""),
|
|
744
|
-
triggerPrice: Number(o.stopPrice) > 0 ? String(o.stopPrice) : null,
|
|
745
|
-
});
|
|
784
|
+
const orders = await this._signedGetEip712("/fapi/v3/allOrders", { symbol: sym, limit: String(limit) }, r);
|
|
785
|
+
if (!Array.isArray(orders)) {
|
|
786
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders for ${sym} is not a list (${JSON.stringify(orders).slice(0, 120)})`, { exchange: "aster" });
|
|
787
|
+
}
|
|
788
|
+
for (const o of orders) {
|
|
789
|
+
const side = field(o, "side").toLowerCase();
|
|
790
|
+
if (side !== "buy" && side !== "sell") {
|
|
791
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row with unknown side "${side}"`, { exchange: "aster" });
|
|
746
792
|
}
|
|
793
|
+
const time = Number(field(o, "time"));
|
|
794
|
+
if (!Number.isFinite(time))
|
|
795
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster allOrders row with a non-numeric time`, { exchange: "aster" });
|
|
796
|
+
rows.push({
|
|
797
|
+
time,
|
|
798
|
+
order: {
|
|
799
|
+
orderId: field(o, "orderId"),
|
|
800
|
+
symbol: this._fromApi(field(o, "symbol")),
|
|
801
|
+
side,
|
|
802
|
+
price: field(o, "price"),
|
|
803
|
+
size: field(o, "origQty"),
|
|
804
|
+
filled: field(o, "executedQty"),
|
|
805
|
+
status: field(o, "status"),
|
|
806
|
+
type: field(o, "type"),
|
|
807
|
+
triggerPrice: Number(o.stopPrice) > 0 ? String(o.stopPrice) : null,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
747
810
|
}
|
|
748
|
-
catch { /* skip */ }
|
|
749
811
|
}
|
|
750
|
-
return
|
|
812
|
+
return rows.sort((x, y) => y.time - x.time).slice(0, limit).map(x => x.order);
|
|
751
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Recent fills. `GET /fapi/v3/userTrades` requires a symbol (asterdex/api-docs V3), so the
|
|
816
|
+
* symbols come from the open positions plus every symbol with a COMMISSION income row in the
|
|
817
|
+
* venue's default window (`GET /fapi/v3/income`: "the recent 7-day data" when no time is sent).
|
|
818
|
+
* A failed read throws (never an empty list); a row without a readable field → EXCHANGE_ERROR.
|
|
819
|
+
*/
|
|
752
820
|
async getTradeHistory(limit = 30) {
|
|
753
|
-
const positions = await this.getPositions();
|
|
754
|
-
const apiSymbols = new Set(positions.map(p => this._toApi(p.symbol)));
|
|
755
|
-
if (apiSymbols.size === 0)
|
|
756
|
-
apiSymbols.add("BTCUSDT");
|
|
757
821
|
const r = this._resolveSigner();
|
|
822
|
+
const apiSymbols = await this._tradedApiSymbols(r);
|
|
823
|
+
const field = (t, k) => {
|
|
824
|
+
const v = t[k];
|
|
825
|
+
if ((typeof v === "string" && v !== "") || typeof v === "number")
|
|
826
|
+
return String(v);
|
|
827
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row without a readable ${k}: ${JSON.stringify(t).slice(0, 160)}`, { exchange: "aster" });
|
|
828
|
+
};
|
|
758
829
|
const allTrades = [];
|
|
759
830
|
for (const sym of apiSymbols) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
time: Number(t.time ?? 0),
|
|
768
|
-
symbol: this._fromApi(String(t.symbol ?? "")),
|
|
769
|
-
side: t.buyer ? "buy" : "sell",
|
|
770
|
-
price: String(t.price ?? "0"),
|
|
771
|
-
size: String(t.qty ?? "0"),
|
|
772
|
-
fee: String(t.commission ?? "0"),
|
|
773
|
-
});
|
|
831
|
+
const trades = await this._signedGetEip712("/fapi/v3/userTrades", { symbol: sym, limit: String(limit) }, r);
|
|
832
|
+
if (!Array.isArray(trades)) {
|
|
833
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades for ${sym} is not a list (${JSON.stringify(trades).slice(0, 120)})`, { exchange: "aster" });
|
|
834
|
+
}
|
|
835
|
+
for (const t of trades) {
|
|
836
|
+
if (typeof t.buyer !== "boolean") {
|
|
837
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row without a boolean buyer: ${JSON.stringify(t).slice(0, 160)}`, { exchange: "aster" });
|
|
774
838
|
}
|
|
839
|
+
const time = Number(field(t, "time"));
|
|
840
|
+
if (!Number.isFinite(time))
|
|
841
|
+
throw new PerpError("EXCHANGE_ERROR", `Aster userTrades row with a non-numeric time`, { exchange: "aster" });
|
|
842
|
+
allTrades.push({
|
|
843
|
+
time,
|
|
844
|
+
symbol: this._fromApi(field(t, "symbol")),
|
|
845
|
+
side: t.buyer ? "buy" : "sell",
|
|
846
|
+
price: field(t, "price"),
|
|
847
|
+
size: field(t, "qty"),
|
|
848
|
+
fee: field(t, "commission"),
|
|
849
|
+
});
|
|
775
850
|
}
|
|
776
|
-
catch { /* skip */ }
|
|
777
851
|
}
|
|
778
852
|
return allTrades.sort((a, b) => b.time - a.time).slice(0, limit);
|
|
779
853
|
}
|
|
@@ -42,12 +42,6 @@ import type { VenueCreateContext, VenueDefinition } from "../venue.js";
|
|
|
42
42
|
import type { AmendRequest, CancelResult, LeverageResult, Network, OrderRef, OrderRequest, OrderResult, VenueMarket } from "../venue-types.js";
|
|
43
43
|
import { type BuilderFeeGate } from "../builder-fee.js";
|
|
44
44
|
import { OndoClient } from "./client.js";
|
|
45
|
-
interface OndoWithdrawal {
|
|
46
|
-
withdrawal_status: string;
|
|
47
|
-
withdrawal_id: string;
|
|
48
|
-
customer_withdrawal_id: string;
|
|
49
|
-
hash?: string;
|
|
50
|
-
}
|
|
51
45
|
export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
52
46
|
readonly name: "ondo";
|
|
53
47
|
readonly aliases: readonly ["ondo-perps"];
|
|
@@ -72,8 +66,6 @@ export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
72
66
|
get restClient(): OndoClient;
|
|
73
67
|
/** Account id from GET /v1/account — the `from.id`/`to.id` of withdrawals and spot↔perp transfers. */
|
|
74
68
|
accountId(): Promise<string>;
|
|
75
|
-
/** Withdrawal address book (GET /v1/wallet/address_book) — withdrawals go only to these addresses. */
|
|
76
|
-
addressBook(): Promise<string[]>;
|
|
77
69
|
private pairOf;
|
|
78
70
|
/**
|
|
79
71
|
* An order the venue returned as "pending" (not yet open / filled) is read back by id
|
|
@@ -152,15 +144,9 @@ export declare class OndoAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
152
144
|
triggerType?: "mark" | "last" | "mid";
|
|
153
145
|
}): Promise<OrderResult>;
|
|
154
146
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
147
|
+
* Withdrawals are not available over the API (account owner, 2026-10-13): Ondo pays only
|
|
148
|
+
* address-book destinations and refuses API-key auth for adding one
|
|
149
|
+
* (`key_not_allowed_for_action`). Refused before any request — withdraw in the web app.
|
|
158
150
|
*/
|
|
159
|
-
withdraw(
|
|
160
|
-
assetId?: number;
|
|
161
|
-
routeType?: number;
|
|
162
|
-
}): Promise<OndoWithdrawal & {
|
|
163
|
-
withdrawalFeeUSD: string;
|
|
164
|
-
}>;
|
|
151
|
+
withdraw(_amount: string, _destination: string): Promise<never>;
|
|
165
152
|
}
|
|
166
|
-
export {};
|
|
@@ -41,7 +41,6 @@ import { alignProtectionPrice, floorSize, formatComputedPrice, requireOnTick, sl
|
|
|
41
41
|
import { unsupported, validateLegacyTif, validateOrderRequest } from "../trading-v2.js";
|
|
42
42
|
import { networkConfig, requireCapabilities } from "../venue.js";
|
|
43
43
|
import { parseOrderRef, toLegacyMarketInfo } from "../venue-types.js";
|
|
44
|
-
import { ONDO_WITHDRAW_NETWORK } from "../venues/ondo.js";
|
|
45
44
|
import { builderGateOf, builderRejectionOf, NO_BUILDER, requireFeeBps } from "../builder-fee.js";
|
|
46
45
|
import { loadOndoCredentials, ONDO_APPROVE_REMEDIATION } from "./agent.js";
|
|
47
46
|
import { OndoClient } from "./client.js";
|
|
@@ -217,10 +216,6 @@ export class OndoAdapter {
|
|
|
217
216
|
const account = await this.client.get("/v1/account", null);
|
|
218
217
|
return account.accountID;
|
|
219
218
|
}
|
|
220
|
-
/** Withdrawal address book (GET /v1/wallet/address_book) — withdrawals go only to these addresses. */
|
|
221
|
-
async addressBook() {
|
|
222
|
-
return (await import("./funds.js")).ondoAddressBook(this.client);
|
|
223
|
-
}
|
|
224
219
|
// ── Helpers ──
|
|
225
220
|
async pairOf(symbol) {
|
|
226
221
|
const market = ondoVenueSymbol(symbol);
|
|
@@ -806,30 +801,11 @@ export class OndoAdapter {
|
|
|
806
801
|
}
|
|
807
802
|
// ── Withdraw (capabilities.WithdrawCapable) ──
|
|
808
803
|
/**
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
804
|
+
* Withdrawals are not available over the API (account owner, 2026-10-13): Ondo pays only
|
|
805
|
+
* address-book destinations and refuses API-key auth for adding one
|
|
806
|
+
* (`key_not_allowed_for_action`). Refused before any request — withdraw in the web app.
|
|
812
807
|
*/
|
|
813
|
-
async withdraw(
|
|
814
|
-
|
|
815
|
-
throw new PerpError("INVALID_PARAMS", "Ondo Perps withdraw takes no assetId/routeType (USDC on Ethereum only)");
|
|
816
|
-
}
|
|
817
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(destination)) {
|
|
818
|
-
throw new PerpError("INVALID_PARAMS", `Ondo Perps withdraw destination must be an Ethereum address (got "${destination}")`);
|
|
819
|
-
}
|
|
820
|
-
if (!/^\d+(\.\d+)?$/.test(amount) || !isPositive(amount)) {
|
|
821
|
-
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
822
|
-
}
|
|
823
|
-
// AccountInfo.withdrawalFeeUSD (required in the OpenAPI spec) is reported with the result.
|
|
824
|
-
const account = await this.client.get("/v1/account", null);
|
|
825
|
-
const res = await this.client.post("/v1/withdraw", {
|
|
826
|
-
customer_withdrawal_id: randomUUID(),
|
|
827
|
-
symbol: "USDC",
|
|
828
|
-
network: ONDO_WITHDRAW_NETWORK,
|
|
829
|
-
amount,
|
|
830
|
-
address: destination,
|
|
831
|
-
from: { id: account.accountID, wallet: "margin" },
|
|
832
|
-
});
|
|
833
|
-
return { ...res, withdrawalFeeUSD: account.withdrawalFeeUSD };
|
|
808
|
+
async withdraw(_amount, _destination) {
|
|
809
|
+
throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
|
|
834
810
|
}
|
|
835
811
|
}
|