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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx signing context: the EIP-712 domain, the permit target (router) and the maintenance flag,
|
|
3
|
+
* fetched at runtime and checked against the per-network pins (typed-data.ts RISEX_PINS) before
|
|
4
|
+
* anything is signed. Also the master / signer nonce allocation and on-chain permission reads.
|
|
5
|
+
*/
|
|
6
|
+
import type { Network } from "../venue-types.js";
|
|
7
|
+
import type { RisexClient } from "./client.js";
|
|
8
|
+
import { type NonceSlot, type RisexDomain } from "./typed-data.js";
|
|
9
|
+
export interface RisexSigningContext {
|
|
10
|
+
domain: RisexDomain;
|
|
11
|
+
router: string;
|
|
12
|
+
auth: string;
|
|
13
|
+
}
|
|
14
|
+
interface SystemConfig {
|
|
15
|
+
addresses?: {
|
|
16
|
+
router?: unknown;
|
|
17
|
+
auth?: unknown;
|
|
18
|
+
};
|
|
19
|
+
is_maintenance_mode?: unknown;
|
|
20
|
+
maintenance_message?: unknown;
|
|
21
|
+
maintenance_end_time?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** Refuse writes during venue maintenance (system config `is_maintenance_mode`). */
|
|
24
|
+
export declare function assertNotMaintenance(cfg: SystemConfig): void;
|
|
25
|
+
export declare function loadSigningContext(client: RisexClient, net: Network): Promise<RisexSigningContext>;
|
|
26
|
+
/** Allocate (and burn) the next nonce pair for `account` (see nonce.ts). */
|
|
27
|
+
export declare function allocateAccountNonce(client: RisexClient, net: Network, account: string, dir?: string): Promise<NonceSlot>;
|
|
28
|
+
export interface RisexSessionKey {
|
|
29
|
+
/** uint32 unix seconds (word 0). */
|
|
30
|
+
expiration: number;
|
|
31
|
+
/** Permission bitmap (word 1); a new signer has 0xffffffff. */
|
|
32
|
+
bitmap: bigint;
|
|
33
|
+
/** 1 = Active, 3 = Revoked (word 2). */
|
|
34
|
+
status: number;
|
|
35
|
+
}
|
|
36
|
+
export interface RisexChainReader {
|
|
37
|
+
hasPermission(account: string, signer: string, permission: number): Promise<boolean>;
|
|
38
|
+
isNonceUsed(account: string, slot: NonceSlot): Promise<boolean>;
|
|
39
|
+
/** Raw `sessionKeys(account, signer)` (selector 0x96ade1f9): three 32-byte words. */
|
|
40
|
+
sessionKey(account: string, signer: string): Promise<RisexSessionKey>;
|
|
41
|
+
/** Whether permission id `id` exists (hasPermission does not revert for it). RPC failures throw. */
|
|
42
|
+
permissionIdExists(id: number): Promise<boolean>;
|
|
43
|
+
/** EIP-1967 implementation address of `proxy`. */
|
|
44
|
+
implementationOf(proxy: string): Promise<string>;
|
|
45
|
+
/** Authorization.getAccountRegistry(). */
|
|
46
|
+
accountRegistry(): Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
/** Decode the 96-byte `sessionKeys` return (expiration, bitmap, status). */
|
|
49
|
+
export declare function decodeSessionKey(raw: string): RisexSessionKey;
|
|
50
|
+
/** What to do when a pinned RISEx contract changed (F4): everything signed is refused until re-review. */
|
|
51
|
+
export declare function risexUpgradeRemediation(net: Network): string;
|
|
52
|
+
/**
|
|
53
|
+
* The contracts perp-cli reviewed are still the ones deployed (read-only): the EIP-1967
|
|
54
|
+
* implementations of the Authorization, the router and the account registry, the registry address
|
|
55
|
+
* the Authorization reads, and no permission id ≥ 5. Any change → POLICY_DENIED (fail closed).
|
|
56
|
+
* The RISEx admin can upgrade any of them; this is the detection, not a prevention.
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertReviewedContracts(chain: RisexChainReader, net: Network): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Prove on chain that `signer` can only trade for `account` (read-only):
|
|
61
|
+
* 1. assertReviewedContracts (pinned implementations of Authorization / router / account registry,
|
|
62
|
+
* no permission id ≥ 5) — bits 5–31 of the bitmap, which a new signer holds and which cannot be
|
|
63
|
+
* cleared, map to no permission while this holds;
|
|
64
|
+
* 2. the signer is Active and its raw bitmap's permission bits 0..4 are exactly {2} (trading);
|
|
65
|
+
* 3. hasPermission(0..4) agrees ({2} only) — so the verdict does not rest on the bit mapping alone.
|
|
66
|
+
* Throws POLICY_DENIED (never a default) when any check fails; RPC failures propagate.
|
|
67
|
+
*/
|
|
68
|
+
export declare function assertTradingOnlyOnChain(chain: RisexChainReader, net: Network, account: string, signer: string): Promise<RisexSessionKey>;
|
|
69
|
+
/** Read-only on-chain reads against the pinned Authorization contract. */
|
|
70
|
+
export declare function chainReader(net: Network): RisexChainReader;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx signing context: the EIP-712 domain, the permit target (router) and the maintenance flag,
|
|
3
|
+
* fetched at runtime and checked against the per-network pins (typed-data.ts RISEX_PINS) before
|
|
4
|
+
* anything is signed. Also the master / signer nonce allocation and on-chain permission reads.
|
|
5
|
+
*/
|
|
6
|
+
import { Interface, getAddress } from "ethers";
|
|
7
|
+
import { PerpError } from "../../errors.js";
|
|
8
|
+
import { assertRpcChainId, evmProvider } from "../../evm-master-tx.js";
|
|
9
|
+
import { allocateNonce, parseRemoteNonceState } from "./nonce.js";
|
|
10
|
+
import { assertPinned, EIP1967_IMPLEMENTATION_SLOT, RISEX_AUTH_IFACE, RISEX_PERMISSION_ID_MASK, RISEX_PERMISSION_IDS, RISEX_PINS, RISEX_TRADING_ONLY_BITS, RISEX_TRADING_PERMISSION, } from "./typed-data.js";
|
|
11
|
+
/** Refuse writes during venue maintenance (system config `is_maintenance_mode`). */
|
|
12
|
+
export function assertNotMaintenance(cfg) {
|
|
13
|
+
if (cfg.is_maintenance_mode === true) {
|
|
14
|
+
const end = typeof cfg.maintenance_end_time === "string" && /^\d+$/.test(cfg.maintenance_end_time)
|
|
15
|
+
? new Date(Number(cfg.maintenance_end_time) * 1000).toISOString() : null;
|
|
16
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `RISEx is in maintenance${typeof cfg.maintenance_message === "string" && cfg.maintenance_message !== "" ? `: ${cfg.maintenance_message}` : ""} — nothing was sent`, {
|
|
17
|
+
exchange: "risex", sent: false, remediation: end !== null ? `Retry after ${end}` : "Retry after the maintenance window",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function loadSigningContext(client, net) {
|
|
22
|
+
const [d, cfg] = await Promise.all([
|
|
23
|
+
client.get("/v1/auth/eip712-domain"),
|
|
24
|
+
client.get("/v1/system/config"),
|
|
25
|
+
]);
|
|
26
|
+
if (typeof d.name !== "string" || typeof d.version !== "string" || typeof d.chain_id !== "string" || typeof d.verifying_contract !== "string") {
|
|
27
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/auth/eip712-domain: malformed response", { exchange: "risex" });
|
|
28
|
+
}
|
|
29
|
+
const router = cfg.addresses?.router;
|
|
30
|
+
const auth = cfg.addresses?.auth;
|
|
31
|
+
if (typeof router !== "string" || typeof auth !== "string") {
|
|
32
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx /v1/system/config: no addresses.router / addresses.auth", { exchange: "risex" });
|
|
33
|
+
}
|
|
34
|
+
const domain = { name: d.name, version: d.version, chainId: Number(d.chain_id), verifyingContract: d.verifying_contract };
|
|
35
|
+
assertPinned(net, domain, router, auth);
|
|
36
|
+
assertNotMaintenance(cfg);
|
|
37
|
+
return { domain, router, auth };
|
|
38
|
+
}
|
|
39
|
+
/** Allocate (and burn) the next nonce pair for `account` (see nonce.ts). */
|
|
40
|
+
export function allocateAccountNonce(client, net, account, dir) {
|
|
41
|
+
return allocateNonce(net, account, async () => parseRemoteNonceState(await client.get(`/v1/nonce-state/${account.toLowerCase()}`)), dir);
|
|
42
|
+
}
|
|
43
|
+
/** Decode the 96-byte `sessionKeys` return (expiration, bitmap, status). */
|
|
44
|
+
export function decodeSessionKey(raw) {
|
|
45
|
+
if (!/^0x[0-9a-fA-F]{192}$/.test(raw)) {
|
|
46
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx sessionKeys returned ${raw.length} hex chars (expected 3 words)`, { exchange: "risex" });
|
|
47
|
+
}
|
|
48
|
+
const word = (k) => BigInt(`0x${raw.slice(2 + 64 * k, 66 + 64 * k)}`);
|
|
49
|
+
return { expiration: Number(word(0)), bitmap: word(1), status: Number(word(2)) };
|
|
50
|
+
}
|
|
51
|
+
/** What to do when a pinned RISEx contract changed (F4): everything signed is refused until re-review. */
|
|
52
|
+
export function risexUpgradeRemediation(net) {
|
|
53
|
+
const n = net === "testnet" ? " -n testnet" : "";
|
|
54
|
+
return "Every signed RISEx action is refused through perp-cli until the new contract is reviewed — this includes cancel and cancel-all, which also go through a permit. " +
|
|
55
|
+
"Manage open orders and positions in the RISEx app (app.rise.trade). If you do not trust the new implementation, revoke the signer: " +
|
|
56
|
+
`perp${n} wallet agent revoke risex <name>. Update perp-cli once a release has reviewed the new implementation.`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The contracts perp-cli reviewed are still the ones deployed (read-only): the EIP-1967
|
|
60
|
+
* implementations of the Authorization, the router and the account registry, the registry address
|
|
61
|
+
* the Authorization reads, and no permission id ≥ 5. Any change → POLICY_DENIED (fail closed).
|
|
62
|
+
* The RISEx admin can upgrade any of them; this is the detection, not a prevention.
|
|
63
|
+
*/
|
|
64
|
+
export async function assertReviewedContracts(chain, net) {
|
|
65
|
+
const pin = RISEX_PINS[net];
|
|
66
|
+
const refuse = (what) => new PerpError("POLICY_DENIED", `RISEx ${what} — its permission model must be re-reviewed before perp-cli signs anything`, {
|
|
67
|
+
exchange: "risex", remediation: risexUpgradeRemediation(net),
|
|
68
|
+
});
|
|
69
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
70
|
+
const [authImpl, routerImpl, registry] = await Promise.all([
|
|
71
|
+
chain.implementationOf(pin.auth), chain.implementationOf(pin.router), chain.accountRegistry(),
|
|
72
|
+
]);
|
|
73
|
+
if (!same(authImpl, pin.authImplementation))
|
|
74
|
+
throw refuse(`Authorization contract was upgraded (implementation ${authImpl}, reviewed ${pin.authImplementation})`);
|
|
75
|
+
if (!same(routerImpl, pin.routerImplementation))
|
|
76
|
+
throw refuse(`router was upgraded (implementation ${routerImpl}, reviewed ${pin.routerImplementation})`);
|
|
77
|
+
if (!same(registry, pin.accountRegistry))
|
|
78
|
+
throw refuse(`Authorization now reads account registry ${registry} (reviewed ${pin.accountRegistry})`);
|
|
79
|
+
const registryImpl = await chain.implementationOf(pin.accountRegistry);
|
|
80
|
+
if (!same(registryImpl, pin.accountRegistryImplementation))
|
|
81
|
+
throw refuse(`account registry was upgraded (implementation ${registryImpl}, reviewed ${pin.accountRegistryImplementation})`);
|
|
82
|
+
if (await chain.permissionIdExists(RISEX_PERMISSION_IDS.length)) {
|
|
83
|
+
throw refuse(`now has permission id ${RISEX_PERMISSION_IDS.length}: signers approved by perp-cli may hold it (their bitmap bits ≥5 are set)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Prove on chain that `signer` can only trade for `account` (read-only):
|
|
88
|
+
* 1. assertReviewedContracts (pinned implementations of Authorization / router / account registry,
|
|
89
|
+
* no permission id ≥ 5) — bits 5–31 of the bitmap, which a new signer holds and which cannot be
|
|
90
|
+
* cleared, map to no permission while this holds;
|
|
91
|
+
* 2. the signer is Active and its raw bitmap's permission bits 0..4 are exactly {2} (trading);
|
|
92
|
+
* 3. hasPermission(0..4) agrees ({2} only) — so the verdict does not rest on the bit mapping alone.
|
|
93
|
+
* Throws POLICY_DENIED (never a default) when any check fails; RPC failures propagate.
|
|
94
|
+
*/
|
|
95
|
+
export async function assertTradingOnlyOnChain(chain, net, account, signer) {
|
|
96
|
+
await assertReviewedContracts(chain, net);
|
|
97
|
+
const sk = await chain.sessionKey(account, signer);
|
|
98
|
+
const low = sk.bitmap & RISEX_PERMISSION_ID_MASK;
|
|
99
|
+
const notTradingOnly = (why) => new PerpError("POLICY_DENIED", `RISEx signer ${signer} is not trading-only on chain (${why})`, {
|
|
100
|
+
exchange: "risex", signer, bitmap: `0x${sk.bitmap.toString(16)}`, status: sk.status,
|
|
101
|
+
remediation: "Revoke it (perp wallet agent revoke risex <name>) and approve a new signer",
|
|
102
|
+
});
|
|
103
|
+
if (sk.status !== 1 || low !== RISEX_TRADING_ONLY_BITS) {
|
|
104
|
+
throw notTradingOnly(`status ${sk.status}, permission bitmap 0x${sk.bitmap.toString(16)}, ids 0..4 = 0b${low.toString(2).padStart(5, "0")}, required 0b00100`);
|
|
105
|
+
}
|
|
106
|
+
const per = await Promise.all(RISEX_PERMISSION_IDS.map(async (id) => [id, await chain.hasPermission(account, signer, id)]));
|
|
107
|
+
const wrong = per.filter(([id, on]) => on !== (id === RISEX_TRADING_PERMISSION));
|
|
108
|
+
if (wrong.length > 0)
|
|
109
|
+
throw notTradingOnly(`hasPermission disagrees with the bitmap: ${wrong.map(([id, on]) => `${id}=${on}`).join(", ")}`);
|
|
110
|
+
return sk;
|
|
111
|
+
}
|
|
112
|
+
const SESSION_KEYS_IFACE = new Interface(["function sessionKeys(address account, address signer) view returns (bytes32, bytes32, bytes32)"]);
|
|
113
|
+
const ZERO_ONE = "0x0000000000000000000000000000000000000001";
|
|
114
|
+
const ZERO_TWO = "0x0000000000000000000000000000000000000002";
|
|
115
|
+
const NONCE_IFACE_FN = "function isNonceUsed(address account, uint48 nonceAnchor, uint8 nonceBitmap) view returns (bool)";
|
|
116
|
+
/** Read-only on-chain reads against the pinned Authorization contract. */
|
|
117
|
+
export function chainReader(net) {
|
|
118
|
+
const pin = RISEX_PINS[net];
|
|
119
|
+
if (pin.rpcUrl === null) {
|
|
120
|
+
throw new PerpError("NOT_IMPLEMENTED", `RISEx ${net} has no known RPC endpoint`, { exchange: "risex" });
|
|
121
|
+
}
|
|
122
|
+
const rpc = evmProvider(pin.rpcUrl, pin.chainId);
|
|
123
|
+
const rpcUrl = pin.rpcUrl;
|
|
124
|
+
// The RPC must serve the pinned chain before any answer from it is trusted (same check as the sender).
|
|
125
|
+
let chainChecked = null;
|
|
126
|
+
const checked = () => (chainChecked ??= assertRpcChainId(rpc, rpcUrl, pin.chainId, "risex").catch(e => { chainChecked = null; throw e; }));
|
|
127
|
+
return {
|
|
128
|
+
async hasPermission(account, signer, permission) {
|
|
129
|
+
if (!RISEX_PERMISSION_IDS.includes(permission))
|
|
130
|
+
throw new PerpError("INVALID_PARAMS", `RISEx permission ${permission} is not 0..4`);
|
|
131
|
+
await checked();
|
|
132
|
+
const raw = await rpc.call({ to: pin.auth, data: RISEX_AUTH_IFACE.encodeFunctionData("hasPermission", [account, signer, permission]) });
|
|
133
|
+
return RISEX_AUTH_IFACE.decodeFunctionResult("hasPermission", raw)[0];
|
|
134
|
+
},
|
|
135
|
+
async sessionKey(account, signer) {
|
|
136
|
+
await checked();
|
|
137
|
+
return decodeSessionKey(await rpc.call({ to: pin.auth, data: SESSION_KEYS_IFACE.encodeFunctionData("sessionKeys", [account, signer]) }));
|
|
138
|
+
},
|
|
139
|
+
async permissionIdExists(id) {
|
|
140
|
+
await checked();
|
|
141
|
+
try {
|
|
142
|
+
await rpc.call({ to: pin.auth, data: RISEX_AUTH_IFACE.encodeFunctionData("hasPermission", [ZERO_ONE, ZERO_TWO, id]) });
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// A revert (enum decode of an unknown id) answers "no"; anything else is a failed read.
|
|
147
|
+
if (e.code === "CALL_EXCEPTION")
|
|
148
|
+
return false;
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async implementationOf(proxy) {
|
|
153
|
+
await checked();
|
|
154
|
+
const word = await rpc.getStorage(proxy, EIP1967_IMPLEMENTATION_SLOT);
|
|
155
|
+
return getAddress(`0x${word.slice(-40)}`);
|
|
156
|
+
},
|
|
157
|
+
async accountRegistry() {
|
|
158
|
+
await checked();
|
|
159
|
+
const word = await rpc.call({ to: pin.auth, data: "0x0ab142ed" }); // getAccountRegistry()
|
|
160
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(word))
|
|
161
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx getAccountRegistry(): malformed return", { exchange: "risex" });
|
|
162
|
+
return getAddress(`0x${word.slice(-40)}`);
|
|
163
|
+
},
|
|
164
|
+
async isNonceUsed(account, slot) {
|
|
165
|
+
await checked();
|
|
166
|
+
const i = new Interface([NONCE_IFACE_FN]);
|
|
167
|
+
const raw = await rpc.call({ to: pin.auth, data: i.encodeFunctionData("isNonceUsed", [account, slot.anchor, slot.bit]) });
|
|
168
|
+
return i.decodeFunctionResult("isNonceUsed", raw)[0];
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx EIP-712 typed data, action hashes and order packing (developer.rise.trade "Integration",
|
|
3
|
+
* checked by RISEx against the mainnet contracts on 2026-08-07).
|
|
4
|
+
*
|
|
5
|
+
* Every signature uses ONE domain: GET /v1/auth/eip712-domain (name "RISEx", version "1",
|
|
6
|
+
* chainId, verifyingContract = the Authorization contract = system-config `addresses.auth`).
|
|
7
|
+
* The permit `target` is the RISExUniversalRouter (`addresses.router`), never the auth contract.
|
|
8
|
+
* Both are pinned per network below and cross-checked at runtime: a mismatch refuses to sign.
|
|
9
|
+
*
|
|
10
|
+
* Type strings are hashed literally (no spaces). Golden typehashes (read off the mainnet
|
|
11
|
+
* Authorization contract) are asserted in tests.
|
|
12
|
+
*
|
|
13
|
+
* Signer permissions (Authorization contract, verified on mainnet 2026-10-14): `sessionKeys`
|
|
14
|
+
* stores a permission bitmap (new signers: 0xffffffff = all); `hasPermission(account, signer, p)`
|
|
15
|
+
* accepts p = 0..4. Every router action (place / cancel / cancel-all / leverage / margin) checks
|
|
16
|
+
* permission 2 (`verifyWitnessSignature(…, permission = 2, …)`, traced on mainnet router txs) —
|
|
17
|
+
* RISEX_TRADING_PERMISSION. perp-cli restricts a signer to trading-only by disabling every other
|
|
18
|
+
* permission with a `DisablePermission` signed by the SIGNER itself (typehash below, matched against
|
|
19
|
+
* the contract's DISABLE_PERMISSION_TYPEHASH(); a master-signed one reverts InvalidSignerSignature),
|
|
20
|
+
* sent on-chain by the master. EnablePermission must be signed by the account, so a signer cannot
|
|
21
|
+
* restore what it gave up (both checked by eth_call on testnet 2026-10-14). The calldata outcomes,
|
|
22
|
+
* revert selectors, the permission-id map and the router traces are recorded in
|
|
23
|
+
* src/__tests__/fixtures/risex/permissions-evidence.json (asserted by risex-evidence.test.ts). The
|
|
24
|
+
* implementation source is not verified on the explorer, so permission NAMES are unknown.
|
|
25
|
+
*/
|
|
26
|
+
import { Interface } from "ethers";
|
|
27
|
+
import type { EvmSigner } from "../../signer/interface.js";
|
|
28
|
+
import type { Network } from "../venue-types.js";
|
|
29
|
+
export interface RisexPins {
|
|
30
|
+
chainId: number;
|
|
31
|
+
/** Authorization contract = EIP-712 verifyingContract. */
|
|
32
|
+
auth: string;
|
|
33
|
+
/** RISExUniversalRouter = permit target. */
|
|
34
|
+
router: string;
|
|
35
|
+
/** JSON-RPC for the on-chain permission restriction / reads; null = none known. */
|
|
36
|
+
rpcUrl: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* EIP-1967 implementation of the Authorization proxy whose permission model was reviewed
|
|
39
|
+
* (ids 0..4; ids ≥5 rejected by ABI enum decoding). An upgrade refuses trading until re-reviewed.
|
|
40
|
+
*/
|
|
41
|
+
authImplementation: string;
|
|
42
|
+
/** EIP-1967 implementation of the RISExUniversalRouter proxy (every permit-gated action). */
|
|
43
|
+
routerImplementation: string;
|
|
44
|
+
/** The account registry the Authorization contract reads (`getAccountRegistry()`; its getUserId is STATICCALLed by hasPermission / sessionKeys). */
|
|
45
|
+
accountRegistry: string;
|
|
46
|
+
/** EIP-1967 implementation of the account registry proxy. */
|
|
47
|
+
accountRegistryImplementation: string;
|
|
48
|
+
}
|
|
49
|
+
/** Verified 2026-10-14 from GET /v1/system/config and /v1/auth/eip712-domain on each network. */
|
|
50
|
+
export declare const RISEX_PINS: Readonly<Record<Network, RisexPins>>;
|
|
51
|
+
export interface RisexDomain {
|
|
52
|
+
name: string;
|
|
53
|
+
version: string;
|
|
54
|
+
chainId: number;
|
|
55
|
+
verifyingContract: string;
|
|
56
|
+
}
|
|
57
|
+
/** Check a fetched domain + router against the pins. Throws EXCHANGE_ERROR (nothing is signed). */
|
|
58
|
+
export declare function assertPinned(net: Network, domain: RisexDomain, router: string, auth: string): void;
|
|
59
|
+
type Fields = Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
}>;
|
|
63
|
+
export declare const RISEX_TYPES: {
|
|
64
|
+
readonly RegisterSigner: Fields;
|
|
65
|
+
readonly VerifySigner: Fields;
|
|
66
|
+
readonly RevokeSigner: Fields;
|
|
67
|
+
readonly VerifyWitness: Fields;
|
|
68
|
+
readonly DisablePermission: Fields;
|
|
69
|
+
};
|
|
70
|
+
export type RisexPrimaryType = keyof typeof RISEX_TYPES;
|
|
71
|
+
/** Canonical EIP-712 type string ("Name(type a,type b)"). */
|
|
72
|
+
export declare function typeString(primary: RisexPrimaryType): string;
|
|
73
|
+
export declare function typeHash(primary: RisexPrimaryType): string;
|
|
74
|
+
/** Read off the mainnet Authorization contract (docs "Verification" + eth_call 2026-10-14). */
|
|
75
|
+
export declare const RISEX_GOLDEN_TYPEHASHES: Readonly<Record<RisexPrimaryType, string>>;
|
|
76
|
+
/** The permission every router action checks (place / cancel / cancel-all / leverage / margin). */
|
|
77
|
+
export declare const RISEX_TRADING_PERMISSION = 2;
|
|
78
|
+
/** EIP-1967 implementation slot (keccak256("eip1967.proxy.implementation") − 1). */
|
|
79
|
+
export declare const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
|
|
80
|
+
/**
|
|
81
|
+
* The low bits of `sessionKeys(account, signer)` word 1 that map to permission ids 0..4; a
|
|
82
|
+
* trading-only signer has exactly bit 2 set among them. Bits 5–31 (set to 1 on every new signer,
|
|
83
|
+
* bitmap 0xffffffff) have no permission id in the reviewed implementation: hasPermission and
|
|
84
|
+
* disablePermission both reject ids ≥5 at ABI decoding, so those bits can neither be read as a
|
|
85
|
+
* permission nor cleared. They are guarded instead by the implementation pin and the "no id 5" probe
|
|
86
|
+
* (assertTradingOnlyOnChain, session.ts).
|
|
87
|
+
*/
|
|
88
|
+
export declare const RISEX_PERMISSION_ID_MASK = 31n;
|
|
89
|
+
export declare const RISEX_TRADING_ONLY_BITS: bigint;
|
|
90
|
+
/** Valid permission ids (hasPermission reverts above 4). */
|
|
91
|
+
export declare const RISEX_PERMISSION_IDS: readonly number[];
|
|
92
|
+
/** Permissions perp-cli disables right after registration (all except trading). */
|
|
93
|
+
export declare const RISEX_NON_TRADING_PERMISSIONS: readonly number[];
|
|
94
|
+
export declare const RISEX_AUTH_IFACE: Interface;
|
|
95
|
+
export declare const ACTION: {
|
|
96
|
+
readonly PLACE_ORDER: "RISE_PERPS_PLACE_ORDER_V1";
|
|
97
|
+
readonly CANCEL_ORDER: "RISE_PERPS_CANCEL_ORDER_V1";
|
|
98
|
+
readonly CANCEL_ALL_ORDERS: "RISE_PERPS_CANCEL_ALL_ORDERS_V1";
|
|
99
|
+
readonly UPDATE_LEVERAGE: "RISE_PERPS_UPDATE_LEVERAGE_V1";
|
|
100
|
+
readonly UPDATE_MARGIN_MODE: "RISE_PERPS_UPDATE_MARGIN_MODE_V1";
|
|
101
|
+
};
|
|
102
|
+
export declare const SIDE: {
|
|
103
|
+
readonly buy: 0;
|
|
104
|
+
readonly sell: 1;
|
|
105
|
+
};
|
|
106
|
+
export declare const ORDER_TYPE: {
|
|
107
|
+
readonly market: 0;
|
|
108
|
+
readonly limit: 1;
|
|
109
|
+
};
|
|
110
|
+
/** Venue TIF codes. ALO is GTC + post_only. */
|
|
111
|
+
export declare const TIF_CODE: {
|
|
112
|
+
readonly GTC: 0;
|
|
113
|
+
readonly GTT: 1;
|
|
114
|
+
readonly FOK: 2;
|
|
115
|
+
readonly IOC: 3;
|
|
116
|
+
};
|
|
117
|
+
export declare const STP_EXPIRE_MAKER = 0;
|
|
118
|
+
export declare const MARGIN_MODE_CODE: {
|
|
119
|
+
readonly cross: 0;
|
|
120
|
+
readonly isolated: 1;
|
|
121
|
+
};
|
|
122
|
+
export interface PackedOrderFields {
|
|
123
|
+
marketId: number;
|
|
124
|
+
sizeSteps: bigint;
|
|
125
|
+
priceTicks: bigint;
|
|
126
|
+
side: 0 | 1;
|
|
127
|
+
postOnly: boolean;
|
|
128
|
+
reduceOnly: boolean;
|
|
129
|
+
stpMode: number;
|
|
130
|
+
orderType: 0 | 1;
|
|
131
|
+
tif: 0 | 1 | 2 | 3;
|
|
132
|
+
}
|
|
133
|
+
/** uint88 order word: [87:70] market, [69:38] size steps, [37:14] price ticks, [13:6] flags, [5:1] version 1. */
|
|
134
|
+
export declare function packOrder(o: PackedOrderFields): bigint;
|
|
135
|
+
/** Inverse of packOrder (tests / diagnostics). */
|
|
136
|
+
export declare function unpackOrder(word: bigint): PackedOrderFields & {
|
|
137
|
+
version: number;
|
|
138
|
+
};
|
|
139
|
+
/** Header flags: 0x01 permit (always), 0x04 client order id present. Builder (0x02) / TTL (0x10) are never sent. */
|
|
140
|
+
export declare function headerFlags(clientOrderId: bigint): number;
|
|
141
|
+
/** keccak256(abi.encode(sel, uint8 header, uint88 order, uint16 builderId=0, uint64 clientOrderId, uint16 ttl=0)). */
|
|
142
|
+
export declare function placeOrderHash(order: bigint, clientOrderId: bigint): string;
|
|
143
|
+
export declare function cancelOrderHash(marketId: number, restingOrderId: bigint): string;
|
|
144
|
+
export declare function cancelAllHash(marketId: number): string;
|
|
145
|
+
export declare function leverageHash(marketId: number, leverage: number): string;
|
|
146
|
+
export declare function marginModeHash(marketId: number, mode: 0 | 1): string;
|
|
147
|
+
export interface NonceSlot {
|
|
148
|
+
anchor: bigint;
|
|
149
|
+
bit: number;
|
|
150
|
+
}
|
|
151
|
+
export interface RisexPermit {
|
|
152
|
+
account: string;
|
|
153
|
+
signer: string;
|
|
154
|
+
nonce_anchor: string;
|
|
155
|
+
nonce_bitmap_index: number;
|
|
156
|
+
deadline: number;
|
|
157
|
+
signature: string;
|
|
158
|
+
}
|
|
159
|
+
/** Sign typed data and verify it recovers `expected` (an OWS canonicalization slip never leaves this process). */
|
|
160
|
+
export declare function signChecked(signer: EvmSigner, domain: RisexDomain, primary: RisexPrimaryType, message: Record<string, unknown>): Promise<string>;
|
|
161
|
+
/** 65-byte r‖s‖v hex → base64 (the permit field is protobuf bytes). */
|
|
162
|
+
export declare function permitSignatureBase64(hexSig: string): string;
|
|
163
|
+
/** The signer's VerifyWitness permit for one action hash. */
|
|
164
|
+
export declare function signPermit(p: {
|
|
165
|
+
signer: EvmSigner;
|
|
166
|
+
account: string;
|
|
167
|
+
router: string;
|
|
168
|
+
domain: RisexDomain;
|
|
169
|
+
actionHash: string;
|
|
170
|
+
slot: NonceSlot;
|
|
171
|
+
deadline: number;
|
|
172
|
+
}): Promise<RisexPermit>;
|
|
173
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RISEx EIP-712 typed data, action hashes and order packing (developer.rise.trade "Integration",
|
|
3
|
+
* checked by RISEx against the mainnet contracts on 2026-08-07).
|
|
4
|
+
*
|
|
5
|
+
* Every signature uses ONE domain: GET /v1/auth/eip712-domain (name "RISEx", version "1",
|
|
6
|
+
* chainId, verifyingContract = the Authorization contract = system-config `addresses.auth`).
|
|
7
|
+
* The permit `target` is the RISExUniversalRouter (`addresses.router`), never the auth contract.
|
|
8
|
+
* Both are pinned per network below and cross-checked at runtime: a mismatch refuses to sign.
|
|
9
|
+
*
|
|
10
|
+
* Type strings are hashed literally (no spaces). Golden typehashes (read off the mainnet
|
|
11
|
+
* Authorization contract) are asserted in tests.
|
|
12
|
+
*
|
|
13
|
+
* Signer permissions (Authorization contract, verified on mainnet 2026-10-14): `sessionKeys`
|
|
14
|
+
* stores a permission bitmap (new signers: 0xffffffff = all); `hasPermission(account, signer, p)`
|
|
15
|
+
* accepts p = 0..4. Every router action (place / cancel / cancel-all / leverage / margin) checks
|
|
16
|
+
* permission 2 (`verifyWitnessSignature(…, permission = 2, …)`, traced on mainnet router txs) —
|
|
17
|
+
* RISEX_TRADING_PERMISSION. perp-cli restricts a signer to trading-only by disabling every other
|
|
18
|
+
* permission with a `DisablePermission` signed by the SIGNER itself (typehash below, matched against
|
|
19
|
+
* the contract's DISABLE_PERMISSION_TYPEHASH(); a master-signed one reverts InvalidSignerSignature),
|
|
20
|
+
* sent on-chain by the master. EnablePermission must be signed by the account, so a signer cannot
|
|
21
|
+
* restore what it gave up (both checked by eth_call on testnet 2026-10-14). The calldata outcomes,
|
|
22
|
+
* revert selectors, the permission-id map and the router traces are recorded in
|
|
23
|
+
* src/__tests__/fixtures/risex/permissions-evidence.json (asserted by risex-evidence.test.ts). The
|
|
24
|
+
* implementation source is not verified on the explorer, so permission NAMES are unknown.
|
|
25
|
+
*/
|
|
26
|
+
import { AbiCoder, Interface, Signature, getAddress, keccak256, toUtf8Bytes, verifyTypedData } from "ethers";
|
|
27
|
+
import { PerpError } from "../../errors.js";
|
|
28
|
+
import { UINT24_MAX, UINT32_MAX, UINT64_MAX } from "./units.js";
|
|
29
|
+
/** Verified 2026-10-14 from GET /v1/system/config and /v1/auth/eip712-domain on each network. */
|
|
30
|
+
export const RISEX_PINS = {
|
|
31
|
+
mainnet: {
|
|
32
|
+
chainId: 4153,
|
|
33
|
+
auth: "0x0D919DAA3f12AE715744Eb648c00066c5DBd66f0",
|
|
34
|
+
router: "0xaadde0cea454f2bcb26f46ed54c5709b7bb34a7e",
|
|
35
|
+
rpcUrl: "https://rpc.risechain.com",
|
|
36
|
+
// eth_getStorageAt(auth, EIP-1967 implementation slot), 2026-10-14.
|
|
37
|
+
authImplementation: "0xCCCb0483342d77AbC4337f3e12514eC8E78b3e28",
|
|
38
|
+
routerImplementation: "0xe2da7a94a81fe9ee58f70ebce862a176280092ae",
|
|
39
|
+
accountRegistry: "0x1238991cac4e65902c08213e79909a9c813eebc3",
|
|
40
|
+
accountRegistryImplementation: "0x531a0c7687cbaf02383b21443619487eb18b91e5",
|
|
41
|
+
},
|
|
42
|
+
testnet: {
|
|
43
|
+
chainId: 11155931,
|
|
44
|
+
auth: "0x6da86f486b5e6536358f5b122dbe184522ca0ee3",
|
|
45
|
+
router: "0x980b8621b8e03c3f396e1dc34c00b14d84f2a20f",
|
|
46
|
+
// eth_chainId 0xaa39db = 11155931 (verified); the testnet system config lists no RPC.
|
|
47
|
+
rpcUrl: "https://testnet.riselabs.xyz",
|
|
48
|
+
authImplementation: "0xc5b84461547eb5397a7c3fd067abfcbc24b53663",
|
|
49
|
+
routerImplementation: "0x9218cd0d7626d9611b62e55dc3659936f9e57f68",
|
|
50
|
+
accountRegistry: "0xf733038054cfb47fa9da1a1d6e79be6fb61f0a70",
|
|
51
|
+
accountRegistryImplementation: "0x99ccc05d52b69bd889a8ab4bc7c12b47aca94b58",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** Check a fetched domain + router against the pins. Throws EXCHANGE_ERROR (nothing is signed). */
|
|
55
|
+
export function assertPinned(net, domain, router, auth) {
|
|
56
|
+
const pin = RISEX_PINS[net];
|
|
57
|
+
const problems = [];
|
|
58
|
+
if (domain.name !== "RISEx" || domain.version !== "1")
|
|
59
|
+
problems.push(`domain ${domain.name}/${domain.version} (expected RISEx/1)`);
|
|
60
|
+
if (domain.chainId !== pin.chainId)
|
|
61
|
+
problems.push(`chainId ${domain.chainId} (pinned ${pin.chainId})`);
|
|
62
|
+
if (domain.verifyingContract.toLowerCase() !== pin.auth.toLowerCase())
|
|
63
|
+
problems.push(`verifyingContract ${domain.verifyingContract} (pinned ${pin.auth})`);
|
|
64
|
+
if (auth.toLowerCase() !== pin.auth.toLowerCase())
|
|
65
|
+
problems.push(`addresses.auth ${auth} (pinned ${pin.auth})`);
|
|
66
|
+
if (router.toLowerCase() !== pin.router.toLowerCase())
|
|
67
|
+
problems.push(`addresses.router ${router} (pinned ${pin.router})`);
|
|
68
|
+
if (problems.length > 0) {
|
|
69
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx ${net} signing configuration changed: ${problems.join("; ")} — nothing was signed`, {
|
|
70
|
+
exchange: "risex",
|
|
71
|
+
remediation: "RISEx redeployed or changed its contracts. perp-cli refuses to sign against unpinned addresses; update perp-cli (RISEX_PINS)",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export const RISEX_TYPES = {
|
|
76
|
+
RegisterSigner: [
|
|
77
|
+
{ name: "account", type: "address" }, { name: "signer", type: "address" }, { name: "message", type: "string" },
|
|
78
|
+
{ name: "expiration", type: "uint32" }, { name: "nonceAnchor", type: "uint48" }, { name: "nonceBitmap", type: "uint8" },
|
|
79
|
+
],
|
|
80
|
+
VerifySigner: [
|
|
81
|
+
{ name: "account", type: "address" }, { name: "nonceAnchor", type: "uint48" }, { name: "nonceBitmap", type: "uint8" },
|
|
82
|
+
],
|
|
83
|
+
RevokeSigner: [
|
|
84
|
+
{ name: "account", type: "address" }, { name: "signer", type: "address" },
|
|
85
|
+
{ name: "nonceAnchor", type: "uint48" }, { name: "nonceBitmap", type: "uint8" },
|
|
86
|
+
],
|
|
87
|
+
VerifyWitness: [
|
|
88
|
+
{ name: "account", type: "address" }, { name: "target", type: "address" }, { name: "hash", type: "bytes32" },
|
|
89
|
+
{ name: "nonceAnchor", type: "uint48" }, { name: "nonceBitmap", type: "uint8" }, { name: "deadline", type: "uint32" },
|
|
90
|
+
],
|
|
91
|
+
DisablePermission: [
|
|
92
|
+
{ name: "account", type: "address" }, { name: "signer", type: "address" }, { name: "permission", type: "uint8" },
|
|
93
|
+
{ name: "nonceAnchor", type: "uint48" }, { name: "nonceBitmap", type: "uint8" },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
/** Canonical EIP-712 type string ("Name(type a,type b)"). */
|
|
97
|
+
export function typeString(primary) {
|
|
98
|
+
return `${primary}(${RISEX_TYPES[primary].map(f => `${f.type} ${f.name}`).join(",")})`;
|
|
99
|
+
}
|
|
100
|
+
export function typeHash(primary) {
|
|
101
|
+
return keccak256(toUtf8Bytes(typeString(primary)));
|
|
102
|
+
}
|
|
103
|
+
/** Read off the mainnet Authorization contract (docs "Verification" + eth_call 2026-10-14). */
|
|
104
|
+
export const RISEX_GOLDEN_TYPEHASHES = {
|
|
105
|
+
RegisterSigner: "0xa526f63b3968e56ae1b177ce9b3dc29766e0891e6397a9c23cf8c53ee8fc8f62",
|
|
106
|
+
VerifySigner: "0x4d298dcceb691695f582cc337308236426a0c97201a31834625e8eadc44d4230",
|
|
107
|
+
RevokeSigner: "0x36db7f392f548b56f37d89469115d138685addf06be45684f9e5b0e8b5d28000",
|
|
108
|
+
VerifyWitness: "0x055e6bcbf2ba5ff1c2ba5dc95b6648a5de6aaab3185251a34e3b88c11e116821",
|
|
109
|
+
DisablePermission: "0xd0246e99c52bee7c7802e154da708a9795b727ad05cb8463639505c1baf0f897",
|
|
110
|
+
};
|
|
111
|
+
// ── Permissions ──
|
|
112
|
+
/** The permission every router action checks (place / cancel / cancel-all / leverage / margin). */
|
|
113
|
+
export const RISEX_TRADING_PERMISSION = 2;
|
|
114
|
+
/** EIP-1967 implementation slot (keccak256("eip1967.proxy.implementation") − 1). */
|
|
115
|
+
export const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
|
|
116
|
+
/**
|
|
117
|
+
* The low bits of `sessionKeys(account, signer)` word 1 that map to permission ids 0..4; a
|
|
118
|
+
* trading-only signer has exactly bit 2 set among them. Bits 5–31 (set to 1 on every new signer,
|
|
119
|
+
* bitmap 0xffffffff) have no permission id in the reviewed implementation: hasPermission and
|
|
120
|
+
* disablePermission both reject ids ≥5 at ABI decoding, so those bits can neither be read as a
|
|
121
|
+
* permission nor cleared. They are guarded instead by the implementation pin and the "no id 5" probe
|
|
122
|
+
* (assertTradingOnlyOnChain, session.ts).
|
|
123
|
+
*/
|
|
124
|
+
export const RISEX_PERMISSION_ID_MASK = 0x1fn;
|
|
125
|
+
export const RISEX_TRADING_ONLY_BITS = 1n << 2n;
|
|
126
|
+
/** Valid permission ids (hasPermission reverts above 4). */
|
|
127
|
+
export const RISEX_PERMISSION_IDS = [0, 1, 2, 3, 4];
|
|
128
|
+
/** Permissions perp-cli disables right after registration (all except trading). */
|
|
129
|
+
export const RISEX_NON_TRADING_PERMISSIONS = RISEX_PERMISSION_IDS.filter(p => p !== RISEX_TRADING_PERMISSION);
|
|
130
|
+
export const RISEX_AUTH_IFACE = new Interface([
|
|
131
|
+
"function disablePermission(address account, address signer, uint48 nonceAnchor, uint8 nonceBitmap, uint8 permission, bytes signature)",
|
|
132
|
+
"function hasPermission(address account, address signer, uint8 permission) view returns (bool)",
|
|
133
|
+
"function getSessionKeyStatus(address account, address signer) view returns (uint8)",
|
|
134
|
+
]);
|
|
135
|
+
// ── Action hashes ──
|
|
136
|
+
const coder = AbiCoder.defaultAbiCoder();
|
|
137
|
+
const selector = (name) => keccak256(toUtf8Bytes(name));
|
|
138
|
+
export const ACTION = {
|
|
139
|
+
PLACE_ORDER: "RISE_PERPS_PLACE_ORDER_V1",
|
|
140
|
+
CANCEL_ORDER: "RISE_PERPS_CANCEL_ORDER_V1",
|
|
141
|
+
CANCEL_ALL_ORDERS: "RISE_PERPS_CANCEL_ALL_ORDERS_V1",
|
|
142
|
+
UPDATE_LEVERAGE: "RISE_PERPS_UPDATE_LEVERAGE_V1",
|
|
143
|
+
UPDATE_MARGIN_MODE: "RISE_PERPS_UPDATE_MARGIN_MODE_V1",
|
|
144
|
+
};
|
|
145
|
+
export const SIDE = { buy: 0, sell: 1 };
|
|
146
|
+
export const ORDER_TYPE = { market: 0, limit: 1 };
|
|
147
|
+
/** Venue TIF codes. ALO is GTC + post_only. */
|
|
148
|
+
export const TIF_CODE = { GTC: 0, GTT: 1, FOK: 2, IOC: 3 };
|
|
149
|
+
export const STP_EXPIRE_MAKER = 0;
|
|
150
|
+
export const MARGIN_MODE_CODE = { cross: 0, isolated: 1 };
|
|
151
|
+
/** uint88 order word: [87:70] market, [69:38] size steps, [37:14] price ticks, [13:6] flags, [5:1] version 1. */
|
|
152
|
+
export function packOrder(o) {
|
|
153
|
+
if (!Number.isInteger(o.marketId) || o.marketId < 0 || o.marketId > 0xffff)
|
|
154
|
+
throw new PerpError("INVALID_PARAMS", `RISEx market id ${o.marketId} out of uint16 range`);
|
|
155
|
+
if (o.sizeSteps <= 0n || o.sizeSteps > UINT32_MAX)
|
|
156
|
+
throw new PerpError("INVALID_PARAMS", `RISEx size steps ${o.sizeSteps} out of uint32 range`);
|
|
157
|
+
if (o.priceTicks < 0n || o.priceTicks > UINT24_MAX)
|
|
158
|
+
throw new PerpError("INVALID_PARAMS", `RISEx price ticks ${o.priceTicks} out of uint24 range`);
|
|
159
|
+
const flags = (o.side & 1) | ((o.postOnly ? 1 : 0) << 1) | ((o.reduceOnly ? 1 : 0) << 2)
|
|
160
|
+
| ((o.stpMode & 3) << 3) | ((o.orderType & 1) << 5) | ((o.tif & 3) << 6);
|
|
161
|
+
return (BigInt(o.marketId) << 70n) | (o.sizeSteps << 38n) | (o.priceTicks << 14n) | (BigInt(flags) << 6n) | (1n << 1n);
|
|
162
|
+
}
|
|
163
|
+
/** Inverse of packOrder (tests / diagnostics). */
|
|
164
|
+
export function unpackOrder(word) {
|
|
165
|
+
const flags = Number((word >> 6n) & 0xffn);
|
|
166
|
+
return {
|
|
167
|
+
marketId: Number((word >> 70n) & 0xffffn),
|
|
168
|
+
sizeSteps: (word >> 38n) & UINT32_MAX,
|
|
169
|
+
priceTicks: (word >> 14n) & UINT24_MAX,
|
|
170
|
+
side: (flags & 1),
|
|
171
|
+
postOnly: ((flags >> 1) & 1) === 1,
|
|
172
|
+
reduceOnly: ((flags >> 2) & 1) === 1,
|
|
173
|
+
stpMode: (flags >> 3) & 3,
|
|
174
|
+
orderType: ((flags >> 5) & 1),
|
|
175
|
+
tif: ((flags >> 6) & 3),
|
|
176
|
+
version: Number((word >> 1n) & 0x1fn),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Header flags: 0x01 permit (always), 0x04 client order id present. Builder (0x02) / TTL (0x10) are never sent. */
|
|
180
|
+
export function headerFlags(clientOrderId) {
|
|
181
|
+
return 0x01 | (clientOrderId !== 0n ? 0x04 : 0);
|
|
182
|
+
}
|
|
183
|
+
/** keccak256(abi.encode(sel, uint8 header, uint88 order, uint16 builderId=0, uint64 clientOrderId, uint16 ttl=0)). */
|
|
184
|
+
export function placeOrderHash(order, clientOrderId) {
|
|
185
|
+
if (clientOrderId < 0n || clientOrderId > UINT64_MAX)
|
|
186
|
+
throw new PerpError("INVALID_PARAMS", `RISEx client order id ${clientOrderId} out of uint64 range`);
|
|
187
|
+
return keccak256(coder.encode(["bytes32", "uint8", "uint256", "uint16", "uint64", "uint16"], [selector(ACTION.PLACE_ORDER), headerFlags(clientOrderId), order, 0, clientOrderId, 0]));
|
|
188
|
+
}
|
|
189
|
+
export function cancelOrderHash(marketId, restingOrderId) {
|
|
190
|
+
return keccak256(coder.encode(["bytes32", "uint256", "uint256"], [selector(ACTION.CANCEL_ORDER), BigInt(marketId), restingOrderId]));
|
|
191
|
+
}
|
|
192
|
+
export function cancelAllHash(marketId) {
|
|
193
|
+
return keccak256(coder.encode(["bytes32", "uint256"], [selector(ACTION.CANCEL_ALL_ORDERS), BigInt(marketId)]));
|
|
194
|
+
}
|
|
195
|
+
export function leverageHash(marketId, leverage) {
|
|
196
|
+
if (!Number.isInteger(leverage) || leverage < 1 || leverage > 255)
|
|
197
|
+
throw new PerpError("INVALID_PARAMS", `RISEx leverage must be an integer 1..255 (got ${leverage})`);
|
|
198
|
+
return keccak256(coder.encode(["bytes32", "uint16", "uint8"], [selector(ACTION.UPDATE_LEVERAGE), marketId, leverage]));
|
|
199
|
+
}
|
|
200
|
+
export function marginModeHash(marketId, mode) {
|
|
201
|
+
return keccak256(coder.encode(["bytes32", "uint16", "uint8"], [selector(ACTION.UPDATE_MARGIN_MODE), marketId, mode]));
|
|
202
|
+
}
|
|
203
|
+
function domainOf(d) {
|
|
204
|
+
return { name: d.name, version: d.version, chainId: d.chainId, verifyingContract: getAddress(d.verifyingContract) };
|
|
205
|
+
}
|
|
206
|
+
/** Sign typed data and verify it recovers `expected` (an OWS canonicalization slip never leaves this process). */
|
|
207
|
+
export async function signChecked(signer, domain, primary, message) {
|
|
208
|
+
const types = { [primary]: RISEX_TYPES[primary] };
|
|
209
|
+
const sig = await signer.signTypedData(domainOf(domain), types, message);
|
|
210
|
+
const recovered = verifyTypedData(domainOf(domain), types, message, sig);
|
|
211
|
+
if (recovered.toLowerCase() !== signer.getAddress().toLowerCase()) {
|
|
212
|
+
throw new PerpError("SIGNATURE_FAILED", `RISEx ${primary} signature recovers ${recovered}, expected ${signer.getAddress()} — nothing was sent`, { exchange: "risex" });
|
|
213
|
+
}
|
|
214
|
+
return Signature.from(sig).serialized;
|
|
215
|
+
}
|
|
216
|
+
/** 65-byte r‖s‖v hex → base64 (the permit field is protobuf bytes). */
|
|
217
|
+
export function permitSignatureBase64(hexSig) {
|
|
218
|
+
const bytes = Buffer.from(hexSig.replace(/^0x/, ""), "hex");
|
|
219
|
+
if (bytes.length !== 65)
|
|
220
|
+
throw new PerpError("SIGNATURE_FAILED", `RISEx permit signature is ${bytes.length} bytes, expected 65`, { exchange: "risex" });
|
|
221
|
+
return bytes.toString("base64");
|
|
222
|
+
}
|
|
223
|
+
/** The signer's VerifyWitness permit for one action hash. */
|
|
224
|
+
export async function signPermit(p) {
|
|
225
|
+
if (p.slot.bit < 0 || p.slot.bit > 207)
|
|
226
|
+
throw new PerpError("INVALID_PARAMS", `RISEx nonce bit ${p.slot.bit} out of 0..207`);
|
|
227
|
+
const message = {
|
|
228
|
+
account: getAddress(p.account), target: getAddress(p.router), hash: p.actionHash,
|
|
229
|
+
nonceAnchor: p.slot.anchor, nonceBitmap: p.slot.bit, deadline: p.deadline,
|
|
230
|
+
};
|
|
231
|
+
const sig = await signChecked(p.signer, p.domain, "VerifyWitness", message);
|
|
232
|
+
return {
|
|
233
|
+
account: getAddress(p.account),
|
|
234
|
+
signer: getAddress(p.signer.getAddress()),
|
|
235
|
+
nonce_anchor: p.slot.anchor.toString(),
|
|
236
|
+
nonce_bitmap_index: p.slot.bit,
|
|
237
|
+
deadline: p.deadline,
|
|
238
|
+
signature: permitSignatureBase64(sig),
|
|
239
|
+
};
|
|
240
|
+
}
|