perp-cli 0.14.3 → 0.15.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 +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-utils.js +5 -5
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +167 -35
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +4 -4
- package/dist/exchanges/aster.js +31 -13
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -12
- package/dist/exchanges/hyperliquid.js +80 -34
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +94 -6
- package/dist/exchanges/interface.js +16 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx adapter — market data (public), account reads (public by address) and trading through the
|
|
3
|
+
* registered, trading-only session signer (EIP-712 VerifyWitness permits; the server submits the
|
|
4
|
+
* on-chain transaction and pays gas).
|
|
5
|
+
*
|
|
6
|
+
* Account address: the signer's account (agent meta), else the master wallet's address (reads
|
|
7
|
+
* only, nothing is signed with it here). Writes need the signer.
|
|
8
|
+
*
|
|
9
|
+
* Order safety:
|
|
10
|
+
* - every refusal (market flags, maintenance, precision, pins) happens before a nonce is
|
|
11
|
+
* allocated or anything is signed;
|
|
12
|
+
* - every permit consumes a fresh (anchor, bit) pair that is persisted BEFORE the send, so a
|
|
13
|
+
* pair is never reused, even when the outcome is unknown;
|
|
14
|
+
* - a client order id is always attached (generated uint64 when the caller gives none);
|
|
15
|
+
* - an unknown send outcome → one read-only reconciliation (permit nonce consumed on-chain? the
|
|
16
|
+
* client order id in open orders / order history?) → a proven result carries notes
|
|
17
|
+
* ["reconciled"]; otherwise ORDER_PENDING ("do NOT resubmit") and the symbol is quarantined.
|
|
18
|
+
* The permit deadline is short (RISEX_PERMIT_TTL_S), so an unproven permit expires quickly.
|
|
19
|
+
*/
|
|
20
|
+
import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
21
|
+
import { quarantineSymbol } from "../../bot/quarantine.js";
|
|
22
|
+
import { alignProtectionPrice, exactUnits, mulDecimal, slippagePrice, subDecimal } from "../precision.js";
|
|
23
|
+
import { newVenueClientOrderId, unsupported, validateLegacyTif, validateOrderRequest, } from "../trading-v2.js";
|
|
24
|
+
import { networkConfig, requireCapabilities, roleUnsupported } from "../venue.js";
|
|
25
|
+
import { parseOrderRef, toLegacyMarketInfo, withExecution, } from "../venue-types.js";
|
|
26
|
+
import { RisexClient, RisexSendUnknown } from "./client.js";
|
|
27
|
+
import { fetchRisexRawMarkets, isListedRisexMarket, risexSymbolOf, toRisexVenueMarkets } from "./public.js";
|
|
28
|
+
import { allocateAccountNonce, assertTradingOnlyOnChain, chainReader, loadSigningContext } from "./session.js";
|
|
29
|
+
import { cancelAllHash, cancelOrderHash, leverageHash, marginModeHash, MARGIN_MODE_CODE, ORDER_TYPE, packOrder, placeOrderHash, SIDE, signPermit, STP_EXPIRE_MAKER, TIF_CODE, } from "./typed-data.js";
|
|
30
|
+
import { compareDecimal, decOrNull, nsToMs, toPriceTicks, toSizeSteps, trimDecimal, UINT64_MAX, wadToDecimal } from "./units.js";
|
|
31
|
+
const MARKETS_TTL_MS = 30_000;
|
|
32
|
+
/** Permit lifetime: long enough for the server to submit, short enough that an unproven permit expires soon. */
|
|
33
|
+
export const RISEX_PERMIT_TTL_S = 120;
|
|
34
|
+
/** How long a successful on-chain trading-only proof is trusted before the next permit re-checks it. */
|
|
35
|
+
export const RISEX_TRADING_ONLY_TTL_MS = 60_000;
|
|
36
|
+
const ORDER_READ_ATTEMPTS = 3;
|
|
37
|
+
const ORDER_READ_DELAY_MS = 700;
|
|
38
|
+
function isRisexMeta(a) {
|
|
39
|
+
return !!a && typeof a === "object" && a.kind === "risex";
|
|
40
|
+
}
|
|
41
|
+
function lower(s) { return s.toLowerCase(); }
|
|
42
|
+
function str(v, what) {
|
|
43
|
+
if (typeof v !== "string" || v === "")
|
|
44
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: missing ${what}`, { exchange: "risex" });
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
function decReq(v, what) {
|
|
48
|
+
const d = decOrNull(v);
|
|
49
|
+
if (d === null)
|
|
50
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: ${what} is not a decimal (${JSON.stringify(v)})`, { exchange: "risex" });
|
|
51
|
+
return trimDecimal(d);
|
|
52
|
+
}
|
|
53
|
+
/** Positive decimal → null for "0" (RISEx publishes "0" for "none", e.g. liquidation_price, avg_price). */
|
|
54
|
+
function positiveOrNull(v) {
|
|
55
|
+
const d = decOrNull(v);
|
|
56
|
+
return d === null || !(Number(d) > 0) ? null : trimDecimal(d);
|
|
57
|
+
}
|
|
58
|
+
export class RisexAdapter {
|
|
59
|
+
name = "risex";
|
|
60
|
+
aliases = ["rise"];
|
|
61
|
+
venue;
|
|
62
|
+
network;
|
|
63
|
+
client;
|
|
64
|
+
deps;
|
|
65
|
+
markets = null;
|
|
66
|
+
constructor(def, network, deps) {
|
|
67
|
+
this.venue = def;
|
|
68
|
+
this.network = network;
|
|
69
|
+
this.client = new RisexClient(networkConfig(def, network).restUrl, def.rateLimit, `risex:${network}`);
|
|
70
|
+
this.deps = deps;
|
|
71
|
+
}
|
|
72
|
+
/** Resolve the account and the trading-only signer from the create context (OWS only). */
|
|
73
|
+
static async create(def, ctx) {
|
|
74
|
+
networkConfig(def, ctx.network);
|
|
75
|
+
const net = ctx.network === "testnet" ? " -n testnet" : "";
|
|
76
|
+
const approveHint = `perp${net} wallet agent approve risex --master <ows-wallet>`;
|
|
77
|
+
if (ctx.readOnly) {
|
|
78
|
+
return new RisexAdapter(def, ctx.network, { account: null, signer: null, meta: null, noSignerReason: null });
|
|
79
|
+
}
|
|
80
|
+
// Master address (for reads): OWS wallet or raw key; nothing is signed with it here.
|
|
81
|
+
let masterAddress = null;
|
|
82
|
+
if (ctx.credentials.owsWallet !== null) {
|
|
83
|
+
const { OwsEvmSigner } = await import("../../signer/ows-evm.js");
|
|
84
|
+
masterAddress = OwsEvmSigner.create(ctx.credentials.owsWallet, ctx.credentials.owsPassphrase ?? "").getAddress();
|
|
85
|
+
}
|
|
86
|
+
else if (ctx.credentials.pk !== null) {
|
|
87
|
+
const { Wallet } = await import("ethers");
|
|
88
|
+
masterAddress = new Wallet(ctx.credentials.pk).address;
|
|
89
|
+
}
|
|
90
|
+
const meta = ctx.noAgent ? null : ctx.credentials.agent;
|
|
91
|
+
// Read-only release (trading role gated): reads need only an account address — the master's, else
|
|
92
|
+
// the one recorded with a stored signer. The signer's key is never opened.
|
|
93
|
+
if (!def.roles.includes("trading")) {
|
|
94
|
+
const recorded = meta !== null && isRisexMeta(meta) && meta.network === ctx.network ? meta.accountAddress : null;
|
|
95
|
+
return new RisexAdapter(def, ctx.network, { account: masterAddress ?? recorded, signer: null, meta: null, noSignerReason: null });
|
|
96
|
+
}
|
|
97
|
+
if (meta === null || !isRisexMeta(meta)) {
|
|
98
|
+
return new RisexAdapter(def, ctx.network, {
|
|
99
|
+
account: masterAddress, signer: null, meta: null,
|
|
100
|
+
noSignerReason: new PerpError("AGENT_NOT_REGISTERED", "No RISEx trading signer is registered on this computer", { exchange: "risex", remediation: approveHint }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let reason = null;
|
|
104
|
+
if (meta.network !== ctx.network) {
|
|
105
|
+
reason = new PerpError("AGENT_NOT_REGISTERED", `RISEx signer "${meta.agentName}" was registered on ${meta.network}, not ${ctx.network}`, { exchange: "risex", remediation: approveHint });
|
|
106
|
+
}
|
|
107
|
+
else if (!(Date.parse(meta.expiresAt) > Date.now())) {
|
|
108
|
+
reason = new PerpError("AGENT_EXPIRED", `RISEx signer "${meta.agentName}" expired at ${meta.expiresAt}`, { exchange: "risex", remediation: `${approveHint} --rotate` });
|
|
109
|
+
}
|
|
110
|
+
else if (meta.permissions?.tradingOnly !== true) {
|
|
111
|
+
reason = new PerpError("POLICY_DENIED", `RISEx signer "${meta.agentName}" is not recorded as restricted to trading-only`, { exchange: "risex", remediation: `${approveHint} --rotate` });
|
|
112
|
+
}
|
|
113
|
+
else if (masterAddress !== null && lower(masterAddress) !== lower(meta.accountAddress)) {
|
|
114
|
+
reason = new PerpError("AGENT_NOT_REGISTERED", `RISEx signer "${meta.agentName}" belongs to ${meta.accountAddress}, not the selected master ${masterAddress}`, { exchange: "risex", remediation: approveHint });
|
|
115
|
+
}
|
|
116
|
+
if (reason !== null) {
|
|
117
|
+
return new RisexAdapter(def, ctx.network, { account: masterAddress ?? meta.accountAddress, signer: null, meta, noSignerReason: reason });
|
|
118
|
+
}
|
|
119
|
+
const { OwsEvmSigner } = await import("../../signer/ows-evm.js");
|
|
120
|
+
const signer = OwsEvmSigner.create(meta.agentWalletName, "");
|
|
121
|
+
if (lower(signer.getAddress()) !== lower(meta.signerAddress)) {
|
|
122
|
+
throw new PerpError("KEY_NOT_FOUND", `OWS wallet "${meta.agentWalletName}" does not hold RISEx signer ${meta.signerAddress}`, { exchange: "risex", remediation: `${approveHint} --rotate` });
|
|
123
|
+
}
|
|
124
|
+
return new RisexAdapter(def, ctx.network, { account: meta.accountAddress, signer, meta, noSignerReason: null });
|
|
125
|
+
}
|
|
126
|
+
// ── Internals ──
|
|
127
|
+
/**
|
|
128
|
+
* Trading capabilities — also the adapter-level release gate: while the trading role is gated
|
|
129
|
+
* (risex-release.ts) this throws CAPABILITY_UNSUPPORTED before any validation, nonce or
|
|
130
|
+
* signature. Read paths never touch it, so the adapter still serves market data / account reads.
|
|
131
|
+
*/
|
|
132
|
+
get caps() {
|
|
133
|
+
return requireCapabilities(this.venue);
|
|
134
|
+
}
|
|
135
|
+
requireAccount() {
|
|
136
|
+
if (this.deps.account === null) {
|
|
137
|
+
const tradingOn = this.venue.roles.includes("trading");
|
|
138
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", `RISEx account reads need an account address: select a master wallet (perp wallet use <name> / --ows <name>)${tradingOn ? " or register a signer" : ""}`, {
|
|
139
|
+
exchange: "risex",
|
|
140
|
+
remediation: tradingOn
|
|
141
|
+
? `perp${this.network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`
|
|
142
|
+
: "Select the wallet that owns the RISEx account: perp wallet use <name> (or pass --ows <name>)",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return this.deps.account;
|
|
146
|
+
}
|
|
147
|
+
requireSigner() {
|
|
148
|
+
requireCapabilities(this.venue); // release gate first: refuses before the signer is looked at
|
|
149
|
+
if (this.deps.signer === null) {
|
|
150
|
+
throw this.deps.noSignerReason ?? new PerpError("AGENT_NOT_REGISTERED", "No RISEx trading signer", {
|
|
151
|
+
exchange: "risex", remediation: "perp wallet agent approve risex --master <ows-wallet>",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { account: this.requireAccount(), signer: this.deps.signer };
|
|
155
|
+
}
|
|
156
|
+
sleep(ms) {
|
|
157
|
+
return this.deps.sleep ? this.deps.sleep(ms) : new Promise(r => setTimeout(r, ms));
|
|
158
|
+
}
|
|
159
|
+
async rawMarkets(fresh) {
|
|
160
|
+
if (!fresh && this.markets && Date.now() - this.markets.at < MARKETS_TTL_MS)
|
|
161
|
+
return this.markets.raw;
|
|
162
|
+
const raw = await fetchRisexRawMarkets(this.client);
|
|
163
|
+
this.markets = { at: Date.now(), raw };
|
|
164
|
+
return raw;
|
|
165
|
+
}
|
|
166
|
+
/** Active market by symbol ("BTC", "BTC/USDC", "BTC-PERP") or market id. */
|
|
167
|
+
async market(symbol, fresh = false) {
|
|
168
|
+
const up = symbol.trim().toUpperCase().replace(/-PERP$/, "");
|
|
169
|
+
const find = (raw) => raw.find(m => isListedRisexMarket(m) && (risexSymbolOf(m) === up || m.config.name.toUpperCase() === up || m.market_id === up));
|
|
170
|
+
let m = find(await this.rawMarkets(fresh));
|
|
171
|
+
if (m === undefined && !fresh)
|
|
172
|
+
m = find(await this.rawMarkets(true));
|
|
173
|
+
if (m === undefined) {
|
|
174
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `RISEx has no active market ${symbol}`, { exchange: "risex", remediation: "List markets: perp -e risex market list" });
|
|
175
|
+
}
|
|
176
|
+
return m;
|
|
177
|
+
}
|
|
178
|
+
async symbolOfId(marketId) {
|
|
179
|
+
const id = String(marketId);
|
|
180
|
+
let m = (await this.rawMarkets(false)).find(x => x.market_id === id);
|
|
181
|
+
if (m === undefined)
|
|
182
|
+
m = (await this.rawMarkets(true)).find(x => x.market_id === id);
|
|
183
|
+
if (m === undefined)
|
|
184
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: unknown market id ${id}`, { exchange: "risex" });
|
|
185
|
+
return risexSymbolOf(m);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The on-chain trading-only proof, re-checked at most every RISEX_TRADING_ONLY_TTL_MS before a
|
|
189
|
+
* permit (long-lived adapters — perp-mcp, bots — must notice a contract upgrade). A failed proof
|
|
190
|
+
* (refusal or RPC error) is never cached.
|
|
191
|
+
*/
|
|
192
|
+
tradingOnlyProof = null;
|
|
193
|
+
async permit(account, signer, actionHash) {
|
|
194
|
+
if (this.tradingOnlyProof === null || Date.now() - this.tradingOnlyProof.at > RISEX_TRADING_ONLY_TTL_MS) {
|
|
195
|
+
const entry = { at: Date.now(), proof: assertTradingOnlyOnChain(this.chainRead(), this.network, account, signer.getAddress()) };
|
|
196
|
+
this.tradingOnlyProof = entry;
|
|
197
|
+
entry.proof.catch(() => { if (this.tradingOnlyProof === entry)
|
|
198
|
+
this.tradingOnlyProof = null; });
|
|
199
|
+
}
|
|
200
|
+
await this.tradingOnlyProof.proof;
|
|
201
|
+
const ctx = await loadSigningContext(this.client, this.network);
|
|
202
|
+
const slot = await allocateAccountNonce(this.client, this.network, account, this.deps.nonceDir);
|
|
203
|
+
const deadline = Math.floor(Date.now() / 1000) + RISEX_PERMIT_TTL_S;
|
|
204
|
+
const permit = await signPermit({ signer, account, router: ctx.router, domain: ctx.domain, actionHash, slot, deadline });
|
|
205
|
+
return { permit, slot };
|
|
206
|
+
}
|
|
207
|
+
chainRead() {
|
|
208
|
+
return this.deps.chain ?? chainReader(this.network);
|
|
209
|
+
}
|
|
210
|
+
quarantine(symbol, reason, remediation) {
|
|
211
|
+
if (this.deps.quarantine)
|
|
212
|
+
return this.deps.quarantine(symbol, reason, remediation);
|
|
213
|
+
// Synchronous: persisted (or logged CRITICAL and held in memory) before the error is thrown.
|
|
214
|
+
quarantineSymbol("risex-adapter", symbol, reason, remediation, m => process.stderr.write(`${m}\n`));
|
|
215
|
+
}
|
|
216
|
+
/** The account reads / permits are for (signer's account, else the master address). */
|
|
217
|
+
accountAddress() {
|
|
218
|
+
return this.requireAccount();
|
|
219
|
+
}
|
|
220
|
+
/** Testnet faucet: 1000 test USDC, once per wallet (also registers the account). Refused on mainnet. */
|
|
221
|
+
async testnetFaucet() {
|
|
222
|
+
requireCapabilities(this.venue); // a write (registers the account): gated with trading
|
|
223
|
+
if (this.network !== "testnet") {
|
|
224
|
+
throw unsupported(this.venue.displayName, "faucet", "the test-USDC faucet exists only on testnet", "Run with -n testnet");
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const res = await this.client.post("/v1/account/deposit", { account: this.requireAccount(), amount: "1000" }, "testnet faucet");
|
|
228
|
+
const h = typeof res.transaction_hash === "string" ? res.transaction_hash : typeof res.tx_hash === "string" ? res.tx_hash : "";
|
|
229
|
+
return { txHash: h === "" ? null : h };
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
if (e instanceof RisexSendUnknown) {
|
|
233
|
+
throw new PerpError("ORDER_PENDING", `RISEx testnet faucet: outcome unknown — ${extractErrorMessage(e.cause)}`, {
|
|
234
|
+
exchange: "risex", remediation: "Check the balance (perp -n testnet -e risex account balance) before requesting again",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ── Market data ──
|
|
241
|
+
async getVenueMarkets() {
|
|
242
|
+
return toRisexVenueMarkets(await this.rawMarkets(true), m => process.stderr.write(`${m}\n`));
|
|
243
|
+
}
|
|
244
|
+
/** Legacy rows plus the venue's market-level order restrictions. */
|
|
245
|
+
async getMarkets() {
|
|
246
|
+
const raw = await this.rawMarkets(true);
|
|
247
|
+
const byId = new Map(raw.map(r => [r.market_id, r]));
|
|
248
|
+
return toRisexVenueMarkets(raw, m => process.stderr.write(`${m}\n`)).map(m => {
|
|
249
|
+
const r = byId.get(m.venueSymbol);
|
|
250
|
+
return { ...toLegacyMarketInfo(m), marketId: m.venueSymbol, reduceOnlyMarket: r.reduce_only, postOnlyMarket: r.post_only, tradable: m.tradable };
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async getOrderbook(symbol) {
|
|
254
|
+
const m = await this.market(symbol);
|
|
255
|
+
const d = await this.client.get("/v1/orderbook", { market_id: m.market_id, limit: 50 });
|
|
256
|
+
const side = (rows, what) => {
|
|
257
|
+
if (!Array.isArray(rows))
|
|
258
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx /v1/orderbook: no ${what}`, { exchange: "risex" });
|
|
259
|
+
return rows.map(r => [decReq(r.price, `${what} price`), decReq(r.quantity, `${what} quantity`)]);
|
|
260
|
+
};
|
|
261
|
+
return { bids: side(d.bids, "bids"), asks: side(d.asks, "asks") };
|
|
262
|
+
}
|
|
263
|
+
async getRecentTrades(symbol, limit = 50) {
|
|
264
|
+
const m = await this.market(symbol);
|
|
265
|
+
const d = await this.client.get(`/v1/markets/id/${m.market_id}/trade-history`, { limit: Math.min(limit, 1000) });
|
|
266
|
+
if (!Array.isArray(d.trades))
|
|
267
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx trade-history: no trades array", { exchange: "risex" });
|
|
268
|
+
const sym = risexSymbolOf(m);
|
|
269
|
+
return d.trades.map(t => {
|
|
270
|
+
const x = t;
|
|
271
|
+
if (x.maker_side !== "BUY" && x.maker_side !== "SELL")
|
|
272
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx trade maker_side ${String(x.maker_side)}`, { exchange: "risex" });
|
|
273
|
+
// The tape side is the taker's: a SELL maker means a buy took it.
|
|
274
|
+
// Public tape carries no fee; "0" follows the legacy ExchangeTrade convention for public trades.
|
|
275
|
+
return { time: nsToMs(str(x.time, "trade time")), symbol: sym, side: x.maker_side === "SELL" ? "buy" : "sell", price: decReq(x.price, "trade price"), size: decReq(x.size, "trade size"), fee: null };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async getFundingHistory(symbol, limit = 100) {
|
|
279
|
+
const m = await this.market(symbol);
|
|
280
|
+
const d = await this.client.get(`/v1/markets/id/${m.market_id}/funding-rate-history`, { limit: Math.min(limit, 1000) });
|
|
281
|
+
if (!Array.isArray(d.records))
|
|
282
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx funding-rate-history: no records", { exchange: "risex" });
|
|
283
|
+
return d.records.map(r => {
|
|
284
|
+
const x = r;
|
|
285
|
+
return { time: nsToMs(str(x.end_time, "funding end_time")), rate: decReq(x.funding_rate, "funding_rate"), price: positiveOrNull(x.index_price) };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async getKlines(symbol, interval, startTime, endTime) {
|
|
289
|
+
const ms = KLINE_INTERVAL_MS[interval];
|
|
290
|
+
if (ms === undefined) {
|
|
291
|
+
throw new PerpError("INVALID_PARAMS", `RISEx: unsupported candle interval "${interval}"`, { remediation: `Use one of: ${Object.keys(KLINE_INTERVAL_MS).join(", ")}` });
|
|
292
|
+
}
|
|
293
|
+
const m = await this.market(symbol);
|
|
294
|
+
const toNs = (t) => (BigInt(Math.floor(t)) * 1000000n).toString();
|
|
295
|
+
const res = await this.client.get(`/v1/markets/id/${m.market_id}/trading-view-data`, {
|
|
296
|
+
interval: (BigInt(ms) * 1000000n).toString(), from: toNs(startTime), to: toNs(endTime),
|
|
297
|
+
});
|
|
298
|
+
const d = res.data;
|
|
299
|
+
if (!Array.isArray(d))
|
|
300
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx trading-view-data: not an array", { exchange: "risex" });
|
|
301
|
+
return d.map(c => {
|
|
302
|
+
const x = c;
|
|
303
|
+
return {
|
|
304
|
+
time: nsToMs(str(x.time, "candle time")),
|
|
305
|
+
open: decReq(x.open, "open"), high: decReq(x.high, "high"), low: decReq(x.low, "low"), close: decReq(x.close, "close"),
|
|
306
|
+
volume: decReq(x.volume, "volume"), quoteVolume: null, trades: null,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// ── Account (public by address) ──
|
|
311
|
+
async portfolio() {
|
|
312
|
+
const d = await this.client.get("/v1/portfolio/details", { account: lower(this.requireAccount()) });
|
|
313
|
+
if (d.summary === null || typeof d.summary !== "object" || !Array.isArray(d.positions)) {
|
|
314
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/portfolio/details: missing summary / positions", { exchange: "risex" });
|
|
315
|
+
}
|
|
316
|
+
return { summary: d.summary, positions: d.positions };
|
|
317
|
+
}
|
|
318
|
+
async getBalance() {
|
|
319
|
+
const { summary: s } = await this.portfolio();
|
|
320
|
+
return {
|
|
321
|
+
equity: decReq(s.total_account_value, "total_account_value"),
|
|
322
|
+
available: decReq(s.free_collateral, "free_collateral"),
|
|
323
|
+
marginUsed: decReq(s.total_initial_margin, "total_initial_margin"),
|
|
324
|
+
unrealizedPnl: decReq(s.total_unrealized_pnl, "total_unrealized_pnl"),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async getPositions() {
|
|
328
|
+
const { positions } = await this.portfolio();
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const p of positions) {
|
|
331
|
+
const size = decReq(p.size, "position size");
|
|
332
|
+
if (Number(size) === 0)
|
|
333
|
+
continue;
|
|
334
|
+
const lev = Number(decReq(p.leverage, "position leverage"));
|
|
335
|
+
out.push({
|
|
336
|
+
symbol: await this.symbolOfId(str(p.market_id, "position market_id")),
|
|
337
|
+
side: size.startsWith("-") ? "short" : "long",
|
|
338
|
+
size: size.replace(/^-/, ""),
|
|
339
|
+
entryPrice: decReq(p.avg_entry_price, "avg_entry_price"),
|
|
340
|
+
markPrice: decReq(p.mark_price, "mark_price"),
|
|
341
|
+
liquidationPrice: positiveOrNull(p.liquidation_price),
|
|
342
|
+
unrealizedPnl: decReq(p.unrealized_pnl, "unrealized_pnl"),
|
|
343
|
+
leverage: lev,
|
|
344
|
+
marginMode: marginModeOf(p.margin_mode),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
async openOrdersRaw(marketId) {
|
|
350
|
+
const d = await this.client.get("/v1/orders/open", { account: lower(this.requireAccount()), market_id: marketId, limit: 1000 });
|
|
351
|
+
if (!Array.isArray(d.orders))
|
|
352
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/orders/open: no orders array", { exchange: "risex" });
|
|
353
|
+
return d.orders;
|
|
354
|
+
}
|
|
355
|
+
async historyOrders(q) {
|
|
356
|
+
const u = new URLSearchParams({ account: lower(this.requireAccount()), limit: String(q.limit ?? 100) });
|
|
357
|
+
if (q.marketId)
|
|
358
|
+
u.set("market_id", q.marketId);
|
|
359
|
+
for (const id of q.orderIds ?? [])
|
|
360
|
+
u.append("order_ids", id);
|
|
361
|
+
const d = await this.client.get(`/v1/orders?${u.toString()}`);
|
|
362
|
+
if (!Array.isArray(d.orders))
|
|
363
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/orders: no orders array", { exchange: "risex" });
|
|
364
|
+
return d.orders;
|
|
365
|
+
}
|
|
366
|
+
async toExchangeOrder(o) {
|
|
367
|
+
return {
|
|
368
|
+
orderId: str(o.id, "order id"),
|
|
369
|
+
symbol: await this.symbolOfId(o.market_id),
|
|
370
|
+
side: o.side === "SELL" ? "sell" : "buy",
|
|
371
|
+
price: decReq(o.price, "order price"),
|
|
372
|
+
size: decReq(o.size, "order size"),
|
|
373
|
+
filled: decReq(o.filled_size, "order filled_size"),
|
|
374
|
+
status: historyStatus(o),
|
|
375
|
+
type: `${String(o.type).toLowerCase()}${o.post_only ? " post-only" : ""}${o.reduce_only ? " reduce-only" : ""}`,
|
|
376
|
+
triggerPrice: null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** On-chain open orders, enriched with the indexer's decimals / fills (one retry for indexer lag). */
|
|
380
|
+
async getOpenOrders() {
|
|
381
|
+
const open = await this.openOrdersRaw(null);
|
|
382
|
+
if (open.length === 0)
|
|
383
|
+
return [];
|
|
384
|
+
const ids = open.map(o => o.order_id);
|
|
385
|
+
const found = new Map();
|
|
386
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
387
|
+
const missing = ids.filter(id => !found.has(id));
|
|
388
|
+
for (let i = 0; i < missing.length; i += 100) {
|
|
389
|
+
for (const h of await this.historyOrders({ orderIds: missing.slice(i, i + 100), limit: 100 }))
|
|
390
|
+
found.set(h.id, h);
|
|
391
|
+
}
|
|
392
|
+
if (ids.every(id => found.has(id)))
|
|
393
|
+
break;
|
|
394
|
+
if (attempt === 0)
|
|
395
|
+
await this.sleep(1_000);
|
|
396
|
+
}
|
|
397
|
+
const stillMissing = ids.filter(id => !found.has(id));
|
|
398
|
+
if (stillMissing.length > 0) {
|
|
399
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: ${stillMissing.length} open order(s) are not indexed yet (${stillMissing.join(", ")})`, {
|
|
400
|
+
exchange: "risex", remediation: "Retry in a few seconds",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return Promise.all(ids.map(id => this.toExchangeOrder(found.get(id))));
|
|
404
|
+
}
|
|
405
|
+
async getOrderHistory(limit = 50) {
|
|
406
|
+
return Promise.all((await this.historyOrders({ limit: Math.min(limit, 1000) })).map(o => this.toExchangeOrder(o)));
|
|
407
|
+
}
|
|
408
|
+
async getTradeHistory(limit = 50) {
|
|
409
|
+
const d = await this.client.get("/v1/trade-history", { account: lower(this.requireAccount()), limit: Math.min(limit, 1000) });
|
|
410
|
+
if (!Array.isArray(d.trades))
|
|
411
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/trade-history: no trades array", { exchange: "risex" });
|
|
412
|
+
return Promise.all(d.trades.map(async (t) => {
|
|
413
|
+
const x = t;
|
|
414
|
+
return {
|
|
415
|
+
time: nsToMs(str(x.time, "fill time")), symbol: await this.symbolOfId(str(x.market_id, "fill market_id")),
|
|
416
|
+
side: x.side === "SELL" ? "sell" : "buy",
|
|
417
|
+
price: decReq(x.price, "fill price"), size: decReq(x.size, "fill size"), fee: decReq(x.fee, "fill fee"),
|
|
418
|
+
};
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
/** `payment` is positive when RECEIVED (RISEx `funding_payment` is positive when paid — negated here). */
|
|
422
|
+
async getFundingPayments(limit = 100) {
|
|
423
|
+
const d = await this.client.get("/v1/account/funding-payments", { account: lower(this.requireAccount()), limit: Math.min(limit, 1000) });
|
|
424
|
+
if (!Array.isArray(d.records))
|
|
425
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx funding-payments: no records", { exchange: "risex" });
|
|
426
|
+
const out = [];
|
|
427
|
+
for (const r of d.records) {
|
|
428
|
+
if (r.entry_type !== undefined && r.entry_type !== "funding")
|
|
429
|
+
continue;
|
|
430
|
+
const paid = decReq(r.funding_payment, "funding_payment");
|
|
431
|
+
out.push({
|
|
432
|
+
time: nsToMs(str(r.block_time, "funding block_time")),
|
|
433
|
+
symbol: await this.symbolOfId(str(r.market_id, "funding market_id")),
|
|
434
|
+
payment: paid.startsWith("-") ? paid.slice(1) : (paid === "0" ? "0" : `-${paid}`),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
// ── TradingV2 ──
|
|
440
|
+
async placeOrder(req) {
|
|
441
|
+
validateOrderRequest(this.venue, this.caps, req);
|
|
442
|
+
if (req.type !== "market" && req.type !== "limit") {
|
|
443
|
+
throw unsupported(this.venue.displayName, "stopOrders", "TP/SL and stop orders are not implemented for RISEx yet", "Use market / limit orders; set protection in the RISEx app");
|
|
444
|
+
}
|
|
445
|
+
const { account, signer } = this.requireSigner();
|
|
446
|
+
const m = await this.market(req.symbol, true);
|
|
447
|
+
const sym = risexSymbolOf(m);
|
|
448
|
+
if (!m.config.unlocked)
|
|
449
|
+
throw new PerpError("ORDER_REJECTED", `RISEx ${sym} is locked for trading`, { exchange: "risex", sent: false });
|
|
450
|
+
if (m.reduce_only && !req.reduceOnly) {
|
|
451
|
+
throw new PerpError("ORDER_REJECTED", `RISEx ${sym} is in reduce-only mode — only reduce-only orders are accepted`, { exchange: "risex", sent: false, remediation: "Close with a reduce-only order, or wait until the market reopens" });
|
|
452
|
+
}
|
|
453
|
+
if (m.post_only && req.tif !== "ALO") {
|
|
454
|
+
throw new PerpError("ORDER_REJECTED", `RISEx ${sym} is in post-only mode — only post-only (ALO) limit orders are accepted`, { exchange: "risex", sent: false, remediation: "Use --tif ALO" });
|
|
455
|
+
}
|
|
456
|
+
const sizeSteps = toSizeSteps(req.size, m.config.step_size);
|
|
457
|
+
const minSteps = toSizeSteps(m.config.min_order_size, m.config.step_size);
|
|
458
|
+
if (sizeSteps < minSteps) {
|
|
459
|
+
throw new PerpError("SIZE_TOO_SMALL", `RISEx ${sym} size ${req.size} is below the minimum ${m.config.min_order_size}`, { exchange: "risex", remediation: `Use a size of at least ${m.config.min_order_size}` });
|
|
460
|
+
}
|
|
461
|
+
let price;
|
|
462
|
+
let referencePrice = null;
|
|
463
|
+
if (req.type === "market") {
|
|
464
|
+
const slip = req.slippage;
|
|
465
|
+
referencePrice = decOrNull(m.mark_price);
|
|
466
|
+
if (referencePrice === null)
|
|
467
|
+
throw new PerpError("PRICE_STALE", `RISEx ${sym}: no mark price to bound the market order`, { exchange: "risex" });
|
|
468
|
+
price = alignProtectionPrice(slippagePrice(referencePrice, req.side, slip.bps), m.config.step_price, req.side);
|
|
469
|
+
// The venue's match band applies to the price actually sent (after tick alignment).
|
|
470
|
+
const band = decOrNull(m.config.match_price_band);
|
|
471
|
+
if (band === null)
|
|
472
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx ${sym}: market config has no match_price_band — nothing was sent`, { exchange: "risex" });
|
|
473
|
+
const dev = subDecimal(price, referencePrice).replace(/^-/, "");
|
|
474
|
+
if (compareDecimal(dev, mulDecimal(band, referencePrice)) > 0) {
|
|
475
|
+
throw new PerpError("INVALID_PARAMS", `slippage ${slip.bps} bps puts the ${sym} limit ${price} outside RISEx's match price band (${band} of mark ${referencePrice})`, {
|
|
476
|
+
exchange: "risex", remediation: `Use a smaller --slippage-bps (the band is ${mulDecimal(band, "10000")} bps)`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
price = req.price;
|
|
482
|
+
}
|
|
483
|
+
const priceTicks = toPriceTicks(price, m.config.step_price);
|
|
484
|
+
const tif = req.type === "market" ? "IOC" : req.tif;
|
|
485
|
+
const tifCode = tif === "ALO" ? TIF_CODE.GTC : TIF_CODE[tif];
|
|
486
|
+
if (tifCode === undefined)
|
|
487
|
+
throw unsupported(this.venue.displayName, "timeInForce", `time-in-force ${tif} is not offered`, `Use one of: ${this.caps.timeInForce.join(", ")}`);
|
|
488
|
+
// The single client-id generator (trading-v2.ts newVenueClientOrderId, crypto randomness).
|
|
489
|
+
const clientOrderId = req.clientOrderId ?? newVenueClientOrderId(this.venue);
|
|
490
|
+
if (!/^[1-9]\d{0,19}$/.test(clientOrderId) || BigInt(clientOrderId) > UINT64_MAX) {
|
|
491
|
+
throw new PerpError("INVALID_PARAMS", `RISEx client order id must be a decimal integer 1..2^64-1 (got "${clientOrderId}")`);
|
|
492
|
+
}
|
|
493
|
+
const fields = {
|
|
494
|
+
marketId: Number(m.market_id), sizeSteps, priceTicks, side: SIDE[req.side], postOnly: tif === "ALO", reduceOnly: req.reduceOnly,
|
|
495
|
+
stpMode: STP_EXPIRE_MAKER, orderType: req.type === "market" ? ORDER_TYPE.market : ORDER_TYPE.limit, tif: tifCode,
|
|
496
|
+
};
|
|
497
|
+
const actionHash = placeOrderHash(packOrder(fields), BigInt(clientOrderId));
|
|
498
|
+
const { permit, slot } = await this.permit(account, signer, actionHash);
|
|
499
|
+
const { logClientId } = await import("../../client-id-tracker.js");
|
|
500
|
+
const now = new Date().toISOString();
|
|
501
|
+
logClientId({ clientOrderId, exchange: "risex", symbol: sym, side: req.side, size: req.size, price, type: req.type, status: "pending", createdAt: now, updatedAt: now });
|
|
502
|
+
const exec = {
|
|
503
|
+
slippageBps: req.slippage?.bps ?? null, slippageSource: req.slippage?.source ?? null,
|
|
504
|
+
protectionPrice: req.type === "market" ? price : null, referencePrice,
|
|
505
|
+
tifRequested: req.tif, tifSent: tif === "ALO" ? "GTC+post_only" : tif,
|
|
506
|
+
};
|
|
507
|
+
const method = req.type === "market" ? "protectedIoc" : "limit";
|
|
508
|
+
let res;
|
|
509
|
+
try {
|
|
510
|
+
res = await this.client.post("/v1/orders/place", {
|
|
511
|
+
market_id: fields.marketId, size_steps: Number(sizeSteps), price_ticks: Number(priceTicks), side: fields.side,
|
|
512
|
+
post_only: fields.postOnly, reduce_only: fields.reduceOnly, stp_mode: fields.stpMode, order_type: fields.orderType,
|
|
513
|
+
time_in_force: fields.tif, builder_id: 0, client_order_id: clientOrderId, ttl_units: 0, no_retry: true, permit,
|
|
514
|
+
}, "place order");
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
if (!(e instanceof RisexSendUnknown)) {
|
|
518
|
+
logClientId({ clientOrderId, exchange: "risex", symbol: sym, side: req.side, size: req.size, price, type: req.type, status: "failed", createdAt: now, updatedAt: new Date().toISOString() });
|
|
519
|
+
throw e;
|
|
520
|
+
}
|
|
521
|
+
return this.reconcilePlace(sym, m.market_id, clientOrderId, slot, account, e, method, exec);
|
|
522
|
+
}
|
|
523
|
+
const orderId = typeof res.order_id === "string" && res.order_id !== "" ? res.order_id : null;
|
|
524
|
+
logClientId({ clientOrderId, exchange: "risex", symbol: sym, side: req.side, size: req.size, price, type: req.type, ...(orderId !== null ? { exchangeOrderId: orderId } : {}), status: "submitted", createdAt: now, updatedAt: new Date().toISOString() });
|
|
525
|
+
const notes = [`block ${String(res.block_number)}`];
|
|
526
|
+
if (tif === "IOC" || tif === "FOK") {
|
|
527
|
+
const filledRaw = typeof res.filled_quantity === "string" ? res.filled_quantity : "";
|
|
528
|
+
const filledSize = filledRaw === "" ? null : wadToDecimal(filledRaw);
|
|
529
|
+
const filledSteps = filledSize === null ? null : exactUnits(filledSize, m.config.step_size);
|
|
530
|
+
const status = filledSize === null ? "accepted"
|
|
531
|
+
: filledSteps === 0n ? "cancelled"
|
|
532
|
+
: filledSteps !== null && filledSteps >= sizeSteps ? "filled" : "partiallyFilled";
|
|
533
|
+
if (status === "partiallyFilled")
|
|
534
|
+
notes.push(`${tif}: the unfilled remainder was cancelled`);
|
|
535
|
+
return withExecution({
|
|
536
|
+
venue: "risex", symbol: sym, status, finality: filledSize === null ? "pending" : "final", orderId, clientOrderId,
|
|
537
|
+
filledSize, avgPrice: null, venueRaw: res,
|
|
538
|
+
}, method, { ...exec, notes });
|
|
539
|
+
}
|
|
540
|
+
// Resting TIF: read the order back (status / immediate fills).
|
|
541
|
+
if (orderId !== null) {
|
|
542
|
+
for (let i = 0; i < ORDER_READ_ATTEMPTS; i++) {
|
|
543
|
+
try {
|
|
544
|
+
const r = await this.getOrderStatus(sym, { kind: "orderId", value: orderId });
|
|
545
|
+
return { ...r, clientOrderId, execution: { ...r.execution, ...exec, method, notes: [...notes, ...r.execution.notes] } };
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
if (!(e instanceof PerpError && e.structured.code === "ORDER_NOT_FOUND"))
|
|
549
|
+
break;
|
|
550
|
+
await this.sleep(ORDER_READ_DELAY_MS);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return withExecution({
|
|
555
|
+
venue: "risex", symbol: sym, status: "accepted", finality: "pending", orderId, clientOrderId, filledSize: null, avgPrice: null, venueRaw: res,
|
|
556
|
+
}, method, { ...exec, notes: [...notes, "included on-chain; status not read back yet"] });
|
|
557
|
+
}
|
|
558
|
+
/** One read-only reconciliation of an unknown place outcome. */
|
|
559
|
+
async reconcilePlace(sym, marketId, clientOrderId, slot, account, cause, method, exec) {
|
|
560
|
+
const statusHint = `perp -e risex${this.network === "testnet" ? " -n testnet" : ""} trade status c:${clientOrderId} --symbol ${sym}`;
|
|
561
|
+
let consumed = null;
|
|
562
|
+
let reconcileError = null;
|
|
563
|
+
try {
|
|
564
|
+
consumed = await this.chainRead().isNonceUsed(account, slot);
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
consumed = null;
|
|
568
|
+
reconcileError = `nonce read: ${extractErrorMessage(e)}`;
|
|
569
|
+
}
|
|
570
|
+
if (consumed === true) {
|
|
571
|
+
try {
|
|
572
|
+
const r = await this.getOrderStatus(sym, { kind: "clientOrderId", value: clientOrderId });
|
|
573
|
+
return { ...r, execution: { ...r.execution, ...exec, method, notes: [...r.execution.notes, "reconciled"] } };
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
reconcileError = `order read: ${extractErrorMessage(e)}`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const remediation = `Do NOT resubmit — the order may have been placed (client id ${clientOrderId}). Check: ${statusHint}. Its permit expires ${RISEX_PERMIT_TTL_S}s after signing`;
|
|
580
|
+
this.quarantine(sym, `RISEx order c:${clientOrderId} outcome unknown (${extractErrorMessage(cause.cause)})`, remediation);
|
|
581
|
+
throw new PerpError("ORDER_PENDING", `RISEx place order: outcome unknown — ${extractErrorMessage(cause.cause)}`, {
|
|
582
|
+
exchange: "risex", clientOrderId, nonce: { anchor: slot.anchor.toString(), bit: slot.bit }, nonceConsumed: consumed, reconcileError, remediation,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async amendOrder(_req) {
|
|
586
|
+
throw unsupported(this.venue.displayName, "amend", "RISEx has no order amend", "Cancel the order and place a new one");
|
|
587
|
+
}
|
|
588
|
+
async cancelByRef(symbol, ref) {
|
|
589
|
+
const { account, signer } = this.requireSigner();
|
|
590
|
+
const m = await this.market(symbol);
|
|
591
|
+
const open = await this.openOrdersRaw(m.market_id);
|
|
592
|
+
const o = open.find(x => (ref.kind === "orderId" ? lower(x.order_id) === lower(ref.value) : x.client_order_id === ref.value));
|
|
593
|
+
if (o === undefined) {
|
|
594
|
+
throw new PerpError("ORDER_NOT_FOUND", `RISEx: no open ${risexSymbolOf(m)} order ${ref.kind === "clientOrderId" ? "c:" : ""}${ref.value}`, {
|
|
595
|
+
exchange: "risex", remediation: `List open orders: perp -e risex account orders`,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const { permit } = await this.permit(account, signer, cancelOrderHash(Number(m.market_id), BigInt(o.resting_order_id)));
|
|
599
|
+
try {
|
|
600
|
+
const res = await this.client.post("/v1/orders/cancel", { market_id: Number(m.market_id), order_id: o.order_id, permit }, "cancel order");
|
|
601
|
+
return res.success === true
|
|
602
|
+
? { venue: "risex", cancelled: [{ kind: "orderId", value: o.order_id }], failed: [], venueRaw: res }
|
|
603
|
+
: { venue: "risex", cancelled: [], failed: [{ ref, error: `venue answered success=${String(res.success)}` }], venueRaw: res };
|
|
604
|
+
}
|
|
605
|
+
catch (e) {
|
|
606
|
+
if (!(e instanceof RisexSendUnknown))
|
|
607
|
+
throw e;
|
|
608
|
+
return {
|
|
609
|
+
venue: "risex", cancelled: [], failed: [],
|
|
610
|
+
pending: [{ ref, reason: extractErrorMessage(e.cause), remediation: "Do NOT resend blindly — check perp -e risex account orders" }],
|
|
611
|
+
venueRaw: null,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async cancelAllV2(symbol) {
|
|
616
|
+
const { account, signer } = this.requireSigner();
|
|
617
|
+
const before = await this.openOrdersRaw(symbol === null ? null : (await this.market(symbol)).market_id);
|
|
618
|
+
const marketIds = [...new Set(before.map(o => String(o.market_id)))];
|
|
619
|
+
const result = { venue: "risex", cancelled: [], failed: [], pending: [], venueRaw: [] };
|
|
620
|
+
for (const id of marketIds) {
|
|
621
|
+
const refs = before.filter(o => String(o.market_id) === id).map(o => ({ kind: "orderId", value: o.order_id }));
|
|
622
|
+
const { permit } = await this.permit(account, signer, cancelAllHash(Number(id)));
|
|
623
|
+
try {
|
|
624
|
+
const res = await this.client.post("/v1/orders/cancel-all", { market_id: Number(id), permit }, "cancel all");
|
|
625
|
+
result.venueRaw.push(res);
|
|
626
|
+
if (res.success !== true) {
|
|
627
|
+
for (const r of refs)
|
|
628
|
+
result.failed.push({ ref: r, error: `cancel-all answered success=${String(res.success)}` });
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const still = new Set((await this.openOrdersRaw(id)).map(o => lower(o.order_id)));
|
|
632
|
+
for (const r of refs) {
|
|
633
|
+
if (still.has(lower(r.value)))
|
|
634
|
+
result.pending.push({ ref: r, reason: "still listed after cancel-all", remediation: "Check perp -e risex account orders" });
|
|
635
|
+
else
|
|
636
|
+
result.cancelled.push(r);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
if (!(e instanceof RisexSendUnknown)) {
|
|
641
|
+
for (const r of refs)
|
|
642
|
+
result.failed.push({ ref: r, error: extractErrorMessage(e) });
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
for (const r of refs)
|
|
646
|
+
result.pending.push({ ref: r, reason: extractErrorMessage(e.cause), remediation: "Do NOT resend blindly — check perp -e risex account orders" });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
async applyLeverage(symbol, leverage, marginMode) {
|
|
652
|
+
const { account, signer } = this.requireSigner();
|
|
653
|
+
const m = await this.market(symbol, true);
|
|
654
|
+
const max = Number(decReq(m.config.max_leverage, "max_leverage"));
|
|
655
|
+
if (!Number.isInteger(max) || max < 1)
|
|
656
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx ${risexSymbolOf(m)}: max_leverage "${m.config.max_leverage}" is not a positive integer — nothing was sent`, { exchange: "risex" });
|
|
657
|
+
if (!Number.isInteger(leverage) || leverage < 1 || leverage > max) {
|
|
658
|
+
throw new PerpError("INVALID_PARAMS", `RISEx ${risexSymbolOf(m)} leverage must be an integer 1..${max} (got ${leverage})`, { exchange: "risex" });
|
|
659
|
+
}
|
|
660
|
+
// The on-chain position slot of this market (also for a market never traded: size 0 + its mode).
|
|
661
|
+
const pos = await this.client.get("/v1/account/position", { account: lower(account), market_id: m.market_id });
|
|
662
|
+
if (pos.position === undefined || pos.position === null)
|
|
663
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/account/position: no position object", { exchange: "risex" });
|
|
664
|
+
const hasPosition = Number(decReq(pos.position.size, "position size")) !== 0;
|
|
665
|
+
const currentMode = marginModeOf(pos.position.margin_mode);
|
|
666
|
+
const raw = [];
|
|
667
|
+
if (marginMode !== null && marginMode !== currentMode) {
|
|
668
|
+
if (hasPosition) {
|
|
669
|
+
throw new PerpError("INVALID_PARAMS", `RISEx cannot switch ${risexSymbolOf(m)} to ${marginMode} margin while a position is open`, {
|
|
670
|
+
exchange: "risex", remediation: "Close the position first",
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const { permit } = await this.permit(account, signer, marginModeHash(Number(m.market_id), MARGIN_MODE_CODE[marginMode]));
|
|
674
|
+
raw.push(await this.sendAccountAction("/v1/account/margin-mode", { market_id: m.market_id, margin_mode: MARGIN_MODE_CODE[marginMode], permit_params: permit }, "margin mode"));
|
|
675
|
+
}
|
|
676
|
+
const { permit } = await this.permit(account, signer, leverageHash(Number(m.market_id), leverage));
|
|
677
|
+
raw.push(await this.sendAccountAction("/v1/account/leverage", { market_id: m.market_id, leverage: String(leverage), permit_params: permit }, "leverage"));
|
|
678
|
+
return { venue: "risex", symbol: risexSymbolOf(m), leverage, marginMode: marginMode ?? currentMode, venueRaw: raw };
|
|
679
|
+
}
|
|
680
|
+
async sendAccountAction(path, body, what) {
|
|
681
|
+
let res;
|
|
682
|
+
try {
|
|
683
|
+
res = await this.client.post(path, body, what);
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
if (e instanceof RisexSendUnknown) {
|
|
687
|
+
throw new PerpError("ORDER_PENDING", `RISEx ${what} update: outcome unknown — ${extractErrorMessage(e.cause)}`, {
|
|
688
|
+
exchange: "risex", remediation: "Do NOT resubmit blindly — check perp -e risex account positions / settings first",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
throw e;
|
|
692
|
+
}
|
|
693
|
+
if (res.receipt !== undefined && res.receipt.status !== 1) {
|
|
694
|
+
throw new PerpError("ACTION_REJECTED", `RISEx ${what} update reverted (receipt status ${String(res.receipt.status)})`, { exchange: "risex" });
|
|
695
|
+
}
|
|
696
|
+
return res;
|
|
697
|
+
}
|
|
698
|
+
async getOrderStatus(symbol, ref) {
|
|
699
|
+
const m = await this.market(symbol);
|
|
700
|
+
const sym = risexSymbolOf(m);
|
|
701
|
+
let o;
|
|
702
|
+
if (ref.kind === "orderId") {
|
|
703
|
+
try {
|
|
704
|
+
const d = await this.client.get(`/v1/orders/by-id/${encodeURIComponent(ref.value)}`, { market_id: m.market_id });
|
|
705
|
+
o = d.order;
|
|
706
|
+
}
|
|
707
|
+
catch (e) {
|
|
708
|
+
if (!(e instanceof PerpError && e.structured.details?.status === 404))
|
|
709
|
+
throw e;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
o = (await this.historyOrders({ marketId: m.market_id, limit: 200 })).find(x => x.client_order_id === ref.value);
|
|
714
|
+
if (o === undefined) {
|
|
715
|
+
const open = (await this.openOrdersRaw(m.market_id)).find(x => x.client_order_id === ref.value);
|
|
716
|
+
if (open !== undefined) {
|
|
717
|
+
return withExecution({
|
|
718
|
+
venue: "risex", symbol: sym, status: "open", finality: "final", orderId: open.order_id, clientOrderId: ref.value,
|
|
719
|
+
filledSize: null, avgPrice: null, venueRaw: open,
|
|
720
|
+
}, "read", { notes: ["on-chain open order (not indexed yet)"] });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (o === undefined || o === null) {
|
|
725
|
+
throw new PerpError("ORDER_NOT_FOUND", `RISEx: order ${ref.kind === "clientOrderId" ? "c:" : ""}${ref.value} not found on ${sym}`, { exchange: "risex" });
|
|
726
|
+
}
|
|
727
|
+
const status = historyStatus(o);
|
|
728
|
+
const filled = decOrNull(o.filled_size);
|
|
729
|
+
return withExecution({
|
|
730
|
+
venue: "risex", symbol: sym, status, finality: "final", orderId: o.id,
|
|
731
|
+
clientOrderId: typeof o.client_order_id === "string" && o.client_order_id !== "" && o.client_order_id !== "0" ? o.client_order_id : null,
|
|
732
|
+
filledSize: filled === null ? null : trimDecimal(filled), avgPrice: positiveOrNull(o.avg_price), venueRaw: o,
|
|
733
|
+
}, "read", { tifSent: o.time_in_force ?? null });
|
|
734
|
+
}
|
|
735
|
+
// ── Legacy trading shims ──
|
|
736
|
+
async marketOrder(symbol, side, size, opts) {
|
|
737
|
+
return this.placeOrder({
|
|
738
|
+
symbol, side, type: "market", size, price: null, triggerPrice: null, triggerSource: null, tif: null,
|
|
739
|
+
reduceOnly: opts.reduceOnly, clientOrderId: opts.clientOrderId ?? null, slippage: opts.slippage, tpsl: null,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async limitOrder(symbol, side, price, size, opts) {
|
|
743
|
+
const tif = opts?.tif !== undefined ? validateLegacyTif(this.venue, this.caps, opts.tif) : "GTC";
|
|
744
|
+
return this.placeOrder({
|
|
745
|
+
symbol, side, type: "limit", size, price, triggerPrice: null, triggerSource: null, tif,
|
|
746
|
+
reduceOnly: opts?.reduceOnly === true, clientOrderId: opts?.clientOrderId ?? null, slippage: null, tpsl: null,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
async editOrder() {
|
|
750
|
+
return this.amendOrder({ symbol: "", ref: { kind: "orderId", value: "" }, price: "0", size: "0", tif: null });
|
|
751
|
+
}
|
|
752
|
+
async cancelOrder(symbol, orderId) {
|
|
753
|
+
return this.cancelByRef(symbol, parseOrderRef(orderId));
|
|
754
|
+
}
|
|
755
|
+
async cancelAllOrders(symbol) {
|
|
756
|
+
return this.cancelAllV2(symbol ?? null);
|
|
757
|
+
}
|
|
758
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
759
|
+
return this.applyLeverage(symbol, leverage, marginMode ?? null);
|
|
760
|
+
}
|
|
761
|
+
async stopOrder() {
|
|
762
|
+
throw unsupported(this.venue.displayName, "stopOrders", "TP/SL and stop orders are not implemented for RISEx yet", "Set TP/SL in the RISEx app");
|
|
763
|
+
}
|
|
764
|
+
/** TP/SL entries are refused before the entry is sent (not implemented yet). */
|
|
765
|
+
stopOrderGate() {
|
|
766
|
+
return unsupported(this.venue.displayName, "stopOrders", "TP/SL and stop orders are not implemented for RISEx yet", "Set TP/SL in the RISEx app");
|
|
767
|
+
}
|
|
768
|
+
/** Bots / TWAP / grid / funding-arb legs stay off until RISEx trading is verified on mainnet. */
|
|
769
|
+
automationGate() {
|
|
770
|
+
if (!this.venue.roles.includes("trading"))
|
|
771
|
+
return roleUnsupported(this.venue, "automation");
|
|
772
|
+
return unsupported(this.venue.displayName, "automation", "unattended trading on RISEx is not enabled until its trading path is verified on mainnet (and it has no TP/SL yet)", "Trade RISEx manually with perp trade …");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** RISEx margin_mode code → mode; anything else is an error, never a default. */
|
|
776
|
+
function marginModeOf(v) {
|
|
777
|
+
if (v === 0)
|
|
778
|
+
return "cross";
|
|
779
|
+
if (v === 1)
|
|
780
|
+
return "isolated";
|
|
781
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: unknown margin_mode ${JSON.stringify(v)}`, { exchange: "risex" });
|
|
782
|
+
}
|
|
783
|
+
function historyStatus(o) {
|
|
784
|
+
const filled = Number(decReq(o.filled_size, "order filled_size"));
|
|
785
|
+
switch (o.status) {
|
|
786
|
+
case "ORDER_STATUS_OPEN": return filled > 0 ? "partiallyFilled" : "open";
|
|
787
|
+
case "ORDER_STATUS_FILLED": return "filled";
|
|
788
|
+
case "ORDER_STATUS_CANCELLED": return "cancelled";
|
|
789
|
+
default:
|
|
790
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx: unknown order status ${String(o.status)}`, { exchange: "risex" });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** Candle intervals → ms (the venue takes the interval in ns). */
|
|
794
|
+
export const KLINE_INTERVAL_MS = {
|
|
795
|
+
"1m": 60_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_000,
|
|
796
|
+
};
|