@reefclaw/openclaw-plugin 0.1.20 → 0.1.22
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/bridge/bridge.d.ts +10 -0
- package/bridge/bridge.js +167 -23
- package/bridge/gateway/tool-discovery.d.ts +1 -1
- package/bridge/gateway/tool-discovery.js +11 -0
- package/bridge/provider.d.ts +85 -6
- package/bridge/providers/gateway.d.ts +13 -3
- package/bridge/providers/gateway.js +18 -5
- package/bridge/providers/onboarding-commands.d.ts +38 -7
- package/bridge/providers/onboarding-commands.js +113 -13
- package/bridge/types.d.ts +1 -1
- package/bridge/types.js +5 -0
- package/ccxt/binance-private.js +6 -0
- package/config/plugin-config-io.d.ts +26 -0
- package/config/plugin-config-io.js +47 -0
- package/config/tool-gate.js +4 -0
- package/index.js +79 -12
- package/ingest/readiness-reporter.js +25 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/security/sealed-credentials.d.ts +38 -0
- package/security/sealed-credentials.js +180 -0
- package/tools/clear-exchange-credentials.js +16 -3
- package/tools/hl-agent-wallet-status.d.ts +29 -0
- package/tools/hl-agent-wallet-status.js +100 -0
- package/tools/hl-provision-agent-wallet.d.ts +43 -0
- package/tools/hl-provision-agent-wallet.js +173 -0
- package/tools/set-exchange-credentials.d.ts +42 -3
- package/tools/set-exchange-credentials.js +208 -27
- package/tools/set-trading-mode.d.ts +7 -0
- package/tools/set-trading-mode.js +15 -0
- package/tools/test-exchange-credentials.d.ts +30 -3
- package/tools/test-exchange-credentials.js +173 -35
- package/types.d.ts +16 -11
- package/venues/hyperliquid/hl-agent-wallet.d.ts +29 -0
- package/venues/hyperliquid/hl-agent-wallet.js +118 -0
- package/venues/hyperliquid/hl-preflight.d.ts +28 -0
- package/venues/hyperliquid/hl-preflight.js +116 -0
package/types.d.ts
CHANGED
|
@@ -111,24 +111,29 @@ export declare const DEFAULT_CONFIG: PluginConfig;
|
|
|
111
111
|
* `exchange` block (openclaw.json legacy fallback). Which fields matter
|
|
112
112
|
* depends on `venue` (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.2):
|
|
113
113
|
*
|
|
114
|
-
* binance (default
|
|
115
|
-
*
|
|
116
|
-
* are ignored.
|
|
114
|
+
* binance (default) — apiKey + secret (HMAC pair) are required;
|
|
115
|
+
* walletAddress/agentPrivateKey are ignored.
|
|
117
116
|
* hyperliquid — walletAddress (MASTER wallet address, 0x…, used for
|
|
118
117
|
* queries; never its private key) + agentPrivateKey (an approved
|
|
119
118
|
* agent/API wallet's key — signs orders, cannot withdraw). apiKey/secret
|
|
120
|
-
* are
|
|
121
|
-
* Phase 3 adapter ships.
|
|
119
|
+
* are absent — an HL block never carries a Binance HMAC pair.
|
|
122
120
|
*
|
|
123
|
-
* apiKey/secret
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
121
|
+
* apiKey/secret are optional at the type level because the on-disk shape is
|
|
122
|
+
* per-venue: every Binance consumer (index.ts boot, set_trading_mode,
|
|
123
|
+
* set_exchange_credentials) truthiness-guards the pair before constructing a
|
|
124
|
+
* BinancePrivateApi, so an HL block without them is both readable AND
|
|
125
|
+
* writable. */
|
|
127
126
|
export interface ExchangeConfig {
|
|
128
127
|
venue?: 'binance' | 'hyperliquid';
|
|
129
|
-
apiKey
|
|
130
|
-
secret
|
|
128
|
+
apiKey?: string;
|
|
129
|
+
secret?: string;
|
|
131
130
|
testnet?: boolean;
|
|
132
131
|
walletAddress?: string;
|
|
133
132
|
agentPrivateKey?: string;
|
|
133
|
+
/** Per-venue testnet memo. `testnet` above is the ACTIVE venue's flag (what
|
|
134
|
+
* boot reads); this remembers the OTHER venue's setting so switching back
|
|
135
|
+
* restores it instead of inheriting the flag from whichever venue was
|
|
136
|
+
* configured last. Traders run both venues and switch between them — a
|
|
137
|
+
* switch must never lose a configured setting. */
|
|
138
|
+
testnetByVenue?: Partial<Record<'binance' | 'hyperliquid', boolean>>;
|
|
134
139
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Generate a fresh secp256k1 private key (0x-prefixed, 64 hex chars) from
|
|
2
|
+
* CSPRNG bytes, rejection-sampled into the valid scalar range. This is how
|
|
3
|
+
* the box mints its own HL agent wallet — the key is BORN here and never
|
|
4
|
+
* leaves the machine (the caller persists it to plugin-config and surfaces
|
|
5
|
+
* only the derived PUBLIC address). */
|
|
6
|
+
export declare function generateAgentPrivateKey(): string;
|
|
7
|
+
/** Master/agent wallet address: 0x + 40 hex chars. */
|
|
8
|
+
export declare function isHexAddress(value: unknown): value is string;
|
|
9
|
+
/** Agent private key: 32 bytes of hex, 0x prefix optional (the HL app shows it
|
|
10
|
+
* with the prefix; some wallets export without). */
|
|
11
|
+
export declare function isHexPrivateKey(value: unknown): value is string;
|
|
12
|
+
/** Canonical stored form: trimmed, 0x-prefixed, original casing preserved. */
|
|
13
|
+
export declare function normalizeHexPrivateKey(value: string): string;
|
|
14
|
+
export type DeriveAddressResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
address: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: 'invalid_key' | 'unavailable';
|
|
20
|
+
};
|
|
21
|
+
/** Test seam: reset the memoized keccak loader (unit tests only). */
|
|
22
|
+
export declare function __resetKeccakCacheForTests(): void;
|
|
23
|
+
/** Derive the Ethereum address a private key controls (lowercase 0x…40 hex).
|
|
24
|
+
* 'invalid_key' = the key is malformed or outside the secp256k1 range;
|
|
25
|
+
* 'unavailable' = the keccak dependency could not be loaded (callers must
|
|
26
|
+
* fail OPEN — skip the check, never block credential entry on infra). */
|
|
27
|
+
export declare function deriveAddressFromPrivateKey(privateKey: string): Promise<DeriveAddressResult>;
|
|
28
|
+
/** Case-insensitive address equality (addresses may arrive checksummed). */
|
|
29
|
+
export declare function sameAddress(a: string, b: string): boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Hyperliquid agent-wallet helpers — format validation + address derivation.
|
|
2
|
+
//
|
|
3
|
+
// Why derivation exists at all: the single most catastrophic mistake a
|
|
4
|
+
// non-technical trader can make in the HL onboarding form is pasting their
|
|
5
|
+
// MASTER wallet's private key where the AGENT key belongs (the master key can
|
|
6
|
+
// withdraw everything; the agent key can only trade). The form cannot tell the
|
|
7
|
+
// two apart — both are 32-byte hex — but the box can: derive the address from
|
|
8
|
+
// the pasted key and refuse when it equals the master walletAddress. The same
|
|
9
|
+
// derived address also lets test_exchange_credentials verify the agent is
|
|
10
|
+
// actually APPROVED for the master account (POST /info extraAgents) before the
|
|
11
|
+
// user goes live.
|
|
12
|
+
//
|
|
13
|
+
// Derivation = secp256k1 pubkey (node:crypto createECDH — stable Node API) +
|
|
14
|
+
// keccak-256 of the 64-byte public key, last 20 bytes (the Ethereum address
|
|
15
|
+
// rule). Node has no keccak (its sha3-256 is NIST-padded, a different hash),
|
|
16
|
+
// so keccak_256 comes from ccxt's vendored noble-hashes — ccxt is pinned at
|
|
17
|
+
// 4.5.37 and the deep path is guarded by a known-vector unit test
|
|
18
|
+
// (hl-agent-wallet.test.ts), so a ccxt bump that moves the file fails loudly.
|
|
19
|
+
// If the import ever breaks at runtime we degrade gracefully: derivation
|
|
20
|
+
// reports 'unavailable' and callers skip the address checks rather than
|
|
21
|
+
// blocking credential entry.
|
|
22
|
+
//
|
|
23
|
+
// SECURITY: this module handles the raw agent private key in memory only —
|
|
24
|
+
// it never logs it, never persists it, and never puts it in a return value.
|
|
25
|
+
import { createECDH, randomBytes } from 'node:crypto';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { pathToFileURL } from 'node:url';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
/** secp256k1 group order n — a private key must be in [1, n-1]. */
|
|
30
|
+
const SECP256K1_ORDER = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
|
|
31
|
+
/** Generate a fresh secp256k1 private key (0x-prefixed, 64 hex chars) from
|
|
32
|
+
* CSPRNG bytes, rejection-sampled into the valid scalar range. This is how
|
|
33
|
+
* the box mints its own HL agent wallet — the key is BORN here and never
|
|
34
|
+
* leaves the machine (the caller persists it to plugin-config and surfaces
|
|
35
|
+
* only the derived PUBLIC address). */
|
|
36
|
+
export function generateAgentPrivateKey() {
|
|
37
|
+
// Rejection probability per draw is ~2^-128 (order ≈ 2^256) — the loop is
|
|
38
|
+
// for correctness, not because retries are expected.
|
|
39
|
+
for (;;) {
|
|
40
|
+
const candidate = randomBytes(32);
|
|
41
|
+
const scalar = BigInt(`0x${candidate.toString('hex')}`);
|
|
42
|
+
if (scalar > 0n && scalar < SECP256K1_ORDER) {
|
|
43
|
+
return `0x${candidate.toString('hex')}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Master/agent wallet address: 0x + 40 hex chars. */
|
|
48
|
+
export function isHexAddress(value) {
|
|
49
|
+
return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value.trim());
|
|
50
|
+
}
|
|
51
|
+
/** Agent private key: 32 bytes of hex, 0x prefix optional (the HL app shows it
|
|
52
|
+
* with the prefix; some wallets export without). */
|
|
53
|
+
export function isHexPrivateKey(value) {
|
|
54
|
+
return typeof value === 'string' && /^(0x)?[0-9a-fA-F]{64}$/.test(value.trim());
|
|
55
|
+
}
|
|
56
|
+
/** Canonical stored form: trimmed, 0x-prefixed, original casing preserved. */
|
|
57
|
+
export function normalizeHexPrivateKey(value) {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
return trimmed.startsWith('0x') || trimmed.startsWith('0X') ? `0x${trimmed.slice(2)}` : `0x${trimmed}`;
|
|
60
|
+
}
|
|
61
|
+
let keccakPromise = null;
|
|
62
|
+
/** Load keccak_256 from ccxt's vendored noble-hashes. The package's `exports`
|
|
63
|
+
* map only exposes ".", so we resolve the package root from the main entry and
|
|
64
|
+
* import the file by path (file URLs bypass the exports map). Returns null on
|
|
65
|
+
* any failure — derivation then reports 'unavailable'. */
|
|
66
|
+
function loadKeccak() {
|
|
67
|
+
if (!keccakPromise) {
|
|
68
|
+
keccakPromise = (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const req = createRequire(import.meta.url);
|
|
71
|
+
const entry = req.resolve('ccxt');
|
|
72
|
+
const marker = `${path.sep}ccxt${path.sep}`;
|
|
73
|
+
const idx = entry.lastIndexOf(marker);
|
|
74
|
+
if (idx === -1)
|
|
75
|
+
return null;
|
|
76
|
+
const root = entry.slice(0, idx + marker.length - 1);
|
|
77
|
+
const sha3 = path.join(root, 'js', 'src', 'static_dependencies', 'noble-hashes', 'sha3.js');
|
|
78
|
+
const mod = (await import(pathToFileURL(sha3).href));
|
|
79
|
+
return typeof mod.keccak_256 === 'function' ? mod.keccak_256 : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}
|
|
86
|
+
return keccakPromise;
|
|
87
|
+
}
|
|
88
|
+
/** Test seam: reset the memoized keccak loader (unit tests only). */
|
|
89
|
+
export function __resetKeccakCacheForTests() {
|
|
90
|
+
keccakPromise = null;
|
|
91
|
+
}
|
|
92
|
+
/** Derive the Ethereum address a private key controls (lowercase 0x…40 hex).
|
|
93
|
+
* 'invalid_key' = the key is malformed or outside the secp256k1 range;
|
|
94
|
+
* 'unavailable' = the keccak dependency could not be loaded (callers must
|
|
95
|
+
* fail OPEN — skip the check, never block credential entry on infra). */
|
|
96
|
+
export async function deriveAddressFromPrivateKey(privateKey) {
|
|
97
|
+
if (!isHexPrivateKey(privateKey))
|
|
98
|
+
return { ok: false, reason: 'invalid_key' };
|
|
99
|
+
const keccak = await loadKeccak();
|
|
100
|
+
if (!keccak)
|
|
101
|
+
return { ok: false, reason: 'unavailable' };
|
|
102
|
+
try {
|
|
103
|
+
const ecdh = createECDH('secp256k1');
|
|
104
|
+
ecdh.setPrivateKey(Buffer.from(normalizeHexPrivateKey(privateKey).slice(2), 'hex'));
|
|
105
|
+
const uncompressed = ecdh.getPublicKey(null, 'uncompressed'); // 0x04 || X || Y (65 bytes)
|
|
106
|
+
const digest = keccak(Uint8Array.from(uncompressed.subarray(1)));
|
|
107
|
+
const address = `0x${Buffer.from(digest.subarray(digest.length - 20)).toString('hex')}`;
|
|
108
|
+
return { ok: true, address };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// createECDH throws on out-of-range keys — malformed input, not infra.
|
|
112
|
+
return { ok: false, reason: 'invalid_key' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Case-insensitive address equality (addresses may arrive checksummed). */
|
|
116
|
+
export function sameAddress(a, b) {
|
|
117
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
118
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Approvals expiring inside this window get a renewal warning — long enough
|
|
2
|
+
* to act on, short enough not to nag for months. */
|
|
3
|
+
export declare const HL_EXPIRY_WARN_MS: number;
|
|
4
|
+
export interface HlPreflightInput {
|
|
5
|
+
walletAddress: string;
|
|
6
|
+
/** Derived agent address to check approval for; null = skip the check
|
|
7
|
+
* (derivation unavailable). */
|
|
8
|
+
agentAddress: string | null;
|
|
9
|
+
testnet: boolean;
|
|
10
|
+
/** Injectable for tests; production uses global fetch. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
export interface HlPreflightOutcome {
|
|
14
|
+
/** clearinghouseState answered and parsed — the master account is queryable. */
|
|
15
|
+
reachable: boolean;
|
|
16
|
+
/** Set when !reachable — the raw error string for the caller's message. */
|
|
17
|
+
unreachableError?: string;
|
|
18
|
+
/** Perp equity + spot USDC; null when unreachable. */
|
|
19
|
+
balanceUSDC: number | null;
|
|
20
|
+
/** true/false = extraAgents answered definitively; null = could not check. */
|
|
21
|
+
agentApproved: boolean | null;
|
|
22
|
+
/** Approval expiry (epoch ms) when the exchange reports one. */
|
|
23
|
+
agentValidUntil: number | null;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function hlApiBase(testnet: boolean): string;
|
|
27
|
+
export declare function hlInfo(fetchImpl: typeof fetch, testnet: boolean, body: Record<string, unknown>): Promise<unknown>;
|
|
28
|
+
export declare function hlPreflight(input: HlPreflightInput): Promise<HlPreflightOutcome>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Hyperliquid credential preflight — the UNSIGNED verification core shared by
|
|
2
|
+
// test_exchange_credentials (candidate creds) and hl_agent_wallet_status
|
|
3
|
+
// (stored creds). Deliberately takes ADDRESSES ONLY — this module never sees a
|
|
4
|
+
// private key, so it stays outside the credential-holder set.
|
|
5
|
+
//
|
|
6
|
+
// Checks (all keyless POST /info reads — the master address is the query key):
|
|
7
|
+
// 1. clearinghouseState(master) → perp equity. Failure here = "unreachable";
|
|
8
|
+
// callers hard-fail with the error string.
|
|
9
|
+
// 2. spotClearinghouseState(master) → spot USDC (the unified account
|
|
10
|
+
// collateralizes perps from spot — verified live on testnet 2026-07-12).
|
|
11
|
+
// Cosmetic: failure is silently skipped, perp equity alone still answers.
|
|
12
|
+
// 3. extraAgents(master) → is `agentAddress` in the approved list, and when
|
|
13
|
+
// does the approval expire? Shape [{name,address,validUntil(ms)}] —
|
|
14
|
+
// verified vs Chainstack/QuickNode HL API references 2026-08-01 (the
|
|
15
|
+
// official gitbook omits this info type). A definitive miss is the
|
|
16
|
+
// caller's hard fail; an endpoint error degrades to a warning
|
|
17
|
+
// (fail-open on infra, closed on an explicit "not approved").
|
|
18
|
+
import { sameAddress } from './hl-agent-wallet.js';
|
|
19
|
+
/** Approvals expiring inside this window get a renewal warning — long enough
|
|
20
|
+
* to act on, short enough not to nag for months. */
|
|
21
|
+
export const HL_EXPIRY_WARN_MS = 14 * 24 * 60 * 60 * 1000;
|
|
22
|
+
export function hlApiBase(testnet) {
|
|
23
|
+
return testnet ? 'https://api.hyperliquid-testnet.xyz' : 'https://api.hyperliquid.xyz';
|
|
24
|
+
}
|
|
25
|
+
export async function hlInfo(fetchImpl, testnet, body) {
|
|
26
|
+
const res = await fetchImpl(`${hlApiBase(testnet)}/info`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
signal: AbortSignal.timeout(15_000),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
throw new Error(`POST /info ${String(body.type)} → ${res.status}`);
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
function msg(err) {
|
|
37
|
+
return err instanceof Error ? err.message : String(err);
|
|
38
|
+
}
|
|
39
|
+
export async function hlPreflight(input) {
|
|
40
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
41
|
+
const { walletAddress, testnet } = input;
|
|
42
|
+
const warnings = [];
|
|
43
|
+
// 1+2. Master account state.
|
|
44
|
+
let balanceUSDC = null;
|
|
45
|
+
try {
|
|
46
|
+
const state = (await hlInfo(fetchImpl, testnet, {
|
|
47
|
+
type: 'clearinghouseState',
|
|
48
|
+
user: walletAddress,
|
|
49
|
+
}));
|
|
50
|
+
const perpEquity = Number(state?.marginSummary?.accountValue);
|
|
51
|
+
let total = Number.isFinite(perpEquity) ? perpEquity : 0;
|
|
52
|
+
try {
|
|
53
|
+
const spot = (await hlInfo(fetchImpl, testnet, {
|
|
54
|
+
type: 'spotClearinghouseState',
|
|
55
|
+
user: walletAddress,
|
|
56
|
+
}));
|
|
57
|
+
for (const b of spot?.balances ?? []) {
|
|
58
|
+
if (String(b?.coin) === 'USDC') {
|
|
59
|
+
const t = Number(b?.total);
|
|
60
|
+
if (Number.isFinite(t))
|
|
61
|
+
total += t;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Spot read is cosmetic — perp equity alone is still a real answer.
|
|
67
|
+
}
|
|
68
|
+
balanceUSDC = total;
|
|
69
|
+
if (total <= 0) {
|
|
70
|
+
warnings.push('The master account holds no USDC yet — deposit/bridge USDC to Hyperliquid before going live.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
reachable: false,
|
|
76
|
+
unreachableError: msg(e),
|
|
77
|
+
balanceUSDC: null,
|
|
78
|
+
agentApproved: null,
|
|
79
|
+
agentValidUntil: null,
|
|
80
|
+
warnings,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// 3. Agent approval.
|
|
84
|
+
let agentApproved = null;
|
|
85
|
+
let agentValidUntil = null;
|
|
86
|
+
if (input.agentAddress) {
|
|
87
|
+
try {
|
|
88
|
+
const agents = (await hlInfo(fetchImpl, testnet, {
|
|
89
|
+
type: 'extraAgents',
|
|
90
|
+
user: walletAddress,
|
|
91
|
+
}));
|
|
92
|
+
if (Array.isArray(agents)) {
|
|
93
|
+
const match = agents.find((a) => typeof a?.address === 'string' && sameAddress(a.address, input.agentAddress));
|
|
94
|
+
agentApproved = match != null;
|
|
95
|
+
if (match && Number.isFinite(Number(match.validUntil))) {
|
|
96
|
+
agentValidUntil = Number(match.validUntil);
|
|
97
|
+
const remaining = agentValidUntil - Date.now();
|
|
98
|
+
if (remaining <= 0) {
|
|
99
|
+
agentApproved = false;
|
|
100
|
+
warnings.push('The agent wallet approval has EXPIRED — re-approve it in the Hyperliquid API page.');
|
|
101
|
+
}
|
|
102
|
+
else if (remaining < HL_EXPIRY_WARN_MS) {
|
|
103
|
+
warnings.push(`The agent wallet approval expires in ${Math.ceil(remaining / (24 * 60 * 60 * 1000))} day(s) — renew it soon (approvals last at most 180 days).`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
warnings.push('Could not read the approved-agents list — approval not verified.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
warnings.push('Could not read the approved-agents list — approval not verified.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { reachable: true, balanceUSDC, agentApproved, agentValidUntil, warnings };
|
|
116
|
+
}
|