perp-cli 0.9.7 → 0.11.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/dist/bot/engine.js +23 -4
- package/dist/bot/presets.js +3 -3
- package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
- package/dist/bot/strategies/funding-arb-strategy.js +41 -176
- package/dist/bot/strategies/funding-arb-utils.js +9 -1
- package/dist/bot/strategies/funding-arb-v2-strategy.js +20 -11
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +2 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +26 -18
- package/dist/bot/strategies/spot-perp-executor.js +4 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/ows.d.ts +2 -0
- package/dist/commands/ows.js +887 -0
- package/dist/commands/wallet.js +378 -160
- package/dist/config.d.ts +9 -0
- package/dist/config.js +55 -9
- package/dist/exchanges/aster.js +2 -1
- package/dist/exchanges/hyperliquid.d.ts +1 -0
- package/dist/exchanges/hyperliquid.js +5 -0
- package/dist/exchanges/interface.d.ts +2 -0
- package/dist/exchanges/lighter-spot.js +5 -2
- package/dist/exchanges/lighter.js +14 -4
- package/dist/guardrail/contracts.d.ts +14 -0
- package/dist/guardrail/contracts.js +72 -0
- package/dist/guardrail/perp-guardrail.d.ts +21 -0
- package/dist/guardrail/perp-guardrail.js +137 -0
- package/dist/index.js +54 -4
- package/dist/mcp-server.js +11 -9
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +2 -0
- package/dist/signer/index.d.ts +2 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/ows-evm.d.ts +18 -0
- package/dist/signer/ows-evm.js +67 -0
- package/dist/signer/ows-loader.d.ts +2 -0
- package/dist/signer/ows-loader.js +6 -0
- package/dist/signer/ows-solana.d.ts +25 -0
- package/dist/signer/ows-solana.js +56 -0
- package/package.json +4 -2
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export type Exchange = "pacifica" | "hyperliquid" | "lighter" | "aster";
|
|
|
5
5
|
* Returns null if no key is configured.
|
|
6
6
|
*/
|
|
7
7
|
export declare function tryLoadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve an OWS wallet name to a signer-ready marker.
|
|
10
|
+
* Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveOwsWallet(walletName: string): string | null;
|
|
13
|
+
/** Check if a key string represents an OWS wallet reference. */
|
|
14
|
+
export declare function isOwsKey(key: string): boolean;
|
|
15
|
+
/** Extract OWS wallet name from an ows: prefixed key string. */
|
|
16
|
+
export declare function getOwsWalletName(key: string): string;
|
|
8
17
|
export declare function loadPrivateKey(exchange: Exchange, pkOverride?: string, walletName?: string): Promise<string>;
|
|
9
18
|
export declare function parseSolanaKeypair(input: string): Keypair;
|
|
10
19
|
export declare function isEvmPrivateKey(input: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,8 @@ import bs58 from "bs58";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
6
8
|
/**
|
|
7
9
|
* Try to load a private key without throwing if none is found.
|
|
8
10
|
* Returns null if no key is configured.
|
|
@@ -15,9 +17,40 @@ export async function tryLoadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an OWS wallet name to a signer-ready marker.
|
|
22
|
+
* Returns the OWS wallet name prefixed with "ows:" so callers can detect OWS mode.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveOwsWallet(walletName) {
|
|
25
|
+
try {
|
|
26
|
+
const { getWallet } = _require("@open-wallet-standard/core");
|
|
27
|
+
getWallet(walletName); // throws if not found
|
|
28
|
+
return `ows:${walletName}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Check if a key string represents an OWS wallet reference. */
|
|
35
|
+
export function isOwsKey(key) {
|
|
36
|
+
return key.startsWith("ows:");
|
|
37
|
+
}
|
|
38
|
+
/** Extract OWS wallet name from an ows: prefixed key string. */
|
|
39
|
+
export function getOwsWalletName(key) {
|
|
40
|
+
return key.slice(4);
|
|
41
|
+
}
|
|
18
42
|
export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
19
|
-
//
|
|
43
|
+
// 0a. OWS wallet (--ows flag sets walletName with "ows:" prefix)
|
|
44
|
+
if (walletName?.startsWith("ows:")) {
|
|
45
|
+
return walletName; // pass through — adapters detect the ows: prefix
|
|
46
|
+
}
|
|
47
|
+
// 0b. Wallet name override (--wallet flag) — check OWS first, then local
|
|
20
48
|
if (walletName) {
|
|
49
|
+
// Try OWS vault
|
|
50
|
+
const owsKey = resolveOwsWallet(walletName);
|
|
51
|
+
if (owsKey)
|
|
52
|
+
return owsKey;
|
|
53
|
+
// Fall back to local wallets
|
|
21
54
|
const { getWalletKeyByName } = await import("./commands/wallet.js");
|
|
22
55
|
const key = getWalletKeyByName(walletName);
|
|
23
56
|
if (!key)
|
|
@@ -27,7 +60,20 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
27
60
|
// 1. CLI flag
|
|
28
61
|
if (pkOverride)
|
|
29
62
|
return pkOverride;
|
|
30
|
-
// 2.
|
|
63
|
+
// 2. OWS active wallet (highest priority after explicit flags)
|
|
64
|
+
try {
|
|
65
|
+
const { loadSettings } = await import("./settings.js");
|
|
66
|
+
const settings = loadSettings();
|
|
67
|
+
if (settings.owsActiveWallet) {
|
|
68
|
+
const owsKey = resolveOwsWallet(settings.owsActiveWallet);
|
|
69
|
+
if (owsKey)
|
|
70
|
+
return owsKey;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// settings module not available, skip
|
|
75
|
+
}
|
|
76
|
+
// 3. Exchange-specific env vars
|
|
31
77
|
const envMap = {
|
|
32
78
|
pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
|
|
33
79
|
hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
|
|
@@ -38,10 +84,10 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
38
84
|
if (process.env[envVar])
|
|
39
85
|
return process.env[envVar];
|
|
40
86
|
}
|
|
41
|
-
//
|
|
87
|
+
// 4. Generic fallback
|
|
42
88
|
if (process.env.PRIVATE_KEY)
|
|
43
89
|
return process.env.PRIVATE_KEY;
|
|
44
|
-
//
|
|
90
|
+
// 5. Legacy: Active wallet from wallets.json
|
|
45
91
|
try {
|
|
46
92
|
const { getActiveWalletKey } = await import("./commands/wallet.js");
|
|
47
93
|
const walletKey = getActiveWalletKey(exchange);
|
|
@@ -56,12 +102,12 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
56
102
|
if (existsSync(keyFile)) {
|
|
57
103
|
return readFileSync(keyFile, "utf-8").trim();
|
|
58
104
|
}
|
|
59
|
-
throw new Error(`No
|
|
60
|
-
` Quick start: ${chalk.cyan("perp
|
|
105
|
+
throw new Error(`No wallet configured for ${exchange}.\n\n` +
|
|
106
|
+
` Quick start: ${chalk.cyan("perp wallet generate")}\n\n` +
|
|
61
107
|
` Or manually:\n` +
|
|
62
|
-
` perp wallet
|
|
63
|
-
` ${
|
|
64
|
-
` --private-key <key>
|
|
108
|
+
` perp wallet import <key> Import key to OWS vault\n` +
|
|
109
|
+
` perp wallet set ${exchange} <key> Legacy: set env var\n` +
|
|
110
|
+
` --private-key <key> Per-command override`);
|
|
65
111
|
}
|
|
66
112
|
export function parseSolanaKeypair(input) {
|
|
67
113
|
// Try base58 first
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -457,7 +457,8 @@ export class AsterAdapter {
|
|
|
457
457
|
}
|
|
458
458
|
if (!res.ok) {
|
|
459
459
|
const text = await res.text();
|
|
460
|
-
|
|
460
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
461
|
+
throw new Error(`${method} ${path} failed (${res.status}): ${clean}`);
|
|
461
462
|
}
|
|
462
463
|
return res.json();
|
|
463
464
|
}
|
|
@@ -4,6 +4,7 @@ export class HyperliquidAdapter {
|
|
|
4
4
|
name = "hyperliquid";
|
|
5
5
|
chain = "evm";
|
|
6
6
|
aliases = ["hl"];
|
|
7
|
+
isUnifiedAccount = true;
|
|
7
8
|
sdk;
|
|
8
9
|
_address;
|
|
9
10
|
_privateKey;
|
|
@@ -54,6 +55,7 @@ export class HyperliquidAdapter {
|
|
|
54
55
|
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
55
56
|
setSigner(signer) {
|
|
56
57
|
this._evmSigner = signer;
|
|
58
|
+
this._address = signer.getAddress();
|
|
57
59
|
}
|
|
58
60
|
async init() {
|
|
59
61
|
// Suppress "WebSocket connected" noise from hyperliquid SDK
|
|
@@ -70,6 +72,9 @@ export class HyperliquidAdapter {
|
|
|
70
72
|
this._evmSigner = await LocalEvmSigner.create(this._privateKey);
|
|
71
73
|
this._address = this._evmSigner.getAddress();
|
|
72
74
|
}
|
|
75
|
+
else if (this._evmSigner && !this._address) {
|
|
76
|
+
this._address = this._evmSigner.getAddress();
|
|
77
|
+
}
|
|
73
78
|
// Build asset index map
|
|
74
79
|
await this._loadAssetMap();
|
|
75
80
|
}
|
|
@@ -66,6 +66,8 @@ export interface ExchangeAdapter {
|
|
|
66
66
|
readonly name: string;
|
|
67
67
|
readonly chain?: string;
|
|
68
68
|
readonly aliases?: readonly string[];
|
|
69
|
+
/** True if perp and spot share the same balance (e.g., Hyperliquid). False if separate accounts (e.g., Lighter). */
|
|
70
|
+
readonly isUnifiedAccount?: boolean;
|
|
69
71
|
init?(): Promise<void>;
|
|
70
72
|
getMarkets(): Promise<ExchangeMarketInfo[]>;
|
|
71
73
|
getOrderbook(symbol: string): Promise<{
|
|
@@ -528,8 +528,11 @@ export class LighterSpotAdapter {
|
|
|
528
528
|
for (const [k, v] of Object.entries(params))
|
|
529
529
|
url.searchParams.set(k, v);
|
|
530
530
|
const res = await fetch(url.toString());
|
|
531
|
-
if (!res.ok)
|
|
532
|
-
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
const text = await res.text();
|
|
533
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
534
|
+
throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
|
|
535
|
+
}
|
|
533
536
|
return res.json();
|
|
534
537
|
}
|
|
535
538
|
async _restGetAuth(path, params) {
|
|
@@ -64,6 +64,7 @@ export class LighterAdapter {
|
|
|
64
64
|
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
65
65
|
setSigner(signer) {
|
|
66
66
|
this._evmSigner = signer;
|
|
67
|
+
this._address = signer.getAddress();
|
|
67
68
|
}
|
|
68
69
|
async init() {
|
|
69
70
|
// Initialize EVM signer if not externally injected (skip if no key — read-only mode)
|
|
@@ -71,6 +72,9 @@ export class LighterAdapter {
|
|
|
71
72
|
this._evmSigner = await LocalEvmSigner.create(this._evmKey);
|
|
72
73
|
this._address = this._evmSigner.getAddress();
|
|
73
74
|
}
|
|
75
|
+
else if (this._evmSigner && !this._address) {
|
|
76
|
+
this._address = this._evmSigner.getAddress();
|
|
77
|
+
}
|
|
74
78
|
// Fetch account index from REST API (skip if no address — read-only mode)
|
|
75
79
|
if (!this._address) {
|
|
76
80
|
await this._refreshMarketMap();
|
|
@@ -961,8 +965,11 @@ export class LighterAdapter {
|
|
|
961
965
|
is_external_deposit: "true",
|
|
962
966
|
}),
|
|
963
967
|
});
|
|
964
|
-
if (!res.ok)
|
|
965
|
-
|
|
968
|
+
if (!res.ok) {
|
|
969
|
+
const text = await res.text();
|
|
970
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
971
|
+
throw new Error(`createIntentAddress failed: ${clean}`);
|
|
972
|
+
}
|
|
966
973
|
return res.json();
|
|
967
974
|
}
|
|
968
975
|
/**
|
|
@@ -1035,8 +1042,11 @@ export class LighterAdapter {
|
|
|
1035
1042
|
for (const [k, v] of Object.entries(params))
|
|
1036
1043
|
url.searchParams.set(k, v);
|
|
1037
1044
|
const res = await fetch(url.toString());
|
|
1038
|
-
if (!res.ok)
|
|
1039
|
-
|
|
1045
|
+
if (!res.ok) {
|
|
1046
|
+
const text = await res.text();
|
|
1047
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1048
|
+
throw new Error(`GET ${path} failed (${res.status}): ${clean}`);
|
|
1049
|
+
}
|
|
1040
1050
|
return res.json();
|
|
1041
1051
|
}
|
|
1042
1052
|
/** Generate a unique client order index (uint48). */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEX contract whitelist for perp-guardrail policy executable.
|
|
3
|
+
* Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
|
|
4
|
+
*/
|
|
5
|
+
export declare const USDC_ADDRESSES: Record<string, string>;
|
|
6
|
+
export declare const ALLOWED_CONTRACTS: Record<string, string[]>;
|
|
7
|
+
export declare const ALLOWED_CHAINS: string[];
|
|
8
|
+
export declare const DEFAULT_LIMITS: {
|
|
9
|
+
max_tx_usd: number;
|
|
10
|
+
max_daily_usd: number;
|
|
11
|
+
max_withdraw_usd: number;
|
|
12
|
+
max_daily_withdraw_usd: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const DANGEROUS_SELECTORS: string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEX contract whitelist for perp-guardrail policy executable.
|
|
3
|
+
* Keyed by CAIP-2 chain ID → array of allowed contract/program addresses.
|
|
4
|
+
*/
|
|
5
|
+
// ── Pacifica (Solana) ──
|
|
6
|
+
const PACIFICA_MAINNET = [
|
|
7
|
+
"PCFA5iYgmqK6MqPhWNKg7Yv7auX7VZ4Cx7T1eJyrAMH", // Pacifica program
|
|
8
|
+
"9Gdmhq4Gv1LnNMp7aiS1HSVd7pNnXNMsbuXALCQRmGjY", // Central state
|
|
9
|
+
"72R843XwZxqWhsJceARQQTTbYtWy6Zw9et2YV4FpRHTa", // Vault
|
|
10
|
+
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
|
|
11
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
|
|
12
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token
|
|
13
|
+
"11111111111111111111111111111111", // System Program
|
|
14
|
+
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", // CCTP V2 Token Messenger
|
|
15
|
+
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", // CCTP V2 Message Transmitter
|
|
16
|
+
];
|
|
17
|
+
const PACIFICA_TESTNET = [
|
|
18
|
+
"peRPsYCcB1J9jvrs29jiGdjkytxs8uHLmSPLKKP9ptm", // Testnet program
|
|
19
|
+
"2zPRq1Qvdq5A4Ld6WsH7usgCge4ApZRYfhhf5VAjfXxv", // Testnet central state
|
|
20
|
+
"5SDFdHZGTZbyRYu54CgmRkCGnPHC5pYaN27p7XGLqnBs", // Testnet vault
|
|
21
|
+
"USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM", // Testnet USDC
|
|
22
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
23
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
24
|
+
"11111111111111111111111111111111",
|
|
25
|
+
];
|
|
26
|
+
// ── Hyperliquid (Arbitrum + HyperCore) ──
|
|
27
|
+
const HYPERLIQUID_CONTRACTS = [
|
|
28
|
+
"0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757", // CctpForwarder (HyperCore)
|
|
29
|
+
];
|
|
30
|
+
// ── CCTP V2 (shared across EVM chains) ──
|
|
31
|
+
const CCTP_V2_EVM = [
|
|
32
|
+
"0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", // TokenMessengerV2
|
|
33
|
+
"0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", // MessageTransmitterV2
|
|
34
|
+
];
|
|
35
|
+
// ── USDC addresses ──
|
|
36
|
+
export const USDC_ADDRESSES = {
|
|
37
|
+
"eip155:1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum
|
|
38
|
+
"eip155:42161": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum
|
|
39
|
+
"eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
|
|
40
|
+
};
|
|
41
|
+
// ── Complete whitelist by CAIP-2 chain ──
|
|
42
|
+
export const ALLOWED_CONTRACTS = {
|
|
43
|
+
// Solana mainnet
|
|
44
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": PACIFICA_MAINNET,
|
|
45
|
+
// Solana devnet
|
|
46
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": PACIFICA_TESTNET,
|
|
47
|
+
// Arbitrum (Hyperliquid deposits + CCTP)
|
|
48
|
+
"eip155:42161": [...HYPERLIQUID_CONTRACTS, ...CCTP_V2_EVM, USDC_ADDRESSES["eip155:42161"]],
|
|
49
|
+
// Hyperliquid signing domain (phantom chain for typed data)
|
|
50
|
+
"eip155:1337": [], // no contract calls, only typed data signing
|
|
51
|
+
// Base (CCTP bridge + USDC)
|
|
52
|
+
"eip155:8453": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:8453"]],
|
|
53
|
+
// Lighter (ZK rollup)
|
|
54
|
+
"eip155:304": [], // signing-based, no direct contract interaction
|
|
55
|
+
"eip155:300": [], // Lighter testnet
|
|
56
|
+
// Ethereum mainnet (CCTP + USDC)
|
|
57
|
+
"eip155:1": [...CCTP_V2_EVM, USDC_ADDRESSES["eip155:1"]],
|
|
58
|
+
};
|
|
59
|
+
// ── Allowed chain IDs (all chains our DEXes operate on) ──
|
|
60
|
+
export const ALLOWED_CHAINS = Object.keys(ALLOWED_CONTRACTS);
|
|
61
|
+
// ── Default guardrail limits ──
|
|
62
|
+
export const DEFAULT_LIMITS = {
|
|
63
|
+
max_tx_usd: 1000,
|
|
64
|
+
max_daily_usd: 5000,
|
|
65
|
+
max_withdraw_usd: 500,
|
|
66
|
+
max_daily_withdraw_usd: 2000,
|
|
67
|
+
};
|
|
68
|
+
// ── Common blocked selectors ──
|
|
69
|
+
export const DANGEROUS_SELECTORS = [
|
|
70
|
+
"0x095ea7b3", // approve(address,uint256) — unlimited token approvals
|
|
71
|
+
"0x23b872dd", // transferFrom(address,address,uint256)
|
|
72
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* perp-guardrail — OWS Policy Engine custom executable.
|
|
4
|
+
*
|
|
5
|
+
* Protocol:
|
|
6
|
+
* stdin → PolicyContext JSON (from OWS engine)
|
|
7
|
+
* stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
|
|
8
|
+
*
|
|
9
|
+
* Validates:
|
|
10
|
+
* 1. Contract address whitelist (per chain)
|
|
11
|
+
* 2. Per-transaction USD amount limit
|
|
12
|
+
* 3. Daily cumulative spending limit
|
|
13
|
+
* 4. Withdrawal limits (per-tx and daily)
|
|
14
|
+
* 5. Blocked function selectors (e.g. approve)
|
|
15
|
+
* 6. Allowed trading symbols
|
|
16
|
+
*
|
|
17
|
+
* Configuration is read from PolicyContext.policy_config:
|
|
18
|
+
* { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
|
|
19
|
+
* allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* perp-guardrail — OWS Policy Engine custom executable.
|
|
4
|
+
*
|
|
5
|
+
* Protocol:
|
|
6
|
+
* stdin → PolicyContext JSON (from OWS engine)
|
|
7
|
+
* stdout → PolicyResult JSON {"allow": bool, "reason"?: string}
|
|
8
|
+
*
|
|
9
|
+
* Validates:
|
|
10
|
+
* 1. Contract address whitelist (per chain)
|
|
11
|
+
* 2. Per-transaction USD amount limit
|
|
12
|
+
* 3. Daily cumulative spending limit
|
|
13
|
+
* 4. Withdrawal limits (per-tx and daily)
|
|
14
|
+
* 5. Blocked function selectors (e.g. approve)
|
|
15
|
+
* 6. Allowed trading symbols
|
|
16
|
+
*
|
|
17
|
+
* Configuration is read from PolicyContext.policy_config:
|
|
18
|
+
* { max_tx_usd, max_daily_usd, max_withdraw_usd, max_daily_withdraw_usd,
|
|
19
|
+
* allowed_symbols?, block_selectors?, allowed_contracts?, allowed_chains? }
|
|
20
|
+
*/
|
|
21
|
+
import { ALLOWED_CONTRACTS, ALLOWED_CHAINS, DEFAULT_LIMITS } from "./contracts.js";
|
|
22
|
+
function deny(reason) {
|
|
23
|
+
return { allow: false, reason };
|
|
24
|
+
}
|
|
25
|
+
function allow() {
|
|
26
|
+
return { allow: true };
|
|
27
|
+
}
|
|
28
|
+
function weiToUsd(weiStr, ethPriceUsd) {
|
|
29
|
+
const wei = BigInt(weiStr || "0");
|
|
30
|
+
const ethValue = Number(wei) / 1e18;
|
|
31
|
+
return ethValue * ethPriceUsd;
|
|
32
|
+
}
|
|
33
|
+
// Well-known EVM function selectors
|
|
34
|
+
const KNOWN_SELECTORS = {
|
|
35
|
+
"0x095ea7b3": "approve",
|
|
36
|
+
"0x23b872dd": "transferFrom",
|
|
37
|
+
"0xa9059cbb": "transfer",
|
|
38
|
+
"0x3ccfd60b": "withdraw",
|
|
39
|
+
"0x2e1a7d4d": "withdraw(uint256)",
|
|
40
|
+
"0xd0e30db0": "deposit",
|
|
41
|
+
};
|
|
42
|
+
function isWithdrawTx(data) {
|
|
43
|
+
if (!data || data === "0x")
|
|
44
|
+
return false;
|
|
45
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
46
|
+
return selector === "0x3ccfd60b" || selector === "0x2e1a7d4d";
|
|
47
|
+
}
|
|
48
|
+
function evaluate(ctx) {
|
|
49
|
+
const config = ctx.policy_config ?? {};
|
|
50
|
+
const maxTxUsd = config.max_tx_usd ?? DEFAULT_LIMITS.max_tx_usd;
|
|
51
|
+
const maxDailyUsd = config.max_daily_usd ?? DEFAULT_LIMITS.max_daily_usd;
|
|
52
|
+
const maxWithdrawUsd = config.max_withdraw_usd ?? maxTxUsd;
|
|
53
|
+
const maxDailyWithdrawUsd = config.max_daily_withdraw_usd ?? maxDailyUsd;
|
|
54
|
+
const contracts = config.allowed_contracts ?? ALLOWED_CONTRACTS;
|
|
55
|
+
const chains = config.allowed_chains ?? ALLOWED_CHAINS;
|
|
56
|
+
const ethPrice = config.eth_price_usd ?? 3500;
|
|
57
|
+
const blockSelectors = config.block_selectors ?? [];
|
|
58
|
+
// 1. Chain check
|
|
59
|
+
if (!chains.includes(ctx.chain_id)) {
|
|
60
|
+
return deny(`chain ${ctx.chain_id} not in allowed list`);
|
|
61
|
+
}
|
|
62
|
+
// 2. Contract whitelist (skip for signing-only chains like eip155:1337, eip155:304)
|
|
63
|
+
const chainContracts = contracts[ctx.chain_id];
|
|
64
|
+
if (chainContracts && chainContracts.length > 0 && ctx.transaction.to) {
|
|
65
|
+
const target = ctx.transaction.to.toLowerCase();
|
|
66
|
+
const allowed = chainContracts.map(a => a.toLowerCase());
|
|
67
|
+
if (!allowed.includes(target)) {
|
|
68
|
+
return deny(`contract ${ctx.transaction.to} not whitelisted for ${ctx.chain_id}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 3. Blocked function selectors (e.g. approve, transferFrom)
|
|
72
|
+
if (blockSelectors.length > 0 && ctx.transaction.data && ctx.transaction.data !== "0x") {
|
|
73
|
+
const selector = ctx.transaction.data.slice(0, 10).toLowerCase();
|
|
74
|
+
if (blockSelectors.map(s => s.toLowerCase()).includes(selector)) {
|
|
75
|
+
const name = KNOWN_SELECTORS[selector] || selector;
|
|
76
|
+
return deny(`function ${name} is blocked by policy`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 4. Per-transaction amount
|
|
80
|
+
if (ctx.transaction.value && ctx.transaction.value !== "0") {
|
|
81
|
+
const txUsd = weiToUsd(ctx.transaction.value, ethPrice);
|
|
82
|
+
if (txUsd > maxTxUsd) {
|
|
83
|
+
return deny(`tx value $${txUsd.toFixed(2)} exceeds limit $${maxTxUsd}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 5. Withdrawal-specific limits
|
|
87
|
+
if (isWithdrawTx(ctx.transaction.data)) {
|
|
88
|
+
if (ctx.transaction.value && ctx.transaction.value !== "0") {
|
|
89
|
+
const withdrawUsd = weiToUsd(ctx.transaction.value, ethPrice);
|
|
90
|
+
if (withdrawUsd > maxWithdrawUsd) {
|
|
91
|
+
return deny(`withdrawal $${withdrawUsd.toFixed(2)} exceeds per-tx limit $${maxWithdrawUsd}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Daily withdrawal check uses same daily_total (OWS tracks cumulative)
|
|
95
|
+
if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
|
|
96
|
+
const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
|
|
97
|
+
if (dailyUsd > maxDailyWithdrawUsd) {
|
|
98
|
+
return deny(`daily withdrawals $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyWithdrawUsd}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 6. Daily spending limit (all operations)
|
|
103
|
+
if (ctx.spending.daily_total && ctx.spending.daily_total !== "0") {
|
|
104
|
+
const dailyUsd = weiToUsd(ctx.spending.daily_total, ethPrice);
|
|
105
|
+
if (dailyUsd > maxDailyUsd) {
|
|
106
|
+
return deny(`daily spending $${dailyUsd.toFixed(2)} exceeds limit $${maxDailyUsd}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return allow();
|
|
110
|
+
}
|
|
111
|
+
// ── Main: read stdin, evaluate, write stdout ──
|
|
112
|
+
async function main() {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
for await (const chunk of process.stdin) {
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
}
|
|
117
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
118
|
+
if (!input) {
|
|
119
|
+
process.stdout.write(JSON.stringify(deny("empty stdin")));
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
let ctx;
|
|
123
|
+
try {
|
|
124
|
+
ctx = JSON.parse(input);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
process.stdout.write(JSON.stringify(deny("invalid JSON on stdin")));
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
const result = evaluate(ctx);
|
|
131
|
+
process.stdout.write(JSON.stringify(result));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
main().catch((e) => {
|
|
135
|
+
process.stdout.write(JSON.stringify(deny(`internal error: ${e instanceof Error ? e.message : String(e)}`)));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ config();
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { Keypair } from "@solana/web3.js";
|
|
13
|
-
import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
|
|
13
|
+
import { tryLoadPrivateKey, parseSolanaKeypair, isOwsKey, getOwsWalletName } from "./config.js";
|
|
14
14
|
import { PacificaAdapter } from "./exchanges/pacifica.js";
|
|
15
15
|
import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
|
|
16
16
|
import { resolveExchangeName } from "./exchanges/registry.js";
|
|
@@ -21,6 +21,7 @@ import { registerManageCommands } from "./commands/manage.js";
|
|
|
21
21
|
// stream commands removed — WS feeds still used by dashboard/event-stream internally
|
|
22
22
|
import { registerArbCommands } from "./commands/arb.js";
|
|
23
23
|
import { registerWalletCommands } from "./commands/wallet.js";
|
|
24
|
+
import { registerOwsCommands } from "./commands/ows.js";
|
|
24
25
|
import { registerBridgeCommands } from "./commands/bridge.js";
|
|
25
26
|
// deposit + withdraw merged into funds
|
|
26
27
|
import { registerFundsCommands } from "./commands/funds.js";
|
|
@@ -60,6 +61,8 @@ program
|
|
|
60
61
|
.option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
|
|
61
62
|
.option("--dry-run", "Simulate trades without executing (log as simulated)")
|
|
62
63
|
.option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
|
|
64
|
+
.option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
|
|
65
|
+
.option("--ows-key <token>", "OWS API key token (ows_key_...) for policy-gated agent access")
|
|
63
66
|
.option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
|
|
64
67
|
.configureOutput({
|
|
65
68
|
writeErr: (str) => {
|
|
@@ -90,8 +93,10 @@ function getExchange() {
|
|
|
90
93
|
}
|
|
91
94
|
async function getAdapter() {
|
|
92
95
|
const opts = program.opts();
|
|
93
|
-
const
|
|
94
|
-
|
|
96
|
+
const owsName = opts.ows;
|
|
97
|
+
const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
|
|
98
|
+
const walletName = owsName ? `ows:${owsName}` : opts.wallet;
|
|
99
|
+
// Skip cache when --wallet/--ows is specified (different wallet = different account)
|
|
95
100
|
if (!walletName && _adapter)
|
|
96
101
|
return _adapter;
|
|
97
102
|
const exchange = resolveExchangeAlias(opts.exchange);
|
|
@@ -99,6 +104,10 @@ async function getAdapter() {
|
|
|
99
104
|
const isTestnet = network === "testnet";
|
|
100
105
|
// Try to load key — null means no key configured (read-only mode)
|
|
101
106
|
const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
|
|
107
|
+
// OWS wallet detected — use OWS signers instead of raw keys
|
|
108
|
+
if (pk && isOwsKey(pk)) {
|
|
109
|
+
return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
|
|
110
|
+
}
|
|
102
111
|
switch (exchange) {
|
|
103
112
|
case "pacifica": {
|
|
104
113
|
const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
|
|
@@ -170,6 +179,44 @@ async function getAdapter() {
|
|
|
170
179
|
}
|
|
171
180
|
return _adapter;
|
|
172
181
|
}
|
|
182
|
+
/** Initialize adapter with OWS wallet signer injection. */
|
|
183
|
+
async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToken) {
|
|
184
|
+
const { OwsEvmSigner } = await import("./signer/ows-evm.js");
|
|
185
|
+
const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
|
|
186
|
+
// If an OWS API key token is provided, use it as passphrase → routes through policy engine
|
|
187
|
+
const passphrase = owsKeyToken || "";
|
|
188
|
+
switch (exchange) {
|
|
189
|
+
case "pacifica": {
|
|
190
|
+
const pacNetwork = (isTestnet ? "testnet" : "mainnet");
|
|
191
|
+
const settings = loadSettings();
|
|
192
|
+
const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
|
|
193
|
+
const dummyKeypair = Keypair.generate();
|
|
194
|
+
_pacificaAdapter = new PacificaAdapter(dummyKeypair, pacNetwork, builderCode, true);
|
|
195
|
+
_pacificaAdapter.setSigner(OwsSolanaSigner.create(owsWalletName, passphrase));
|
|
196
|
+
_adapter = _pacificaAdapter;
|
|
197
|
+
return _adapter;
|
|
198
|
+
}
|
|
199
|
+
case "hyperliquid": {
|
|
200
|
+
_hlAdapter = new HyperliquidAdapter(undefined, isTestnet);
|
|
201
|
+
if (opts.dex)
|
|
202
|
+
_hlAdapter.setDex(opts.dex);
|
|
203
|
+
_hlAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
|
|
204
|
+
await _hlAdapter.init();
|
|
205
|
+
_adapter = _hlAdapter;
|
|
206
|
+
return _adapter;
|
|
207
|
+
}
|
|
208
|
+
case "lighter": {
|
|
209
|
+
const { LighterAdapter } = await import("./exchanges/lighter.js");
|
|
210
|
+
_lighterAdapter = new LighterAdapter("", isTestnet);
|
|
211
|
+
_lighterAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
|
|
212
|
+
await _lighterAdapter.init();
|
|
213
|
+
_adapter = _lighterAdapter;
|
|
214
|
+
return _adapter;
|
|
215
|
+
}
|
|
216
|
+
default:
|
|
217
|
+
throw new Error(`Unknown exchange: ${exchange}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
173
220
|
// Sync wrapper for commands that need adapter (lazy init)
|
|
174
221
|
function getAdapterSync() {
|
|
175
222
|
if (!_adapter)
|
|
@@ -201,6 +248,7 @@ registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
|
|
|
201
248
|
// stream commands removed
|
|
202
249
|
registerArbCommands(program, isJson, getAdapterForExchange);
|
|
203
250
|
registerWalletCommands(program, isJson);
|
|
251
|
+
registerOwsCommands(program, isJson);
|
|
204
252
|
registerBridgeCommands(program, isJson);
|
|
205
253
|
registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
|
|
206
254
|
// alert commands removed
|
|
@@ -632,6 +680,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
632
680
|
const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
|
|
633
681
|
process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
|
|
634
682
|
process.env.LIGHTER_PRIVATE_KEY);
|
|
683
|
+
const hasOwsWallet = !!settings.owsActiveWallet;
|
|
635
684
|
// ── ASCII banner ──
|
|
636
685
|
const banner = [
|
|
637
686
|
"",
|
|
@@ -645,9 +694,10 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
645
694
|
chalk.gray(` 4 exchanges · 19 strategies · v${_pkg.version}`),
|
|
646
695
|
];
|
|
647
696
|
console.log(banner.join("\n"));
|
|
648
|
-
if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
|
|
697
|
+
if (!status.hasWallets && !hasEnvKey && !hasOwsWallet && !settings.defaultExchange) {
|
|
649
698
|
// Fresh install — onboarding
|
|
650
699
|
console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
|
|
700
|
+
console.log(` ${chalk.cyan("perp wallet generate")} create OWS encrypted wallet`);
|
|
651
701
|
console.log(` ${chalk.cyan("perp setup")} setup wizard`);
|
|
652
702
|
console.log(chalk.gray(`\n Explore without a wallet:`));
|
|
653
703
|
console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
|
package/dist/mcp-server.js
CHANGED
|
@@ -878,11 +878,11 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
878
878
|
else if (category === "wallet") {
|
|
879
879
|
explanation = {
|
|
880
880
|
command,
|
|
881
|
-
description: { show: "Show configured wallets with public addresses", list: "List
|
|
881
|
+
description: { show: "Show configured wallets with public addresses", list: "List all wallets (OWS vault + legacy)", balance: "Check on-chain USDC/SOL/ETH balances", generate: "Generate a new OWS wallet (encrypted, multi-chain)", import: "Import key into OWS vault", migrate: "Migrate legacy wallets to OWS encrypted vault", set: "Set private key for an exchange (legacy)", use: "Set active wallet (OWS or legacy)", remove: "Remove a wallet", rename: "Rename a wallet", ows: "OWS vault management (create, list, info, delete)" }[sub] || `Wallet: ${sub}`,
|
|
882
882
|
parameters: [],
|
|
883
|
-
risks: sub === "generate" ? ["
|
|
883
|
+
risks: sub === "generate" ? ["Save the mnemonic securely — it cannot be recovered. Keys are stored in ~/.ows/ encrypted vault."] : sub === "remove" ? ["Wallet will be removed — ensure you have backups"] : [],
|
|
884
884
|
category: "read",
|
|
885
|
-
relatedCommands: ["perp wallet
|
|
885
|
+
relatedCommands: ["perp wallet list", "perp wallet balance", "perp wallet ows list"],
|
|
886
886
|
};
|
|
887
887
|
}
|
|
888
888
|
else if (category === "backtest") {
|
|
@@ -1124,17 +1124,19 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1124
1124
|
},
|
|
1125
1125
|
},
|
|
1126
1126
|
wallet: {
|
|
1127
|
-
description: "Wallet management",
|
|
1127
|
+
description: "Wallet management (OWS encrypted vault + legacy)",
|
|
1128
1128
|
subcommands: {
|
|
1129
1129
|
show: { usage: "perp wallet show", description: "Show configured wallets with public addresses" },
|
|
1130
|
-
list: { usage: "perp wallet list", description: "List wallets" },
|
|
1130
|
+
list: { usage: "perp wallet list", description: "List all wallets (OWS vault + legacy)" },
|
|
1131
1131
|
balance: { usage: "perp wallet balance [name]", description: "On-chain balances" },
|
|
1132
|
-
generate: { usage: "perp wallet generate
|
|
1133
|
-
import: { usage: "perp wallet import
|
|
1134
|
-
|
|
1135
|
-
|
|
1132
|
+
generate: { usage: "perp wallet generate [name]", description: "Generate new OWS wallet (encrypted, multi-chain)" },
|
|
1133
|
+
import: { usage: "perp wallet import <privateKey>", description: "Import key into OWS vault" },
|
|
1134
|
+
migrate: { usage: "perp wallet migrate", description: "Migrate legacy wallets to OWS vault" },
|
|
1135
|
+
set: { usage: "perp wallet set <exchange> <key>", description: "Set private key for exchange (legacy)" },
|
|
1136
|
+
use: { usage: "perp wallet use <name>", description: "Set active wallet (OWS or legacy)" },
|
|
1136
1137
|
remove: { usage: "perp wallet remove <name>", description: "Remove a wallet" },
|
|
1137
1138
|
rename: { usage: "perp wallet rename <oldName> <newName>", description: "Rename a wallet" },
|
|
1139
|
+
ows: { usage: "perp wallet ows <sub>", description: "OWS vault: create, list, info, delete, policy, key, fund, pay, setup" },
|
|
1138
1140
|
},
|
|
1139
1141
|
},
|
|
1140
1142
|
history: {
|