@usesigil/kit 0.2.3 → 0.4.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/agent-errors.d.ts.map +1 -1
- package/dist/agent-errors.js +2 -2
- package/dist/agent-errors.js.map +1 -1
- package/dist/balance-tracker.d.ts +20 -0
- package/dist/balance-tracker.d.ts.map +1 -1
- package/dist/balance-tracker.js +18 -5
- package/dist/balance-tracker.js.map +1 -1
- package/dist/create-vault.d.ts.map +1 -1
- package/dist/create-vault.js +22 -0
- package/dist/create-vault.js.map +1 -1
- package/dist/dashboard/index.d.ts +18 -2
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +26 -0
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/mutations.d.ts.map +1 -1
- package/dist/dashboard/mutations.js +3 -3
- package/dist/dashboard/mutations.js.map +1 -1
- package/dist/dashboard/reads.d.ts +124 -1
- package/dist/dashboard/reads.d.ts.map +1 -1
- package/dist/dashboard/reads.js +564 -328
- package/dist/dashboard/reads.js.map +1 -1
- package/dist/dashboard/types.d.ts +120 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/index.js.map +1 -1
- package/dist/owner-transaction.d.ts.map +1 -1
- package/dist/owner-transaction.js +6 -4
- package/dist/owner-transaction.js.map +1 -1
- package/dist/presets.d.ts +19 -11
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +10 -13
- package/dist/presets.js.map +1 -1
- package/dist/rpc-helpers.d.ts +23 -0
- package/dist/rpc-helpers.d.ts.map +1 -1
- package/dist/rpc-helpers.js +45 -0
- package/dist/rpc-helpers.js.map +1 -1
- package/dist/seal.d.ts +8 -0
- package/dist/seal.d.ts.map +1 -1
- package/dist/seal.js +15 -5
- package/dist/seal.js.map +1 -1
- package/dist/state-resolver.d.ts.map +1 -1
- package/dist/state-resolver.js +35 -24
- package/dist/state-resolver.js.map +1 -1
- package/package.json +2 -2
package/dist/dashboard/reads.js
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each function is stateless (fetches fresh from RPC), composes existing
|
|
5
5
|
* SDK functions, and returns raw values with toJSON() for MCP serialization.
|
|
6
|
+
*
|
|
7
|
+
* S14: the five view types are composed by pure `build*` helpers that take a
|
|
8
|
+
* shared {@link OverviewContext}. The existing reads each assemble their own
|
|
9
|
+
* context with minimal fetches; `getOverview` fetches once and shares the
|
|
10
|
+
* context across all helpers so derived values (security posture etc.) are
|
|
11
|
+
* computed exactly once.
|
|
6
12
|
*/
|
|
7
13
|
import { isSome } from "@solana/kit";
|
|
8
14
|
import { toDxError } from "./errors.js";
|
|
9
15
|
import { resolveVaultStateForOwner, getSpendingHistory, getPendingPolicyForVault, } from "../state-resolver.js";
|
|
10
|
-
import { getVaultPnL } from "../balance-tracker.js";
|
|
16
|
+
import { getVaultPnL, getVaultPnLFromState } from "../balance-tracker.js";
|
|
11
17
|
import { getSecurityPosture } from "../security-analytics.js";
|
|
12
18
|
import { evaluateAlertConditions } from "../security-analytics.js";
|
|
13
19
|
import { getAgentProfile } from "../agent-analytics.js";
|
|
@@ -42,75 +48,460 @@ function serializeBigints(obj) {
|
|
|
42
48
|
}
|
|
43
49
|
return obj;
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Default size of the activity window included in `getOverview`. Consumers
|
|
53
|
+
* may override via `GetOverviewOptions.activityLimit`.
|
|
54
|
+
*
|
|
55
|
+
* The value matches `getAgents`' existing per-agent enrichment window so one
|
|
56
|
+
* fetch serves both the overview's activity feed and the agents' last-action
|
|
57
|
+
* fields without inflating RPC cost.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_OVERVIEW_ACTIVITY_LIMIT = 100;
|
|
60
|
+
/**
|
|
61
|
+
* Shared "is this an account-not-found error?" predicate.
|
|
62
|
+
*
|
|
63
|
+
* Both `getPolicy` and `getOverview` treat a missing `PendingPolicyUpdate`
|
|
64
|
+
* account as "no pending update" (not an error). The current Kit doesn't
|
|
65
|
+
* expose a typed `AccountNotFound` SolanaError at this call site, so both
|
|
66
|
+
* paths fall back to substring matching. Extracting it here means the
|
|
67
|
+
* fragility lives in one place and only one site needs to update if Kit
|
|
68
|
+
* ever surfaces a typed variant.
|
|
69
|
+
*/
|
|
70
|
+
function isAccountNotFoundError(err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return (message.includes("could not find") ||
|
|
73
|
+
message.includes("Account does not exist"));
|
|
74
|
+
}
|
|
75
|
+
// ─── Build helpers (pure composition — no RPC) ───────────────────────────────
|
|
76
|
+
// Each helper accepts an OverviewContext and returns one view type. `getOverview`
|
|
77
|
+
// pre-populates memoized derivations (posture/breakdown/alerts) so repeat calls
|
|
78
|
+
// share one computation; existing reads pass a minimal ctx and the helper
|
|
79
|
+
// derives what it needs from `ctx.state`.
|
|
80
|
+
/**
|
|
81
|
+
* Guard for state fields that `resolveVaultStateForOwner` normally guarantees.
|
|
82
|
+
*
|
|
83
|
+
* `state.vault` and `state.policy` are non-null on any success path from the
|
|
84
|
+
* resolver, but consumers that hand-construct an {@link OverviewContext} for
|
|
85
|
+
* testing or custom composition could pass a partial shape. Fail fast with a
|
|
86
|
+
* labeled error instead of a cryptic "cannot read properties of null".
|
|
87
|
+
*/
|
|
88
|
+
function requireCtxField(value, field) {
|
|
89
|
+
if (value === null || value === undefined) {
|
|
90
|
+
throw new Error(`[dashboard/reads] OverviewContext.state.${field} is required but missing`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compose {@link VaultState} from a pre-fetched {@link OverviewContext}.
|
|
96
|
+
*
|
|
97
|
+
* Requires `ctx.state`. Uses `ctx.pnl` when present; otherwise defaults to
|
|
98
|
+
* zero P&L. Uses `ctx.posture` when memoized; otherwise computes from state.
|
|
99
|
+
*
|
|
100
|
+
* @experimental Part of the `build*` composition surface introduced alongside
|
|
101
|
+
* `getOverview` (S14). Signature and JSON shape may shift before v1.0; if you
|
|
102
|
+
* depend on it, pin your SDK version and watch the changeset.
|
|
103
|
+
*
|
|
104
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
105
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
106
|
+
*/
|
|
107
|
+
export function buildVaultState(ctx) {
|
|
108
|
+
const v = requireCtxField(ctx.state.vault, "vault");
|
|
109
|
+
const posture = ctx.posture ?? getSecurityPosture(asVaultState(ctx.state));
|
|
110
|
+
const pnlPercent = ctx.pnl && Number.isFinite(ctx.pnl.pnlPercent) ? ctx.pnl.pnlPercent : 0;
|
|
111
|
+
const pnlAbsolute = ctx.pnl ? ctx.pnl.pnl : 0n;
|
|
112
|
+
const bal = ctx.state.stablecoinBalances;
|
|
113
|
+
const total = bal.usdc + bal.usdt;
|
|
114
|
+
const tokens = [
|
|
115
|
+
...(bal.usdc > 0n ? [{ mint: "USDC", amount: bal.usdc, decimals: 6 }] : []),
|
|
116
|
+
...(bal.usdt > 0n ? [{ mint: "USDT", amount: bal.usdt, decimals: 6 }] : []),
|
|
117
|
+
];
|
|
118
|
+
const checks = posture.checks.map((c) => ({
|
|
119
|
+
name: c.id,
|
|
120
|
+
passed: c.passed,
|
|
121
|
+
}));
|
|
122
|
+
const level = posture.criticalFailures.length > 0
|
|
123
|
+
? "critical"
|
|
124
|
+
: posture.failCount > 0
|
|
125
|
+
? "elevated"
|
|
126
|
+
: "healthy";
|
|
127
|
+
const vaultAddr = ctx.vault;
|
|
128
|
+
const status = (v.status === 0 ? "active" : v.status === 1 ? "frozen" : "closed");
|
|
129
|
+
return {
|
|
130
|
+
vault: {
|
|
131
|
+
address: vaultAddr,
|
|
132
|
+
status,
|
|
133
|
+
owner: v.owner,
|
|
134
|
+
agentCount: v.agents?.length ?? 0,
|
|
135
|
+
openPositions: v.openPositions,
|
|
136
|
+
totalVolume: v.totalVolume,
|
|
137
|
+
totalFees: v.totalFeesCollected,
|
|
138
|
+
},
|
|
139
|
+
balance: { total, tokens },
|
|
140
|
+
pnl: { percent: pnlPercent, absolute: pnlAbsolute },
|
|
141
|
+
health: { level, alertCount: posture.failCount, checks },
|
|
142
|
+
toJSON: () => ({
|
|
74
143
|
vault: {
|
|
75
|
-
address:
|
|
76
|
-
status
|
|
144
|
+
address: vaultAddr,
|
|
145
|
+
status,
|
|
77
146
|
owner: v.owner,
|
|
78
147
|
agentCount: v.agents?.length ?? 0,
|
|
79
148
|
openPositions: v.openPositions,
|
|
80
|
-
totalVolume: v.totalVolume,
|
|
81
|
-
totalFees: v.totalFeesCollected,
|
|
149
|
+
totalVolume: bs(v.totalVolume),
|
|
150
|
+
totalFees: bs(v.totalFeesCollected),
|
|
82
151
|
},
|
|
83
|
-
balance: {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
absolute: pnl.pnl,
|
|
152
|
+
balance: {
|
|
153
|
+
total: bs(total),
|
|
154
|
+
tokens: tokens.map((t) => ({ ...t, amount: bs(t.amount) })),
|
|
87
155
|
},
|
|
156
|
+
pnl: { percent: pnlPercent, absolute: bs(pnlAbsolute) },
|
|
88
157
|
health: { level, alertCount: posture.failCount, checks },
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Compose {@link AgentData}[] from a pre-fetched {@link OverviewContext}.
|
|
163
|
+
*
|
|
164
|
+
* Requires `ctx.state`. Uses `ctx.activity` to populate per-agent last-action
|
|
165
|
+
* and blocked-count fields; when absent, those fields default to empty/zero.
|
|
166
|
+
*
|
|
167
|
+
* @experimental Part of the `build*` composition surface (S14). Signature and
|
|
168
|
+
* JSON shape may shift before v1.0.
|
|
169
|
+
*
|
|
170
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
171
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
172
|
+
*/
|
|
173
|
+
export function buildAgents(ctx) {
|
|
174
|
+
const state = ctx.state;
|
|
175
|
+
const v = requireCtxField(state.vault, "vault");
|
|
176
|
+
const vaultAgents = v.agents;
|
|
177
|
+
if (!vaultAgents || vaultAgents.length === 0)
|
|
178
|
+
return [];
|
|
179
|
+
const activity = ctx.activity ?? [];
|
|
180
|
+
const blockedCutoffMs = Date.now() - 24 * 3600 * 1000;
|
|
181
|
+
return vaultAgents.map((entry) => {
|
|
182
|
+
const addr = entry.pubkey;
|
|
183
|
+
const profile = getAgentProfile(asVaultState(state), addr);
|
|
184
|
+
const budget = state.allAgentBudgets.get(addr);
|
|
185
|
+
const spentAmt = budget?.spent24h ?? 0n;
|
|
186
|
+
const capAmt = budget?.cap ?? 0n;
|
|
187
|
+
const pct = capAmt > 0n ? Number((spentAmt * 10000n) / capAmt) / 100 : 0;
|
|
188
|
+
// Items are newest-first (getSignaturesForAddress ordering).
|
|
189
|
+
const agentActivity = activity.filter((item) => item.agent !== null && item.agent === addr);
|
|
190
|
+
const last = agentActivity[0];
|
|
191
|
+
const lastActionType = last
|
|
192
|
+
? mapCategory(last.category ?? "unknown", last.eventType ?? "", last.actionType ?? undefined)
|
|
193
|
+
: "";
|
|
194
|
+
const lastActionProtocol = last?.protocolName ?? "";
|
|
195
|
+
const lastActionTimestamp = last ? last.timestamp * 1000 : 0;
|
|
196
|
+
const blockedCount24h = agentActivity.filter((item) => !item.success && item.timestamp * 1000 >= blockedCutoffMs).length;
|
|
197
|
+
return {
|
|
198
|
+
address: addr,
|
|
199
|
+
status: (profile?.paused ? "paused" : "active"),
|
|
200
|
+
capabilityLabel: profile?.capabilityLabel ?? "Disabled",
|
|
201
|
+
capability: profile?.capability ?? 0,
|
|
202
|
+
spending: { amount: spentAmt, limit: capAmt, percent: pct },
|
|
203
|
+
lastActionType,
|
|
204
|
+
lastActionProtocol,
|
|
205
|
+
lastActionTimestamp,
|
|
206
|
+
blockedCount24h,
|
|
207
|
+
toJSON: () => ({
|
|
208
|
+
address: addr,
|
|
209
|
+
status: profile?.paused ? "paused" : "active",
|
|
210
|
+
capabilityLabel: profile?.capabilityLabel ?? "Disabled",
|
|
211
|
+
capability: profile?.capability ?? 0,
|
|
212
|
+
spending: { amount: bs(spentAmt), limit: bs(capAmt), percent: pct },
|
|
213
|
+
lastActionType,
|
|
214
|
+
lastActionProtocol,
|
|
215
|
+
lastActionTimestamp,
|
|
216
|
+
blockedCount24h,
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Compose {@link SpendingData} from a pre-fetched {@link OverviewContext}.
|
|
223
|
+
*
|
|
224
|
+
* Requires `ctx.state`. Uses `ctx.breakdown` when memoized.
|
|
225
|
+
*
|
|
226
|
+
* @experimental Part of the `build*` composition surface (S14). Signature and
|
|
227
|
+
* JSON shape may shift before v1.0.
|
|
228
|
+
*
|
|
229
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
230
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
231
|
+
*/
|
|
232
|
+
export function buildSpending(ctx) {
|
|
233
|
+
const state = ctx.state;
|
|
234
|
+
const breakdown = ctx.breakdown ?? getSpendingBreakdown(asVaultState(state));
|
|
235
|
+
const nowUnix = BigInt(Math.floor(Date.now() / 1000));
|
|
236
|
+
const epochs = getSpendingHistory(state.tracker, nowUnix);
|
|
237
|
+
const chart = epochs.map((e) => ({
|
|
238
|
+
time: new Date(e.timestamp * 1000).toISOString(),
|
|
239
|
+
amount: Number(e.usdAmount) / 1_000_000,
|
|
240
|
+
}));
|
|
241
|
+
const { spent24h: spent, cap, remaining } = state.globalBudget;
|
|
242
|
+
const percent = cap > 0n ? Number((spent * 10000n) / cap) / 100 : 0;
|
|
243
|
+
const velocityPerMs = spent > 0n ? Number(spent) / (24 * 3600 * 1000) : 0;
|
|
244
|
+
const rundown = velocityPerMs > 0 && remaining > 0n
|
|
245
|
+
? Math.floor(Number(remaining) / velocityPerMs)
|
|
246
|
+
: 0;
|
|
247
|
+
const protoBreak = breakdown.byProtocol.map((p) => ({
|
|
248
|
+
name: resolveProtocolName(p.protocol),
|
|
249
|
+
programId: p.protocol,
|
|
250
|
+
amount: p.spent24h,
|
|
251
|
+
percent: p.utilization,
|
|
252
|
+
}));
|
|
253
|
+
return {
|
|
254
|
+
global: { today: spent, cap, remaining, percent, rundownMs: rundown },
|
|
255
|
+
chart,
|
|
256
|
+
protocolBreakdown: protoBreak,
|
|
257
|
+
toJSON: () => ({
|
|
258
|
+
global: {
|
|
259
|
+
today: bs(spent),
|
|
260
|
+
cap: bs(cap),
|
|
261
|
+
remaining: bs(remaining),
|
|
262
|
+
percent,
|
|
263
|
+
rundownMs: rundown,
|
|
264
|
+
},
|
|
265
|
+
chart,
|
|
266
|
+
protocolBreakdown: protoBreak.map((p) => ({
|
|
267
|
+
...p,
|
|
268
|
+
amount: bs(p.amount),
|
|
269
|
+
})),
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Compose {@link HealthData} from a pre-fetched {@link OverviewContext}.
|
|
275
|
+
*
|
|
276
|
+
* Requires `ctx.state.vault` when neither `ctx.posture` nor `ctx.alerts` is
|
|
277
|
+
* memoized (downstream `getSecurityPosture` / `evaluateAlertConditions`
|
|
278
|
+
* dereference it). Fully memoized callers can pass a minimal state shape.
|
|
279
|
+
*
|
|
280
|
+
* @experimental Part of the `build*` composition surface (S14). Signature and
|
|
281
|
+
* JSON shape may shift before v1.0.
|
|
282
|
+
*
|
|
283
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
284
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
285
|
+
*/
|
|
286
|
+
export function buildHealth(ctx) {
|
|
287
|
+
// When both posture and alerts are memoized, the helpers below never run
|
|
288
|
+
// and state.vault is untouched — the memoized path is the whole point of
|
|
289
|
+
// OverviewContext. Guard only when at least one derivation will execute.
|
|
290
|
+
if (ctx.posture === undefined || ctx.alerts === undefined) {
|
|
291
|
+
requireCtxField(ctx.state.vault, "vault");
|
|
292
|
+
}
|
|
293
|
+
const posture = ctx.posture ?? getSecurityPosture(asVaultState(ctx.state));
|
|
294
|
+
const alerts = ctx.alerts ?? evaluateAlertConditions(ctx.state, ctx.vault);
|
|
295
|
+
const level = posture.criticalFailures.length > 0
|
|
296
|
+
? "critical"
|
|
297
|
+
: posture.failCount > 0
|
|
298
|
+
? "elevated"
|
|
299
|
+
: "healthy";
|
|
300
|
+
const critAlerts = alerts.filter((a) => a.severity === "critical");
|
|
301
|
+
const lastBlock = critAlerts.length > 0
|
|
302
|
+
? {
|
|
303
|
+
agent: critAlerts[0].agentAddress || "",
|
|
304
|
+
reason: critAlerts[0].title,
|
|
305
|
+
amount: 0n,
|
|
306
|
+
timestamp: Date.now(),
|
|
307
|
+
}
|
|
308
|
+
: undefined;
|
|
309
|
+
const checks = posture.checks.map((c) => ({
|
|
310
|
+
name: c.id,
|
|
311
|
+
passed: c.passed,
|
|
312
|
+
}));
|
|
313
|
+
return {
|
|
314
|
+
level,
|
|
315
|
+
blockedCount24h: critAlerts.length,
|
|
316
|
+
checks,
|
|
317
|
+
lastBlock,
|
|
318
|
+
toJSON: () => ({
|
|
319
|
+
level,
|
|
320
|
+
blockedCount24h: critAlerts.length,
|
|
321
|
+
checks,
|
|
322
|
+
lastBlock: lastBlock
|
|
323
|
+
? { ...lastBlock, amount: bs(lastBlock.amount) }
|
|
324
|
+
: undefined,
|
|
325
|
+
}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Compose {@link PolicyData} from a pre-fetched {@link OverviewContext}.
|
|
330
|
+
*
|
|
331
|
+
* Requires `ctx.state`. Uses `ctx.pendingPolicy` (which may be `null` to mean
|
|
332
|
+
* "confirmed no pending update"); when `undefined` treats as no pending update.
|
|
333
|
+
*
|
|
334
|
+
* @experimental Part of the `build*` composition surface (S14). Signature and
|
|
335
|
+
* JSON shape may shift before v1.0.
|
|
336
|
+
*
|
|
337
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
338
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
339
|
+
*/
|
|
340
|
+
export function buildPolicy(ctx) {
|
|
341
|
+
const state = ctx.state;
|
|
342
|
+
const pendingPolicy = ctx.pendingPolicy ?? null;
|
|
343
|
+
const p = requireCtxField(state.policy, "policy");
|
|
344
|
+
const protocols = (p.protocols || []);
|
|
345
|
+
const approvedApps = protocols.map((addr) => ({
|
|
346
|
+
name: resolveProtocolName(addr),
|
|
347
|
+
programId: addr,
|
|
348
|
+
}));
|
|
349
|
+
const modeMap = {
|
|
350
|
+
0: "unrestricted",
|
|
351
|
+
1: "whitelist",
|
|
352
|
+
2: "blacklist",
|
|
353
|
+
};
|
|
354
|
+
const dailyCap = p.dailySpendingCapUsd;
|
|
355
|
+
const maxPerTrade = p.maxTransactionSizeUsd ?? 0n;
|
|
356
|
+
const protocolCaps = (p.protocolCaps || []);
|
|
357
|
+
const sessionExpiry = p.sessionExpirySlots;
|
|
358
|
+
const policyVer = (p.policyVersion ?? 0n);
|
|
359
|
+
const timelockSec = Number(p.timelockDuration);
|
|
360
|
+
let pendingUpdate;
|
|
361
|
+
if (pendingPolicy) {
|
|
362
|
+
const pp = pendingPolicy;
|
|
363
|
+
const executesAtSec = Number(pp.executesAt ?? 0);
|
|
364
|
+
const appliesAt = Number.isFinite(executesAtSec) ? executesAtSec * 1000 : 0;
|
|
365
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
366
|
+
// Decode each Option<T> field from PendingPolicyUpdate. Only Some fields
|
|
367
|
+
// land in `changes`. 14 fields total — every timelockable PolicyConfig field.
|
|
368
|
+
// Source: programs/sigil/src/state/pending_policy.rs
|
|
369
|
+
const changes = {};
|
|
370
|
+
if (isSome(pp.dailySpendingCapUsd))
|
|
371
|
+
changes.dailyCap = pp.dailySpendingCapUsd.value;
|
|
372
|
+
if (isSome(pp.maxTransactionAmountUsd))
|
|
373
|
+
changes.maxPerTrade = pp.maxTransactionAmountUsd.value;
|
|
374
|
+
if (isSome(pp.protocols))
|
|
375
|
+
changes.approvedApps = pp.protocols.value;
|
|
376
|
+
if (isSome(pp.protocolMode))
|
|
377
|
+
changes.protocolMode = modeMap[pp.protocolMode.value] || "unrestricted";
|
|
378
|
+
if (isSome(pp.hasProtocolCaps))
|
|
379
|
+
changes.hasProtocolCaps = pp.hasProtocolCaps.value;
|
|
380
|
+
if (isSome(pp.protocolCaps))
|
|
381
|
+
changes.protocolCaps = pp.protocolCaps.value;
|
|
382
|
+
if (isSome(pp.canOpenPositions))
|
|
383
|
+
changes.canOpenPositions = pp.canOpenPositions.value;
|
|
384
|
+
if (isSome(pp.maxConcurrentPositions))
|
|
385
|
+
changes.maxConcurrentPositions = pp.maxConcurrentPositions.value;
|
|
386
|
+
if (isSome(pp.maxSlippageBps))
|
|
387
|
+
changes.maxSlippageBps = pp.maxSlippageBps.value;
|
|
388
|
+
if (isSome(pp.maxLeverageBps))
|
|
389
|
+
changes.leverageLimit = pp.maxLeverageBps.value;
|
|
390
|
+
if (isSome(pp.allowedDestinations))
|
|
391
|
+
changes.allowedDestinations = pp.allowedDestinations.value;
|
|
392
|
+
if (isSome(pp.developerFeeRate))
|
|
393
|
+
changes.developerFeeRate = pp.developerFeeRate.value;
|
|
394
|
+
if (isSome(pp.sessionExpirySlots))
|
|
395
|
+
changes.sessionExpirySlots = pp.sessionExpirySlots.value;
|
|
396
|
+
if (isSome(pp.timelockDuration))
|
|
397
|
+
changes.timelock = Number(pp.timelockDuration.value);
|
|
398
|
+
pendingUpdate = {
|
|
399
|
+
changes,
|
|
400
|
+
appliesAt,
|
|
401
|
+
canApply: executesAtSec > 0 && executesAtSec <= nowSec,
|
|
402
|
+
canCancel: true,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
dailyCap,
|
|
407
|
+
maxPerTrade,
|
|
408
|
+
approvedApps,
|
|
409
|
+
protocolMode: modeMap[p.protocolMode] || "unrestricted",
|
|
410
|
+
hasProtocolCaps: p.hasProtocolCaps,
|
|
411
|
+
protocolCaps,
|
|
412
|
+
canOpenPositions: p.canOpenPositions,
|
|
413
|
+
maxConcurrentPositions: p.maxConcurrentPositions,
|
|
414
|
+
maxSlippageBps: p.maxSlippageBps,
|
|
415
|
+
leverageLimitBps: p.maxLeverageBps,
|
|
416
|
+
allowedDestinations: (p.allowedDestinations || []),
|
|
417
|
+
developerFeeRate: p.developerFeeRate,
|
|
418
|
+
sessionExpirySlots: sessionExpiry,
|
|
419
|
+
timelockSeconds: timelockSec,
|
|
420
|
+
policyVersion: policyVer,
|
|
421
|
+
pendingUpdate,
|
|
422
|
+
toJSON: () => ({
|
|
423
|
+
dailyCap: bs(dailyCap),
|
|
424
|
+
maxPerTrade: bs(maxPerTrade),
|
|
425
|
+
approvedApps,
|
|
426
|
+
protocolMode: modeMap[p.protocolMode] || "unrestricted",
|
|
427
|
+
hasProtocolCaps: p.hasProtocolCaps,
|
|
428
|
+
protocolCaps: protocolCaps.map(bs),
|
|
429
|
+
canOpenPositions: p.canOpenPositions,
|
|
430
|
+
maxConcurrentPositions: p.maxConcurrentPositions,
|
|
431
|
+
maxSlippageBps: p.maxSlippageBps,
|
|
432
|
+
leverageLimitBps: p.maxLeverageBps,
|
|
433
|
+
allowedDestinations: (p.allowedDestinations || []),
|
|
434
|
+
developerFeeRate: p.developerFeeRate,
|
|
435
|
+
sessionExpirySlots: bs(sessionExpiry),
|
|
436
|
+
timelockSeconds: timelockSec,
|
|
437
|
+
policyVersion: bs(policyVer),
|
|
438
|
+
pendingUpdate: pendingUpdate
|
|
439
|
+
? {
|
|
440
|
+
changes: serializeBigints(pendingUpdate.changes),
|
|
441
|
+
appliesAt: pendingUpdate.appliesAt,
|
|
442
|
+
canApply: pendingUpdate.canApply,
|
|
443
|
+
canCancel: pendingUpdate.canCancel,
|
|
444
|
+
}
|
|
445
|
+
: undefined,
|
|
446
|
+
}),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Map raw {@link VaultActivityItem}[] to {@link ActivityRow}[] with stable
|
|
451
|
+
* derived IDs and toJSON serializers. Pure — no filtering applied.
|
|
452
|
+
*
|
|
453
|
+
* Both `getActivity` (which then filters) and `getOverview` (which returns
|
|
454
|
+
* unfiltered) consume the output.
|
|
455
|
+
*
|
|
456
|
+
* @experimental Part of the `build*` composition surface (S14). Signature and
|
|
457
|
+
* JSON shape may shift before v1.0.
|
|
458
|
+
*
|
|
459
|
+
* @see OwnerClient.getOverview — the stable single-call alternative that
|
|
460
|
+
* pre-populates a full {@link OverviewContext} for you.
|
|
461
|
+
*/
|
|
462
|
+
export function buildActivityRows(items) {
|
|
463
|
+
return items.map((item) => {
|
|
464
|
+
const cat = item.category ?? "unknown";
|
|
465
|
+
const evt = item.eventType ?? "";
|
|
466
|
+
const act = item.actionType ?? undefined;
|
|
467
|
+
const posEffect = item.positionEffect ?? undefined;
|
|
468
|
+
const type = mapCategory(cat, evt, act, posEffect);
|
|
469
|
+
const amt = item.amount ?? 0n;
|
|
470
|
+
const sig = item.txSignature || `evt-${item.timestamp}-${item.eventType}`;
|
|
471
|
+
return {
|
|
472
|
+
id: sig,
|
|
473
|
+
timestamp: item.timestamp * 1000,
|
|
474
|
+
type,
|
|
475
|
+
protocol: item.protocolName || "",
|
|
476
|
+
protocolId: item.protocol || "",
|
|
477
|
+
agent: item.agent || "",
|
|
478
|
+
amount: amt,
|
|
479
|
+
status: item.success ? "approved" : "blocked",
|
|
480
|
+
reason: item.success ? undefined : item.description,
|
|
481
|
+
txSignature: item.txSignature,
|
|
89
482
|
toJSON: () => ({
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
totalVolume: bs(v.totalVolume),
|
|
101
|
-
totalFees: bs(v.totalFeesCollected),
|
|
102
|
-
},
|
|
103
|
-
balance: {
|
|
104
|
-
total: bs(total),
|
|
105
|
-
tokens: tokens.map((t) => ({ ...t, amount: bs(t.amount) })),
|
|
106
|
-
},
|
|
107
|
-
pnl: {
|
|
108
|
-
percent: Number.isFinite(pnl.pnlPercent) ? pnl.pnlPercent : 0,
|
|
109
|
-
absolute: bs(pnl.pnl),
|
|
110
|
-
},
|
|
111
|
-
health: { level, alertCount: posture.failCount, checks },
|
|
483
|
+
id: sig,
|
|
484
|
+
timestamp: item.timestamp * 1000,
|
|
485
|
+
type,
|
|
486
|
+
protocol: item.protocolName || "",
|
|
487
|
+
protocolId: item.protocol || "",
|
|
488
|
+
agent: item.agent || "",
|
|
489
|
+
amount: bs(amt),
|
|
490
|
+
status: item.success ? "approved" : "blocked",
|
|
491
|
+
reason: item.success ? undefined : item.description,
|
|
492
|
+
txSignature: item.txSignature,
|
|
112
493
|
}),
|
|
113
494
|
};
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
// ─── getVaultState ───────────────────────────────────────────────────────────
|
|
498
|
+
export async function getVaultState(rpc, vault, network) {
|
|
499
|
+
try {
|
|
500
|
+
const [state, pnl] = await Promise.all([
|
|
501
|
+
resolveVaultStateForOwner(rpc, vault, undefined, toNet(network)),
|
|
502
|
+
getVaultPnL(rpc, vault, toNet(network)),
|
|
503
|
+
]);
|
|
504
|
+
return buildVaultState({ vault, state, pnl });
|
|
114
505
|
}
|
|
115
506
|
catch (err) {
|
|
116
507
|
throw toDxError(err, "OwnerClient.getVaultState");
|
|
@@ -119,70 +510,27 @@ export async function getVaultState(rpc, vault, network) {
|
|
|
119
510
|
// ─── getAgents ───────────────────────────────────────────────────────────────
|
|
120
511
|
export async function getAgents(rpc, vault, network) {
|
|
121
512
|
try {
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
513
|
+
// Single getVaultActivity call is shared across all agents (N+1 prevention).
|
|
514
|
+
// Activity is enrichment, so a fetch failure degrades gracefully to empty
|
|
515
|
+
// last-action fields. Window: 100 most recent signatures — large enough to
|
|
516
|
+
// surface last action for low-volume agents without inflating RPC cost.
|
|
517
|
+
//
|
|
518
|
+
// Fix for docs/SECURITY-FINDINGS-2026-04-07.md Finding 5: the previous
|
|
519
|
+
// `.catch(() => [])` swallowed activity-fetch failures silently. If Helius
|
|
520
|
+
// started rate-limiting getSignaturesForAddress, every dashboard call would
|
|
521
|
+
// show "last action: never" for every agent forever and nobody would
|
|
522
|
+
// notice. Graceful degradation is still the right behavior (activity is
|
|
523
|
+
// enrichment, not core), but it must be observable — console.warn is the
|
|
524
|
+
// minimum bar.
|
|
127
525
|
const [state, activity] = await Promise.all([
|
|
128
526
|
resolveVaultStateForOwner(rpc, vault, undefined, toNet(network)),
|
|
129
|
-
// Fix for docs/SECURITY-FINDINGS-2026-04-07.md Finding 5: the
|
|
130
|
-
// previous `.catch(() => [])` swallowed activity-fetch failures
|
|
131
|
-
// silently. If Helius started rate-limiting getSignaturesForAddress,
|
|
132
|
-
// every dashboard call would show "last action: never" for every
|
|
133
|
-
// agent forever and nobody would notice. Graceful degradation is
|
|
134
|
-
// still the right behavior (activity is enrichment, not core), but
|
|
135
|
-
// it must be observable — console.warn is the minimum bar.
|
|
136
527
|
getVaultActivity(rpc, vault, 100, toNet(network)).catch((err) => {
|
|
137
528
|
// eslint-disable-next-line no-console
|
|
138
529
|
console.warn("[OwnerClient.getAgents] activity enrichment failed — falling back to empty last-action fields:", err instanceof Error ? err.message : String(err));
|
|
139
530
|
return [];
|
|
140
531
|
}),
|
|
141
532
|
]);
|
|
142
|
-
|
|
143
|
-
if (!vaultAgents || vaultAgents.length === 0)
|
|
144
|
-
return [];
|
|
145
|
-
const blockedCutoffMs = Date.now() - 24 * 3600 * 1000;
|
|
146
|
-
return vaultAgents.map((entry) => {
|
|
147
|
-
const addr = entry.pubkey;
|
|
148
|
-
const profile = getAgentProfile(asVaultState(state), addr);
|
|
149
|
-
const budget = state.allAgentBudgets.get(addr);
|
|
150
|
-
const spentAmt = budget?.spent24h ?? 0n;
|
|
151
|
-
const capAmt = budget?.cap ?? 0n;
|
|
152
|
-
const pct = capAmt > 0n ? Number((spentAmt * 10000n) / capAmt) / 100 : 0;
|
|
153
|
-
// Derive last-action and blocked-count fields from the shared activity
|
|
154
|
-
// feed. Items are newest-first (getSignaturesForAddress ordering).
|
|
155
|
-
const agentActivity = activity.filter((item) => item.agent !== null && item.agent === addr);
|
|
156
|
-
const last = agentActivity[0];
|
|
157
|
-
const lastActionType = last
|
|
158
|
-
? mapCategory(last.category ?? "unknown", last.eventType ?? "", last.actionType ?? undefined)
|
|
159
|
-
: "";
|
|
160
|
-
const lastActionProtocol = last?.protocolName ?? "";
|
|
161
|
-
const lastActionTimestamp = last ? last.timestamp * 1000 : 0;
|
|
162
|
-
const blockedCount24h = agentActivity.filter((item) => !item.success && item.timestamp * 1000 >= blockedCutoffMs).length;
|
|
163
|
-
return {
|
|
164
|
-
address: addr,
|
|
165
|
-
status: (profile?.paused ? "paused" : "active"),
|
|
166
|
-
capabilityLabel: profile?.capabilityLabel ?? "Disabled",
|
|
167
|
-
capability: profile?.capability ?? 0,
|
|
168
|
-
spending: { amount: spentAmt, limit: capAmt, percent: pct },
|
|
169
|
-
lastActionType,
|
|
170
|
-
lastActionProtocol,
|
|
171
|
-
lastActionTimestamp,
|
|
172
|
-
blockedCount24h,
|
|
173
|
-
toJSON: () => ({
|
|
174
|
-
address: addr,
|
|
175
|
-
status: profile?.paused ? "paused" : "active",
|
|
176
|
-
capabilityLabel: profile?.capabilityLabel ?? "Disabled",
|
|
177
|
-
capability: profile?.capability ?? 0,
|
|
178
|
-
spending: { amount: bs(spentAmt), limit: bs(capAmt), percent: pct },
|
|
179
|
-
lastActionType,
|
|
180
|
-
lastActionProtocol,
|
|
181
|
-
lastActionTimestamp,
|
|
182
|
-
blockedCount24h,
|
|
183
|
-
}),
|
|
184
|
-
};
|
|
185
|
-
});
|
|
533
|
+
return buildAgents({ vault, state, activity });
|
|
186
534
|
}
|
|
187
535
|
catch (err) {
|
|
188
536
|
throw toDxError(err, "OwnerClient.getAgents");
|
|
@@ -192,44 +540,7 @@ export async function getAgents(rpc, vault, network) {
|
|
|
192
540
|
export async function getSpending(rpc, vault, network) {
|
|
193
541
|
try {
|
|
194
542
|
const state = await resolveVaultStateForOwner(rpc, vault, undefined, toNet(network));
|
|
195
|
-
|
|
196
|
-
const nowUnix = BigInt(Math.floor(Date.now() / 1000));
|
|
197
|
-
const epochs = getSpendingHistory(state.tracker, nowUnix);
|
|
198
|
-
const chart = epochs.map((e) => ({
|
|
199
|
-
time: new Date(e.timestamp * 1000).toISOString(),
|
|
200
|
-
amount: Number(e.usdAmount) / 1_000_000,
|
|
201
|
-
}));
|
|
202
|
-
const { spent24h: spent, cap, remaining } = state.globalBudget;
|
|
203
|
-
const percent = cap > 0n ? Number((spent * 10000n) / cap) / 100 : 0;
|
|
204
|
-
const velocityPerMs = spent > 0n ? Number(spent) / (24 * 3600 * 1000) : 0;
|
|
205
|
-
const rundown = velocityPerMs > 0 && remaining > 0n
|
|
206
|
-
? Math.floor(Number(remaining) / velocityPerMs)
|
|
207
|
-
: 0;
|
|
208
|
-
const protoBreak = breakdown.byProtocol.map((p) => ({
|
|
209
|
-
name: resolveProtocolName(p.protocol),
|
|
210
|
-
programId: p.protocol,
|
|
211
|
-
amount: p.spent24h,
|
|
212
|
-
percent: p.utilization,
|
|
213
|
-
}));
|
|
214
|
-
return {
|
|
215
|
-
global: { today: spent, cap, remaining, percent, rundownMs: rundown },
|
|
216
|
-
chart,
|
|
217
|
-
protocolBreakdown: protoBreak,
|
|
218
|
-
toJSON: () => ({
|
|
219
|
-
global: {
|
|
220
|
-
today: bs(spent),
|
|
221
|
-
cap: bs(cap),
|
|
222
|
-
remaining: bs(remaining),
|
|
223
|
-
percent,
|
|
224
|
-
rundownMs: rundown,
|
|
225
|
-
},
|
|
226
|
-
chart,
|
|
227
|
-
protocolBreakdown: protoBreak.map((p) => ({
|
|
228
|
-
...p,
|
|
229
|
-
amount: bs(p.amount),
|
|
230
|
-
})),
|
|
231
|
-
}),
|
|
232
|
-
};
|
|
543
|
+
return buildSpending({ vault, state });
|
|
233
544
|
}
|
|
234
545
|
catch (err) {
|
|
235
546
|
throw toDxError(err, "OwnerClient.getSpending");
|
|
@@ -240,39 +551,7 @@ export async function getActivity(rpc, vault, network, filters) {
|
|
|
240
551
|
try {
|
|
241
552
|
const limit = filters?.limit ?? 50;
|
|
242
553
|
const items = await getVaultActivity(rpc, vault, limit, toNet(network));
|
|
243
|
-
let rows = items
|
|
244
|
-
const cat = item.category ?? "unknown";
|
|
245
|
-
const evt = item.eventType ?? "";
|
|
246
|
-
const act = item.actionType ?? undefined;
|
|
247
|
-
const posEffect = item.positionEffect ?? undefined;
|
|
248
|
-
const type = mapCategory(cat, evt, act, posEffect);
|
|
249
|
-
const amt = item.amount ?? 0n;
|
|
250
|
-
const sig = item.txSignature || `evt-${item.timestamp}-${item.eventType}`;
|
|
251
|
-
return {
|
|
252
|
-
id: sig,
|
|
253
|
-
timestamp: item.timestamp * 1000,
|
|
254
|
-
type,
|
|
255
|
-
protocol: item.protocolName || "",
|
|
256
|
-
protocolId: item.protocol || "",
|
|
257
|
-
agent: item.agent || "",
|
|
258
|
-
amount: amt,
|
|
259
|
-
status: item.success ? "approved" : "blocked",
|
|
260
|
-
reason: item.success ? undefined : item.description,
|
|
261
|
-
txSignature: item.txSignature,
|
|
262
|
-
toJSON: () => ({
|
|
263
|
-
id: sig,
|
|
264
|
-
timestamp: item.timestamp * 1000,
|
|
265
|
-
type,
|
|
266
|
-
protocol: item.protocolName || "",
|
|
267
|
-
protocolId: item.protocol || "",
|
|
268
|
-
agent: item.agent || "",
|
|
269
|
-
amount: bs(amt),
|
|
270
|
-
status: item.success ? "approved" : "blocked",
|
|
271
|
-
reason: item.success ? undefined : item.description,
|
|
272
|
-
txSignature: item.txSignature,
|
|
273
|
-
}),
|
|
274
|
-
};
|
|
275
|
-
});
|
|
554
|
+
let rows = buildActivityRows(items);
|
|
276
555
|
if (filters?.agent)
|
|
277
556
|
rows = rows.filter((r) => r.agent === filters.agent);
|
|
278
557
|
if (filters?.protocol)
|
|
@@ -352,40 +631,7 @@ function rangeToMs(r) {
|
|
|
352
631
|
export async function getHealth(rpc, vault, network) {
|
|
353
632
|
try {
|
|
354
633
|
const state = await resolveVaultStateForOwner(rpc, vault, undefined, toNet(network));
|
|
355
|
-
|
|
356
|
-
const alerts = evaluateAlertConditions(state, vault);
|
|
357
|
-
const level = posture.criticalFailures.length > 0
|
|
358
|
-
? "critical"
|
|
359
|
-
: posture.failCount > 0
|
|
360
|
-
? "elevated"
|
|
361
|
-
: "healthy";
|
|
362
|
-
const critAlerts = alerts.filter((a) => a.severity === "critical");
|
|
363
|
-
const lastBlock = critAlerts.length > 0
|
|
364
|
-
? {
|
|
365
|
-
agent: critAlerts[0].agentAddress || "",
|
|
366
|
-
reason: critAlerts[0].title,
|
|
367
|
-
amount: 0n,
|
|
368
|
-
timestamp: Date.now(),
|
|
369
|
-
}
|
|
370
|
-
: undefined;
|
|
371
|
-
const checks = posture.checks.map((c) => ({
|
|
372
|
-
name: c.id,
|
|
373
|
-
passed: c.passed,
|
|
374
|
-
}));
|
|
375
|
-
return {
|
|
376
|
-
level,
|
|
377
|
-
blockedCount24h: critAlerts.length,
|
|
378
|
-
checks,
|
|
379
|
-
lastBlock,
|
|
380
|
-
toJSON: () => ({
|
|
381
|
-
level,
|
|
382
|
-
blockedCount24h: critAlerts.length,
|
|
383
|
-
checks,
|
|
384
|
-
lastBlock: lastBlock
|
|
385
|
-
? { ...lastBlock, amount: bs(lastBlock.amount) }
|
|
386
|
-
: undefined,
|
|
387
|
-
}),
|
|
388
|
-
};
|
|
634
|
+
return buildHealth({ vault, state });
|
|
389
635
|
}
|
|
390
636
|
catch (err) {
|
|
391
637
|
throw toDxError(err, "OwnerClient.getHealth");
|
|
@@ -399,124 +645,114 @@ export async function getPolicy(rpc, vault, network) {
|
|
|
399
645
|
getPendingPolicyForVault(rpc, vault).catch((err) => {
|
|
400
646
|
// Account-not-found is expected (no pending update) — return null.
|
|
401
647
|
// Re-throw RPC errors so they're not silently swallowed.
|
|
402
|
-
|
|
403
|
-
if (message.includes("could not find") ||
|
|
404
|
-
message.includes("Account does not exist")) {
|
|
648
|
+
if (isAccountNotFoundError(err))
|
|
405
649
|
return null;
|
|
406
|
-
}
|
|
407
650
|
throw err;
|
|
408
651
|
}),
|
|
409
652
|
]);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
653
|
+
return buildPolicy({ vault, state, pendingPolicy });
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
throw toDxError(err, "OwnerClient.getPolicy");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// ─── getOverview (S14) ───────────────────────────────────────────────────────
|
|
660
|
+
/**
|
|
661
|
+
* Single-call overview bundle — resolves vault state once, composes all five
|
|
662
|
+
* view types (vault, agents, spending, health, policy) plus a raw activity
|
|
663
|
+
* list, with PnL derived from the resolved state (no duplicate resolve).
|
|
664
|
+
*
|
|
665
|
+
* **Actual RPC shape.** Calling the five individual reads duplicates the
|
|
666
|
+
* vault-state resolution up to five times. `getOverview` resolves state
|
|
667
|
+
* exactly once and computes PnL from it via {@link getVaultPnLFromState}.
|
|
668
|
+
* The activity fetch is independent: `getVaultActivity(limit)` issues one
|
|
669
|
+
* `getSignaturesForAddress` followed by up to `limit` sequential
|
|
670
|
+
* `getTransaction` calls, so the wall-time cost of the activity feed
|
|
671
|
+
* dominates regardless of this method. Net savings vs. five separate reads:
|
|
672
|
+
* state resolution count drops from ~5 → 1.
|
|
673
|
+
*
|
|
674
|
+
* Activity is **unfiltered**. For filtered activity, call {@link getActivity}
|
|
675
|
+
* with `ActivityFilters`.
|
|
676
|
+
*
|
|
677
|
+
* Graceful degradation: activity fetch failure degrades to empty activity
|
|
678
|
+
* (same observable pattern as `getAgents`, documented in
|
|
679
|
+
* `docs/SECURITY-FINDINGS-2026-04-07.md` Finding 5); pending-policy
|
|
680
|
+
* account-not-found is treated as "no pending update" (same as `getPolicy`).
|
|
681
|
+
* **PnL and state-resolution errors are NOT degraded** and propagate via
|
|
682
|
+
* `toDxError`. A pending-policy error that is NOT account-not-found (e.g.
|
|
683
|
+
* network failure) also propagates — it is NOT treated as "no pending
|
|
684
|
+
* update", even on the `includeActivity: false` lightweight path.
|
|
685
|
+
*/
|
|
686
|
+
export async function getOverview(rpc, vault, network, options) {
|
|
687
|
+
try {
|
|
688
|
+
const includeActivity = options?.includeActivity ?? true;
|
|
689
|
+
const activityLimit = options?.activityLimit ?? DEFAULT_OVERVIEW_ACTIVITY_LIMIT;
|
|
690
|
+
const net = toNet(network);
|
|
691
|
+
// Fan out every independent fetch in one Promise.all. State resolution,
|
|
692
|
+
// activity, and pending-policy have no cross-dependency, so wall time
|
|
693
|
+
// collapses to the slowest of the three. PnL is derived from state
|
|
694
|
+
// synchronously after — one state resolve, zero duplication.
|
|
695
|
+
const [state, activity, pendingPolicy] = await Promise.all([
|
|
696
|
+
resolveVaultStateForOwner(rpc, vault, undefined, net),
|
|
697
|
+
includeActivity
|
|
698
|
+
? getVaultActivity(rpc, vault, activityLimit, net).catch((err) => {
|
|
699
|
+
// Same graceful-degradation pattern as getAgents
|
|
700
|
+
// (docs/SECURITY-FINDINGS-2026-04-07.md Finding 5): activity is
|
|
701
|
+
// enrichment, not core, but the failure must be observable.
|
|
702
|
+
// eslint-disable-next-line no-console
|
|
703
|
+
console.warn("[OwnerClient.getOverview] activity fetch failed — falling back to empty:", err instanceof Error ? err.message : String(err));
|
|
704
|
+
return [];
|
|
705
|
+
})
|
|
706
|
+
: Promise.resolve(undefined),
|
|
707
|
+
getPendingPolicyForVault(rpc, vault).catch((err) => {
|
|
708
|
+
if (isAccountNotFoundError(err))
|
|
709
|
+
return null;
|
|
710
|
+
throw err;
|
|
711
|
+
}),
|
|
712
|
+
]);
|
|
713
|
+
// PnL is pure from resolved state — no extra RPC.
|
|
714
|
+
const pnl = getVaultPnLFromState(state);
|
|
715
|
+
// Compute the three state-derived values exactly once and memoize on ctx.
|
|
716
|
+
// Every build* helper reads these via the `ctx.field ?? derive()` fallback
|
|
717
|
+
// so the memoized value short-circuits re-derivation.
|
|
718
|
+
const posture = getSecurityPosture(asVaultState(state));
|
|
719
|
+
const breakdown = getSpendingBreakdown(asVaultState(state));
|
|
720
|
+
const alerts = evaluateAlertConditions(state, vault);
|
|
721
|
+
const ctx = {
|
|
722
|
+
vault,
|
|
723
|
+
state,
|
|
724
|
+
pnl,
|
|
725
|
+
activity,
|
|
726
|
+
pendingPolicy,
|
|
727
|
+
posture,
|
|
728
|
+
breakdown,
|
|
729
|
+
alerts,
|
|
420
730
|
};
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
let pendingUpdate;
|
|
428
|
-
if (pendingPolicy) {
|
|
429
|
-
const pp = pendingPolicy;
|
|
430
|
-
const executesAtSec = Number(pp.executesAt ?? 0);
|
|
431
|
-
const appliesAt = Number.isFinite(executesAtSec)
|
|
432
|
-
? executesAtSec * 1000
|
|
433
|
-
: 0;
|
|
434
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
435
|
-
// Decode each Option<T> field from PendingPolicyUpdate. Only Some fields
|
|
436
|
-
// land in `changes`. 14 fields total — every timelockable PolicyConfig field.
|
|
437
|
-
// Source: programs/sigil/src/state/pending_policy.rs
|
|
438
|
-
const changes = {};
|
|
439
|
-
if (isSome(pp.dailySpendingCapUsd))
|
|
440
|
-
changes.dailyCap = pp.dailySpendingCapUsd.value;
|
|
441
|
-
if (isSome(pp.maxTransactionAmountUsd))
|
|
442
|
-
changes.maxPerTrade = pp.maxTransactionAmountUsd.value;
|
|
443
|
-
if (isSome(pp.protocols))
|
|
444
|
-
changes.approvedApps = pp.protocols.value;
|
|
445
|
-
if (isSome(pp.protocolMode))
|
|
446
|
-
changes.protocolMode = modeMap[pp.protocolMode.value] || "unrestricted";
|
|
447
|
-
if (isSome(pp.hasProtocolCaps))
|
|
448
|
-
changes.hasProtocolCaps = pp.hasProtocolCaps.value;
|
|
449
|
-
if (isSome(pp.protocolCaps))
|
|
450
|
-
changes.protocolCaps = pp.protocolCaps.value;
|
|
451
|
-
if (isSome(pp.canOpenPositions))
|
|
452
|
-
changes.canOpenPositions = pp.canOpenPositions.value;
|
|
453
|
-
if (isSome(pp.maxConcurrentPositions))
|
|
454
|
-
changes.maxConcurrentPositions = pp.maxConcurrentPositions.value;
|
|
455
|
-
if (isSome(pp.maxSlippageBps))
|
|
456
|
-
changes.maxSlippageBps = pp.maxSlippageBps.value;
|
|
457
|
-
if (isSome(pp.maxLeverageBps))
|
|
458
|
-
changes.leverageLimit = pp.maxLeverageBps.value;
|
|
459
|
-
if (isSome(pp.allowedDestinations))
|
|
460
|
-
changes.allowedDestinations = pp.allowedDestinations.value;
|
|
461
|
-
if (isSome(pp.developerFeeRate))
|
|
462
|
-
changes.developerFeeRate = pp.developerFeeRate.value;
|
|
463
|
-
if (isSome(pp.sessionExpirySlots))
|
|
464
|
-
changes.sessionExpirySlots = pp.sessionExpirySlots.value;
|
|
465
|
-
if (isSome(pp.timelockDuration))
|
|
466
|
-
changes.timelock = Number(pp.timelockDuration.value);
|
|
467
|
-
pendingUpdate = {
|
|
468
|
-
changes,
|
|
469
|
-
appliesAt,
|
|
470
|
-
canApply: executesAtSec > 0 && executesAtSec <= nowSec,
|
|
471
|
-
canCancel: true,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
731
|
+
const vaultView = buildVaultState(ctx);
|
|
732
|
+
const agentsView = buildAgents(ctx);
|
|
733
|
+
const spendingView = buildSpending(ctx);
|
|
734
|
+
const healthView = buildHealth(ctx);
|
|
735
|
+
const policyView = buildPolicy(ctx);
|
|
736
|
+
const activityRows = buildActivityRows(activity ?? []);
|
|
474
737
|
return {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
canOpenPositions: p.canOpenPositions,
|
|
482
|
-
maxConcurrentPositions: p.maxConcurrentPositions,
|
|
483
|
-
maxSlippageBps: p.maxSlippageBps,
|
|
484
|
-
leverageLimitBps: p.maxLeverageBps,
|
|
485
|
-
allowedDestinations: (p.allowedDestinations || []),
|
|
486
|
-
developerFeeRate: p.developerFeeRate,
|
|
487
|
-
sessionExpirySlots: sessionExpiry,
|
|
488
|
-
timelockSeconds: timelockSec,
|
|
489
|
-
policyVersion: policyVer,
|
|
490
|
-
pendingUpdate,
|
|
738
|
+
vault: vaultView,
|
|
739
|
+
agents: agentsView,
|
|
740
|
+
spending: spendingView,
|
|
741
|
+
health: healthView,
|
|
742
|
+
policy: policyView,
|
|
743
|
+
activity: activityRows,
|
|
491
744
|
toJSON: () => ({
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
canOpenPositions: p.canOpenPositions,
|
|
499
|
-
maxConcurrentPositions: p.maxConcurrentPositions,
|
|
500
|
-
maxSlippageBps: p.maxSlippageBps,
|
|
501
|
-
leverageLimitBps: p.maxLeverageBps,
|
|
502
|
-
allowedDestinations: (p.allowedDestinations || []),
|
|
503
|
-
developerFeeRate: p.developerFeeRate,
|
|
504
|
-
sessionExpirySlots: bs(sessionExpiry),
|
|
505
|
-
timelockSeconds: timelockSec,
|
|
506
|
-
policyVersion: bs(policyVer),
|
|
507
|
-
pendingUpdate: pendingUpdate
|
|
508
|
-
? {
|
|
509
|
-
changes: serializeBigints(pendingUpdate.changes),
|
|
510
|
-
appliesAt: pendingUpdate.appliesAt,
|
|
511
|
-
canApply: pendingUpdate.canApply,
|
|
512
|
-
canCancel: pendingUpdate.canCancel,
|
|
513
|
-
}
|
|
514
|
-
: undefined,
|
|
745
|
+
vault: vaultView.toJSON(),
|
|
746
|
+
agents: agentsView.map((a) => a.toJSON()),
|
|
747
|
+
spending: spendingView.toJSON(),
|
|
748
|
+
health: healthView.toJSON(),
|
|
749
|
+
policy: policyView.toJSON(),
|
|
750
|
+
activity: activityRows.map((r) => r.toJSON()),
|
|
515
751
|
}),
|
|
516
752
|
};
|
|
517
753
|
}
|
|
518
754
|
catch (err) {
|
|
519
|
-
throw toDxError(err, "OwnerClient.
|
|
755
|
+
throw toDxError(err, "OwnerClient.getOverview");
|
|
520
756
|
}
|
|
521
757
|
}
|
|
522
758
|
//# sourceMappingURL=reads.js.map
|