perp-cli 0.14.2 → 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 +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- 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 +23 -12
- 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 +168 -36
- 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/dashboard/ui.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 +9 -4
- package/dist/exchanges/aster.js +125 -55
- 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 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- 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.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- 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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx REST transport.
|
|
3
|
+
*
|
|
4
|
+
* Every response is `{data, request_id}` on success or `{error:{code, message}, request_id}` on
|
|
5
|
+
* failure, where `code` is a gRPC status name ("InvalidArgument", "FailedPrecondition",
|
|
6
|
+
* "Unauthenticated", "ResourceExhausted", "Internal", "Unavailable", …) and the message may carry a
|
|
7
|
+
* decoded contract revert ("FailedPrecondition: PlaceOrder reverted: SignerNotAuthorized(address)").
|
|
8
|
+
*
|
|
9
|
+
* Reads (GET) map to EXCHANGE_UNREACHABLE / EXCHANGE_ERROR / RATE_LIMITED. Writes (POST) are
|
|
10
|
+
* NEVER retried and classify their outcome:
|
|
11
|
+
* - no connection at all → not sent (definite, `sent: false`)
|
|
12
|
+
* - 4xx gRPC rejection (InvalidArgument, FailedPrecondition, Unauthenticated, NotFound,
|
|
13
|
+
* PermissionDenied, OutOfRange, AlreadyExists) → definite rejection (`sent: false` /
|
|
14
|
+
* ORDER_REJECTED) with the decoded error name
|
|
15
|
+
* - 429 ResourceExhausted → RATE_LIMITED, definite (`sent: false`)
|
|
16
|
+
* - timeout, reset, 5xx (E0701 ChainTransactionFailed, E0702 SequencerUnavailable, …),
|
|
17
|
+
* an unreadable body → outcome UNKNOWN (RisexSendUnknown)
|
|
18
|
+
*/
|
|
19
|
+
import { PerpError, extractErrorMessage, isConnectFailure } from "../../errors.js";
|
|
20
|
+
import { acquireRate, rateLimitedError } from "../rate-limiter.js";
|
|
21
|
+
export const RISEX_HTTP_TIMEOUT_MS = 30_000;
|
|
22
|
+
/** gRPC codes that are a definite refusal of the request (nothing executed). */
|
|
23
|
+
const DEFINITE_REJECT_CODES = new Set([
|
|
24
|
+
"InvalidArgument", "FailedPrecondition", "Unauthenticated", "NotFound", "PermissionDenied", "OutOfRange", "AlreadyExists",
|
|
25
|
+
]);
|
|
26
|
+
/** Decoded contract error name in a revert message ("… reverted: InvalidNonceIndex(uint8) => (208)" → "InvalidNonceIndex"). */
|
|
27
|
+
export function revertNameOf(message) {
|
|
28
|
+
const m = /reverted:\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(message);
|
|
29
|
+
return m ? m[1] : null;
|
|
30
|
+
}
|
|
31
|
+
/** The send of a write could not be classified: it may have executed. Carries the cause for the caller's reconciliation. */
|
|
32
|
+
export class RisexSendUnknown extends Error {
|
|
33
|
+
what;
|
|
34
|
+
cause;
|
|
35
|
+
status;
|
|
36
|
+
constructor(what, cause, status) {
|
|
37
|
+
super(`${what}: outcome unknown — ${extractErrorMessage(cause)}`);
|
|
38
|
+
this.what = what;
|
|
39
|
+
this.cause = cause;
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.name = "RisexSendUnknown";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function errorBodyOf(parsed) {
|
|
45
|
+
const e = parsed?.error;
|
|
46
|
+
if (e === null || typeof e !== "object")
|
|
47
|
+
return null;
|
|
48
|
+
const code = e.code;
|
|
49
|
+
const message = e.message;
|
|
50
|
+
return { code: typeof code === "string" ? code : "Unknown", message: typeof message === "string" ? message : JSON.stringify(e) };
|
|
51
|
+
}
|
|
52
|
+
function readError(path, status, body, text) {
|
|
53
|
+
const msg = body !== null ? `${body.code}: ${body.message}` : `HTTP ${status} ${text.slice(0, 200)}`;
|
|
54
|
+
if (status === 404 || body?.code === "NotFound") {
|
|
55
|
+
return new PerpError("EXCHANGE_ERROR", `RISEx ${path}: ${msg}`, { exchange: "risex", status });
|
|
56
|
+
}
|
|
57
|
+
if (body?.code === "InvalidArgument") {
|
|
58
|
+
return new PerpError("INVALID_PARAMS", `RISEx ${path}: ${msg}`, { exchange: "risex", status });
|
|
59
|
+
}
|
|
60
|
+
if (status >= 500 || body?.code === "Unavailable" || body?.code === "Internal") {
|
|
61
|
+
return new PerpError("EXCHANGE_UNREACHABLE", `RISEx ${path}: ${msg}`, { exchange: "risex", status });
|
|
62
|
+
}
|
|
63
|
+
return new PerpError("EXCHANGE_ERROR", `RISEx ${path}: ${msg}`, { exchange: "risex", status });
|
|
64
|
+
}
|
|
65
|
+
export class RisexClient {
|
|
66
|
+
restUrl;
|
|
67
|
+
rateLimit;
|
|
68
|
+
rateKey;
|
|
69
|
+
constructor(restUrl, rateLimit, rateKey) {
|
|
70
|
+
this.restUrl = restUrl;
|
|
71
|
+
this.rateLimit = rateLimit;
|
|
72
|
+
this.rateKey = rateKey;
|
|
73
|
+
}
|
|
74
|
+
url(path, query) {
|
|
75
|
+
const u = new URL(this.restUrl + path);
|
|
76
|
+
if (query)
|
|
77
|
+
for (const [k, v] of Object.entries(query))
|
|
78
|
+
if (v !== null && v !== undefined)
|
|
79
|
+
u.searchParams.set(k, String(v));
|
|
80
|
+
return u.toString();
|
|
81
|
+
}
|
|
82
|
+
/** GET → `data`. */
|
|
83
|
+
async get(path, query) {
|
|
84
|
+
await acquireRate(this.rateKey, this.rateLimit, path);
|
|
85
|
+
let res;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetch(this.url(path, query), { method: "GET", signal: AbortSignal.timeout(RISEX_HTTP_TIMEOUT_MS) });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `RISEx ${path}: ${extractErrorMessage(e)}`, { exchange: "risex" });
|
|
91
|
+
}
|
|
92
|
+
let text;
|
|
93
|
+
try {
|
|
94
|
+
text = await res.text();
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `RISEx ${path}: response body unreadable (${extractErrorMessage(e)})`, { exchange: "risex" });
|
|
98
|
+
}
|
|
99
|
+
if (res.status === 429)
|
|
100
|
+
throw rateLimitedError("risex", res.headers.get("retry-after"), text);
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
parsed = undefined;
|
|
107
|
+
}
|
|
108
|
+
const err = errorBodyOf(parsed);
|
|
109
|
+
if (!res.ok || err !== null)
|
|
110
|
+
throw readError(path, res.status, err, text);
|
|
111
|
+
const data = parsed?.data;
|
|
112
|
+
if (data === undefined || data === null) {
|
|
113
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx ${path}: response has no data`, { exchange: "risex", status: res.status });
|
|
114
|
+
}
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* POST a write. Returns `data` on success. Definite refusals throw a PerpError with
|
|
119
|
+
* `details.sent === false`; anything that may have executed throws RisexSendUnknown.
|
|
120
|
+
*/
|
|
121
|
+
async post(path, body, what) {
|
|
122
|
+
await acquireRate(this.rateKey, this.rateLimit, path);
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetch(this.url(path), {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify(body),
|
|
129
|
+
signal: AbortSignal.timeout(RISEX_HTTP_TIMEOUT_MS),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
if (isConnectFailure(e)) {
|
|
134
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `RISEx ${what}: could not connect — nothing was sent`, { exchange: "risex", sent: false });
|
|
135
|
+
}
|
|
136
|
+
throw new RisexSendUnknown(`RISEx ${what}`, e, null);
|
|
137
|
+
}
|
|
138
|
+
let text;
|
|
139
|
+
try {
|
|
140
|
+
text = await res.text();
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
throw new RisexSendUnknown(`RISEx ${what}`, e, res.status);
|
|
144
|
+
}
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(text);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
parsed = undefined;
|
|
151
|
+
}
|
|
152
|
+
const err = errorBodyOf(parsed);
|
|
153
|
+
if (res.status === 429 || err?.code === "ResourceExhausted") {
|
|
154
|
+
const r = rateLimitedError("risex", res.headers.get("retry-after"), text);
|
|
155
|
+
throw new PerpError("RATE_LIMITED", `RISEx ${what} refused: ${err !== null ? err.message : "rate limited"}`, {
|
|
156
|
+
...(r.structured.details ?? {}), exchange: "risex", sent: false,
|
|
157
|
+
remediation: "Nothing was executed. Wait, then retry (RISEx also applies a per-address transaction quota: +1 per $5 traded)",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (err !== null && res.status >= 400 && res.status < 500 && DEFINITE_REJECT_CODES.has(err.code)) {
|
|
161
|
+
const revert = revertNameOf(err.message);
|
|
162
|
+
throw new PerpError("ORDER_REJECTED", `RISEx ${what} rejected: ${err.code}: ${err.message}`, {
|
|
163
|
+
exchange: "risex", sent: false, grpcCode: err.code, ...(revert !== null ? { revert } : {}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (!res.ok || err !== null || parsed === undefined) {
|
|
167
|
+
throw new RisexSendUnknown(`RISEx ${what}`, new Error(err !== null ? `${err.code}: ${err.message}` : `HTTP ${res.status} ${text.slice(0, 200)}`), res.status);
|
|
168
|
+
}
|
|
169
|
+
const data = parsed.data;
|
|
170
|
+
if (data === undefined || data === null)
|
|
171
|
+
throw new RisexSendUnknown(`RISEx ${what}`, new Error("response has no data"), res.status);
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx permit nonces: (anchor uint48, bit 0..207) pairs, each consumable once on-chain.
|
|
3
|
+
*
|
|
4
|
+
* Allocation (one per signed action), under an exclusive per-account file lock:
|
|
5
|
+
* remote = GET /v1/nonce-state/{account} → nonce_anchor A
|
|
6
|
+
* base = (A + 1, bit 0) — "sign with nonce_anchor + 1 and start at bit 0"
|
|
7
|
+
* local = last pair THIS machine handed out for the account (~/.perp/risex-nonce/…)
|
|
8
|
+
* next = local successor when local ≥ base (bit + 1; after bit 207 the next anchor), else base
|
|
9
|
+
* The contract accepts only the current anchor or current + 1, so a successor beyond A + 1 (208
|
|
10
|
+
* permits signed on A + 1 while the chain has not caught up) is refused, never guessed.
|
|
11
|
+
* `current_bitmap_index` describes the CURRENT anchor A, which perp-cli deliberately never signs on
|
|
12
|
+
* (developer.rise.trade "Integration" → Nonces: "Sign with nonce_anchor + 1 and start at bit 0 …
|
|
13
|
+
* starting a fresh anchor guarantees you cannot collide with a permit already in flight"), so it
|
|
14
|
+
* does not enter the rule. Gaps in a bitmap are harmless (same doc).
|
|
15
|
+
*
|
|
16
|
+
* The allocated pair is persisted BEFORE the caller signs or sends: a pair is handed out once and
|
|
17
|
+
* never reused, even when the send fails or its outcome is unknown (an unknown outcome burns the
|
|
18
|
+
* bit). Restart-safe: the next allocation takes the max of the chain state and the local file.
|
|
19
|
+
* The state file holds no secrets.
|
|
20
|
+
*/
|
|
21
|
+
import type { Network } from "../venue-types.js";
|
|
22
|
+
import type { NonceSlot } from "./typed-data.js";
|
|
23
|
+
export declare const RISEX_MAX_BIT = 207;
|
|
24
|
+
export interface RemoteNonceState {
|
|
25
|
+
anchor: bigint;
|
|
26
|
+
currentBitmapIndex: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function risexNonceDir(): string;
|
|
29
|
+
/** Pure allocation rule (see module doc). */
|
|
30
|
+
export declare function nextSlot(remote: RemoteNonceState, local: NonceSlot | null): NonceSlot;
|
|
31
|
+
export declare function parseRemoteNonceState(data: unknown): RemoteNonceState;
|
|
32
|
+
/**
|
|
33
|
+
* Allocate one nonce pair for `account` and persist it before returning (burned from here on).
|
|
34
|
+
* `fetchRemote` reads /v1/nonce-state inside the lock.
|
|
35
|
+
*/
|
|
36
|
+
export declare function allocateNonce(net: Network, account: string, fetchRemote: () => Promise<RemoteNonceState>, dir?: string): Promise<NonceSlot>;
|
|
37
|
+
/** Last pair handed out on this machine (diagnostics / reconciliation), or null. */
|
|
38
|
+
export declare function lastAllocated(net: Network, account: string, dir?: string): NonceSlot | null;
|
|
39
|
+
/** Whether (anchor, bit) is consumed according to /v1/nonce-state's `bitmap` (only when the anchor matches). null = cannot tell. */
|
|
40
|
+
export declare function bitConsumed(state: {
|
|
41
|
+
anchor: bigint;
|
|
42
|
+
bitmap: string | null;
|
|
43
|
+
}, slot: NonceSlot): boolean | null;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx permit nonces: (anchor uint48, bit 0..207) pairs, each consumable once on-chain.
|
|
3
|
+
*
|
|
4
|
+
* Allocation (one per signed action), under an exclusive per-account file lock:
|
|
5
|
+
* remote = GET /v1/nonce-state/{account} → nonce_anchor A
|
|
6
|
+
* base = (A + 1, bit 0) — "sign with nonce_anchor + 1 and start at bit 0"
|
|
7
|
+
* local = last pair THIS machine handed out for the account (~/.perp/risex-nonce/…)
|
|
8
|
+
* next = local successor when local ≥ base (bit + 1; after bit 207 the next anchor), else base
|
|
9
|
+
* The contract accepts only the current anchor or current + 1, so a successor beyond A + 1 (208
|
|
10
|
+
* permits signed on A + 1 while the chain has not caught up) is refused, never guessed.
|
|
11
|
+
* `current_bitmap_index` describes the CURRENT anchor A, which perp-cli deliberately never signs on
|
|
12
|
+
* (developer.rise.trade "Integration" → Nonces: "Sign with nonce_anchor + 1 and start at bit 0 …
|
|
13
|
+
* starting a fresh anchor guarantees you cannot collide with a permit already in flight"), so it
|
|
14
|
+
* does not enter the rule. Gaps in a bitmap are harmless (same doc).
|
|
15
|
+
*
|
|
16
|
+
* The allocated pair is persisted BEFORE the caller signs or sends: a pair is handed out once and
|
|
17
|
+
* never reused, even when the send fails or its outcome is unknown (an unknown outcome burns the
|
|
18
|
+
* bit). Restart-safe: the next allocation takes the max of the chain state and the local file.
|
|
19
|
+
* The state file holds no secrets.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { PerpError } from "../../errors.js";
|
|
24
|
+
export const RISEX_MAX_BIT = 207;
|
|
25
|
+
const LOCK_WAIT_MS = 5_000;
|
|
26
|
+
const LOCK_STALE_MS = 60_000;
|
|
27
|
+
export function risexNonceDir() {
|
|
28
|
+
return resolve(process.env.HOME || "~", ".perp", "risex-nonce");
|
|
29
|
+
}
|
|
30
|
+
function statePath(dir, net, account) {
|
|
31
|
+
return resolve(dir, `${net}-${account.toLowerCase()}.json`);
|
|
32
|
+
}
|
|
33
|
+
function readLocal(path) {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return null;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
parsed = null;
|
|
42
|
+
}
|
|
43
|
+
const p = parsed;
|
|
44
|
+
if (p === null || typeof p.anchor !== "string" || !/^\d+$/.test(p.anchor) || typeof p.bit !== "number" || !Number.isInteger(p.bit) || p.bit < 0 || p.bit > RISEX_MAX_BIT) {
|
|
45
|
+
throw new PerpError("INVALID_PARAMS", `RISEx nonce state ${path} is corrupt`, {
|
|
46
|
+
exchange: "risex",
|
|
47
|
+
remediation: `Delete ${path} (it only records the last permit nonce handed out; the chain state is re-read) and retry`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { anchor: BigInt(p.anchor), bit: p.bit };
|
|
51
|
+
}
|
|
52
|
+
function writeLocal(path, slot) {
|
|
53
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
54
|
+
writeFileSync(tmp, JSON.stringify({ anchor: slot.anchor.toString(), bit: slot.bit, updatedAt: new Date().toISOString() }), { mode: 0o600 });
|
|
55
|
+
renameSync(tmp, path);
|
|
56
|
+
}
|
|
57
|
+
function pidAlive(pid) {
|
|
58
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return e.code === "EPERM";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A lock is stale only when it is older than LOCK_STALE_MS AND its holder process is gone. A young
|
|
70
|
+
* lock, a live holder, or unparseable content (never written that way — the lock is linked into
|
|
71
|
+
* place complete) is respected.
|
|
72
|
+
*/
|
|
73
|
+
function lockIsStale(content, mtimeMs) {
|
|
74
|
+
if (Date.now() - mtimeMs <= LOCK_STALE_MS)
|
|
75
|
+
return false;
|
|
76
|
+
const pid = Number(content.split("\n")[0]);
|
|
77
|
+
return !pidAlive(pid);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Take over a stale lock without racing another process doing the same: the lock is RENAMED to a
|
|
81
|
+
* name unique to this process (atomic — only one taker wins), and only if the moved file is still
|
|
82
|
+
* the one judged stale is it deleted; a fresh lock moved by mistake is put back (link fails if a
|
|
83
|
+
* new lock already exists, in which case that one stands).
|
|
84
|
+
*/
|
|
85
|
+
function takeOverStale(lock, seen) {
|
|
86
|
+
const mine = `${lock}.stale.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
87
|
+
try {
|
|
88
|
+
renameSync(lock, mine);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
} // another process took it first
|
|
93
|
+
let moved = null;
|
|
94
|
+
try {
|
|
95
|
+
moved = readFileSync(mine, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
catch { /* vanished */ }
|
|
98
|
+
if (moved !== null && moved !== seen) {
|
|
99
|
+
try {
|
|
100
|
+
linkSync(mine, lock);
|
|
101
|
+
}
|
|
102
|
+
catch { /* a newer lock exists: it stands */ }
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(mine);
|
|
106
|
+
}
|
|
107
|
+
catch { /* already gone */ }
|
|
108
|
+
}
|
|
109
|
+
async function withLock(path, fn) {
|
|
110
|
+
const lock = `${path}.lock`;
|
|
111
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
112
|
+
const token = `${process.pid}\n${Date.now()}\n${Math.random().toString(36).slice(2)}`;
|
|
113
|
+
for (;;) {
|
|
114
|
+
// The lock is written to a private temp file first and LINKED into place (link fails if the
|
|
115
|
+
// lock exists), so a reader never sees an empty or partial lock.
|
|
116
|
+
const tmp = `${lock}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
117
|
+
writeFileSync(tmp, token, { mode: 0o600 });
|
|
118
|
+
try {
|
|
119
|
+
linkSync(tmp, lock);
|
|
120
|
+
unlinkSync(tmp);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
try {
|
|
125
|
+
unlinkSync(tmp);
|
|
126
|
+
}
|
|
127
|
+
catch { /* gone */ }
|
|
128
|
+
if (e.code !== "EEXIST")
|
|
129
|
+
throw e;
|
|
130
|
+
let content;
|
|
131
|
+
let mtimeMs;
|
|
132
|
+
try {
|
|
133
|
+
content = readFileSync(lock, "utf-8");
|
|
134
|
+
mtimeMs = statSync(lock).mtimeMs;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
} // released between the open and the read: try again
|
|
139
|
+
if (lockIsStale(content, mtimeMs)) {
|
|
140
|
+
takeOverStale(lock, content);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (Date.now() > deadline) {
|
|
144
|
+
throw new PerpError("LOCK_HELD", `RISEx nonce state is locked by another perp process (${lock})`, {
|
|
145
|
+
exchange: "risex", remediation: `Retry shortly; if no other perp process is running, delete ${lock}`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
await new Promise(r => setTimeout(r, 25));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return await fn();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
// Release only our own lock (never one a stale-takeover replaced).
|
|
156
|
+
try {
|
|
157
|
+
if (readFileSync(lock, "utf-8") === token)
|
|
158
|
+
unlinkSync(lock);
|
|
159
|
+
}
|
|
160
|
+
catch { /* already gone */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Pure allocation rule (see module doc). */
|
|
164
|
+
export function nextSlot(remote, local) {
|
|
165
|
+
const base = { anchor: remote.anchor + 1n, bit: 0 };
|
|
166
|
+
let next = base;
|
|
167
|
+
if (local !== null && (local.anchor > base.anchor || (local.anchor === base.anchor && local.bit >= base.bit))) {
|
|
168
|
+
next = local.bit + 1 > RISEX_MAX_BIT ? { anchor: local.anchor + 1n, bit: 0 } : { anchor: local.anchor, bit: local.bit + 1 };
|
|
169
|
+
}
|
|
170
|
+
if (next.anchor > remote.anchor + 1n) {
|
|
171
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx nonce anchor ${remote.anchor + 1n} is used up locally and the chain still reports anchor ${remote.anchor} — nothing was signed`, {
|
|
172
|
+
exchange: "risex", remediation: "Wait a few seconds for pending RISEx actions to land, then retry",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
export function parseRemoteNonceState(data) {
|
|
178
|
+
const d = data;
|
|
179
|
+
if (d === null || typeof d.nonce_anchor !== "string" || !/^\d+$/.test(d.nonce_anchor) || typeof d.current_bitmap_index !== "number") {
|
|
180
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx /v1/nonce-state: malformed response ${JSON.stringify(data).slice(0, 120)}`, { exchange: "risex" });
|
|
181
|
+
}
|
|
182
|
+
return { anchor: BigInt(d.nonce_anchor), currentBitmapIndex: d.current_bitmap_index };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Allocate one nonce pair for `account` and persist it before returning (burned from here on).
|
|
186
|
+
* `fetchRemote` reads /v1/nonce-state inside the lock.
|
|
187
|
+
*/
|
|
188
|
+
export async function allocateNonce(net, account, fetchRemote, dir = risexNonceDir()) {
|
|
189
|
+
if (!existsSync(dir))
|
|
190
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
191
|
+
const path = statePath(dir, net, account);
|
|
192
|
+
return withLock(path, async () => {
|
|
193
|
+
const remote = await fetchRemote();
|
|
194
|
+
const slot = nextSlot(remote, readLocal(path));
|
|
195
|
+
writeLocal(path, slot);
|
|
196
|
+
return slot;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** Last pair handed out on this machine (diagnostics / reconciliation), or null. */
|
|
200
|
+
export function lastAllocated(net, account, dir = risexNonceDir()) {
|
|
201
|
+
return readLocal(statePath(dir, net, account));
|
|
202
|
+
}
|
|
203
|
+
/** Whether (anchor, bit) is consumed according to /v1/nonce-state's `bitmap` (only when the anchor matches). null = cannot tell. */
|
|
204
|
+
export function bitConsumed(state, slot) {
|
|
205
|
+
if (state.anchor > slot.anchor)
|
|
206
|
+
return null; // the chain moved past this anchor: the bitmap no longer describes it
|
|
207
|
+
if (state.anchor < slot.anchor)
|
|
208
|
+
return false; // the anchor was never reached on-chain
|
|
209
|
+
if (state.bitmap === null || !/^0x[0-9a-fA-F]+$/.test(state.bitmap))
|
|
210
|
+
return null;
|
|
211
|
+
return ((BigInt(state.bitmap) >> BigInt(slot.bit)) & 1n) === 1n;
|
|
212
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx public market data — GET /v1/markets (one call serves markets and funding).
|
|
3
|
+
*
|
|
4
|
+
* Mapping:
|
|
5
|
+
* - symbol: config.name "BTC/USDC" → "BTC"; venueSymbol = market_id ("1").
|
|
6
|
+
* - inactive markets (`active: false`, e.g. the deprecated DOGE market 13) are excluded.
|
|
7
|
+
* tradable = active && config.unlocked && !reduce_only (a reduce-only market can only close).
|
|
8
|
+
* - tick = config.step_price, step = config.step_size, min size = config.min_order_size.
|
|
9
|
+
* - assetClass from `category`: crypto / commodity / stocks (→ equity, or etf when tagged
|
|
10
|
+
* "etf") / index_etf (→ etf). An unknown category is skipped with a warning.
|
|
11
|
+
* - funding: `current_funding_rate` is a FRACTION PER HOUR and equals the last settled print in
|
|
12
|
+
* /funding-rate-history (lastSettled) — verified against public funding payments
|
|
13
|
+
* (payment = position size × index × rate, 2026-10-14). `funding_interval` is in ns and must
|
|
14
|
+
* be 3600e9; a market with another interval gets no funding row (warned).
|
|
15
|
+
* - all timestamps are ns → ms.
|
|
16
|
+
*/
|
|
17
|
+
import { type VenuePublicData } from "../venue.js";
|
|
18
|
+
import type { AssetClass, FundingRateBasis, Network, VenueFundingRate, VenueMarket } from "../venue-types.js";
|
|
19
|
+
import { RisexClient } from "./client.js";
|
|
20
|
+
export declare const RISEX_FUNDING_BASIS: FundingRateBasis;
|
|
21
|
+
export declare const RISEX_FUNDING_INTERVAL_NS = "3600000000000";
|
|
22
|
+
export interface RisexMarketConfig {
|
|
23
|
+
name: string;
|
|
24
|
+
step_size: string;
|
|
25
|
+
step_price: string;
|
|
26
|
+
max_leverage: string;
|
|
27
|
+
min_order_size: string;
|
|
28
|
+
unlocked: boolean;
|
|
29
|
+
match_price_band?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface RisexRawMarket {
|
|
32
|
+
market_id: string;
|
|
33
|
+
config: RisexMarketConfig;
|
|
34
|
+
mark_price?: string;
|
|
35
|
+
index_price?: string;
|
|
36
|
+
last_price?: string;
|
|
37
|
+
open_interest?: string;
|
|
38
|
+
quote_volume_24h?: string;
|
|
39
|
+
funding_interval?: string;
|
|
40
|
+
next_funding_time?: string;
|
|
41
|
+
current_funding_rate?: string;
|
|
42
|
+
predicted_funding_rate?: string;
|
|
43
|
+
post_only: boolean;
|
|
44
|
+
reduce_only: boolean;
|
|
45
|
+
active: boolean;
|
|
46
|
+
category: string;
|
|
47
|
+
search_tags?: string[];
|
|
48
|
+
}
|
|
49
|
+
export declare function risexClient(net: Network): RisexClient;
|
|
50
|
+
export declare function fetchRisexRawMarkets(client: RisexClient): Promise<RisexRawMarket[]>;
|
|
51
|
+
/**
|
|
52
|
+
* A listed market: `active` and not a deprecated listing ("DOGE/USDC [deprecated-…]" — testnet keeps
|
|
53
|
+
* such a market active but locked, next to its replacement with the same base).
|
|
54
|
+
*/
|
|
55
|
+
export declare function isListedRisexMarket(m: Pick<RisexRawMarket, "active" | "config">): boolean;
|
|
56
|
+
/** "BTC/USDC" → "BTC". */
|
|
57
|
+
export declare function risexSymbolOf(m: Pick<RisexRawMarket, "config">): string;
|
|
58
|
+
export declare function risexAssetClass(m: Pick<RisexRawMarket, "category" | "search_tags">): AssetClass | null;
|
|
59
|
+
/** Hourly funding fraction, or null when the market's interval is not one hour / no rate is published. */
|
|
60
|
+
export declare function risexHourlyFunding(m: RisexRawMarket): string | null;
|
|
61
|
+
/** Active markets only (inactive / deprecated markets are excluded). Unknown categories are warned and skipped. */
|
|
62
|
+
export declare function toRisexVenueMarkets(raw: readonly RisexRawMarket[], warn: (msg: string) => void): VenueMarket[];
|
|
63
|
+
export declare function toRisexFundingRates(raw: readonly RisexRawMarket[], markets: readonly VenueMarket[]): VenueFundingRate[];
|
|
64
|
+
export declare const risexPublicData: VenuePublicData;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx public market data — GET /v1/markets (one call serves markets and funding).
|
|
3
|
+
*
|
|
4
|
+
* Mapping:
|
|
5
|
+
* - symbol: config.name "BTC/USDC" → "BTC"; venueSymbol = market_id ("1").
|
|
6
|
+
* - inactive markets (`active: false`, e.g. the deprecated DOGE market 13) are excluded.
|
|
7
|
+
* tradable = active && config.unlocked && !reduce_only (a reduce-only market can only close).
|
|
8
|
+
* - tick = config.step_price, step = config.step_size, min size = config.min_order_size.
|
|
9
|
+
* - assetClass from `category`: crypto / commodity / stocks (→ equity, or etf when tagged
|
|
10
|
+
* "etf") / index_etf (→ etf). An unknown category is skipped with a warning.
|
|
11
|
+
* - funding: `current_funding_rate` is a FRACTION PER HOUR and equals the last settled print in
|
|
12
|
+
* /funding-rate-history (lastSettled) — verified against public funding payments
|
|
13
|
+
* (payment = position size × index × rate, 2026-10-14). `funding_interval` is in ns and must
|
|
14
|
+
* be 3600e9; a market with another interval gets no funding row (warned).
|
|
15
|
+
* - all timestamps are ns → ms.
|
|
16
|
+
*/
|
|
17
|
+
import { canonicalFor } from "../canonical-symbols.js";
|
|
18
|
+
import { networkConfig } from "../venue.js";
|
|
19
|
+
import { risexVenue } from "../venues/risex.js";
|
|
20
|
+
import { RisexClient } from "./client.js";
|
|
21
|
+
import { decOrNull, nsToMsOrNull, trimDecimal } from "./units.js";
|
|
22
|
+
import { PerpError } from "../../errors.js";
|
|
23
|
+
export const RISEX_FUNDING_BASIS = { kind: "perHours", hours: 1 };
|
|
24
|
+
export const RISEX_FUNDING_INTERVAL_NS = "3600000000000";
|
|
25
|
+
export function risexClient(net) {
|
|
26
|
+
return new RisexClient(networkConfig(risexVenue, net).restUrl, risexVenue.rateLimit, `risex:${net}`);
|
|
27
|
+
}
|
|
28
|
+
export async function fetchRisexRawMarkets(client) {
|
|
29
|
+
const data = await client.get("/v1/markets");
|
|
30
|
+
if (!Array.isArray(data.markets))
|
|
31
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/markets: missing markets array", { exchange: "risex" });
|
|
32
|
+
for (const m of data.markets) {
|
|
33
|
+
if (typeof m?.market_id !== "string" || m.config === null || typeof m.config !== "object" || typeof m.config.name !== "string"
|
|
34
|
+
|| typeof m.active !== "boolean" || typeof m.reduce_only !== "boolean" || typeof m.post_only !== "boolean") {
|
|
35
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx /v1/markets: malformed market row ${JSON.stringify(m).slice(0, 120)}`, { exchange: "risex" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return data.markets;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A listed market: `active` and not a deprecated listing ("DOGE/USDC [deprecated-…]" — testnet keeps
|
|
42
|
+
* such a market active but locked, next to its replacement with the same base).
|
|
43
|
+
*/
|
|
44
|
+
export function isListedRisexMarket(m) {
|
|
45
|
+
return m.active && !/\[deprecated/i.test(m.config.name);
|
|
46
|
+
}
|
|
47
|
+
/** "BTC/USDC" → "BTC". */
|
|
48
|
+
export function risexSymbolOf(m) {
|
|
49
|
+
const base = m.config.name.split("/")[0].trim().toUpperCase();
|
|
50
|
+
if (base === "")
|
|
51
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx market name "${m.config.name}" has no base`, { exchange: "risex" });
|
|
52
|
+
return base;
|
|
53
|
+
}
|
|
54
|
+
export function risexAssetClass(m) {
|
|
55
|
+
switch (m.category) {
|
|
56
|
+
case "crypto": return "crypto";
|
|
57
|
+
case "commodity": return "commodity";
|
|
58
|
+
case "stocks": return (m.search_tags ?? []).includes("etf") ? "etf" : "equity";
|
|
59
|
+
case "index_etf": return "etf";
|
|
60
|
+
default: return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Hourly funding fraction, or null when the market's interval is not one hour / no rate is published. */
|
|
64
|
+
export function risexHourlyFunding(m) {
|
|
65
|
+
if (m.funding_interval !== RISEX_FUNDING_INTERVAL_NS)
|
|
66
|
+
return null;
|
|
67
|
+
const r = decOrNull(m.current_funding_rate);
|
|
68
|
+
return r === null ? null : trimDecimal(r);
|
|
69
|
+
}
|
|
70
|
+
/** Active markets only (inactive / deprecated markets are excluded). Unknown categories are warned and skipped. */
|
|
71
|
+
export function toRisexVenueMarkets(raw, warn) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const m of raw) {
|
|
74
|
+
if (!isListedRisexMarket(m))
|
|
75
|
+
continue;
|
|
76
|
+
const assetClass = risexAssetClass(m);
|
|
77
|
+
if (assetClass === null) {
|
|
78
|
+
warn(`[risex] skipped market ${m.market_id} (${m.config.name}): unknown category "${m.category}"`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const mark = decOrNull(m.mark_price);
|
|
82
|
+
if (mark === null) {
|
|
83
|
+
warn(`[risex] skipped market ${m.market_id} (${m.config.name}): no mark price`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (m.funding_interval !== RISEX_FUNDING_INTERVAL_NS) {
|
|
87
|
+
warn(`[risex] market ${m.config.name}: funding interval ${String(m.funding_interval)} ns is not 1h — funding shown as unknown`);
|
|
88
|
+
}
|
|
89
|
+
const symbol = risexSymbolOf(m);
|
|
90
|
+
const maxLev = Number(m.config.max_leverage);
|
|
91
|
+
const idx = decOrNull(m.index_price);
|
|
92
|
+
out.push({
|
|
93
|
+
venue: "risex",
|
|
94
|
+
venueSymbol: m.market_id,
|
|
95
|
+
symbol,
|
|
96
|
+
canonical: canonicalFor("risex", symbol, assetClass),
|
|
97
|
+
assetClass,
|
|
98
|
+
settle: "USDC",
|
|
99
|
+
quoteAsset: "USDC",
|
|
100
|
+
markPrice: trimDecimal(mark),
|
|
101
|
+
indexPrice: idx === null ? null : trimDecimal(idx),
|
|
102
|
+
fundingRate: risexHourlyFunding(m),
|
|
103
|
+
fundingRateBasis: RISEX_FUNDING_BASIS,
|
|
104
|
+
settlementIntervalHours: 1,
|
|
105
|
+
// Base units [inferred: BTC open_interest ≈ 204 vs config.open_interest_limit 750 — contracts,
|
|
106
|
+
// not USD; the docs do not state the unit], as Hyperliquid publishes it.
|
|
107
|
+
openInterest: decOrNull(m.open_interest),
|
|
108
|
+
volume24h: decOrNull(m.quote_volume_24h),
|
|
109
|
+
tickSize: decOrNull(m.config.step_price),
|
|
110
|
+
stepSize: decOrNull(m.config.step_size),
|
|
111
|
+
minSize: decOrNull(m.config.min_order_size),
|
|
112
|
+
minNotional: null,
|
|
113
|
+
maxLeverage: Number.isInteger(maxLev) && maxLev > 0 ? maxLev : null,
|
|
114
|
+
marginTiers: null,
|
|
115
|
+
session: null,
|
|
116
|
+
tradable: m.active && m.config.unlocked === true && !m.reduce_only,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
export function toRisexFundingRates(raw, markets) {
|
|
122
|
+
const byId = new Map(raw.map(r => [r.market_id, r]));
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const m of markets) {
|
|
125
|
+
if (m.fundingRate === null)
|
|
126
|
+
continue;
|
|
127
|
+
const r = byId.get(m.venueSymbol);
|
|
128
|
+
out.push({
|
|
129
|
+
venue: "risex",
|
|
130
|
+
venueSymbol: m.venueSymbol,
|
|
131
|
+
symbol: m.symbol,
|
|
132
|
+
canonical: m.canonical,
|
|
133
|
+
rate: m.fundingRate,
|
|
134
|
+
basis: RISEX_FUNDING_BASIS,
|
|
135
|
+
markPrice: m.markPrice,
|
|
136
|
+
nextFundingTime: r === undefined ? null : nsToMsOrNull(r.next_funding_time),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
const warnStderr = (msg) => { process.stderr.write(`${msg}\n`); };
|
|
142
|
+
export const risexPublicData = {
|
|
143
|
+
async fetchMarkets(net) {
|
|
144
|
+
return toRisexVenueMarkets(await fetchRisexRawMarkets(risexClient(net)), warnStderr);
|
|
145
|
+
},
|
|
146
|
+
async fetchFundingRates(net) {
|
|
147
|
+
const raw = await fetchRisexRawMarkets(risexClient(net));
|
|
148
|
+
return toRisexFundingRates(raw, toRisexVenueMarkets(raw, warnStderr));
|
|
149
|
+
},
|
|
150
|
+
};
|