@waiaas/daemon 2.10.0 → 2.11.0-rc
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/api/helpers/resolve-chain-id.d.ts +2 -0
- package/dist/api/helpers/resolve-chain-id.d.ts.map +1 -0
- package/dist/api/helpers/resolve-chain-id.js +21 -0
- package/dist/api/helpers/resolve-chain-id.js.map +1 -0
- package/dist/api/routes/actions.d.ts.map +1 -1
- package/dist/api/routes/actions.js +1 -20
- package/dist/api/routes/actions.js.map +1 -1
- package/dist/api/routes/admin-actions.d.ts.map +1 -1
- package/dist/api/routes/admin-actions.js +1 -19
- package/dist/api/routes/admin-actions.js.map +1 -1
- package/dist/api/routes/admin-auth.d.ts +9 -0
- package/dist/api/routes/admin-auth.d.ts.map +1 -0
- package/dist/api/routes/admin-auth.js +453 -0
- package/dist/api/routes/admin-auth.js.map +1 -0
- package/dist/api/routes/admin-monitoring.d.ts +10 -0
- package/dist/api/routes/admin-monitoring.d.ts.map +1 -0
- package/dist/api/routes/admin-monitoring.js +788 -0
- package/dist/api/routes/admin-monitoring.js.map +1 -0
- package/dist/api/routes/admin-notifications.d.ts +9 -0
- package/dist/api/routes/admin-notifications.d.ts.map +1 -0
- package/dist/api/routes/admin-notifications.js +210 -0
- package/dist/api/routes/admin-notifications.js.map +1 -0
- package/dist/api/routes/admin-settings.d.ts +9 -0
- package/dist/api/routes/admin-settings.d.ts.map +1 -0
- package/dist/api/routes/admin-settings.js +433 -0
- package/dist/api/routes/admin-settings.js.map +1 -0
- package/dist/api/routes/admin-wallets.d.ts +16 -0
- package/dist/api/routes/admin-wallets.d.ts.map +1 -0
- package/dist/api/routes/admin-wallets.js +582 -0
- package/dist/api/routes/admin-wallets.js.map +1 -0
- package/dist/api/routes/admin.d.ts +8 -26
- package/dist/api/routes/admin.d.ts.map +1 -1
- package/dist/api/routes/admin.js +20 -2536
- package/dist/api/routes/admin.js.map +1 -1
- package/dist/api/routes/erc8004.d.ts.map +1 -1
- package/dist/api/routes/erc8004.js +0 -1
- package/dist/api/routes/erc8004.js.map +1 -1
- package/dist/api/routes/mcp.d.ts +2 -0
- package/dist/api/routes/mcp.d.ts.map +1 -1
- package/dist/api/routes/mcp.js +2 -1
- package/dist/api/routes/mcp.js.map +1 -1
- package/dist/api/routes/nft-approvals.js +4 -4
- package/dist/api/routes/nft-approvals.js.map +1 -1
- package/dist/api/routes/sessions.d.ts +2 -0
- package/dist/api/routes/sessions.d.ts.map +1 -1
- package/dist/api/routes/sessions.js +5 -3
- package/dist/api/routes/sessions.js.map +1 -1
- package/dist/api/routes/wallets.d.ts.map +1 -1
- package/dist/api/routes/wallets.js +3 -3
- package/dist/api/routes/wallets.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +2 -0
- package/dist/api/server.js.map +1 -1
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/infrastructure/nft/nft-indexer-client.d.ts.map +1 -1
- package/dist/infrastructure/nft/nft-indexer-client.js +3 -3
- package/dist/infrastructure/nft/nft-indexer-client.js.map +1 -1
- package/dist/infrastructure/oracle/coingecko-forex.d.ts.map +1 -1
- package/dist/infrastructure/oracle/coingecko-forex.js +2 -3
- package/dist/infrastructure/oracle/coingecko-forex.js.map +1 -1
- package/dist/infrastructure/oracle/coingecko-oracle.d.ts.map +1 -1
- package/dist/infrastructure/oracle/coingecko-oracle.js +2 -3
- package/dist/infrastructure/oracle/coingecko-oracle.js.map +1 -1
- package/dist/infrastructure/oracle/pyth-oracle.d.ts +0 -1
- package/dist/infrastructure/oracle/pyth-oracle.d.ts.map +1 -1
- package/dist/infrastructure/oracle/pyth-oracle.js +3 -3
- package/dist/infrastructure/oracle/pyth-oracle.js.map +1 -1
- package/dist/lifecycle/workers.d.ts.map +1 -1
- package/dist/lifecycle/workers.js +2 -1
- package/dist/lifecycle/workers.js.map +1 -1
- package/dist/pipeline/dry-run.d.ts.map +1 -1
- package/dist/pipeline/dry-run.js +4 -3
- package/dist/pipeline/dry-run.js.map +1 -1
- package/dist/pipeline/stages.d.ts.map +1 -1
- package/dist/pipeline/stages.js +4 -3
- package/dist/pipeline/stages.js.map +1 -1
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.js +2 -2
- package/dist/services/signing-sdk/channels/ntfy-signing-channel.js.map +1 -1
- package/dist/services/signing-sdk/channels/telegram-signing-channel.js +1 -1
- package/dist/services/signing-sdk/channels/telegram-signing-channel.js.map +1 -1
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js +2 -2
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js.map +1 -1
- package/dist/services/signing-sdk/sign-request-builder.d.ts +2 -0
- package/dist/services/signing-sdk/sign-request-builder.d.ts.map +1 -1
- package/dist/services/signing-sdk/sign-request-builder.js +20 -3
- package/dist/services/signing-sdk/sign-request-builder.js.map +1 -1
- package/dist/services/signing-sdk/sign-response-handler.js +4 -4
- package/dist/services/signing-sdk/sign-response-handler.js.map +1 -1
- package/package.json +5 -5
- package/public/admin/assets/index-By5VUJ-B.js +3 -0
- package/public/admin/index.html +1 -1
- package/public/admin/assets/index--wQVT9Dz.js +0 -3
package/dist/api/routes/admin.js
CHANGED
|
@@ -1,2550 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Admin route
|
|
2
|
+
* Admin route aggregator: delegates to domain-specific modules.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* POST /admin/notifications/test - Send test notification (masterAuth)
|
|
12
|
-
* GET /admin/notifications/log - Query notification logs (masterAuth)
|
|
13
|
-
* GET /admin/settings - Get all settings (masterAuth)
|
|
14
|
-
* PUT /admin/settings - Update settings (masterAuth)
|
|
15
|
-
* POST /admin/settings/test-rpc - Test RPC connectivity (masterAuth)
|
|
16
|
-
* GET /admin/oracle-status - Oracle cache/source/cross-validation status (masterAuth)
|
|
17
|
-
* GET /admin/api-keys - List Action Provider API key status (masterAuth)
|
|
18
|
-
* PUT /admin/api-keys/:provider - Set or update API key (masterAuth)
|
|
19
|
-
* DELETE /admin/api-keys/:provider - Delete API key (masterAuth)
|
|
20
|
-
* GET /admin/forex/rates - Forex exchange rates for display currency (masterAuth)
|
|
21
|
-
* GET /admin/telegram-users - List Telegram bot users (masterAuth)
|
|
22
|
-
* PUT /admin/telegram-users/:chatId - Update Telegram user role (masterAuth)
|
|
23
|
-
* DELETE /admin/telegram-users/:chatId - Delete Telegram user (masterAuth)
|
|
24
|
-
* GET /admin/transactions - Cross-wallet transaction list with filters (masterAuth)
|
|
25
|
-
* GET /admin/incoming - Cross-wallet incoming transaction list with filters (masterAuth)
|
|
26
|
-
* GET /admin/rpc-status - Per-network RPC pool endpoint status (masterAuth)
|
|
27
|
-
* POST /admin/transactions/:id/cancel - Cancel a QUEUED (DELAY) transaction (masterAuth)
|
|
28
|
-
* POST /admin/transactions/:id/reject - Reject a pending approval transaction (masterAuth)
|
|
4
|
+
* Domain modules:
|
|
5
|
+
* - admin-auth.ts: status, kill-switch, recover, shutdown, password, rotate-secret
|
|
6
|
+
* - admin-notifications.ts: notifications status/test/log
|
|
7
|
+
* - admin-settings.ts: settings CRUD, test-rpc, oracle, API keys, forex, RPC status
|
|
8
|
+
* - admin-wallets.ts: wallet transactions/balance/staking, telegram users, DeFi positions
|
|
9
|
+
* - admin-monitoring.ts: cross-wallet transactions, incoming, agent-prompt, session-reissue,
|
|
10
|
+
* tx cancel/reject, backup, stats, autostop
|
|
29
11
|
*
|
|
30
12
|
* @see docs/37-rest-api-complete-spec.md
|
|
31
13
|
* @see docs/36-killswitch-evm-freeze.md
|
|
32
14
|
*/
|
|
33
|
-
import { OpenAPIHono
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import { buildConnectInfoPrompt } from './connect-info.js';
|
|
41
|
-
import { getSettingDefinition, ActionTierOverrideSchema } from '../../infrastructure/settings/index.js';
|
|
42
|
-
import { resolveRpcUrl } from '../../infrastructure/adapter-pool.js';
|
|
43
|
-
import { AdminStatusResponseSchema, KillSwitchResponseSchema, KillSwitchActivateResponseSchema, KillSwitchEscalateResponseSchema, MasterPasswordChangeRequestSchema, MasterPasswordChangeResponseSchema, RecoverResponseSchema, KillSwitchRecoverRequestSchema, ShutdownResponseSchema, RotateSecretResponseSchema, NotificationStatusResponseSchema, NotificationTestRequestSchema, NotificationTestResponseSchema, NotificationLogResponseSchema, SettingsResponseSchema, SettingsUpdateRequestSchema, SettingsUpdateResponseSchema, TestRpcRequestSchema, TestRpcResponseSchema, OracleStatusResponseSchema, AgentPromptRequestSchema, AgentPromptResponseSchema, SessionReissueResponseSchema, StakingPositionsResponseSchema, RpcStatusResponseSchema, BackupInfoResponseSchema, BackupListResponseSchema, ErrorResponseSchema, buildErrorResponses, openApiValidationHook, } from './openapi-schemas.js';
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Route definitions
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
const statusRoute = createRoute({
|
|
48
|
-
method: 'get',
|
|
49
|
-
path: '/admin/status',
|
|
50
|
-
tags: ['Admin'],
|
|
51
|
-
summary: 'Get daemon status',
|
|
52
|
-
responses: {
|
|
53
|
-
200: {
|
|
54
|
-
description: 'Daemon status',
|
|
55
|
-
content: { 'application/json': { schema: AdminStatusResponseSchema } },
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
const activateKillSwitchRoute = createRoute({
|
|
60
|
-
method: 'post',
|
|
61
|
-
path: '/admin/kill-switch',
|
|
62
|
-
tags: ['Admin'],
|
|
63
|
-
summary: 'Activate kill switch',
|
|
64
|
-
responses: {
|
|
65
|
-
200: {
|
|
66
|
-
description: 'Kill switch activated',
|
|
67
|
-
content: { 'application/json': { schema: KillSwitchActivateResponseSchema } },
|
|
68
|
-
},
|
|
69
|
-
...buildErrorResponses(['KILL_SWITCH_ACTIVE']),
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
const getKillSwitchRoute = createRoute({
|
|
73
|
-
method: 'get',
|
|
74
|
-
path: '/admin/kill-switch',
|
|
75
|
-
tags: ['Admin'],
|
|
76
|
-
summary: 'Get kill switch state',
|
|
77
|
-
responses: {
|
|
78
|
-
200: {
|
|
79
|
-
description: 'Kill switch state',
|
|
80
|
-
content: { 'application/json': { schema: KillSwitchResponseSchema } },
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
const escalateKillSwitchRoute = createRoute({
|
|
85
|
-
method: 'post',
|
|
86
|
-
path: '/admin/kill-switch/escalate',
|
|
87
|
-
tags: ['Admin'],
|
|
88
|
-
summary: 'Escalate kill switch to LOCKED',
|
|
89
|
-
responses: {
|
|
90
|
-
200: {
|
|
91
|
-
description: 'Kill switch escalated to LOCKED',
|
|
92
|
-
content: { 'application/json': { schema: KillSwitchEscalateResponseSchema } },
|
|
93
|
-
},
|
|
94
|
-
...buildErrorResponses(['INVALID_STATE_TRANSITION']),
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
const recoverRoute = createRoute({
|
|
98
|
-
method: 'post',
|
|
99
|
-
path: '/admin/recover',
|
|
100
|
-
tags: ['Admin'],
|
|
101
|
-
summary: 'Recover from kill switch (dual-auth)',
|
|
102
|
-
request: {
|
|
103
|
-
body: {
|
|
104
|
-
content: { 'application/json': { schema: KillSwitchRecoverRequestSchema } },
|
|
105
|
-
required: false,
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
responses: {
|
|
109
|
-
200: {
|
|
110
|
-
description: 'Kill switch deactivated',
|
|
111
|
-
content: { 'application/json': { schema: RecoverResponseSchema } },
|
|
112
|
-
},
|
|
113
|
-
...buildErrorResponses(['KILL_SWITCH_NOT_ACTIVE', 'INVALID_STATE_TRANSITION', 'INVALID_SIGNATURE']),
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
const shutdownRoute = createRoute({
|
|
117
|
-
method: 'post',
|
|
118
|
-
path: '/admin/shutdown',
|
|
119
|
-
tags: ['Admin'],
|
|
120
|
-
summary: 'Initiate graceful shutdown',
|
|
121
|
-
responses: {
|
|
122
|
-
200: {
|
|
123
|
-
description: 'Shutdown initiated',
|
|
124
|
-
content: { 'application/json': { schema: ShutdownResponseSchema } },
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
const masterPasswordChangeRoute = createRoute({
|
|
129
|
-
method: 'put',
|
|
130
|
-
path: '/admin/master-password',
|
|
131
|
-
tags: ['Admin'],
|
|
132
|
-
summary: 'Change master password',
|
|
133
|
-
request: {
|
|
134
|
-
body: {
|
|
135
|
-
content: { 'application/json': { schema: MasterPasswordChangeRequestSchema } },
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
responses: {
|
|
139
|
-
200: {
|
|
140
|
-
description: 'Master password changed successfully',
|
|
141
|
-
content: { 'application/json': { schema: MasterPasswordChangeResponseSchema } },
|
|
142
|
-
},
|
|
143
|
-
...buildErrorResponses(['INVALID_MASTER_PASSWORD', 'ACTION_VALIDATION_FAILED']),
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
const rotateSecretRoute = createRoute({
|
|
147
|
-
method: 'post',
|
|
148
|
-
path: '/admin/rotate-secret',
|
|
149
|
-
tags: ['Admin'],
|
|
150
|
-
summary: 'Rotate JWT secret',
|
|
151
|
-
responses: {
|
|
152
|
-
200: {
|
|
153
|
-
description: 'JWT secret rotated',
|
|
154
|
-
content: { 'application/json': { schema: RotateSecretResponseSchema } },
|
|
155
|
-
},
|
|
156
|
-
...buildErrorResponses(['ROTATION_TOO_RECENT']),
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
const notificationsStatusRoute = createRoute({
|
|
160
|
-
method: 'get',
|
|
161
|
-
path: '/admin/notifications/status',
|
|
162
|
-
tags: ['Admin'],
|
|
163
|
-
summary: 'Get notification channel status',
|
|
164
|
-
responses: {
|
|
165
|
-
200: {
|
|
166
|
-
description: 'Notification channel status (no credentials)',
|
|
167
|
-
content: { 'application/json': { schema: NotificationStatusResponseSchema } },
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
const notificationsTestRoute = createRoute({
|
|
172
|
-
method: 'post',
|
|
173
|
-
path: '/admin/notifications/test',
|
|
174
|
-
tags: ['Admin'],
|
|
175
|
-
summary: 'Send test notification',
|
|
176
|
-
request: {
|
|
177
|
-
body: {
|
|
178
|
-
content: { 'application/json': { schema: NotificationTestRequestSchema } },
|
|
179
|
-
required: false,
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
responses: {
|
|
183
|
-
200: {
|
|
184
|
-
description: 'Test notification results',
|
|
185
|
-
content: { 'application/json': { schema: NotificationTestResponseSchema } },
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
const notificationLogQuerySchema = z.object({
|
|
190
|
-
page: z.string().optional().default('1'),
|
|
191
|
-
pageSize: z.string().optional().default('20'),
|
|
192
|
-
channel: z.string().optional(),
|
|
193
|
-
status: z.string().optional(),
|
|
194
|
-
eventType: z.string().optional(),
|
|
195
|
-
since: z.string().optional(),
|
|
196
|
-
until: z.string().optional(),
|
|
197
|
-
});
|
|
198
|
-
const notificationsLogRoute = createRoute({
|
|
199
|
-
method: 'get',
|
|
200
|
-
path: '/admin/notifications/log',
|
|
201
|
-
tags: ['Admin'],
|
|
202
|
-
summary: 'Query notification delivery logs',
|
|
203
|
-
request: {
|
|
204
|
-
query: notificationLogQuerySchema,
|
|
205
|
-
},
|
|
206
|
-
responses: {
|
|
207
|
-
200: {
|
|
208
|
-
description: 'Paginated notification logs',
|
|
209
|
-
content: { 'application/json': { schema: NotificationLogResponseSchema } },
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// Settings route definitions
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
const settingsGetRoute = createRoute({
|
|
217
|
-
method: 'get',
|
|
218
|
-
path: '/admin/settings',
|
|
219
|
-
tags: ['Admin'],
|
|
220
|
-
summary: 'Get all settings grouped by category',
|
|
221
|
-
responses: {
|
|
222
|
-
200: {
|
|
223
|
-
description: 'All settings with credentials masked as boolean',
|
|
224
|
-
content: { 'application/json': { schema: SettingsResponseSchema } },
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
const settingsPutRoute = createRoute({
|
|
229
|
-
method: 'put',
|
|
230
|
-
path: '/admin/settings',
|
|
231
|
-
tags: ['Admin'],
|
|
232
|
-
summary: 'Update settings',
|
|
233
|
-
request: {
|
|
234
|
-
body: {
|
|
235
|
-
content: { 'application/json': { schema: SettingsUpdateRequestSchema } },
|
|
236
|
-
required: true,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
responses: {
|
|
240
|
-
200: {
|
|
241
|
-
description: 'Updated settings',
|
|
242
|
-
content: { 'application/json': { schema: SettingsUpdateResponseSchema } },
|
|
243
|
-
},
|
|
244
|
-
...buildErrorResponses(['ACTION_VALIDATION_FAILED']),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
const testRpcRoute = createRoute({
|
|
248
|
-
method: 'post',
|
|
249
|
-
path: '/admin/settings/test-rpc',
|
|
250
|
-
tags: ['Admin'],
|
|
251
|
-
summary: 'Test RPC endpoint connectivity',
|
|
252
|
-
request: {
|
|
253
|
-
body: {
|
|
254
|
-
content: { 'application/json': { schema: TestRpcRequestSchema } },
|
|
255
|
-
required: true,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
responses: {
|
|
259
|
-
200: {
|
|
260
|
-
description: 'RPC connectivity test result',
|
|
261
|
-
content: { 'application/json': { schema: TestRpcResponseSchema } },
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// RPC Pool status route definition
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
const rpcStatusRoute = createRoute({
|
|
269
|
-
method: 'get',
|
|
270
|
-
path: '/admin/rpc-status',
|
|
271
|
-
tags: ['Admin'],
|
|
272
|
-
summary: 'Get per-network RPC pool endpoint status',
|
|
273
|
-
responses: {
|
|
274
|
-
200: {
|
|
275
|
-
description: 'RPC pool status per network',
|
|
276
|
-
content: { 'application/json': { schema: RpcStatusResponseSchema } },
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
// Oracle status route definition
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
const oracleStatusRoute = createRoute({
|
|
284
|
-
method: 'get',
|
|
285
|
-
path: '/admin/oracle-status',
|
|
286
|
-
tags: ['Admin'],
|
|
287
|
-
summary: 'Get oracle cache statistics and source status',
|
|
288
|
-
responses: {
|
|
289
|
-
200: {
|
|
290
|
-
description: 'Oracle status',
|
|
291
|
-
content: { 'application/json': { schema: OracleStatusResponseSchema } },
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
// API Keys route definitions
|
|
297
|
-
// ---------------------------------------------------------------------------
|
|
298
|
-
const apiKeysListResponseSchema = z.object({
|
|
299
|
-
keys: z.array(z.object({
|
|
300
|
-
providerName: z.string(),
|
|
301
|
-
hasKey: z.boolean(),
|
|
302
|
-
maskedKey: z.string().nullable(),
|
|
303
|
-
requiresApiKey: z.boolean(),
|
|
304
|
-
updatedAt: z.string().nullable(),
|
|
305
|
-
})),
|
|
306
|
-
});
|
|
307
|
-
const apiKeysListRoute = createRoute({
|
|
308
|
-
method: 'get',
|
|
309
|
-
path: '/admin/api-keys',
|
|
310
|
-
tags: ['Admin'],
|
|
311
|
-
summary: 'List Action Provider API key status',
|
|
312
|
-
responses: {
|
|
313
|
-
200: {
|
|
314
|
-
description: 'API key status per provider',
|
|
315
|
-
content: { 'application/json': { schema: apiKeysListResponseSchema } },
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
const apiKeyPutRoute = createRoute({
|
|
320
|
-
method: 'put',
|
|
321
|
-
path: '/admin/api-keys/{provider}',
|
|
322
|
-
tags: ['Admin'],
|
|
323
|
-
summary: 'Set or update Action Provider API key',
|
|
324
|
-
request: {
|
|
325
|
-
params: z.object({ provider: z.string() }),
|
|
326
|
-
body: {
|
|
327
|
-
content: {
|
|
328
|
-
'application/json': {
|
|
329
|
-
schema: z.object({ apiKey: z.string().min(1) }),
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
required: true,
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
responses: {
|
|
336
|
-
200: {
|
|
337
|
-
description: 'API key saved',
|
|
338
|
-
content: {
|
|
339
|
-
'application/json': {
|
|
340
|
-
schema: z.object({
|
|
341
|
-
success: z.boolean(),
|
|
342
|
-
providerName: z.string(),
|
|
343
|
-
}),
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
const apiKeyDeleteRoute = createRoute({
|
|
350
|
-
method: 'delete',
|
|
351
|
-
path: '/admin/api-keys/{provider}',
|
|
352
|
-
tags: ['Admin'],
|
|
353
|
-
summary: 'Delete Action Provider API key',
|
|
354
|
-
request: {
|
|
355
|
-
params: z.object({ provider: z.string() }),
|
|
356
|
-
},
|
|
357
|
-
responses: {
|
|
358
|
-
200: {
|
|
359
|
-
description: 'API key deleted',
|
|
360
|
-
content: {
|
|
361
|
-
'application/json': {
|
|
362
|
-
schema: z.object({ success: z.boolean() }),
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
},
|
|
366
|
-
...buildErrorResponses(['ACTION_NOT_FOUND']),
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
// Forex rates route definitions
|
|
371
|
-
// ---------------------------------------------------------------------------
|
|
372
|
-
const forexRatesQuerySchema = z.object({
|
|
373
|
-
currencies: z.string().optional().openapi({
|
|
374
|
-
description: 'Comma-separated currency codes (e.g. KRW,JPY,EUR). If omitted, returns empty.',
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
const forexRatesRoute = createRoute({
|
|
378
|
-
method: 'get',
|
|
379
|
-
path: '/admin/forex/rates',
|
|
380
|
-
tags: ['Admin'],
|
|
381
|
-
summary: 'Get forex exchange rates for display currencies',
|
|
382
|
-
request: {
|
|
383
|
-
query: forexRatesQuerySchema,
|
|
384
|
-
},
|
|
385
|
-
responses: {
|
|
386
|
-
200: {
|
|
387
|
-
description: 'Forex rates with preview strings',
|
|
388
|
-
content: {
|
|
389
|
-
'application/json': {
|
|
390
|
-
schema: z.object({
|
|
391
|
-
rates: z.record(z.string(), z.object({
|
|
392
|
-
rate: z.number(),
|
|
393
|
-
preview: z.string(),
|
|
394
|
-
})),
|
|
395
|
-
}),
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
});
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
// Admin wallet route definitions
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
const adminWalletTransactionsRoute = createRoute({
|
|
405
|
-
method: 'get',
|
|
406
|
-
path: '/admin/wallets/{id}/transactions',
|
|
407
|
-
tags: ['Admin'],
|
|
408
|
-
summary: 'Get wallet transactions',
|
|
409
|
-
request: {
|
|
410
|
-
params: z.object({ id: z.string().uuid() }),
|
|
411
|
-
query: z.object({
|
|
412
|
-
limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
|
|
413
|
-
offset: z.coerce.number().int().min(0).default(0).optional(),
|
|
414
|
-
}),
|
|
415
|
-
},
|
|
416
|
-
responses: {
|
|
417
|
-
200: {
|
|
418
|
-
description: 'Wallet transaction list',
|
|
419
|
-
content: {
|
|
420
|
-
'application/json': {
|
|
421
|
-
schema: z.object({
|
|
422
|
-
items: z.array(z.object({
|
|
423
|
-
id: z.string(),
|
|
424
|
-
type: z.string(),
|
|
425
|
-
status: z.string(),
|
|
426
|
-
toAddress: z.string().nullable(),
|
|
427
|
-
amount: z.string().nullable(),
|
|
428
|
-
formattedAmount: z.string().nullable(),
|
|
429
|
-
amountUsd: z.number().nullable(),
|
|
430
|
-
network: z.string().nullable(),
|
|
431
|
-
txHash: z.string().nullable(),
|
|
432
|
-
createdAt: z.number().nullable(),
|
|
433
|
-
})),
|
|
434
|
-
total: z.number().int(),
|
|
435
|
-
}),
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
...buildErrorResponses(['WALLET_NOT_FOUND']),
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
const adminWalletBalanceRoute = createRoute({
|
|
443
|
-
method: 'get',
|
|
444
|
-
path: '/admin/wallets/{id}/balance',
|
|
445
|
-
tags: ['Admin'],
|
|
446
|
-
summary: 'Get wallet balance across all available networks',
|
|
447
|
-
request: {
|
|
448
|
-
params: z.object({ id: z.string().uuid() }),
|
|
449
|
-
},
|
|
450
|
-
responses: {
|
|
451
|
-
200: {
|
|
452
|
-
description: 'Wallet balances per network',
|
|
453
|
-
content: {
|
|
454
|
-
'application/json': {
|
|
455
|
-
schema: z.object({
|
|
456
|
-
balances: z.array(z.object({
|
|
457
|
-
network: z.string(),
|
|
458
|
-
native: z
|
|
459
|
-
.object({
|
|
460
|
-
balance: z.string(),
|
|
461
|
-
symbol: z.string(),
|
|
462
|
-
usd: z.number().nullable().optional(),
|
|
463
|
-
})
|
|
464
|
-
.nullable(),
|
|
465
|
-
tokens: z.array(z.object({
|
|
466
|
-
symbol: z.string(),
|
|
467
|
-
balance: z.string(),
|
|
468
|
-
address: z.string(),
|
|
469
|
-
})),
|
|
470
|
-
error: z.string().optional(),
|
|
471
|
-
})),
|
|
472
|
-
}),
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
...buildErrorResponses(['WALLET_NOT_FOUND']),
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
const adminWalletStakingRoute = createRoute({
|
|
480
|
-
method: 'get',
|
|
481
|
-
path: '/admin/wallets/{id}/staking',
|
|
482
|
-
tags: ['Admin'],
|
|
483
|
-
summary: 'Get wallet staking positions',
|
|
484
|
-
description: 'Returns staking positions (Lido stETH, Jito JitoSOL) for a specific wallet with balance, APY, pending unstake status.',
|
|
485
|
-
request: {
|
|
486
|
-
params: z.object({ id: z.string().uuid() }),
|
|
487
|
-
},
|
|
488
|
-
responses: {
|
|
489
|
-
200: {
|
|
490
|
-
description: 'Staking positions for the wallet',
|
|
491
|
-
content: { 'application/json': { schema: StakingPositionsResponseSchema } },
|
|
492
|
-
},
|
|
493
|
-
...buildErrorResponses(['WALLET_NOT_FOUND']),
|
|
494
|
-
},
|
|
495
|
-
});
|
|
496
|
-
// ---------------------------------------------------------------------------
|
|
497
|
-
// DeFi Positions route definition
|
|
498
|
-
// ---------------------------------------------------------------------------
|
|
499
|
-
const adminDefiPositionsRoute = createRoute({
|
|
500
|
-
method: 'get',
|
|
501
|
-
path: '/admin/defi/positions',
|
|
502
|
-
tags: ['Admin'],
|
|
503
|
-
summary: 'Get all DeFi positions across wallets',
|
|
504
|
-
description: 'Returns all active DeFi positions across all wallets with aggregated totals. ' +
|
|
505
|
-
'Optionally filter by wallet_id. masterAuth required.',
|
|
506
|
-
request: {
|
|
507
|
-
query: z.object({
|
|
508
|
-
wallet_id: z.string().uuid().optional(),
|
|
509
|
-
}),
|
|
510
|
-
},
|
|
511
|
-
responses: {
|
|
512
|
-
200: {
|
|
513
|
-
description: 'Active DeFi positions with aggregates',
|
|
514
|
-
content: {
|
|
515
|
-
'application/json': {
|
|
516
|
-
schema: z.object({
|
|
517
|
-
positions: z.array(z.object({
|
|
518
|
-
id: z.string(),
|
|
519
|
-
walletId: z.string(),
|
|
520
|
-
category: z.string(),
|
|
521
|
-
provider: z.string(),
|
|
522
|
-
chain: z.string(),
|
|
523
|
-
network: z.string().nullable(),
|
|
524
|
-
assetId: z.string().nullable(),
|
|
525
|
-
amount: z.string(),
|
|
526
|
-
amountUsd: z.number().nullable(),
|
|
527
|
-
status: z.string(),
|
|
528
|
-
openedAt: z.number(),
|
|
529
|
-
lastSyncedAt: z.number(),
|
|
530
|
-
})),
|
|
531
|
-
totalValueUsd: z.number().nullable(),
|
|
532
|
-
worstHealthFactor: z.number().nullable(),
|
|
533
|
-
activeCount: z.number(),
|
|
534
|
-
}),
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
// ---------------------------------------------------------------------------
|
|
541
|
-
// Telegram Users route definitions
|
|
542
|
-
// ---------------------------------------------------------------------------
|
|
543
|
-
const TelegramUserSchema = z.object({
|
|
544
|
-
chat_id: z.number(),
|
|
545
|
-
username: z.string().nullable(),
|
|
546
|
-
role: z.enum(['PENDING', 'ADMIN', 'READONLY']),
|
|
547
|
-
registered_at: z.number(),
|
|
548
|
-
approved_at: z.number().nullable(),
|
|
549
|
-
});
|
|
550
|
-
const telegramUsersListRoute = createRoute({
|
|
551
|
-
method: 'get',
|
|
552
|
-
path: '/admin/telegram-users',
|
|
553
|
-
tags: ['Admin'],
|
|
554
|
-
summary: 'List Telegram bot users',
|
|
555
|
-
responses: {
|
|
556
|
-
200: {
|
|
557
|
-
description: 'Telegram users list',
|
|
558
|
-
content: {
|
|
559
|
-
'application/json': {
|
|
560
|
-
schema: z.object({
|
|
561
|
-
users: z.array(TelegramUserSchema),
|
|
562
|
-
total: z.number(),
|
|
563
|
-
}),
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
const telegramUserUpdateRoute = createRoute({
|
|
570
|
-
method: 'put',
|
|
571
|
-
path: '/admin/telegram-users/{chatId}',
|
|
572
|
-
tags: ['Admin'],
|
|
573
|
-
summary: 'Update Telegram user role',
|
|
574
|
-
request: {
|
|
575
|
-
params: z.object({ chatId: z.coerce.number() }),
|
|
576
|
-
body: {
|
|
577
|
-
content: {
|
|
578
|
-
'application/json': {
|
|
579
|
-
schema: z.object({
|
|
580
|
-
role: z.enum(['ADMIN', 'READONLY']),
|
|
581
|
-
}),
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
required: true,
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
responses: {
|
|
588
|
-
200: {
|
|
589
|
-
description: 'User role updated',
|
|
590
|
-
content: {
|
|
591
|
-
'application/json': {
|
|
592
|
-
schema: z.object({
|
|
593
|
-
success: z.boolean(),
|
|
594
|
-
chat_id: z.number(),
|
|
595
|
-
role: z.enum(['ADMIN', 'READONLY']),
|
|
596
|
-
}),
|
|
597
|
-
},
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
...buildErrorResponses(['WALLET_NOT_FOUND']),
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
const telegramUserDeleteRoute = createRoute({
|
|
604
|
-
method: 'delete',
|
|
605
|
-
path: '/admin/telegram-users/{chatId}',
|
|
606
|
-
tags: ['Admin'],
|
|
607
|
-
summary: 'Delete Telegram user',
|
|
608
|
-
request: {
|
|
609
|
-
params: z.object({ chatId: z.coerce.number() }),
|
|
610
|
-
},
|
|
611
|
-
responses: {
|
|
612
|
-
200: {
|
|
613
|
-
description: 'User deleted',
|
|
614
|
-
content: {
|
|
615
|
-
'application/json': {
|
|
616
|
-
schema: z.object({ success: z.boolean() }),
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
...buildErrorResponses(['WALLET_NOT_FOUND']),
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
|
-
// ---------------------------------------------------------------------------
|
|
624
|
-
// Cross-wallet admin transaction route definitions
|
|
625
|
-
// ---------------------------------------------------------------------------
|
|
626
|
-
const adminTransactionsQuerySchema = z.object({
|
|
627
|
-
offset: z.coerce.number().int().min(0).default(0).optional(),
|
|
628
|
-
limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
|
|
629
|
-
wallet_id: z.string().uuid().optional(),
|
|
630
|
-
type: z.string().optional(),
|
|
631
|
-
status: z.string().optional(),
|
|
632
|
-
network: z.string().optional(),
|
|
633
|
-
since: z.coerce.number().optional(),
|
|
634
|
-
until: z.coerce.number().optional(),
|
|
635
|
-
search: z.string().optional(),
|
|
636
|
-
});
|
|
637
|
-
const adminTransactionsRoute = createRoute({
|
|
638
|
-
method: 'get',
|
|
639
|
-
path: '/admin/transactions',
|
|
640
|
-
tags: ['Admin'],
|
|
641
|
-
summary: 'List cross-wallet transactions with filters and pagination',
|
|
642
|
-
request: {
|
|
643
|
-
query: adminTransactionsQuerySchema,
|
|
644
|
-
},
|
|
645
|
-
responses: {
|
|
646
|
-
200: {
|
|
647
|
-
description: 'Paginated cross-wallet transaction list',
|
|
648
|
-
content: {
|
|
649
|
-
'application/json': {
|
|
650
|
-
schema: z.object({
|
|
651
|
-
items: z.array(z.object({
|
|
652
|
-
id: z.string(),
|
|
653
|
-
walletId: z.string(),
|
|
654
|
-
walletName: z.string().nullable(),
|
|
655
|
-
type: z.string(),
|
|
656
|
-
status: z.string(),
|
|
657
|
-
tier: z.string().nullable(),
|
|
658
|
-
toAddress: z.string().nullable(),
|
|
659
|
-
amount: z.string().nullable(),
|
|
660
|
-
amountUsd: z.number().nullable(),
|
|
661
|
-
network: z.string().nullable(),
|
|
662
|
-
txHash: z.string().nullable(),
|
|
663
|
-
chain: z.string(),
|
|
664
|
-
createdAt: z.number().nullable(),
|
|
665
|
-
})),
|
|
666
|
-
total: z.number().int(),
|
|
667
|
-
offset: z.number().int(),
|
|
668
|
-
limit: z.number().int(),
|
|
669
|
-
}),
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
const adminIncomingQuerySchema = z.object({
|
|
676
|
-
offset: z.coerce.number().int().min(0).default(0).optional(),
|
|
677
|
-
limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
|
|
678
|
-
wallet_id: z.string().uuid().optional(),
|
|
679
|
-
chain: z.string().optional(),
|
|
680
|
-
status: z.string().optional(),
|
|
681
|
-
suspicious: z.enum(['true', 'false']).optional(),
|
|
682
|
-
});
|
|
683
|
-
const adminIncomingRoute = createRoute({
|
|
684
|
-
method: 'get',
|
|
685
|
-
path: '/admin/incoming',
|
|
686
|
-
tags: ['Admin'],
|
|
687
|
-
summary: 'List cross-wallet incoming transactions with filters and pagination',
|
|
688
|
-
request: {
|
|
689
|
-
query: adminIncomingQuerySchema,
|
|
690
|
-
},
|
|
691
|
-
responses: {
|
|
692
|
-
200: {
|
|
693
|
-
description: 'Paginated cross-wallet incoming transaction list',
|
|
694
|
-
content: {
|
|
695
|
-
'application/json': {
|
|
696
|
-
schema: z.object({
|
|
697
|
-
items: z.array(z.object({
|
|
698
|
-
id: z.string(),
|
|
699
|
-
txHash: z.string(),
|
|
700
|
-
walletId: z.string(),
|
|
701
|
-
walletName: z.string().nullable(),
|
|
702
|
-
fromAddress: z.string(),
|
|
703
|
-
amount: z.string(),
|
|
704
|
-
tokenAddress: z.string().nullable(),
|
|
705
|
-
chain: z.string(),
|
|
706
|
-
network: z.string(),
|
|
707
|
-
status: z.string(),
|
|
708
|
-
blockNumber: z.number().nullable(),
|
|
709
|
-
detectedAt: z.number().nullable(),
|
|
710
|
-
confirmedAt: z.number().nullable(),
|
|
711
|
-
suspicious: z.boolean(),
|
|
712
|
-
})),
|
|
713
|
-
total: z.number().int(),
|
|
714
|
-
offset: z.number().int(),
|
|
715
|
-
limit: z.number().int(),
|
|
716
|
-
}),
|
|
717
|
-
},
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
});
|
|
15
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
16
|
+
import { openApiValidationHook } from './openapi-schemas.js';
|
|
17
|
+
import { registerAdminAuthRoutes } from './admin-auth.js';
|
|
18
|
+
import { registerAdminNotificationRoutes } from './admin-notifications.js';
|
|
19
|
+
import { registerAdminSettingsRoutes } from './admin-settings.js';
|
|
20
|
+
import { registerAdminWalletRoutes } from './admin-wallets.js';
|
|
21
|
+
import { registerAdminMonitoringRoutes } from './admin-monitoring.js';
|
|
722
22
|
// ---------------------------------------------------------------------------
|
|
723
23
|
// Route factory
|
|
724
24
|
// ---------------------------------------------------------------------------
|
|
725
|
-
/**
|
|
726
|
-
* Create admin route sub-router.
|
|
727
|
-
*
|
|
728
|
-
* GET /admin/status - Daemon health info (masterAuth)
|
|
729
|
-
* POST /admin/kill-switch - Activate kill switch (masterAuth)
|
|
730
|
-
* GET /admin/kill-switch - Get kill switch state (public)
|
|
731
|
-
* POST /admin/recover - Deactivate kill switch (masterAuth)
|
|
732
|
-
* POST /admin/shutdown - Graceful shutdown (masterAuth)
|
|
733
|
-
* POST /admin/rotate-secret - Rotate JWT secret (masterAuth)
|
|
734
|
-
* GET /admin/notifications/status - Notification channel status (masterAuth)
|
|
735
|
-
* POST /admin/notifications/test - Send test notification (masterAuth)
|
|
736
|
-
* GET /admin/notifications/log - Query notification logs (masterAuth)
|
|
737
|
-
* GET /admin/settings - Get all settings (masterAuth)
|
|
738
|
-
* PUT /admin/settings - Update settings (masterAuth)
|
|
739
|
-
* POST /admin/settings/test-rpc - Test RPC connectivity (masterAuth)
|
|
740
|
-
* GET /admin/oracle-status - Oracle cache/source/cross-validation status (masterAuth)
|
|
741
|
-
* GET /admin/api-keys - List Action Provider API key status (masterAuth)
|
|
742
|
-
* PUT /admin/api-keys/:provider - Set or update API key (masterAuth)
|
|
743
|
-
* DELETE /admin/api-keys/:provider - Delete API key (masterAuth)
|
|
744
|
-
* GET /admin/forex/rates - Forex exchange rates (masterAuth)
|
|
745
|
-
* GET /admin/wallets/:id/balance - Wallet native+token balance (masterAuth)
|
|
746
|
-
* GET /admin/wallets/:id/transactions - Wallet transaction history (masterAuth)
|
|
747
|
-
* GET /admin/telegram-users - List Telegram bot users (masterAuth)
|
|
748
|
-
* PUT /admin/telegram-users/:chatId - Update Telegram user role (masterAuth)
|
|
749
|
-
* DELETE /admin/telegram-users/:chatId - Delete Telegram user (masterAuth)
|
|
750
|
-
* GET /admin/transactions - Cross-wallet transaction list (masterAuth)
|
|
751
|
-
* GET /admin/incoming - Cross-wallet incoming tx list (masterAuth)
|
|
752
|
-
* POST /admin/transactions/:id/cancel - Cancel delayed transaction (masterAuth)
|
|
753
|
-
* POST /admin/transactions/:id/reject - Reject pending approval transaction (masterAuth)
|
|
754
|
-
*/
|
|
755
|
-
// ---------------------------------------------------------------------------
|
|
756
|
-
// Amount formatting helpers (#168)
|
|
757
|
-
// ---------------------------------------------------------------------------
|
|
758
|
-
const NATIVE_DECIMALS = { solana: 9, ethereum: 18 };
|
|
759
|
-
const NATIVE_SYMBOLS = { solana: 'SOL', ethereum: 'ETH' };
|
|
760
|
-
/**
|
|
761
|
-
* Format raw blockchain amount to human-readable string with token symbol.
|
|
762
|
-
* Returns null if formatting is not possible (unknown token, null amount, etc).
|
|
763
|
-
*/
|
|
764
|
-
function formatTxAmount(amount, chain, network, tokenAddress, db) {
|
|
765
|
-
if (!amount || amount === '0')
|
|
766
|
-
return amount;
|
|
767
|
-
try {
|
|
768
|
-
if (tokenAddress) {
|
|
769
|
-
// Token transfer: look up decimals/symbol from token_registry
|
|
770
|
-
const token = db
|
|
771
|
-
.select({ symbol: tokenRegistry.symbol, decimals: tokenRegistry.decimals })
|
|
772
|
-
.from(tokenRegistry)
|
|
773
|
-
.where(and(eq(tokenRegistry.address, tokenAddress), network ? eq(tokenRegistry.network, network) : undefined))
|
|
774
|
-
.limit(1)
|
|
775
|
-
.get();
|
|
776
|
-
if (!token)
|
|
777
|
-
return null; // unknown token → caller falls back to raw
|
|
778
|
-
return `${formatAmount(BigInt(amount), token.decimals)} ${token.symbol}`;
|
|
779
|
-
}
|
|
780
|
-
// Native transfer
|
|
781
|
-
const decimals = NATIVE_DECIMALS[chain] ?? 18;
|
|
782
|
-
const symbol = NATIVE_SYMBOLS[chain] ?? chain.toUpperCase();
|
|
783
|
-
return `${formatAmount(BigInt(amount), decimals)} ${symbol}`;
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
25
|
export function adminRoutes(deps) {
|
|
790
26
|
const router = new OpenAPIHono({ defaultHook: openApiValidationHook });
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
router
|
|
795
|
-
|
|
796
|
-
const uptime = nowSec - deps.startTime;
|
|
797
|
-
// Count wallets
|
|
798
|
-
const walletCountResult = deps.db
|
|
799
|
-
.select({ count: sql `count(*)` })
|
|
800
|
-
.from(wallets)
|
|
801
|
-
.get();
|
|
802
|
-
const walletCount = walletCountResult?.count ?? 0;
|
|
803
|
-
// Count active sessions (not expired, not revoked)
|
|
804
|
-
// Use raw SQL with integer comparison (expiresAt is stored as epoch seconds)
|
|
805
|
-
const activeSessionResult = deps.db
|
|
806
|
-
.select({ count: sql `count(*)` })
|
|
807
|
-
.from(sessions)
|
|
808
|
-
.where(sql `${sessions.revokedAt} IS NULL AND (${sessions.expiresAt} = 0 OR ${sessions.expiresAt} > ${nowSec})`)
|
|
809
|
-
.get();
|
|
810
|
-
const activeSessionCount = activeSessionResult?.count ?? 0;
|
|
811
|
-
// Count policies
|
|
812
|
-
const policyCountResult = deps.db
|
|
813
|
-
.select({ count: sql `count(*)` })
|
|
814
|
-
.from(policies)
|
|
815
|
-
.get();
|
|
816
|
-
const policyCount = policyCountResult?.count ?? 0;
|
|
817
|
-
// Count recent transactions (24h) -- created_at stored as epoch seconds in integer column
|
|
818
|
-
const cutoffSec = nowSec - 86400;
|
|
819
|
-
const recentTxCountResult = deps.db
|
|
820
|
-
.select({ count: sql `count(*)` })
|
|
821
|
-
.from(transactions)
|
|
822
|
-
.where(sql `${transactions.createdAt} > ${cutoffSec}`)
|
|
823
|
-
.get();
|
|
824
|
-
const recentTxCount = recentTxCountResult?.count ?? 0;
|
|
825
|
-
// Count failed transactions (24h)
|
|
826
|
-
const failedTxCountResult = deps.db
|
|
827
|
-
.select({ count: sql `count(*)` })
|
|
828
|
-
.from(transactions)
|
|
829
|
-
.where(sql `${transactions.status} = 'FAILED' AND ${transactions.createdAt} > ${cutoffSec}`)
|
|
830
|
-
.get();
|
|
831
|
-
const failedTxCount = failedTxCountResult?.count ?? 0;
|
|
832
|
-
// Recent 5 transactions with wallet name
|
|
833
|
-
const recentTxRows = deps.db
|
|
834
|
-
.select({
|
|
835
|
-
id: transactions.id,
|
|
836
|
-
walletId: transactions.walletId,
|
|
837
|
-
walletName: wallets.name,
|
|
838
|
-
type: transactions.type,
|
|
839
|
-
status: transactions.status,
|
|
840
|
-
toAddress: transactions.toAddress,
|
|
841
|
-
amount: transactions.amount,
|
|
842
|
-
amountUsd: transactions.amountUsd,
|
|
843
|
-
network: transactions.network,
|
|
844
|
-
txHash: transactions.txHash,
|
|
845
|
-
chain: transactions.chain,
|
|
846
|
-
tokenMint: transactions.tokenMint,
|
|
847
|
-
contractAddress: transactions.contractAddress,
|
|
848
|
-
createdAt: transactions.createdAt,
|
|
849
|
-
})
|
|
850
|
-
.from(transactions)
|
|
851
|
-
.leftJoin(wallets, eq(transactions.walletId, wallets.id))
|
|
852
|
-
.orderBy(desc(transactions.createdAt))
|
|
853
|
-
.limit(5)
|
|
854
|
-
.all();
|
|
855
|
-
const recentTransactions = recentTxRows.map((tx) => {
|
|
856
|
-
const tokenAddr = tx.tokenMint ?? tx.contractAddress ?? null;
|
|
857
|
-
return {
|
|
858
|
-
id: tx.id,
|
|
859
|
-
walletId: tx.walletId,
|
|
860
|
-
walletName: tx.walletName ?? null,
|
|
861
|
-
type: tx.type,
|
|
862
|
-
status: tx.status,
|
|
863
|
-
toAddress: tx.toAddress ?? null,
|
|
864
|
-
amount: tx.amount ?? null,
|
|
865
|
-
formattedAmount: formatTxAmount(tx.amount ?? null, tx.chain, tx.network ?? null, tokenAddr, deps.db),
|
|
866
|
-
amountUsd: tx.amountUsd ?? null,
|
|
867
|
-
network: tx.network ?? null,
|
|
868
|
-
txHash: tx.txHash ?? null,
|
|
869
|
-
createdAt: tx.createdAt instanceof Date
|
|
870
|
-
? Math.floor(tx.createdAt.getTime() / 1000)
|
|
871
|
-
: (typeof tx.createdAt === 'number' ? tx.createdAt : null),
|
|
872
|
-
};
|
|
873
|
-
});
|
|
874
|
-
const ksState = deps.killSwitchService
|
|
875
|
-
? deps.killSwitchService.getState()
|
|
876
|
-
: deps.getKillSwitchState();
|
|
877
|
-
const latestVersion = deps.versionCheckService?.getLatest() ?? null;
|
|
878
|
-
const semverMod = await import('semver');
|
|
879
|
-
const updateAvailable = latestVersion !== null
|
|
880
|
-
&& semverMod.default.valid(latestVersion) !== null
|
|
881
|
-
&& semverMod.default.gt(latestVersion, deps.version);
|
|
882
|
-
// Check for auto-provisioned status (recovery.key exists in data dir)
|
|
883
|
-
const { existsSync } = await import('node:fs');
|
|
884
|
-
const { join } = await import('node:path');
|
|
885
|
-
const autoProvisioned = deps.dataDir
|
|
886
|
-
? existsSync(join(deps.dataDir, 'recovery.key'))
|
|
887
|
-
: false;
|
|
888
|
-
return c.json({
|
|
889
|
-
status: 'running',
|
|
890
|
-
version: deps.version,
|
|
891
|
-
latestVersion,
|
|
892
|
-
updateAvailable,
|
|
893
|
-
uptime,
|
|
894
|
-
walletCount,
|
|
895
|
-
activeSessionCount,
|
|
896
|
-
killSwitchState: ksState.state,
|
|
897
|
-
adminTimeout: deps.adminTimeout,
|
|
898
|
-
timestamp: nowSec,
|
|
899
|
-
policyCount,
|
|
900
|
-
recentTxCount,
|
|
901
|
-
failedTxCount,
|
|
902
|
-
autoProvisioned,
|
|
903
|
-
recentTransactions,
|
|
904
|
-
}, 200);
|
|
905
|
-
});
|
|
906
|
-
// ---------------------------------------------------------------------------
|
|
907
|
-
// POST /admin/kill-switch
|
|
908
|
-
// ---------------------------------------------------------------------------
|
|
909
|
-
router.openapi(activateKillSwitchRoute, async (c) => {
|
|
910
|
-
if (deps.killSwitchService) {
|
|
911
|
-
const result = deps.killSwitchService.activateWithCascade('master');
|
|
912
|
-
if (!result.success) {
|
|
913
|
-
throw new WAIaaSError('KILL_SWITCH_ACTIVE', {
|
|
914
|
-
message: result.error ?? 'Kill switch is already active',
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
const state = deps.killSwitchService.getState();
|
|
918
|
-
return c.json({
|
|
919
|
-
state: 'SUSPENDED',
|
|
920
|
-
activatedAt: state.activatedAt ?? Math.floor(Date.now() / 1000),
|
|
921
|
-
}, 200);
|
|
922
|
-
}
|
|
923
|
-
// Legacy fallback (no KillSwitchService)
|
|
924
|
-
const ksState = deps.getKillSwitchState();
|
|
925
|
-
if (ksState.state !== 'ACTIVE' && ksState.state !== 'NORMAL') {
|
|
926
|
-
throw new WAIaaSError('KILL_SWITCH_ACTIVE', {
|
|
927
|
-
message: 'Kill switch is already activated',
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
931
|
-
deps.setKillSwitchState('SUSPENDED', 'master');
|
|
932
|
-
return c.json({
|
|
933
|
-
state: 'SUSPENDED',
|
|
934
|
-
activatedAt: nowSec,
|
|
935
|
-
}, 200);
|
|
936
|
-
});
|
|
937
|
-
// ---------------------------------------------------------------------------
|
|
938
|
-
// GET /admin/kill-switch
|
|
939
|
-
// ---------------------------------------------------------------------------
|
|
940
|
-
router.openapi(getKillSwitchRoute, async (c) => {
|
|
941
|
-
if (deps.killSwitchService) {
|
|
942
|
-
const ksState = deps.killSwitchService.getState();
|
|
943
|
-
return c.json({
|
|
944
|
-
state: ksState.state,
|
|
945
|
-
activatedAt: ksState.activatedAt,
|
|
946
|
-
activatedBy: ksState.activatedBy,
|
|
947
|
-
}, 200);
|
|
948
|
-
}
|
|
949
|
-
const ksState = deps.getKillSwitchState();
|
|
950
|
-
return c.json({
|
|
951
|
-
state: ksState.state,
|
|
952
|
-
activatedAt: ksState.activatedAt,
|
|
953
|
-
activatedBy: ksState.activatedBy,
|
|
954
|
-
}, 200);
|
|
955
|
-
});
|
|
956
|
-
// ---------------------------------------------------------------------------
|
|
957
|
-
// POST /admin/kill-switch/escalate
|
|
958
|
-
// ---------------------------------------------------------------------------
|
|
959
|
-
router.openapi(escalateKillSwitchRoute, async (c) => {
|
|
960
|
-
if (deps.killSwitchService) {
|
|
961
|
-
const result = deps.killSwitchService.escalateWithCascade('master');
|
|
962
|
-
if (!result.success) {
|
|
963
|
-
throw new WAIaaSError('INVALID_STATE_TRANSITION', {
|
|
964
|
-
message: result.error ?? 'Cannot escalate kill switch',
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
const state = deps.killSwitchService.getState();
|
|
968
|
-
return c.json({
|
|
969
|
-
state: 'LOCKED',
|
|
970
|
-
escalatedAt: state.activatedAt ?? Math.floor(Date.now() / 1000),
|
|
971
|
-
}, 200);
|
|
972
|
-
}
|
|
973
|
-
throw new WAIaaSError('INVALID_STATE_TRANSITION', {
|
|
974
|
-
message: 'Kill switch service not available',
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
// ---------------------------------------------------------------------------
|
|
978
|
-
// POST /admin/recover (dual-auth recovery)
|
|
979
|
-
// ---------------------------------------------------------------------------
|
|
980
|
-
router.openapi(recoverRoute, async (c) => {
|
|
981
|
-
if (deps.killSwitchService) {
|
|
982
|
-
const currentState = deps.killSwitchService.getState();
|
|
983
|
-
if (currentState.state === 'ACTIVE') {
|
|
984
|
-
throw new WAIaaSError('KILL_SWITCH_NOT_ACTIVE', {
|
|
985
|
-
message: 'Kill switch is not active, nothing to recover',
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
// Master password (masterAuth middleware) is sufficient for recovery.
|
|
989
|
-
// Self-hosted daemon: admin with master password = server/DB access.
|
|
990
|
-
// Dual-auth adds no real security but blocks emergency recovery.
|
|
991
|
-
// LOCKED recovery: additional wait time (5 seconds)
|
|
992
|
-
if (currentState.state === 'LOCKED') {
|
|
993
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
994
|
-
const success = deps.killSwitchService.recoverFromLocked();
|
|
995
|
-
if (!success) {
|
|
996
|
-
throw new WAIaaSError('INVALID_STATE_TRANSITION', {
|
|
997
|
-
message: 'Failed to recover from LOCKED state (concurrent state change)',
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
// SUSPENDED recovery
|
|
1003
|
-
const success = deps.killSwitchService.recoverFromSuspended();
|
|
1004
|
-
if (!success) {
|
|
1005
|
-
throw new WAIaaSError('INVALID_STATE_TRANSITION', {
|
|
1006
|
-
message: 'Failed to recover from SUSPENDED state (concurrent state change)',
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1011
|
-
// Send recovery notification
|
|
1012
|
-
if (deps.notificationService) {
|
|
1013
|
-
void deps.notificationService.notify('KILL_SWITCH_RECOVERED', 'system', {});
|
|
1014
|
-
}
|
|
1015
|
-
return c.json({
|
|
1016
|
-
state: 'ACTIVE',
|
|
1017
|
-
recoveredAt: nowSec,
|
|
1018
|
-
}, 200);
|
|
1019
|
-
}
|
|
1020
|
-
// Legacy fallback
|
|
1021
|
-
const ksState = deps.getKillSwitchState();
|
|
1022
|
-
if (ksState.state === 'NORMAL' || ksState.state === 'ACTIVE') {
|
|
1023
|
-
throw new WAIaaSError('KILL_SWITCH_NOT_ACTIVE', {
|
|
1024
|
-
message: 'Kill switch is not active, nothing to recover',
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
deps.setKillSwitchState('ACTIVE');
|
|
1028
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1029
|
-
return c.json({
|
|
1030
|
-
state: 'ACTIVE',
|
|
1031
|
-
recoveredAt: nowSec,
|
|
1032
|
-
}, 200);
|
|
1033
|
-
});
|
|
1034
|
-
// ---------------------------------------------------------------------------
|
|
1035
|
-
// POST /admin/shutdown
|
|
1036
|
-
// ---------------------------------------------------------------------------
|
|
1037
|
-
router.openapi(shutdownRoute, async (c) => {
|
|
1038
|
-
if (deps.requestShutdown) {
|
|
1039
|
-
deps.requestShutdown();
|
|
1040
|
-
}
|
|
1041
|
-
return c.json({
|
|
1042
|
-
message: 'Shutdown initiated',
|
|
1043
|
-
}, 200);
|
|
1044
|
-
});
|
|
1045
|
-
// ---------------------------------------------------------------------------
|
|
1046
|
-
// PUT /admin/master-password
|
|
1047
|
-
// ---------------------------------------------------------------------------
|
|
1048
|
-
router.openapi(masterPasswordChangeRoute, async (c) => {
|
|
1049
|
-
const body = c.req.valid('json');
|
|
1050
|
-
const newPassword = body.newPassword;
|
|
1051
|
-
if (!deps.passwordRef) {
|
|
1052
|
-
throw new WAIaaSError('ACTION_VALIDATION_FAILED', {
|
|
1053
|
-
message: 'Password change not supported (passwordRef not available)',
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
const oldPassword = deps.passwordRef.password;
|
|
1057
|
-
if (newPassword === oldPassword) {
|
|
1058
|
-
throw new WAIaaSError('ACTION_VALIDATION_FAILED', {
|
|
1059
|
-
message: 'New password must be different from the current password',
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
// 1. Re-encrypt keystore files
|
|
1063
|
-
const { join } = await import('node:path');
|
|
1064
|
-
const { reEncryptKeystores, reEncryptSettings } = await import('../../infrastructure/keystore/re-encrypt.js');
|
|
1065
|
-
const keystoreDir = deps.dataDir ? join(deps.dataDir, 'keystore') : null;
|
|
1066
|
-
let walletsReEncrypted = 0;
|
|
1067
|
-
if (keystoreDir) {
|
|
1068
|
-
const { existsSync } = await import('node:fs');
|
|
1069
|
-
if (existsSync(keystoreDir)) {
|
|
1070
|
-
walletsReEncrypted = await reEncryptKeystores(keystoreDir, oldPassword, newPassword);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
// 2. Re-encrypt settings + API keys in DB
|
|
1074
|
-
const settingsReEncrypted = reEncryptSettings(deps.db, deps.sqlite, oldPassword, newPassword);
|
|
1075
|
-
// 3. Compute new Argon2id hash
|
|
1076
|
-
const argon2 = await import('argon2');
|
|
1077
|
-
const newHash = await argon2.default.hash(newPassword, {
|
|
1078
|
-
type: argon2.default.argon2id,
|
|
1079
|
-
memoryCost: 19456,
|
|
1080
|
-
timeCost: 2,
|
|
1081
|
-
parallelism: 1,
|
|
1082
|
-
});
|
|
1083
|
-
// 4. Update DB master_password_hash
|
|
1084
|
-
const { keyValueStore } = await import('../../infrastructure/database/schema.js');
|
|
1085
|
-
deps.db
|
|
1086
|
-
.update(keyValueStore)
|
|
1087
|
-
.set({ value: newHash, updatedAt: new Date() })
|
|
1088
|
-
.where(eq(keyValueStore.key, 'master_password_hash'))
|
|
1089
|
-
.run();
|
|
1090
|
-
// 5. Update in-memory ref (live swap)
|
|
1091
|
-
deps.passwordRef.password = newPassword;
|
|
1092
|
-
deps.passwordRef.hash = newHash;
|
|
1093
|
-
// 6. Delete recovery.key if it exists (auto-provision → manual)
|
|
1094
|
-
if (deps.dataDir) {
|
|
1095
|
-
const recoveryPath = join(deps.dataDir, 'recovery.key');
|
|
1096
|
-
const { existsSync, unlinkSync } = await import('node:fs');
|
|
1097
|
-
if (existsSync(recoveryPath)) {
|
|
1098
|
-
try {
|
|
1099
|
-
unlinkSync(recoveryPath);
|
|
1100
|
-
}
|
|
1101
|
-
catch {
|
|
1102
|
-
// Non-fatal: recovery.key cleanup failure
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
return c.json({
|
|
1107
|
-
message: 'Master password changed successfully',
|
|
1108
|
-
walletsReEncrypted,
|
|
1109
|
-
settingsReEncrypted,
|
|
1110
|
-
}, 200);
|
|
1111
|
-
});
|
|
1112
|
-
// ---------------------------------------------------------------------------
|
|
1113
|
-
// POST /admin/rotate-secret
|
|
1114
|
-
// ---------------------------------------------------------------------------
|
|
1115
|
-
router.openapi(rotateSecretRoute, async (c) => {
|
|
1116
|
-
if (!deps.jwtSecretManager) {
|
|
1117
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1118
|
-
message: 'JWT secret manager not available',
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
await deps.jwtSecretManager.rotateSecret();
|
|
1122
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1123
|
-
return c.json({
|
|
1124
|
-
rotatedAt: nowSec,
|
|
1125
|
-
message: 'JWT secret rotated. Old tokens valid for 5 minutes.',
|
|
1126
|
-
}, 200);
|
|
1127
|
-
});
|
|
1128
|
-
// ---------------------------------------------------------------------------
|
|
1129
|
-
// GET /admin/notifications/status
|
|
1130
|
-
// ---------------------------------------------------------------------------
|
|
1131
|
-
router.openapi(notificationsStatusRoute, async (c) => {
|
|
1132
|
-
const ss = deps.settingsService;
|
|
1133
|
-
const svc = deps.notificationService;
|
|
1134
|
-
const channelNames = svc ? svc.getChannelNames() : [];
|
|
1135
|
-
// Read from SettingsService (dynamic) instead of static config snapshot
|
|
1136
|
-
const enabled = ss ? ss.get('notifications.enabled') === 'true' : (deps.notificationConfig?.enabled ?? false);
|
|
1137
|
-
const channels = [
|
|
1138
|
-
{
|
|
1139
|
-
name: 'telegram',
|
|
1140
|
-
enabled: !!((ss ? ss.get('notifications.telegram_bot_token') : deps.notificationConfig?.telegram_bot_token) &&
|
|
1141
|
-
(ss ? ss.get('notifications.telegram_chat_id') : deps.notificationConfig?.telegram_chat_id) &&
|
|
1142
|
-
channelNames.includes('telegram')),
|
|
1143
|
-
},
|
|
1144
|
-
{
|
|
1145
|
-
name: 'discord',
|
|
1146
|
-
enabled: !!((ss ? ss.get('notifications.discord_webhook_url') : deps.notificationConfig?.discord_webhook_url) &&
|
|
1147
|
-
channelNames.includes('discord')),
|
|
1148
|
-
},
|
|
1149
|
-
(() => {
|
|
1150
|
-
// v29.10+: ntfy topics are per-wallet in wallet_apps table, not global config
|
|
1151
|
-
const ntfyWalletCount = deps.db
|
|
1152
|
-
.select({ count: sql `count(*)` })
|
|
1153
|
-
.from(walletApps)
|
|
1154
|
-
.where(sql `${walletApps.signTopic} IS NOT NULL OR ${walletApps.notifyTopic} IS NOT NULL`)
|
|
1155
|
-
.get()?.count ?? 0;
|
|
1156
|
-
return {
|
|
1157
|
-
name: 'ntfy',
|
|
1158
|
-
enabled: ntfyWalletCount > 0,
|
|
1159
|
-
configuredWallets: ntfyWalletCount,
|
|
1160
|
-
};
|
|
1161
|
-
})(),
|
|
1162
|
-
{
|
|
1163
|
-
name: 'slack',
|
|
1164
|
-
enabled: !!((ss ? ss.get('notifications.slack_webhook_url') : deps.notificationConfig?.slack_webhook_url) &&
|
|
1165
|
-
channelNames.includes('slack')),
|
|
1166
|
-
},
|
|
1167
|
-
];
|
|
1168
|
-
return c.json({
|
|
1169
|
-
enabled,
|
|
1170
|
-
channels,
|
|
1171
|
-
}, 200);
|
|
1172
|
-
});
|
|
1173
|
-
// ---------------------------------------------------------------------------
|
|
1174
|
-
// POST /admin/notifications/test
|
|
1175
|
-
// ---------------------------------------------------------------------------
|
|
1176
|
-
router.openapi(notificationsTestRoute, async (c) => {
|
|
1177
|
-
const svc = deps.notificationService;
|
|
1178
|
-
if (!svc) {
|
|
1179
|
-
return c.json({ results: [] }, 200);
|
|
1180
|
-
}
|
|
1181
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1182
|
-
const allChannels = svc.getChannels();
|
|
1183
|
-
const targetChannels = body.channel
|
|
1184
|
-
? allChannels.filter((ch) => ch.name === body.channel)
|
|
1185
|
-
: allChannels;
|
|
1186
|
-
const testPayload = {
|
|
1187
|
-
eventType: 'TX_CONFIRMED',
|
|
1188
|
-
walletId: 'admin-test',
|
|
1189
|
-
title: '[Test] Notification Test',
|
|
1190
|
-
body: 'WAIaaS notification test',
|
|
1191
|
-
message: '[Test] Notification Test\nWAIaaS notification test',
|
|
1192
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
1193
|
-
};
|
|
1194
|
-
const results = [];
|
|
1195
|
-
for (const ch of targetChannels) {
|
|
1196
|
-
try {
|
|
1197
|
-
await ch.send(testPayload);
|
|
1198
|
-
results.push({ channel: ch.name, success: true });
|
|
1199
|
-
}
|
|
1200
|
-
catch (err) {
|
|
1201
|
-
results.push({
|
|
1202
|
-
channel: ch.name,
|
|
1203
|
-
success: false,
|
|
1204
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
return c.json({ results }, 200);
|
|
1209
|
-
});
|
|
1210
|
-
// ---------------------------------------------------------------------------
|
|
1211
|
-
// GET /admin/notifications/log
|
|
1212
|
-
// ---------------------------------------------------------------------------
|
|
1213
|
-
router.openapi(notificationsLogRoute, async (c) => {
|
|
1214
|
-
const query = c.req.valid('query');
|
|
1215
|
-
const page = Math.max(1, parseInt(query.page ?? '1', 10) || 1);
|
|
1216
|
-
const pageSize = Math.min(100, Math.max(1, parseInt(query.pageSize ?? '20', 10) || 20));
|
|
1217
|
-
const offset = (page - 1) * pageSize;
|
|
1218
|
-
// Build where conditions
|
|
1219
|
-
const conditions = [];
|
|
1220
|
-
if (query.channel) {
|
|
1221
|
-
conditions.push(eq(notificationLogs.channel, query.channel));
|
|
1222
|
-
}
|
|
1223
|
-
if (query.status) {
|
|
1224
|
-
conditions.push(eq(notificationLogs.status, query.status));
|
|
1225
|
-
}
|
|
1226
|
-
if (query.eventType) {
|
|
1227
|
-
conditions.push(eq(notificationLogs.eventType, query.eventType));
|
|
1228
|
-
}
|
|
1229
|
-
if (query.since) {
|
|
1230
|
-
const sinceTs = parseInt(query.since, 10);
|
|
1231
|
-
if (!isNaN(sinceTs)) {
|
|
1232
|
-
conditions.push(gte(notificationLogs.createdAt, new Date(sinceTs * 1000)));
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (query.until) {
|
|
1236
|
-
const untilTs = parseInt(query.until, 10);
|
|
1237
|
-
if (!isNaN(untilTs)) {
|
|
1238
|
-
conditions.push(lte(notificationLogs.createdAt, new Date(untilTs * 1000)));
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1242
|
-
// Query total count
|
|
1243
|
-
const totalResult = deps.db
|
|
1244
|
-
.select({ count: drizzleCount() })
|
|
1245
|
-
.from(notificationLogs)
|
|
1246
|
-
.where(whereClause)
|
|
1247
|
-
.get();
|
|
1248
|
-
const total = totalResult?.count ?? 0;
|
|
1249
|
-
// Query logs with pagination
|
|
1250
|
-
const rows = deps.db
|
|
1251
|
-
.select()
|
|
1252
|
-
.from(notificationLogs)
|
|
1253
|
-
.where(whereClause)
|
|
1254
|
-
.orderBy(desc(notificationLogs.createdAt))
|
|
1255
|
-
.limit(pageSize)
|
|
1256
|
-
.offset(offset)
|
|
1257
|
-
.all();
|
|
1258
|
-
const logs = rows.map((row) => ({
|
|
1259
|
-
id: row.id,
|
|
1260
|
-
eventType: row.eventType,
|
|
1261
|
-
walletId: row.walletId ?? null,
|
|
1262
|
-
channel: row.channel,
|
|
1263
|
-
status: row.status,
|
|
1264
|
-
error: row.error ?? null,
|
|
1265
|
-
message: row.message ?? null,
|
|
1266
|
-
createdAt: row.createdAt instanceof Date
|
|
1267
|
-
? Math.floor(row.createdAt.getTime() / 1000)
|
|
1268
|
-
: (typeof row.createdAt === 'number' ? row.createdAt : 0),
|
|
1269
|
-
}));
|
|
1270
|
-
return c.json({ logs, total, page, pageSize }, 200);
|
|
1271
|
-
});
|
|
1272
|
-
// ---------------------------------------------------------------------------
|
|
1273
|
-
// GET /admin/settings
|
|
1274
|
-
// ---------------------------------------------------------------------------
|
|
1275
|
-
router.openapi(settingsGetRoute, async (c) => {
|
|
1276
|
-
if (!deps.settingsService) {
|
|
1277
|
-
const emptyCategory = {};
|
|
1278
|
-
return c.json({
|
|
1279
|
-
notifications: emptyCategory,
|
|
1280
|
-
rpc: emptyCategory,
|
|
1281
|
-
security: emptyCategory,
|
|
1282
|
-
daemon: emptyCategory,
|
|
1283
|
-
walletconnect: emptyCategory,
|
|
1284
|
-
oracle: emptyCategory,
|
|
1285
|
-
display: emptyCategory,
|
|
1286
|
-
autostop: emptyCategory,
|
|
1287
|
-
monitoring: emptyCategory,
|
|
1288
|
-
telegram: emptyCategory,
|
|
1289
|
-
signing_sdk: emptyCategory,
|
|
1290
|
-
gas_condition: emptyCategory,
|
|
1291
|
-
}, 200);
|
|
1292
|
-
}
|
|
1293
|
-
const masked = deps.settingsService.getAllMasked();
|
|
1294
|
-
return c.json(masked, 200);
|
|
1295
|
-
});
|
|
1296
|
-
// ---------------------------------------------------------------------------
|
|
1297
|
-
// PUT /admin/settings
|
|
1298
|
-
// ---------------------------------------------------------------------------
|
|
1299
|
-
router.openapi(settingsPutRoute, async (c) => {
|
|
1300
|
-
const body = c.req.valid('json');
|
|
1301
|
-
const entries = body.settings;
|
|
1302
|
-
// Validate all keys exist in SETTING_DEFINITIONS (or dynamic tier pattern)
|
|
1303
|
-
for (const entry of entries) {
|
|
1304
|
-
const def = getSettingDefinition(entry.key);
|
|
1305
|
-
if (!def) {
|
|
1306
|
-
throw new WAIaaSError('ACTION_VALIDATION_FAILED', {
|
|
1307
|
-
message: `Unknown setting key: ${entry.key}`,
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
// [Phase 331] Validate tier override values
|
|
1311
|
-
if (entry.key.startsWith('actions.') && entry.key.endsWith('_tier')) {
|
|
1312
|
-
const parsed = ActionTierOverrideSchema.safeParse(entry.value);
|
|
1313
|
-
if (!parsed.success) {
|
|
1314
|
-
throw new WAIaaSError('ACTION_VALIDATION_FAILED', {
|
|
1315
|
-
message: `Invalid tier value '${entry.value}' for key '${entry.key}'. Must be one of: INSTANT, NOTIFY, DELAY, APPROVAL, or empty string.`,
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
if (!deps.settingsService) {
|
|
1321
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1322
|
-
message: 'Settings service not available',
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
// Persist all values
|
|
1326
|
-
deps.settingsService.setMany(entries);
|
|
1327
|
-
// Notify hot-reload callback if provided
|
|
1328
|
-
if (deps.onSettingsChanged) {
|
|
1329
|
-
deps.onSettingsChanged(entries.map((e) => e.key));
|
|
1330
|
-
}
|
|
1331
|
-
const masked = deps.settingsService.getAllMasked();
|
|
1332
|
-
return c.json({
|
|
1333
|
-
updated: entries.length,
|
|
1334
|
-
settings: masked,
|
|
1335
|
-
}, 200);
|
|
1336
|
-
});
|
|
1337
|
-
// ---------------------------------------------------------------------------
|
|
1338
|
-
// POST /admin/settings/test-rpc
|
|
1339
|
-
// ---------------------------------------------------------------------------
|
|
1340
|
-
router.openapi(testRpcRoute, async (c) => {
|
|
1341
|
-
const body = c.req.valid('json');
|
|
1342
|
-
const { url, chain } = body;
|
|
1343
|
-
const rpcMethod = chain === 'solana' ? 'getBlockHeight' : 'eth_blockNumber';
|
|
1344
|
-
const rpcBody = JSON.stringify({
|
|
1345
|
-
jsonrpc: '2.0',
|
|
1346
|
-
method: rpcMethod,
|
|
1347
|
-
params: [],
|
|
1348
|
-
id: 1,
|
|
1349
|
-
});
|
|
1350
|
-
const startMs = performance.now();
|
|
1351
|
-
try {
|
|
1352
|
-
const response = await fetch(url, {
|
|
1353
|
-
method: 'POST',
|
|
1354
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1355
|
-
body: rpcBody,
|
|
1356
|
-
signal: AbortSignal.timeout(5000),
|
|
1357
|
-
});
|
|
1358
|
-
const latencyMs = Math.round(performance.now() - startMs);
|
|
1359
|
-
const result = (await response.json());
|
|
1360
|
-
if (result.error) {
|
|
1361
|
-
return c.json({
|
|
1362
|
-
success: false,
|
|
1363
|
-
latencyMs,
|
|
1364
|
-
error: result.error.message ?? 'RPC error',
|
|
1365
|
-
}, 200);
|
|
1366
|
-
}
|
|
1367
|
-
// Parse block number from result
|
|
1368
|
-
let blockNumber;
|
|
1369
|
-
if (chain === 'solana') {
|
|
1370
|
-
blockNumber = typeof result.result === 'number' ? result.result : undefined;
|
|
1371
|
-
}
|
|
1372
|
-
else {
|
|
1373
|
-
// eth_blockNumber returns hex string
|
|
1374
|
-
blockNumber =
|
|
1375
|
-
typeof result.result === 'string'
|
|
1376
|
-
? parseInt(result.result, 16)
|
|
1377
|
-
: undefined;
|
|
1378
|
-
}
|
|
1379
|
-
return c.json({
|
|
1380
|
-
success: true,
|
|
1381
|
-
latencyMs,
|
|
1382
|
-
blockNumber,
|
|
1383
|
-
}, 200);
|
|
1384
|
-
}
|
|
1385
|
-
catch (err) {
|
|
1386
|
-
const latencyMs = Math.round(performance.now() - startMs);
|
|
1387
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1388
|
-
return c.json({
|
|
1389
|
-
success: false,
|
|
1390
|
-
latencyMs,
|
|
1391
|
-
error: errorMessage,
|
|
1392
|
-
}, 200);
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
// ---------------------------------------------------------------------------
|
|
1396
|
-
// GET /admin/oracle-status
|
|
1397
|
-
// ---------------------------------------------------------------------------
|
|
1398
|
-
router.openapi(oracleStatusRoute, async (c) => {
|
|
1399
|
-
const stats = deps.priceOracle?.getCacheStats() ?? { hits: 0, misses: 0, staleHits: 0, size: 0, evictions: 0 };
|
|
1400
|
-
return c.json({
|
|
1401
|
-
cache: stats,
|
|
1402
|
-
sources: {
|
|
1403
|
-
pyth: {
|
|
1404
|
-
available: !!deps.priceOracle,
|
|
1405
|
-
baseUrl: 'https://hermes.pyth.network',
|
|
1406
|
-
},
|
|
1407
|
-
coingecko: {
|
|
1408
|
-
available: deps.oracleConfig?.coingeckoApiKeyConfigured ?? false,
|
|
1409
|
-
apiKeyConfigured: deps.oracleConfig?.coingeckoApiKeyConfigured ?? false,
|
|
1410
|
-
},
|
|
1411
|
-
},
|
|
1412
|
-
crossValidation: {
|
|
1413
|
-
enabled: deps.oracleConfig?.coingeckoApiKeyConfigured ?? false,
|
|
1414
|
-
threshold: deps.oracleConfig?.crossValidationThreshold ?? 5,
|
|
1415
|
-
},
|
|
1416
|
-
}, 200);
|
|
1417
|
-
});
|
|
1418
|
-
// ---------------------------------------------------------------------------
|
|
1419
|
-
// GET /admin/api-keys
|
|
1420
|
-
// ---------------------------------------------------------------------------
|
|
1421
|
-
router.openapi(apiKeysListRoute, async (c) => {
|
|
1422
|
-
const registry = deps.actionProviderRegistry;
|
|
1423
|
-
const ss = deps.settingsService;
|
|
1424
|
-
if (!registry) {
|
|
1425
|
-
return c.json({ keys: [] }, 200);
|
|
1426
|
-
}
|
|
1427
|
-
const providers = registry.listProviders();
|
|
1428
|
-
const keys = providers.map((p) => {
|
|
1429
|
-
const hasKey = ss ? ss.hasApiKey(p.name) : false;
|
|
1430
|
-
const maskedKey = ss ? ss.getApiKeyMasked(p.name) : null;
|
|
1431
|
-
const updatedAt = ss ? ss.getApiKeyUpdatedAt(p.name) : null;
|
|
1432
|
-
return {
|
|
1433
|
-
providerName: p.name,
|
|
1434
|
-
hasKey,
|
|
1435
|
-
maskedKey,
|
|
1436
|
-
requiresApiKey: p.requiresApiKey ?? false,
|
|
1437
|
-
updatedAt: updatedAt instanceof Date ? updatedAt.toISOString() : null,
|
|
1438
|
-
};
|
|
1439
|
-
});
|
|
1440
|
-
return c.json({ keys }, 200);
|
|
1441
|
-
});
|
|
1442
|
-
// ---------------------------------------------------------------------------
|
|
1443
|
-
// PUT /admin/api-keys/:provider
|
|
1444
|
-
// ---------------------------------------------------------------------------
|
|
1445
|
-
router.openapi(apiKeyPutRoute, async (c) => {
|
|
1446
|
-
const { provider } = c.req.valid('param');
|
|
1447
|
-
const body = c.req.valid('json');
|
|
1448
|
-
if (!deps.settingsService) {
|
|
1449
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1450
|
-
message: 'Settings service not available',
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
const settingKey = `actions.${provider}_api_key`;
|
|
1454
|
-
deps.settingsService.setApiKey(provider, body.apiKey);
|
|
1455
|
-
// Trigger hot-reload so providers pick up the new key immediately
|
|
1456
|
-
if (deps.onSettingsChanged) {
|
|
1457
|
-
deps.onSettingsChanged([settingKey]);
|
|
1458
|
-
}
|
|
1459
|
-
return c.json({ success: true, providerName: provider }, 200);
|
|
1460
|
-
});
|
|
1461
|
-
// ---------------------------------------------------------------------------
|
|
1462
|
-
// DELETE /admin/api-keys/:provider
|
|
1463
|
-
// ---------------------------------------------------------------------------
|
|
1464
|
-
router.openapi(apiKeyDeleteRoute, async (c) => {
|
|
1465
|
-
const { provider } = c.req.valid('param');
|
|
1466
|
-
if (!deps.settingsService) {
|
|
1467
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1468
|
-
message: 'Settings service not available',
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
// Check if key exists before "deleting"
|
|
1472
|
-
if (!deps.settingsService.hasApiKey(provider)) {
|
|
1473
|
-
throw new WAIaaSError('ACTION_NOT_FOUND', {
|
|
1474
|
-
message: `No API key found for provider '${provider}'`,
|
|
1475
|
-
details: { providerName: provider },
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
|
-
const settingKey = `actions.${provider}_api_key`;
|
|
1479
|
-
deps.settingsService.setApiKey(provider, '');
|
|
1480
|
-
// Trigger hot-reload
|
|
1481
|
-
if (deps.onSettingsChanged) {
|
|
1482
|
-
deps.onSettingsChanged([settingKey]);
|
|
1483
|
-
}
|
|
1484
|
-
return c.json({ success: true }, 200);
|
|
1485
|
-
});
|
|
1486
|
-
// ---------------------------------------------------------------------------
|
|
1487
|
-
// GET /admin/forex/rates
|
|
1488
|
-
// ---------------------------------------------------------------------------
|
|
1489
|
-
router.openapi(forexRatesRoute, async (c) => {
|
|
1490
|
-
const { currencies: currenciesParam } = c.req.valid('query');
|
|
1491
|
-
const rates = {};
|
|
1492
|
-
if (!currenciesParam || !deps.forexRateService) {
|
|
1493
|
-
return c.json({ rates }, 200);
|
|
1494
|
-
}
|
|
1495
|
-
// Parse comma-separated currency codes, validate each
|
|
1496
|
-
const codes = currenciesParam
|
|
1497
|
-
.split(',')
|
|
1498
|
-
.map((s) => s.trim().toUpperCase())
|
|
1499
|
-
.filter((s) => CurrencyCodeSchema.safeParse(s).success);
|
|
1500
|
-
if (codes.length === 0) {
|
|
1501
|
-
return c.json({ rates }, 200);
|
|
1502
|
-
}
|
|
1503
|
-
const rateMap = await deps.forexRateService.getRates(codes);
|
|
1504
|
-
for (const [code, forexRate] of rateMap) {
|
|
1505
|
-
rates[code] = {
|
|
1506
|
-
rate: forexRate.rate,
|
|
1507
|
-
preview: formatRatePreview(forexRate.rate, code),
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
return c.json({ rates }, 200);
|
|
1511
|
-
});
|
|
1512
|
-
// ---------------------------------------------------------------------------
|
|
1513
|
-
// GET /admin/wallets/:id/transactions
|
|
1514
|
-
// ---------------------------------------------------------------------------
|
|
1515
|
-
router.openapi(adminWalletTransactionsRoute, async (c) => {
|
|
1516
|
-
const { id } = c.req.valid('param');
|
|
1517
|
-
const query = c.req.valid('query');
|
|
1518
|
-
const limit = query.limit ?? 20;
|
|
1519
|
-
const offset = query.offset ?? 0;
|
|
1520
|
-
// Verify wallet exists
|
|
1521
|
-
const wallet = deps.db.select().from(wallets).where(eq(wallets.id, id)).get();
|
|
1522
|
-
if (!wallet) {
|
|
1523
|
-
throw new WAIaaSError('WALLET_NOT_FOUND');
|
|
1524
|
-
}
|
|
1525
|
-
// Query transactions for this wallet
|
|
1526
|
-
const rows = deps.db
|
|
1527
|
-
.select()
|
|
1528
|
-
.from(transactions)
|
|
1529
|
-
.where(eq(transactions.walletId, id))
|
|
1530
|
-
.orderBy(desc(transactions.createdAt))
|
|
1531
|
-
.limit(limit)
|
|
1532
|
-
.offset(offset)
|
|
1533
|
-
.all();
|
|
1534
|
-
// Total count
|
|
1535
|
-
const totalResult = deps.db
|
|
1536
|
-
.select({ count: sql `count(*)` })
|
|
1537
|
-
.from(transactions)
|
|
1538
|
-
.where(eq(transactions.walletId, id))
|
|
1539
|
-
.get();
|
|
1540
|
-
const total = totalResult?.count ?? 0;
|
|
1541
|
-
const items = rows.map((tx) => {
|
|
1542
|
-
const tokenAddr = tx.tokenMint ?? tx.contractAddress ?? null;
|
|
1543
|
-
return {
|
|
1544
|
-
id: tx.id,
|
|
1545
|
-
type: tx.type,
|
|
1546
|
-
status: tx.status,
|
|
1547
|
-
toAddress: tx.toAddress ?? null,
|
|
1548
|
-
amount: tx.amount ?? null,
|
|
1549
|
-
formattedAmount: formatTxAmount(tx.amount ?? null, tx.chain, tx.network ?? null, tokenAddr, deps.db),
|
|
1550
|
-
amountUsd: tx.amountUsd ?? null,
|
|
1551
|
-
network: tx.network ?? null,
|
|
1552
|
-
txHash: tx.txHash ?? null,
|
|
1553
|
-
createdAt: tx.createdAt instanceof Date
|
|
1554
|
-
? Math.floor(tx.createdAt.getTime() / 1000)
|
|
1555
|
-
: (typeof tx.createdAt === 'number' ? tx.createdAt : null),
|
|
1556
|
-
};
|
|
1557
|
-
});
|
|
1558
|
-
return c.json({ items, total }, 200);
|
|
1559
|
-
});
|
|
1560
|
-
// ---------------------------------------------------------------------------
|
|
1561
|
-
// GET /admin/wallets/:id/balance
|
|
1562
|
-
// ---------------------------------------------------------------------------
|
|
1563
|
-
router.openapi(adminWalletBalanceRoute, async (c) => {
|
|
1564
|
-
const { id } = c.req.valid('param');
|
|
1565
|
-
// Verify wallet exists
|
|
1566
|
-
const wallet = deps.db.select().from(wallets).where(eq(wallets.id, id)).get();
|
|
1567
|
-
if (!wallet) {
|
|
1568
|
-
throw new WAIaaSError('WALLET_NOT_FOUND');
|
|
1569
|
-
}
|
|
1570
|
-
// If no adapter pool, return empty balances
|
|
1571
|
-
if (!deps.adapterPool) {
|
|
1572
|
-
return c.json({ balances: [] }, 200);
|
|
1573
|
-
}
|
|
1574
|
-
const chain = wallet.chain;
|
|
1575
|
-
const env = wallet.environment;
|
|
1576
|
-
const networks = getNetworksForEnvironment(chain, env);
|
|
1577
|
-
const results = await Promise.allSettled(networks.map(async (network) => {
|
|
1578
|
-
const rpcUrl = resolveRpcUrl(deps.daemonConfig.rpc, wallet.chain, network);
|
|
1579
|
-
if (!rpcUrl) {
|
|
1580
|
-
return { network, native: null, tokens: [], error: 'RPC endpoint not configured' };
|
|
1581
|
-
}
|
|
1582
|
-
const adapter = await deps.adapterPool.resolve(chain, network, rpcUrl);
|
|
1583
|
-
const balanceInfo = await adapter.getBalance(wallet.publicKey);
|
|
1584
|
-
const nativeBalance = (Number(balanceInfo.balance) / 10 ** balanceInfo.decimals).toString();
|
|
1585
|
-
// Resolve USD price for native token if price oracle is available
|
|
1586
|
-
let nativeUsd = null;
|
|
1587
|
-
if (deps.priceOracle) {
|
|
1588
|
-
try {
|
|
1589
|
-
const priceInfo = await deps.priceOracle.getNativePrice(chain);
|
|
1590
|
-
nativeUsd = Number(nativeBalance) * priceInfo.usdPrice;
|
|
1591
|
-
}
|
|
1592
|
-
catch { /* non-critical: USD price unavailable */ }
|
|
1593
|
-
}
|
|
1594
|
-
const assets = await adapter.getAssets(wallet.publicKey);
|
|
1595
|
-
const tokens = assets
|
|
1596
|
-
.filter((a) => !a.isNative)
|
|
1597
|
-
.map((a) => ({
|
|
1598
|
-
symbol: a.symbol,
|
|
1599
|
-
balance: (Number(a.balance) / 10 ** a.decimals).toString(),
|
|
1600
|
-
address: a.mint,
|
|
1601
|
-
}));
|
|
1602
|
-
return {
|
|
1603
|
-
network,
|
|
1604
|
-
native: { balance: nativeBalance, symbol: balanceInfo.symbol, usd: nativeUsd },
|
|
1605
|
-
tokens,
|
|
1606
|
-
};
|
|
1607
|
-
}));
|
|
1608
|
-
const balances = results.map((r, i) => {
|
|
1609
|
-
if (r.status === 'fulfilled')
|
|
1610
|
-
return r.value;
|
|
1611
|
-
const errorMessage = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
1612
|
-
return { network: networks[i], native: null, tokens: [], error: errorMessage };
|
|
1613
|
-
});
|
|
1614
|
-
return c.json({ balances }, 200);
|
|
1615
|
-
});
|
|
1616
|
-
// ---------------------------------------------------------------------------
|
|
1617
|
-
// GET /admin/wallets/:id/staking
|
|
1618
|
-
// ---------------------------------------------------------------------------
|
|
1619
|
-
router.openapi(adminWalletStakingRoute, async (c) => {
|
|
1620
|
-
const { id } = c.req.valid('param');
|
|
1621
|
-
// Verify wallet exists
|
|
1622
|
-
const wallet = deps.db.select().from(wallets).where(eq(wallets.id, id)).get();
|
|
1623
|
-
if (!wallet) {
|
|
1624
|
-
throw new WAIaaSError('WALLET_NOT_FOUND');
|
|
1625
|
-
}
|
|
1626
|
-
const positions = [];
|
|
1627
|
-
if (!deps.sqlite) {
|
|
1628
|
-
return c.json({ walletId: id, positions }, 200);
|
|
1629
|
-
}
|
|
1630
|
-
const LIDO_APY = '~3.5%';
|
|
1631
|
-
const JITO_APY = '~7.5%';
|
|
1632
|
-
// Reuse aggregation logic inline (same as staking.ts)
|
|
1633
|
-
function aggregateProvider(walletId, providerKey) {
|
|
1634
|
-
const stakeRows = deps.sqlite.prepare(`SELECT amount, bridge_status, created_at, metadata
|
|
1635
|
-
FROM transactions
|
|
1636
|
-
WHERE wallet_id = ? AND status IN ('CONFIRMED', 'COMPLETED')
|
|
1637
|
-
AND metadata LIKE ?
|
|
1638
|
-
ORDER BY created_at ASC`).all(walletId, `%${providerKey}%`);
|
|
1639
|
-
let totalStaked = 0n;
|
|
1640
|
-
let totalUnstaked = 0n;
|
|
1641
|
-
for (const row of stakeRows) {
|
|
1642
|
-
// Fallback: if amount is NULL, try extracting from metadata (CONTRACT_CALL value)
|
|
1643
|
-
let effectiveAmount = row.amount;
|
|
1644
|
-
if (!effectiveAmount && row.metadata) {
|
|
1645
|
-
try {
|
|
1646
|
-
const meta = JSON.parse(row.metadata);
|
|
1647
|
-
const origReq = meta.originalRequest;
|
|
1648
|
-
if (origReq?.value && typeof origReq.value === 'string') {
|
|
1649
|
-
effectiveAmount = origReq.value;
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
catch { /* ignore */ }
|
|
1653
|
-
}
|
|
1654
|
-
if (!effectiveAmount)
|
|
1655
|
-
continue;
|
|
1656
|
-
let isUnstake = false;
|
|
1657
|
-
if (row.metadata) {
|
|
1658
|
-
try {
|
|
1659
|
-
const meta = JSON.parse(row.metadata);
|
|
1660
|
-
if (meta.action === 'unstake' || meta.actionName === 'unstake')
|
|
1661
|
-
isUnstake = true;
|
|
1662
|
-
}
|
|
1663
|
-
catch { /* ignore */ }
|
|
1664
|
-
}
|
|
1665
|
-
try {
|
|
1666
|
-
const amountBig = BigInt(effectiveAmount);
|
|
1667
|
-
if (isUnstake)
|
|
1668
|
-
totalUnstaked += amountBig;
|
|
1669
|
-
else
|
|
1670
|
-
totalStaked += amountBig;
|
|
1671
|
-
}
|
|
1672
|
-
catch { /* skip */ }
|
|
1673
|
-
}
|
|
1674
|
-
const pendingRow = deps.sqlite.prepare(`SELECT amount, bridge_status, created_at
|
|
1675
|
-
FROM transactions
|
|
1676
|
-
WHERE wallet_id = ? AND bridge_status = 'PENDING'
|
|
1677
|
-
AND metadata LIKE ?
|
|
1678
|
-
ORDER BY created_at DESC
|
|
1679
|
-
LIMIT 1`).get(walletId, `%${providerKey}%`);
|
|
1680
|
-
let pendingUnstake = null;
|
|
1681
|
-
if (pendingRow?.amount) {
|
|
1682
|
-
pendingUnstake = {
|
|
1683
|
-
amount: pendingRow.amount,
|
|
1684
|
-
status: (pendingRow.bridge_status ?? 'PENDING'),
|
|
1685
|
-
requestedAt: pendingRow.created_at ?? null,
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
const balanceWei = totalStaked > totalUnstaked ? totalStaked - totalUnstaked : 0n;
|
|
1689
|
-
return { balanceWei, pendingUnstake };
|
|
1690
|
-
}
|
|
1691
|
-
// Ethereum wallet -> Lido
|
|
1692
|
-
if (wallet.chain === 'ethereum') {
|
|
1693
|
-
const { balanceWei, pendingUnstake } = aggregateProvider(id, 'lido_staking');
|
|
1694
|
-
if (balanceWei > 0n || pendingUnstake) {
|
|
1695
|
-
let balanceUsd = null;
|
|
1696
|
-
if (deps.priceOracle && balanceWei > 0n) {
|
|
1697
|
-
try {
|
|
1698
|
-
const priceInfo = await deps.priceOracle.getNativePrice('ethereum');
|
|
1699
|
-
balanceUsd = (Number(balanceWei) / 1e18 * priceInfo.usdPrice).toFixed(2);
|
|
1700
|
-
}
|
|
1701
|
-
catch { /* price unavailable */ }
|
|
1702
|
-
}
|
|
1703
|
-
positions.push({ protocol: 'lido', chain: 'ethereum', asset: 'stETH', balance: balanceWei.toString(), balanceUsd, apy: LIDO_APY, pendingUnstake });
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
// Solana wallet -> Jito
|
|
1707
|
-
if (wallet.chain === 'solana') {
|
|
1708
|
-
const { balanceWei: balanceLamports, pendingUnstake } = aggregateProvider(id, 'jito_staking');
|
|
1709
|
-
if (balanceLamports > 0n || pendingUnstake) {
|
|
1710
|
-
let balanceUsd = null;
|
|
1711
|
-
if (deps.priceOracle && balanceLamports > 0n) {
|
|
1712
|
-
try {
|
|
1713
|
-
const priceInfo = await deps.priceOracle.getNativePrice('solana');
|
|
1714
|
-
balanceUsd = (Number(balanceLamports) / 1e9 * priceInfo.usdPrice).toFixed(2);
|
|
1715
|
-
}
|
|
1716
|
-
catch { /* price unavailable */ }
|
|
1717
|
-
}
|
|
1718
|
-
positions.push({ protocol: 'jito', chain: 'solana', asset: 'JitoSOL', balance: balanceLamports.toString(), balanceUsd, apy: JITO_APY, pendingUnstake });
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
return c.json({ walletId: id, positions }, 200);
|
|
1722
|
-
});
|
|
1723
|
-
// ---------------------------------------------------------------------------
|
|
1724
|
-
// GET /admin/telegram-users
|
|
1725
|
-
// ---------------------------------------------------------------------------
|
|
1726
|
-
router.openapi(telegramUsersListRoute, async (c) => {
|
|
1727
|
-
if (!deps.sqlite) {
|
|
1728
|
-
return c.json({ users: [], total: 0 }, 200);
|
|
1729
|
-
}
|
|
1730
|
-
const rows = deps.sqlite
|
|
1731
|
-
.prepare('SELECT chat_id, username, role, registered_at, approved_at FROM telegram_users ORDER BY registered_at DESC')
|
|
1732
|
-
.all();
|
|
1733
|
-
return c.json({ users: rows, total: rows.length }, 200);
|
|
1734
|
-
});
|
|
1735
|
-
// ---------------------------------------------------------------------------
|
|
1736
|
-
// PUT /admin/telegram-users/:chatId
|
|
1737
|
-
// ---------------------------------------------------------------------------
|
|
1738
|
-
router.openapi(telegramUserUpdateRoute, async (c) => {
|
|
1739
|
-
if (!deps.sqlite) {
|
|
1740
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1741
|
-
message: 'SQLite not available',
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
const { chatId } = c.req.valid('param');
|
|
1745
|
-
const body = c.req.valid('json');
|
|
1746
|
-
const now = Math.floor(Date.now() / 1000);
|
|
1747
|
-
const result = deps.sqlite
|
|
1748
|
-
.prepare('UPDATE telegram_users SET role = ?, approved_at = ? WHERE chat_id = ?')
|
|
1749
|
-
.run(body.role, now, chatId);
|
|
1750
|
-
if (result.changes === 0) {
|
|
1751
|
-
throw new WAIaaSError('WALLET_NOT_FOUND', {
|
|
1752
|
-
message: `Telegram user not found: ${chatId}`,
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
return c.json({ success: true, chat_id: chatId, role: body.role }, 200);
|
|
1756
|
-
});
|
|
1757
|
-
// ---------------------------------------------------------------------------
|
|
1758
|
-
// DELETE /admin/telegram-users/:chatId
|
|
1759
|
-
// ---------------------------------------------------------------------------
|
|
1760
|
-
router.openapi(telegramUserDeleteRoute, async (c) => {
|
|
1761
|
-
if (!deps.sqlite) {
|
|
1762
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
1763
|
-
message: 'SQLite not available',
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
const { chatId } = c.req.valid('param');
|
|
1767
|
-
const result = deps.sqlite
|
|
1768
|
-
.prepare('DELETE FROM telegram_users WHERE chat_id = ?')
|
|
1769
|
-
.run(chatId);
|
|
1770
|
-
if (result.changes === 0) {
|
|
1771
|
-
throw new WAIaaSError('WALLET_NOT_FOUND', {
|
|
1772
|
-
message: `Telegram user not found: ${chatId}`,
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
return c.json({ success: true }, 200);
|
|
1776
|
-
});
|
|
1777
|
-
// ---------------------------------------------------------------------------
|
|
1778
|
-
// GET /admin/transactions (cross-wallet transaction list)
|
|
1779
|
-
// ---------------------------------------------------------------------------
|
|
1780
|
-
router.openapi(adminTransactionsRoute, async (c) => {
|
|
1781
|
-
const query = c.req.valid('query');
|
|
1782
|
-
const offset = query.offset ?? 0;
|
|
1783
|
-
const limit = query.limit ?? 20;
|
|
1784
|
-
// Build WHERE conditions
|
|
1785
|
-
const conditions = [];
|
|
1786
|
-
if (query.wallet_id) {
|
|
1787
|
-
conditions.push(eq(transactions.walletId, query.wallet_id));
|
|
1788
|
-
}
|
|
1789
|
-
if (query.type) {
|
|
1790
|
-
conditions.push(eq(transactions.type, query.type));
|
|
1791
|
-
}
|
|
1792
|
-
if (query.status) {
|
|
1793
|
-
conditions.push(eq(transactions.status, query.status));
|
|
1794
|
-
}
|
|
1795
|
-
if (query.network) {
|
|
1796
|
-
conditions.push(eq(transactions.network, query.network));
|
|
1797
|
-
}
|
|
1798
|
-
if (query.since !== undefined) {
|
|
1799
|
-
conditions.push(sql `${transactions.createdAt} >= ${query.since}`);
|
|
1800
|
-
}
|
|
1801
|
-
if (query.until !== undefined) {
|
|
1802
|
-
conditions.push(sql `${transactions.createdAt} <= ${query.until}`);
|
|
1803
|
-
}
|
|
1804
|
-
if (query.search) {
|
|
1805
|
-
const pattern = `%${query.search}%`;
|
|
1806
|
-
conditions.push(sql `(${transactions.txHash} LIKE ${pattern} OR ${transactions.toAddress} LIKE ${pattern})`);
|
|
1807
|
-
}
|
|
1808
|
-
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1809
|
-
// Count total
|
|
1810
|
-
const totalResult = deps.db
|
|
1811
|
-
.select({ count: drizzleCount() })
|
|
1812
|
-
.from(transactions)
|
|
1813
|
-
.where(whereClause)
|
|
1814
|
-
.get();
|
|
1815
|
-
const total = totalResult?.count ?? 0;
|
|
1816
|
-
// Query with JOIN for walletName
|
|
1817
|
-
const rows = deps.db
|
|
1818
|
-
.select({
|
|
1819
|
-
id: transactions.id,
|
|
1820
|
-
walletId: transactions.walletId,
|
|
1821
|
-
walletName: wallets.name,
|
|
1822
|
-
type: transactions.type,
|
|
1823
|
-
status: transactions.status,
|
|
1824
|
-
tier: transactions.tier,
|
|
1825
|
-
toAddress: transactions.toAddress,
|
|
1826
|
-
amount: transactions.amount,
|
|
1827
|
-
amountUsd: transactions.amountUsd,
|
|
1828
|
-
network: transactions.network,
|
|
1829
|
-
txHash: transactions.txHash,
|
|
1830
|
-
chain: transactions.chain,
|
|
1831
|
-
createdAt: transactions.createdAt,
|
|
1832
|
-
tokenMint: transactions.tokenMint,
|
|
1833
|
-
contractAddress: transactions.contractAddress,
|
|
1834
|
-
})
|
|
1835
|
-
.from(transactions)
|
|
1836
|
-
.leftJoin(wallets, eq(transactions.walletId, wallets.id))
|
|
1837
|
-
.where(whereClause)
|
|
1838
|
-
.orderBy(desc(transactions.createdAt))
|
|
1839
|
-
.offset(offset)
|
|
1840
|
-
.limit(limit)
|
|
1841
|
-
.all();
|
|
1842
|
-
const items = rows.map((row) => {
|
|
1843
|
-
const tokenAddr = row.tokenMint ?? row.contractAddress ?? null;
|
|
1844
|
-
return {
|
|
1845
|
-
id: row.id,
|
|
1846
|
-
walletId: row.walletId,
|
|
1847
|
-
walletName: row.walletName ?? null,
|
|
1848
|
-
type: row.type,
|
|
1849
|
-
status: row.status,
|
|
1850
|
-
tier: row.tier ?? null,
|
|
1851
|
-
toAddress: row.toAddress ?? null,
|
|
1852
|
-
amount: row.amount ?? null,
|
|
1853
|
-
formattedAmount: formatTxAmount(row.amount ?? null, row.chain, row.network ?? null, tokenAddr, deps.db),
|
|
1854
|
-
amountUsd: row.amountUsd ?? null,
|
|
1855
|
-
network: row.network ?? null,
|
|
1856
|
-
txHash: row.txHash ?? null,
|
|
1857
|
-
chain: row.chain,
|
|
1858
|
-
createdAt: row.createdAt instanceof Date
|
|
1859
|
-
? Math.floor(row.createdAt.getTime() / 1000)
|
|
1860
|
-
: (typeof row.createdAt === 'number' ? row.createdAt : null),
|
|
1861
|
-
};
|
|
1862
|
-
});
|
|
1863
|
-
return c.json({ items, total, offset, limit }, 200);
|
|
1864
|
-
});
|
|
1865
|
-
// ---------------------------------------------------------------------------
|
|
1866
|
-
// GET /admin/incoming (cross-wallet incoming transaction list)
|
|
1867
|
-
// ---------------------------------------------------------------------------
|
|
1868
|
-
router.openapi(adminIncomingRoute, async (c) => {
|
|
1869
|
-
const query = c.req.valid('query');
|
|
1870
|
-
const offset = query.offset ?? 0;
|
|
1871
|
-
const limit = query.limit ?? 20;
|
|
1872
|
-
// Build WHERE conditions (no default status filter -- admin sees all)
|
|
1873
|
-
const conditions = [];
|
|
1874
|
-
if (query.wallet_id) {
|
|
1875
|
-
conditions.push(eq(incomingTransactions.walletId, query.wallet_id));
|
|
1876
|
-
}
|
|
1877
|
-
if (query.chain) {
|
|
1878
|
-
conditions.push(eq(incomingTransactions.chain, query.chain));
|
|
1879
|
-
}
|
|
1880
|
-
if (query.status) {
|
|
1881
|
-
conditions.push(eq(incomingTransactions.status, query.status));
|
|
1882
|
-
}
|
|
1883
|
-
if (query.suspicious !== undefined) {
|
|
1884
|
-
conditions.push(eq(incomingTransactions.isSuspicious, query.suspicious === 'true'));
|
|
1885
|
-
}
|
|
1886
|
-
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1887
|
-
// Count total
|
|
1888
|
-
const totalResult = deps.db
|
|
1889
|
-
.select({ count: drizzleCount() })
|
|
1890
|
-
.from(incomingTransactions)
|
|
1891
|
-
.where(whereClause)
|
|
1892
|
-
.get();
|
|
1893
|
-
const total = totalResult?.count ?? 0;
|
|
1894
|
-
// Query with JOIN for walletName
|
|
1895
|
-
const rows = deps.db
|
|
1896
|
-
.select({
|
|
1897
|
-
id: incomingTransactions.id,
|
|
1898
|
-
txHash: incomingTransactions.txHash,
|
|
1899
|
-
walletId: incomingTransactions.walletId,
|
|
1900
|
-
walletName: wallets.name,
|
|
1901
|
-
fromAddress: incomingTransactions.fromAddress,
|
|
1902
|
-
amount: incomingTransactions.amount,
|
|
1903
|
-
tokenAddress: incomingTransactions.tokenAddress,
|
|
1904
|
-
chain: incomingTransactions.chain,
|
|
1905
|
-
network: incomingTransactions.network,
|
|
1906
|
-
status: incomingTransactions.status,
|
|
1907
|
-
blockNumber: incomingTransactions.blockNumber,
|
|
1908
|
-
detectedAt: incomingTransactions.detectedAt,
|
|
1909
|
-
confirmedAt: incomingTransactions.confirmedAt,
|
|
1910
|
-
isSuspicious: incomingTransactions.isSuspicious,
|
|
1911
|
-
})
|
|
1912
|
-
.from(incomingTransactions)
|
|
1913
|
-
.leftJoin(wallets, eq(incomingTransactions.walletId, wallets.id))
|
|
1914
|
-
.where(whereClause)
|
|
1915
|
-
.orderBy(desc(incomingTransactions.detectedAt))
|
|
1916
|
-
.offset(offset)
|
|
1917
|
-
.limit(limit)
|
|
1918
|
-
.all();
|
|
1919
|
-
const items = rows.map((row) => ({
|
|
1920
|
-
id: row.id,
|
|
1921
|
-
txHash: row.txHash,
|
|
1922
|
-
walletId: row.walletId,
|
|
1923
|
-
walletName: row.walletName ?? null,
|
|
1924
|
-
fromAddress: row.fromAddress,
|
|
1925
|
-
amount: row.amount,
|
|
1926
|
-
formattedAmount: formatTxAmount(row.amount, row.chain, row.network, row.tokenAddress ?? null, deps.db),
|
|
1927
|
-
tokenAddress: row.tokenAddress ?? null,
|
|
1928
|
-
chain: row.chain,
|
|
1929
|
-
network: row.network,
|
|
1930
|
-
status: row.status,
|
|
1931
|
-
blockNumber: row.blockNumber ?? null,
|
|
1932
|
-
detectedAt: row.detectedAt instanceof Date
|
|
1933
|
-
? Math.floor(row.detectedAt.getTime() / 1000)
|
|
1934
|
-
: (typeof row.detectedAt === 'number' ? row.detectedAt : null),
|
|
1935
|
-
confirmedAt: row.confirmedAt instanceof Date
|
|
1936
|
-
? Math.floor(row.confirmedAt.getTime() / 1000)
|
|
1937
|
-
: (typeof row.confirmedAt === 'number' ? row.confirmedAt : null),
|
|
1938
|
-
suspicious: row.isSuspicious ?? false,
|
|
1939
|
-
}));
|
|
1940
|
-
return c.json({ items, total, offset, limit }, 200);
|
|
1941
|
-
});
|
|
1942
|
-
// ---------------------------------------------------------------------------
|
|
1943
|
-
// POST /admin/agent-prompt — Generate agent connection prompt
|
|
1944
|
-
// ---------------------------------------------------------------------------
|
|
1945
|
-
const agentPromptRoute = createRoute({
|
|
1946
|
-
method: 'post',
|
|
1947
|
-
path: '/admin/agent-prompt',
|
|
1948
|
-
tags: ['Admin'],
|
|
1949
|
-
summary: 'Generate agent connection prompt (magic word)',
|
|
1950
|
-
request: {
|
|
1951
|
-
body: {
|
|
1952
|
-
content: { 'application/json': { schema: AgentPromptRequestSchema } },
|
|
1953
|
-
},
|
|
1954
|
-
},
|
|
1955
|
-
responses: {
|
|
1956
|
-
201: {
|
|
1957
|
-
description: 'Agent prompt generated',
|
|
1958
|
-
content: { 'application/json': { schema: AgentPromptResponseSchema } },
|
|
1959
|
-
},
|
|
1960
|
-
...buildErrorResponses(['ADAPTER_NOT_AVAILABLE']),
|
|
1961
|
-
},
|
|
1962
|
-
});
|
|
1963
|
-
router.openapi(agentPromptRoute, async (c) => {
|
|
1964
|
-
if (!deps.jwtSecretManager || !deps.daemonConfig) {
|
|
1965
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', { message: 'JWT signing not available' });
|
|
1966
|
-
}
|
|
1967
|
-
const body = c.req.valid('json');
|
|
1968
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1969
|
-
// v29.9: per-session TTL; omit = unlimited
|
|
1970
|
-
const ttl = body.ttl; // undefined = unlimited session
|
|
1971
|
-
const expiresAt = ttl !== undefined ? nowSec + ttl : 0; // 0 = unlimited
|
|
1972
|
-
// Get target wallets (with environment for prompt builder)
|
|
1973
|
-
let targetWallets;
|
|
1974
|
-
if (body.walletIds && body.walletIds.length > 0) {
|
|
1975
|
-
targetWallets = body.walletIds
|
|
1976
|
-
.map((wid) => deps.db.select().from(wallets).where(eq(wallets.id, wid)).get())
|
|
1977
|
-
.filter((w) => w != null && w.status === 'ACTIVE')
|
|
1978
|
-
.map((w) => ({ id: w.id, name: w.name, chain: w.chain, environment: w.environment, publicKey: w.publicKey }));
|
|
1979
|
-
}
|
|
1980
|
-
else {
|
|
1981
|
-
targetWallets = deps.db
|
|
1982
|
-
.select()
|
|
1983
|
-
.from(wallets)
|
|
1984
|
-
.where(eq(wallets.status, 'ACTIVE'))
|
|
1985
|
-
.all()
|
|
1986
|
-
.map((w) => ({ id: w.id, name: w.name, chain: w.chain, environment: w.environment, publicKey: w.publicKey }));
|
|
1987
|
-
}
|
|
1988
|
-
if (targetWallets.length === 0) {
|
|
1989
|
-
return c.json({ prompt: '', walletCount: 0, sessionsCreated: 0, sessionReused: false, expiresAt }, 201);
|
|
1990
|
-
}
|
|
1991
|
-
// Try to reuse an existing valid session covering all target wallets
|
|
1992
|
-
const defaultWallet = targetWallets[0];
|
|
1993
|
-
const targetWalletIds = targetWallets.map((w) => w.id);
|
|
1994
|
-
let sessionId;
|
|
1995
|
-
let sessionReused = false;
|
|
1996
|
-
let sessionsCreated = 1;
|
|
1997
|
-
let actualExpiresAt = expiresAt;
|
|
1998
|
-
// Find active sessions that cover all target wallets
|
|
1999
|
-
// For unlimited sessions, include those with expiresAt=0
|
|
2000
|
-
const candidateSessions = deps.db
|
|
2001
|
-
.select({
|
|
2002
|
-
id: sessions.id,
|
|
2003
|
-
expiresAt: sessions.expiresAt,
|
|
2004
|
-
})
|
|
2005
|
-
.from(sessions)
|
|
2006
|
-
.where(and(isNull(sessions.revokedAt), ttl !== undefined
|
|
2007
|
-
? gt(sessions.expiresAt, new Date((nowSec + Math.max(Math.floor(ttl * 0.1), 3600)) * 1000))
|
|
2008
|
-
: sql `(${sessions.expiresAt} = 0 OR ${sessions.expiresAt} > ${nowSec})`))
|
|
2009
|
-
.all();
|
|
2010
|
-
let reusableSessionId = null;
|
|
2011
|
-
let reusableExpiresAt = 0;
|
|
2012
|
-
for (const candidate of candidateSessions) {
|
|
2013
|
-
// Count how many of our target wallets are linked to this session
|
|
2014
|
-
const linkedCount = deps.db
|
|
2015
|
-
.select({ cnt: drizzleCount() })
|
|
2016
|
-
.from(sessionWallets)
|
|
2017
|
-
.where(and(eq(sessionWallets.sessionId, candidate.id), sql `${sessionWallets.walletId} IN (${sql.join(targetWalletIds.map((id) => sql `${id}`), sql `, `)})`))
|
|
2018
|
-
.get();
|
|
2019
|
-
if (linkedCount && linkedCount.cnt === targetWalletIds.length) {
|
|
2020
|
-
reusableSessionId = candidate.id;
|
|
2021
|
-
reusableExpiresAt = candidate.expiresAt instanceof Date
|
|
2022
|
-
? Math.floor(candidate.expiresAt.getTime() / 1000)
|
|
2023
|
-
: candidate.expiresAt;
|
|
2024
|
-
break;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
if (reusableSessionId) {
|
|
2028
|
-
// Reuse existing session — re-sign JWT with existing session ID
|
|
2029
|
-
sessionId = reusableSessionId;
|
|
2030
|
-
sessionReused = true;
|
|
2031
|
-
sessionsCreated = 0;
|
|
2032
|
-
actualExpiresAt = reusableExpiresAt;
|
|
2033
|
-
}
|
|
2034
|
-
else {
|
|
2035
|
-
// Create a new multi-wallet session
|
|
2036
|
-
sessionId = generateId();
|
|
2037
|
-
deps.db.insert(sessions).values({
|
|
2038
|
-
id: sessionId,
|
|
2039
|
-
tokenHash: '',
|
|
2040
|
-
expiresAt: new Date(expiresAt * 1000),
|
|
2041
|
-
absoluteExpiresAt: new Date(0), // unlimited
|
|
2042
|
-
createdAt: new Date(nowSec * 1000),
|
|
2043
|
-
renewalCount: 0,
|
|
2044
|
-
maxRenewals: 0, // unlimited
|
|
2045
|
-
constraints: null,
|
|
2046
|
-
source: 'api',
|
|
2047
|
-
}).run();
|
|
2048
|
-
// Insert N rows into session_wallets
|
|
2049
|
-
for (let i = 0; i < targetWallets.length; i++) {
|
|
2050
|
-
const w = targetWallets[i];
|
|
2051
|
-
deps.db.insert(sessionWallets).values({
|
|
2052
|
-
sessionId,
|
|
2053
|
-
walletId: w.id,
|
|
2054
|
-
createdAt: new Date(nowSec * 1000),
|
|
2055
|
-
}).run();
|
|
2056
|
-
}
|
|
2057
|
-
void deps.notificationService?.notify('SESSION_CREATED', defaultWallet.id, { sessionId });
|
|
2058
|
-
}
|
|
2059
|
-
// Sign JWT (new or re-signed for reused session; unlimited sessions have no exp)
|
|
2060
|
-
const jwtPayload = {
|
|
2061
|
-
sub: sessionId,
|
|
2062
|
-
iat: nowSec,
|
|
2063
|
-
exp: actualExpiresAt > 0 ? actualExpiresAt : undefined,
|
|
2064
|
-
};
|
|
2065
|
-
const token = await deps.jwtSecretManager.signToken(jwtPayload);
|
|
2066
|
-
if (!sessionReused) {
|
|
2067
|
-
const tokenHash = createHash('sha256').update(token).digest('hex');
|
|
2068
|
-
deps.db.update(sessions).set({ tokenHash }).where(eq(sessions.id, sessionId)).run();
|
|
2069
|
-
}
|
|
2070
|
-
// Query per-wallet policies for prompt builder
|
|
2071
|
-
const promptWallets = targetWallets.map((w) => {
|
|
2072
|
-
const walletPolicies = deps.db
|
|
2073
|
-
.select({ type: policies.type })
|
|
2074
|
-
.from(policies)
|
|
2075
|
-
.where(and(eq(policies.walletId, w.id), eq(policies.enabled, true)))
|
|
2076
|
-
.all();
|
|
2077
|
-
const networks = getNetworksForEnvironment(w.chain, w.environment);
|
|
2078
|
-
return {
|
|
2079
|
-
id: w.id,
|
|
2080
|
-
name: w.name,
|
|
2081
|
-
chain: w.chain,
|
|
2082
|
-
environment: w.environment,
|
|
2083
|
-
address: w.publicKey,
|
|
2084
|
-
networks: networks.map((n) => n),
|
|
2085
|
-
policies: walletPolicies,
|
|
2086
|
-
};
|
|
2087
|
-
});
|
|
2088
|
-
// Compute capabilities dynamically (same logic as connect-info)
|
|
2089
|
-
const capabilities = ['transfer', 'token_transfer', 'balance', 'assets'];
|
|
2090
|
-
if (deps.settingsService) {
|
|
2091
|
-
try {
|
|
2092
|
-
if (deps.settingsService.get('signing_sdk.enabled') === 'true') {
|
|
2093
|
-
capabilities.push('sign');
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
catch {
|
|
2097
|
-
// Setting key not found -- signing not available
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
if (deps.settingsService && deps.actionProviderRegistry) {
|
|
2101
|
-
try {
|
|
2102
|
-
const providers = deps.actionProviderRegistry.listProviders();
|
|
2103
|
-
const hasAnyKey = providers.some((p) => p.requiresApiKey && deps.settingsService.hasApiKey(p.name));
|
|
2104
|
-
if (hasAnyKey) {
|
|
2105
|
-
capabilities.push('actions');
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
catch {
|
|
2109
|
-
// Settings service not available
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
if (deps.daemonConfig?.x402?.enabled === true) {
|
|
2113
|
-
capabilities.push('x402');
|
|
2114
|
-
}
|
|
2115
|
-
// Read default-deny toggles
|
|
2116
|
-
const defaultDeny = {
|
|
2117
|
-
tokenTransfers: deps.settingsService?.get('policy.default_deny_tokens') !== 'false',
|
|
2118
|
-
contractCalls: deps.settingsService?.get('policy.default_deny_contracts') !== 'false',
|
|
2119
|
-
tokenApprovals: deps.settingsService?.get('policy.default_deny_spenders') !== 'false',
|
|
2120
|
-
x402Domains: deps.settingsService?.get('policy.default_deny_x402_domains') !== 'false',
|
|
2121
|
-
};
|
|
2122
|
-
// Build prompt using shared prompt builder
|
|
2123
|
-
const host = c.req.header('Host') ?? 'localhost:3100';
|
|
2124
|
-
const protocol = c.req.header('X-Forwarded-Proto') ?? 'http';
|
|
2125
|
-
const baseUrl = `${protocol}://${host}`;
|
|
2126
|
-
const prompt = buildConnectInfoPrompt({
|
|
2127
|
-
wallets: promptWallets,
|
|
2128
|
-
capabilities,
|
|
2129
|
-
defaultDeny,
|
|
2130
|
-
baseUrl,
|
|
2131
|
-
version: deps.version,
|
|
2132
|
-
});
|
|
2133
|
-
// Append session token so the agent can start using it immediately
|
|
2134
|
-
const fullPrompt = `${prompt}\n\nSession Token: ${token}\nSession ID: ${sessionId}`;
|
|
2135
|
-
return c.json({
|
|
2136
|
-
prompt: fullPrompt,
|
|
2137
|
-
walletCount: targetWallets.length,
|
|
2138
|
-
sessionsCreated,
|
|
2139
|
-
sessionReused,
|
|
2140
|
-
expiresAt: actualExpiresAt,
|
|
2141
|
-
}, 201);
|
|
2142
|
-
});
|
|
2143
|
-
// ---------------------------------------------------------------------------
|
|
2144
|
-
// POST /admin/sessions/:id/reissue — Reissue session token
|
|
2145
|
-
// ---------------------------------------------------------------------------
|
|
2146
|
-
const sessionReissueRoute = createRoute({
|
|
2147
|
-
method: 'post',
|
|
2148
|
-
path: '/admin/sessions/{id}/reissue',
|
|
2149
|
-
tags: ['Admin'],
|
|
2150
|
-
summary: 'Reissue session token (re-sign JWT for existing session)',
|
|
2151
|
-
request: {
|
|
2152
|
-
params: z.object({ id: z.string().uuid() }),
|
|
2153
|
-
},
|
|
2154
|
-
responses: {
|
|
2155
|
-
200: {
|
|
2156
|
-
description: 'Token reissued',
|
|
2157
|
-
content: { 'application/json': { schema: SessionReissueResponseSchema } },
|
|
2158
|
-
},
|
|
2159
|
-
...buildErrorResponses(['SESSION_NOT_FOUND', 'SESSION_REVOKED']),
|
|
2160
|
-
},
|
|
2161
|
-
});
|
|
2162
|
-
router.openapi(sessionReissueRoute, async (c) => {
|
|
2163
|
-
if (!deps.jwtSecretManager) {
|
|
2164
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', { message: 'JWT signing not available' });
|
|
2165
|
-
}
|
|
2166
|
-
const { id: sessionId } = c.req.valid('param');
|
|
2167
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
2168
|
-
// Find session
|
|
2169
|
-
const session = deps.db
|
|
2170
|
-
.select()
|
|
2171
|
-
.from(sessions)
|
|
2172
|
-
.where(eq(sessions.id, sessionId))
|
|
2173
|
-
.get();
|
|
2174
|
-
if (!session) {
|
|
2175
|
-
throw new WAIaaSError('SESSION_NOT_FOUND');
|
|
2176
|
-
}
|
|
2177
|
-
if (session.revokedAt) {
|
|
2178
|
-
throw new WAIaaSError('SESSION_REVOKED');
|
|
2179
|
-
}
|
|
2180
|
-
const expiresAtSec = session.expiresAt instanceof Date
|
|
2181
|
-
? Math.floor(session.expiresAt.getTime() / 1000)
|
|
2182
|
-
: session.expiresAt;
|
|
2183
|
-
if (expiresAtSec > 0 && expiresAtSec <= nowSec) {
|
|
2184
|
-
throw new WAIaaSError('SESSION_NOT_FOUND', { message: 'Session expired' });
|
|
2185
|
-
}
|
|
2186
|
-
// Re-sign JWT (no wallet claim needed -- walletId resolved at request time)
|
|
2187
|
-
const jwtPayload = {
|
|
2188
|
-
sub: sessionId,
|
|
2189
|
-
iat: nowSec,
|
|
2190
|
-
...(expiresAtSec > 0 ? { exp: expiresAtSec } : {}),
|
|
2191
|
-
};
|
|
2192
|
-
const token = await deps.jwtSecretManager.signToken(jwtPayload);
|
|
2193
|
-
// Increment token_issued_count
|
|
2194
|
-
const newCount = (session.tokenIssuedCount ?? 1) + 1;
|
|
2195
|
-
deps.db.update(sessions)
|
|
2196
|
-
.set({ tokenIssuedCount: newCount })
|
|
2197
|
-
.where(eq(sessions.id, sessionId))
|
|
2198
|
-
.run();
|
|
2199
|
-
return c.json({
|
|
2200
|
-
token,
|
|
2201
|
-
sessionId,
|
|
2202
|
-
tokenIssuedCount: newCount,
|
|
2203
|
-
expiresAt: expiresAtSec,
|
|
2204
|
-
}, 200);
|
|
2205
|
-
});
|
|
2206
|
-
// ---------------------------------------------------------------------------
|
|
2207
|
-
// POST /admin/transactions/:id/cancel — Cancel a QUEUED (DELAY) transaction
|
|
2208
|
-
// ---------------------------------------------------------------------------
|
|
2209
|
-
const adminTxCancelRoute = createRoute({
|
|
2210
|
-
method: 'post',
|
|
2211
|
-
path: '/admin/transactions/{id}/cancel',
|
|
2212
|
-
tags: ['Admin'],
|
|
2213
|
-
summary: 'Cancel a delayed (QUEUED) transaction',
|
|
2214
|
-
request: {
|
|
2215
|
-
params: z.object({ id: z.string().uuid() }),
|
|
2216
|
-
},
|
|
2217
|
-
responses: {
|
|
2218
|
-
200: {
|
|
2219
|
-
description: 'Transaction cancelled',
|
|
2220
|
-
content: {
|
|
2221
|
-
'application/json': {
|
|
2222
|
-
schema: z.object({
|
|
2223
|
-
id: z.string(),
|
|
2224
|
-
status: z.literal('CANCELLED'),
|
|
2225
|
-
}),
|
|
2226
|
-
},
|
|
2227
|
-
},
|
|
2228
|
-
},
|
|
2229
|
-
...buildErrorResponses(['TX_NOT_FOUND']),
|
|
2230
|
-
},
|
|
2231
|
-
});
|
|
2232
|
-
router.openapi(adminTxCancelRoute, async (c) => {
|
|
2233
|
-
const { id: txId } = c.req.valid('param');
|
|
2234
|
-
if (!deps.delayQueue) {
|
|
2235
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
2236
|
-
message: 'Delay queue not available',
|
|
2237
|
-
});
|
|
2238
|
-
}
|
|
2239
|
-
deps.delayQueue.cancelDelay(txId);
|
|
2240
|
-
return c.json({ id: txId, status: 'CANCELLED' }, 200);
|
|
2241
|
-
});
|
|
2242
|
-
// ---------------------------------------------------------------------------
|
|
2243
|
-
// POST /admin/transactions/:id/reject — Reject a pending (APPROVAL) transaction
|
|
2244
|
-
// ---------------------------------------------------------------------------
|
|
2245
|
-
const adminTxRejectRoute = createRoute({
|
|
2246
|
-
method: 'post',
|
|
2247
|
-
path: '/admin/transactions/{id}/reject',
|
|
2248
|
-
tags: ['Admin'],
|
|
2249
|
-
summary: 'Reject a pending approval transaction',
|
|
2250
|
-
request: {
|
|
2251
|
-
params: z.object({ id: z.string().uuid() }),
|
|
2252
|
-
},
|
|
2253
|
-
responses: {
|
|
2254
|
-
200: {
|
|
2255
|
-
description: 'Transaction rejected',
|
|
2256
|
-
content: {
|
|
2257
|
-
'application/json': {
|
|
2258
|
-
schema: z.object({
|
|
2259
|
-
id: z.string(),
|
|
2260
|
-
status: z.literal('CANCELLED'),
|
|
2261
|
-
rejectedAt: z.number(),
|
|
2262
|
-
}),
|
|
2263
|
-
},
|
|
2264
|
-
},
|
|
2265
|
-
},
|
|
2266
|
-
...buildErrorResponses(['TX_NOT_FOUND']),
|
|
2267
|
-
},
|
|
2268
|
-
});
|
|
2269
|
-
router.openapi(adminTxRejectRoute, async (c) => {
|
|
2270
|
-
const { id: txId } = c.req.valid('param');
|
|
2271
|
-
if (!deps.approvalWorkflow) {
|
|
2272
|
-
throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
|
|
2273
|
-
message: 'Approval workflow not available',
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
2276
|
-
const result = deps.approvalWorkflow.reject(txId);
|
|
2277
|
-
return c.json({
|
|
2278
|
-
id: txId,
|
|
2279
|
-
status: 'CANCELLED',
|
|
2280
|
-
rejectedAt: result.rejectedAt,
|
|
2281
|
-
}, 200);
|
|
2282
|
-
});
|
|
2283
|
-
// ---------------------------------------------------------------------------
|
|
2284
|
-
// GET /admin/rpc-status
|
|
2285
|
-
// ---------------------------------------------------------------------------
|
|
2286
|
-
router.openapi(rpcStatusRoute, async (c) => {
|
|
2287
|
-
const networks = {};
|
|
2288
|
-
if (deps.rpcPool) {
|
|
2289
|
-
for (const network of deps.rpcPool.getNetworks()) {
|
|
2290
|
-
networks[network] = deps.rpcPool.getStatus(network);
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
// Provide built-in URL defaults so Admin UI doesn't need hardcoded mirror (#197)
|
|
2294
|
-
const builtinUrls = {};
|
|
2295
|
-
for (const [network, urls] of Object.entries(BUILT_IN_RPC_DEFAULTS)) {
|
|
2296
|
-
builtinUrls[network] = [...urls];
|
|
2297
|
-
}
|
|
2298
|
-
return c.json({ networks, builtinUrls }, 200);
|
|
2299
|
-
});
|
|
2300
|
-
// ---------------------------------------------------------------------------
|
|
2301
|
-
// GET /admin/defi/positions
|
|
2302
|
-
// ---------------------------------------------------------------------------
|
|
2303
|
-
router.openapi(adminDefiPositionsRoute, async (c) => {
|
|
2304
|
-
const { wallet_id } = c.req.valid('query');
|
|
2305
|
-
if (!deps.sqlite) {
|
|
2306
|
-
return c.json({ positions: [], totalValueUsd: null, worstHealthFactor: null, activeCount: 0 }, 200);
|
|
2307
|
-
}
|
|
2308
|
-
// Cross-wallet DeFi positions query
|
|
2309
|
-
let rows;
|
|
2310
|
-
if (wallet_id) {
|
|
2311
|
-
rows = deps.sqlite.prepare(`SELECT id, wallet_id, category, provider, chain, network, asset_id,
|
|
2312
|
-
amount, amount_usd, metadata, status, opened_at, last_synced_at
|
|
2313
|
-
FROM defi_positions
|
|
2314
|
-
WHERE wallet_id = ? AND status = 'ACTIVE'
|
|
2315
|
-
ORDER BY category, provider`).all(wallet_id);
|
|
2316
|
-
}
|
|
2317
|
-
else {
|
|
2318
|
-
rows = deps.sqlite.prepare(`SELECT id, wallet_id, category, provider, chain, network, asset_id,
|
|
2319
|
-
amount, amount_usd, metadata, status, opened_at, last_synced_at
|
|
2320
|
-
FROM defi_positions
|
|
2321
|
-
WHERE status = 'ACTIVE'
|
|
2322
|
-
ORDER BY category, provider`).all();
|
|
2323
|
-
}
|
|
2324
|
-
const positions = rows.map((row) => ({
|
|
2325
|
-
id: row.id,
|
|
2326
|
-
walletId: row.wallet_id,
|
|
2327
|
-
category: row.category,
|
|
2328
|
-
provider: row.provider,
|
|
2329
|
-
chain: row.chain,
|
|
2330
|
-
network: row.network,
|
|
2331
|
-
assetId: row.asset_id,
|
|
2332
|
-
amount: row.amount,
|
|
2333
|
-
amountUsd: row.amount_usd,
|
|
2334
|
-
status: row.status,
|
|
2335
|
-
openedAt: row.opened_at,
|
|
2336
|
-
lastSyncedAt: row.last_synced_at,
|
|
2337
|
-
}));
|
|
2338
|
-
// Aggregate totalValueUsd
|
|
2339
|
-
const usdValues = positions.map((p) => p.amountUsd).filter((v) => v !== null);
|
|
2340
|
-
const totalValueUsd = usdValues.length > 0 ? usdValues.reduce((a, b) => a + b, 0) : null;
|
|
2341
|
-
// Worst health factor from metadata JSON
|
|
2342
|
-
let worstHealthFactor = null;
|
|
2343
|
-
for (const row of rows) {
|
|
2344
|
-
if (row.category === 'LENDING' && row.metadata) {
|
|
2345
|
-
try {
|
|
2346
|
-
const meta = JSON.parse(row.metadata);
|
|
2347
|
-
if (typeof meta.healthFactor === 'number' && meta.healthFactor > 0) {
|
|
2348
|
-
if (worstHealthFactor === null || meta.healthFactor < worstHealthFactor) {
|
|
2349
|
-
worstHealthFactor = meta.healthFactor;
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
catch { /* skip */ }
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return c.json({
|
|
2357
|
-
positions,
|
|
2358
|
-
totalValueUsd,
|
|
2359
|
-
worstHealthFactor,
|
|
2360
|
-
activeCount: positions.length,
|
|
2361
|
-
}, 200);
|
|
2362
|
-
});
|
|
2363
|
-
// ---------------------------------------------------------------------------
|
|
2364
|
-
// POST /admin/backup (create encrypted backup)
|
|
2365
|
-
// ---------------------------------------------------------------------------
|
|
2366
|
-
const createBackupRoute = createRoute({
|
|
2367
|
-
method: 'post',
|
|
2368
|
-
path: '/admin/backup',
|
|
2369
|
-
tags: ['Admin'],
|
|
2370
|
-
summary: 'Create an encrypted backup',
|
|
2371
|
-
responses: {
|
|
2372
|
-
200: {
|
|
2373
|
-
description: 'Backup created successfully',
|
|
2374
|
-
content: { 'application/json': { schema: BackupInfoResponseSchema } },
|
|
2375
|
-
},
|
|
2376
|
-
401: {
|
|
2377
|
-
description: 'Master password not available',
|
|
2378
|
-
content: { 'application/json': { schema: ErrorResponseSchema } },
|
|
2379
|
-
},
|
|
2380
|
-
501: {
|
|
2381
|
-
description: 'Backup service not configured',
|
|
2382
|
-
content: { 'application/json': { schema: ErrorResponseSchema } },
|
|
2383
|
-
},
|
|
2384
|
-
...buildErrorResponses(['INVALID_MASTER_PASSWORD']),
|
|
2385
|
-
},
|
|
2386
|
-
});
|
|
2387
|
-
router.openapi(createBackupRoute, async (c) => {
|
|
2388
|
-
if (!deps.encryptedBackupService) {
|
|
2389
|
-
return c.json({ code: 'NOT_CONFIGURED', message: 'Backup service not configured', retryable: false }, 501);
|
|
2390
|
-
}
|
|
2391
|
-
if (!deps.passwordRef?.password) {
|
|
2392
|
-
return c.json({ code: 'INVALID_MASTER_PASSWORD', message: 'Master password not available', retryable: false }, 401);
|
|
2393
|
-
}
|
|
2394
|
-
const info = await deps.encryptedBackupService.createBackup(deps.passwordRef.password);
|
|
2395
|
-
return c.json(info, 200);
|
|
2396
|
-
});
|
|
2397
|
-
// ---------------------------------------------------------------------------
|
|
2398
|
-
// GET /admin/backups (list backups)
|
|
2399
|
-
// ---------------------------------------------------------------------------
|
|
2400
|
-
const listBackupsRoute = createRoute({
|
|
2401
|
-
method: 'get',
|
|
2402
|
-
path: '/admin/backups',
|
|
2403
|
-
tags: ['Admin'],
|
|
2404
|
-
summary: 'List available backups',
|
|
2405
|
-
responses: {
|
|
2406
|
-
200: {
|
|
2407
|
-
description: 'Backup list',
|
|
2408
|
-
content: { 'application/json': { schema: BackupListResponseSchema } },
|
|
2409
|
-
},
|
|
2410
|
-
501: {
|
|
2411
|
-
description: 'Backup service not configured',
|
|
2412
|
-
content: { 'application/json': { schema: ErrorResponseSchema } },
|
|
2413
|
-
},
|
|
2414
|
-
},
|
|
2415
|
-
});
|
|
2416
|
-
router.openapi(listBackupsRoute, async (c) => {
|
|
2417
|
-
if (!deps.encryptedBackupService) {
|
|
2418
|
-
return c.json({ code: 'NOT_CONFIGURED', message: 'Backup service not configured', retryable: false }, 501);
|
|
2419
|
-
}
|
|
2420
|
-
const backups = deps.encryptedBackupService.listBackups();
|
|
2421
|
-
const retentionCount = deps.daemonConfig?.backup?.retention_count ?? 7;
|
|
2422
|
-
return c.json({ backups, total: backups.length, retention_count: retentionCount }, 200);
|
|
2423
|
-
});
|
|
2424
|
-
// ---------------------------------------------------------------------------
|
|
2425
|
-
// GET /admin/stats -- 7-category operational statistics (STAT-01)
|
|
2426
|
-
// ---------------------------------------------------------------------------
|
|
2427
|
-
const adminStatsRoute = createRoute({
|
|
2428
|
-
method: 'get',
|
|
2429
|
-
path: '/admin/stats',
|
|
2430
|
-
tags: ['Admin'],
|
|
2431
|
-
summary: 'Get operational statistics',
|
|
2432
|
-
responses: {
|
|
2433
|
-
200: {
|
|
2434
|
-
description: 'Operational statistics (7 categories)',
|
|
2435
|
-
content: { 'application/json': { schema: z.any() } },
|
|
2436
|
-
},
|
|
2437
|
-
},
|
|
2438
|
-
});
|
|
2439
|
-
router.openapi(adminStatsRoute, async (c) => {
|
|
2440
|
-
if (!deps.adminStatsService) {
|
|
2441
|
-
return c.json({ code: 'NOT_CONFIGURED', message: 'Stats service not configured', retryable: false }, 503);
|
|
2442
|
-
}
|
|
2443
|
-
const stats = deps.adminStatsService.getStats();
|
|
2444
|
-
return c.json(stats, 200);
|
|
2445
|
-
});
|
|
2446
|
-
// ---------------------------------------------------------------------------
|
|
2447
|
-
// GET /admin/autostop/rules -- List AutoStop rules with status (PLUG-03)
|
|
2448
|
-
// ---------------------------------------------------------------------------
|
|
2449
|
-
const autostopRulesRoute = createRoute({
|
|
2450
|
-
method: 'get',
|
|
2451
|
-
path: '/admin/autostop/rules',
|
|
2452
|
-
tags: ['Admin'],
|
|
2453
|
-
summary: 'List AutoStop rules with status',
|
|
2454
|
-
responses: {
|
|
2455
|
-
200: {
|
|
2456
|
-
description: 'AutoStop rules list',
|
|
2457
|
-
content: { 'application/json': { schema: z.any() } },
|
|
2458
|
-
},
|
|
2459
|
-
},
|
|
2460
|
-
});
|
|
2461
|
-
router.openapi(autostopRulesRoute, async (c) => {
|
|
2462
|
-
if (!deps.autoStopService) {
|
|
2463
|
-
return c.json({ globalEnabled: false, rules: [] }, 200);
|
|
2464
|
-
}
|
|
2465
|
-
const status = deps.autoStopService.getStatus();
|
|
2466
|
-
const registry = deps.autoStopService.registry;
|
|
2467
|
-
const rules = registry.getRules().map((r) => {
|
|
2468
|
-
const ruleStatus = r.getStatus();
|
|
2469
|
-
return {
|
|
2470
|
-
id: r.id,
|
|
2471
|
-
displayName: r.displayName,
|
|
2472
|
-
description: r.description,
|
|
2473
|
-
enabled: r.enabled,
|
|
2474
|
-
subscribedEvents: r.subscribedEvents,
|
|
2475
|
-
config: ruleStatus.config,
|
|
2476
|
-
state: ruleStatus.state,
|
|
2477
|
-
};
|
|
2478
|
-
});
|
|
2479
|
-
return c.json({ globalEnabled: status.enabled, rules }, 200);
|
|
2480
|
-
});
|
|
2481
|
-
// ---------------------------------------------------------------------------
|
|
2482
|
-
// PUT /admin/autostop/rules/:id -- Update AutoStop rule (PLUG-03)
|
|
2483
|
-
// ---------------------------------------------------------------------------
|
|
2484
|
-
const autostopRuleUpdateRoute = createRoute({
|
|
2485
|
-
method: 'put',
|
|
2486
|
-
path: '/admin/autostop/rules/{id}',
|
|
2487
|
-
tags: ['Admin'],
|
|
2488
|
-
summary: 'Update AutoStop rule enabled/config',
|
|
2489
|
-
request: {
|
|
2490
|
-
params: z.object({ id: z.string() }),
|
|
2491
|
-
body: {
|
|
2492
|
-
content: {
|
|
2493
|
-
'application/json': {
|
|
2494
|
-
schema: z.object({
|
|
2495
|
-
enabled: z.boolean().optional(),
|
|
2496
|
-
config: z.record(z.unknown()).optional(),
|
|
2497
|
-
}),
|
|
2498
|
-
},
|
|
2499
|
-
},
|
|
2500
|
-
},
|
|
2501
|
-
},
|
|
2502
|
-
responses: {
|
|
2503
|
-
200: {
|
|
2504
|
-
description: 'Rule updated',
|
|
2505
|
-
content: { 'application/json': { schema: z.any() } },
|
|
2506
|
-
},
|
|
2507
|
-
404: {
|
|
2508
|
-
description: 'Rule not found',
|
|
2509
|
-
content: { 'application/json': { schema: z.any() } },
|
|
2510
|
-
},
|
|
2511
|
-
},
|
|
2512
|
-
});
|
|
2513
|
-
router.openapi(autostopRuleUpdateRoute, async (c) => {
|
|
2514
|
-
if (!deps.autoStopService) {
|
|
2515
|
-
throw new WAIaaSError('RULE_NOT_FOUND');
|
|
2516
|
-
}
|
|
2517
|
-
const { id } = c.req.valid('param');
|
|
2518
|
-
const body = c.req.valid('json');
|
|
2519
|
-
const registry = deps.autoStopService.registry;
|
|
2520
|
-
const rule = registry.getRule(id);
|
|
2521
|
-
if (!rule) {
|
|
2522
|
-
throw new WAIaaSError('RULE_NOT_FOUND');
|
|
2523
|
-
}
|
|
2524
|
-
// Update enabled state
|
|
2525
|
-
if (body.enabled !== undefined) {
|
|
2526
|
-
registry.setEnabled(id, body.enabled);
|
|
2527
|
-
// Persist to Admin Settings
|
|
2528
|
-
if (deps.settingsService) {
|
|
2529
|
-
deps.settingsService.set(`autostop.rule.${id}.enabled`, String(body.enabled));
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
// Update config
|
|
2533
|
-
if (body.config) {
|
|
2534
|
-
rule.updateConfig(body.config);
|
|
2535
|
-
}
|
|
2536
|
-
// Return updated rule info
|
|
2537
|
-
const ruleStatus = rule.getStatus();
|
|
2538
|
-
return c.json({
|
|
2539
|
-
id: rule.id,
|
|
2540
|
-
displayName: rule.displayName,
|
|
2541
|
-
description: rule.description,
|
|
2542
|
-
enabled: rule.enabled,
|
|
2543
|
-
subscribedEvents: rule.subscribedEvents,
|
|
2544
|
-
config: ruleStatus.config,
|
|
2545
|
-
state: ruleStatus.state,
|
|
2546
|
-
}, 200);
|
|
2547
|
-
});
|
|
27
|
+
registerAdminAuthRoutes(router, deps);
|
|
28
|
+
registerAdminNotificationRoutes(router, deps);
|
|
29
|
+
registerAdminSettingsRoutes(router, deps);
|
|
30
|
+
registerAdminWalletRoutes(router, deps);
|
|
31
|
+
registerAdminMonitoringRoutes(router, deps);
|
|
2548
32
|
return router;
|
|
2549
33
|
}
|
|
2550
34
|
//# sourceMappingURL=admin.js.map
|