@thirdfy/agent-cli 0.1.48 → 0.2.1
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/CHANGELOG.md +51 -0
- package/README.md +16 -1
- package/bin/thirdfy-agent.mjs +3 -5106
- package/package.json +17 -4
- package/scripts/validate-npm-pack.mjs +43 -0
- package/src/cli/errors.mjs +103 -0
- package/src/cli/flags.mjs +66 -0
- package/src/cli/help.mjs +89 -0
- package/src/cli/manifest.mjs +422 -0
- package/src/cli/options/agent.mjs +9 -0
- package/src/cli/options/analytics.mjs +5 -0
- package/src/cli/options/auth.mjs +10 -0
- package/src/cli/options/bootstrap.mjs +19 -0
- package/src/cli/options/chains.mjs +3 -0
- package/src/cli/options/credentials.mjs +15 -0
- package/src/cli/options/delegation.mjs +22 -0
- package/src/cli/options/execution.mjs +24 -0
- package/src/cli/options/global.mjs +16 -0
- package/src/cli/options/index.mjs +37 -0
- package/src/cli/options/polymarket.mjs +3 -0
- package/src/cli/options/tracking.mjs +18 -0
- package/src/cli/options/wallet.mjs +7 -0
- package/src/cli/program.mjs +5 -0
- package/src/cli/register.mjs +118 -0
- package/src/commands/agent.mjs +194 -0
- package/src/commands/bootstrap.mjs +762 -0
- package/src/commands/credentials.mjs +102 -0
- package/src/commands/delegation.mjs +355 -0
- package/src/commands/discovery.mjs +77 -0
- package/src/commands/doctor.mjs +320 -0
- package/src/commands/execute.mjs +255 -0
- package/src/commands/hyperliquid.mjs +51 -0
- package/src/commands/index.mjs +6 -0
- package/src/commands/polymarket.mjs +89 -0
- package/src/commands/prompt.mjs +33 -0
- package/src/commands/telemetry.mjs +146 -0
- package/src/commands/wallet.mjs +129 -0
- package/src/core/args.mjs +68 -0
- package/src/core/constants.mjs +31 -0
- package/src/core/context.mjs +145 -0
- package/src/core/envelope.mjs +20 -0
- package/src/core/http.mjs +77 -0
- package/src/core/index.mjs +6 -0
- package/src/core/runMode.mjs +72 -0
- package/src/runtime/actions/selection.mjs +339 -0
- package/src/runtime/agentExtract.mjs +52 -0
- package/src/runtime/authHelpers.mjs +53 -0
- package/src/runtime/bitfinexProbe.mjs +121 -0
- package/src/runtime/context.mjs +28 -0
- package/src/runtime/createThirdfyAgentRuntime.mjs +2 -0
- package/src/runtime/errors.mjs +3 -0
- package/src/runtime/execution/amounts.mjs +193 -0
- package/src/runtime/execution/lanes.mjs +75 -0
- package/src/runtime/execution/payloads.mjs +148 -0
- package/src/runtime/execution/runners.mjs +702 -0
- package/src/runtime/handlers.mjs +181 -0
- package/src/runtime/index.mjs +4 -0
- package/src/runtime/main.mjs +204 -0
- package/src/runtime/onboardingHints.mjs +54 -0
- package/src/runtime/owsSigning.mjs +118 -0
- package/src/runtime/providerHints.mjs +414 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { requireFlag } from '../../core/args.mjs';
|
|
3
|
+
import { createCliError } from '../../core/envelope.mjs';
|
|
4
|
+
import {
|
|
5
|
+
normalizeRunMode,
|
|
6
|
+
normalizeManagedRunMode,
|
|
7
|
+
normalizeHybridWalletMode,
|
|
8
|
+
normalizeEffectCheckMode,
|
|
9
|
+
getEffectiveRunMode,
|
|
10
|
+
parseBooleanFlag,
|
|
11
|
+
} from '../../core/runMode.mjs';
|
|
12
|
+
import { apiGet, apiPost } from '../../core/http.mjs';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { normalizeIntentResponse } = require('../../utils/cli/intentNormalization.cjs');
|
|
17
|
+
|
|
18
|
+
export function createExecutionRunners(deps) {
|
|
19
|
+
const {
|
|
20
|
+
buildIntentPayload,
|
|
21
|
+
buildBuildTxPayload,
|
|
22
|
+
buildManagedExecutePayload,
|
|
23
|
+
getPreparedParams,
|
|
24
|
+
resolveEffectiveUserDid,
|
|
25
|
+
resolveTokenInForFunding,
|
|
26
|
+
shouldUseBuildTxPreflight,
|
|
27
|
+
shouldUseBuildTxExecution,
|
|
28
|
+
buildBitfinexOnboardingCommand,
|
|
29
|
+
inferBitfinexPermissionProfileFromAction,
|
|
30
|
+
resolveAgentApiKey,
|
|
31
|
+
loadEthers,
|
|
32
|
+
signAndBroadcastWithOws,
|
|
33
|
+
readEffectBalances,
|
|
34
|
+
resolveUnsignedTx,
|
|
35
|
+
sleepMs,
|
|
36
|
+
} = deps;
|
|
37
|
+
|
|
38
|
+
function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
|
|
39
|
+
if (!normalized || typeof normalized !== 'object') return normalized;
|
|
40
|
+
const next = { ...normalized };
|
|
41
|
+
const reasons = new Set();
|
|
42
|
+
const topBlockedReason = String(next.blockedReason || '').trim();
|
|
43
|
+
if (topBlockedReason) reasons.add(topBlockedReason);
|
|
44
|
+
const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
|
|
45
|
+
for (const key of Object.keys(blockedByReason)) {
|
|
46
|
+
if (String(key || '').trim()) reasons.add(String(key).trim());
|
|
47
|
+
}
|
|
48
|
+
const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
|
|
49
|
+
const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
|
|
50
|
+
const permissionProfile = inferBitfinexPermissionProfileFromAction(resolvedActionLower);
|
|
51
|
+
const cexCredentialMissingSignals = new Set([
|
|
52
|
+
'CEX_CREDENTIAL_NOT_FOUND',
|
|
53
|
+
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
54
|
+
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
55
|
+
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
56
|
+
]);
|
|
57
|
+
const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
|
|
58
|
+
if (looksBitfinexAction && hasMissingCredentialSignal) {
|
|
59
|
+
next.onboardingHint = {
|
|
60
|
+
type: 'bitfinex_credentials',
|
|
61
|
+
message:
|
|
62
|
+
'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
|
|
63
|
+
command: buildBitfinexOnboardingCommand(permissionProfile),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
looksBitfinexAction &&
|
|
68
|
+
runMode !== 'thirdfy' &&
|
|
69
|
+
runMode !== 'agent_wallet' &&
|
|
70
|
+
(topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
|
|
71
|
+
) {
|
|
72
|
+
next.onboardingHint = {
|
|
73
|
+
...(next.onboardingHint || {}),
|
|
74
|
+
runMode: 'thirdfy',
|
|
75
|
+
runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
82
|
+
const resolvedAction = resolved.resolvedAction;
|
|
83
|
+
if (runMode === 'agent_wallet') {
|
|
84
|
+
const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
85
|
+
runMode,
|
|
86
|
+
requireFundingCheck: true,
|
|
87
|
+
preflightSeed: options.managedPreflight,
|
|
88
|
+
});
|
|
89
|
+
const normalized = normalizeIntentResponse({
|
|
90
|
+
success: preflight.success,
|
|
91
|
+
status: preflight.success ? 'ready' : 'failed',
|
|
92
|
+
mode: 'agent_wallet',
|
|
93
|
+
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
94
|
+
signerMethod: preflight.signerMethod || 'managed_wallet_server',
|
|
95
|
+
blockedReason: preflight.blockedReason || null,
|
|
96
|
+
blockedStage: preflight.blockedStage || null,
|
|
97
|
+
error: preflight.success ? null : preflight.error || 'Managed wallet preflight failed',
|
|
98
|
+
preflightBlocked: !preflight.success,
|
|
99
|
+
fundingTokenBalance: preflight.fundingTokenBalance || null,
|
|
100
|
+
executionWalletPreflight: preflight,
|
|
101
|
+
});
|
|
102
|
+
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
103
|
+
normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
|
|
104
|
+
normalized.executionWalletPreflight = preflight;
|
|
105
|
+
normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
|
|
106
|
+
return {
|
|
107
|
+
message: preflight.success ? 'Managed wallet preflight completed' : 'Managed wallet preflight failed',
|
|
108
|
+
route: 'execution_wallet_preflight',
|
|
109
|
+
normalized,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (runMode === 'self') {
|
|
113
|
+
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
114
|
+
const normalized = await executeSelfRun(
|
|
115
|
+
ctx,
|
|
116
|
+
flags,
|
|
117
|
+
resolved,
|
|
118
|
+
{ skipPreflight: route === 'build_tx' }
|
|
119
|
+
);
|
|
120
|
+
const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
121
|
+
return {
|
|
122
|
+
message: blockedByGovernance
|
|
123
|
+
? 'Self preflight blocked by governance'
|
|
124
|
+
: normalized.success
|
|
125
|
+
? 'Self preflight completed (unsigned transaction ready)'
|
|
126
|
+
: 'Self preflight failed',
|
|
127
|
+
route,
|
|
128
|
+
normalized,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (runMode === 'hybrid' && options.hybridWalletMode === 'agent_wallet') {
|
|
132
|
+
const managedPayload = buildIntentPayload(flags, {
|
|
133
|
+
validationOnly: true,
|
|
134
|
+
forceIdempotency: false,
|
|
135
|
+
resolvedAction,
|
|
136
|
+
runMode: 'agent_wallet',
|
|
137
|
+
mirrorOnly: false,
|
|
138
|
+
hybridWalletMode: 'agent_wallet',
|
|
139
|
+
});
|
|
140
|
+
const managedResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', managedPayload);
|
|
141
|
+
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
142
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
143
|
+
validationOnly: true,
|
|
144
|
+
forceIdempotency: false,
|
|
145
|
+
resolvedAction,
|
|
146
|
+
runMode: 'hybrid',
|
|
147
|
+
mirrorOnly: true,
|
|
148
|
+
hybridWalletMode: 'agent_wallet',
|
|
149
|
+
});
|
|
150
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
151
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
152
|
+
return {
|
|
153
|
+
message:
|
|
154
|
+
managedNormalized.success && mirrorNormalized.success
|
|
155
|
+
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
156
|
+
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
157
|
+
route: 'execute_intent_validation_dual',
|
|
158
|
+
normalized: normalizeIntentResponse({
|
|
159
|
+
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
160
|
+
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
161
|
+
mode: 'hybrid',
|
|
162
|
+
hybridWalletMode: 'agent_wallet',
|
|
163
|
+
managed: managedNormalized,
|
|
164
|
+
mirror: mirrorNormalized,
|
|
165
|
+
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const payload = buildIntentPayload(flags, {
|
|
170
|
+
validationOnly: true,
|
|
171
|
+
forceIdempotency: false,
|
|
172
|
+
resolvedAction,
|
|
173
|
+
runMode,
|
|
174
|
+
mirrorOnly: runMode === 'hybrid',
|
|
175
|
+
});
|
|
176
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
177
|
+
const normalized = normalizeIntentResponse(response);
|
|
178
|
+
return {
|
|
179
|
+
message: normalized.success
|
|
180
|
+
? runMode === 'hybrid'
|
|
181
|
+
? 'Hybrid mirror preflight completed'
|
|
182
|
+
: 'Preflight completed'
|
|
183
|
+
: runMode === 'hybrid'
|
|
184
|
+
? 'Hybrid mirror preflight failed'
|
|
185
|
+
: 'Preflight failed',
|
|
186
|
+
route: 'execute_intent_validation',
|
|
187
|
+
normalized,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
192
|
+
if (runMode === 'thirdfy') {
|
|
193
|
+
const normalized = await executeThirdfyRun(ctx, flags, resolved, options, runMode);
|
|
194
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
195
|
+
return {
|
|
196
|
+
code: normalized.success ? 'RUN_QUEUED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
197
|
+
message: normalized.success
|
|
198
|
+
? 'Execution intent queued'
|
|
199
|
+
: preflightBlocked
|
|
200
|
+
? 'Preflight blocked execution'
|
|
201
|
+
: 'Execution failed',
|
|
202
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
203
|
+
normalized,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (runMode === 'agent_wallet') {
|
|
207
|
+
const normalized = await executeManagedWalletRun(ctx, flags, resolved, options);
|
|
208
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
209
|
+
return {
|
|
210
|
+
code: normalized.success ? 'WALLET_EXECUTED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
211
|
+
message: normalized.success
|
|
212
|
+
? 'Managed wallet execution completed'
|
|
213
|
+
: preflightBlocked
|
|
214
|
+
? 'Managed wallet preflight blocked execution'
|
|
215
|
+
: 'Managed wallet execution failed',
|
|
216
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
217
|
+
normalized,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (runMode === 'self') {
|
|
221
|
+
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
222
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
223
|
+
return {
|
|
224
|
+
code: normalized.success
|
|
225
|
+
? 'SELF_UNSIGNED_TX_READY'
|
|
226
|
+
: preflightBlocked
|
|
227
|
+
? 'PREFLIGHT_BLOCKED'
|
|
228
|
+
: 'SELF_EXECUTION_FAILED',
|
|
229
|
+
message: normalized.success
|
|
230
|
+
? 'Unsigned transaction prepared for self-custody execution'
|
|
231
|
+
: preflightBlocked
|
|
232
|
+
? 'Preflight blocked self execution'
|
|
233
|
+
: 'Self-custody execution failed',
|
|
234
|
+
idempotencyKey: null,
|
|
235
|
+
normalized,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const normalized = await executeHybridRun(ctx, flags, resolved, options);
|
|
239
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
240
|
+
const hybridWalletMode = normalizeHybridWalletMode(
|
|
241
|
+
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
code: normalized.success ? 'HYBRID_READY' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'HYBRID_FAILED',
|
|
245
|
+
message: normalized.success
|
|
246
|
+
? hybridWalletMode === 'agent_wallet'
|
|
247
|
+
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
248
|
+
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
249
|
+
: preflightBlocked
|
|
250
|
+
? 'Preflight blocked hybrid execution'
|
|
251
|
+
: 'Hybrid execution failed',
|
|
252
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
253
|
+
normalized,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'thirdfy') {
|
|
258
|
+
const skipPreflight = Boolean(options?.skipPreflight);
|
|
259
|
+
if (!skipPreflight) {
|
|
260
|
+
const preflightPayload = buildIntentPayload(flags, {
|
|
261
|
+
validationOnly: true,
|
|
262
|
+
forceIdempotency: false,
|
|
263
|
+
resolvedAction: resolved.resolvedAction,
|
|
264
|
+
runMode,
|
|
265
|
+
mirrorOnly: false,
|
|
266
|
+
hybridWalletMode: options?.hybridWalletMode,
|
|
267
|
+
});
|
|
268
|
+
const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
|
|
269
|
+
const preflightNormalized = normalizeIntentResponse(preflight);
|
|
270
|
+
const blockedCount = preflightNormalized.blocked || 0;
|
|
271
|
+
if (!preflightNormalized.success || blockedCount > 0) {
|
|
272
|
+
preflightNormalized.success = false;
|
|
273
|
+
preflightNormalized.error = preflightNormalized.error || 'Preflight blocked execution';
|
|
274
|
+
preflightNormalized.preflightBlocked = true;
|
|
275
|
+
return preflightNormalized;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const payload = buildIntentPayload(flags, {
|
|
279
|
+
validationOnly: false,
|
|
280
|
+
forceIdempotency: true,
|
|
281
|
+
resolvedAction: resolved.resolvedAction,
|
|
282
|
+
runMode,
|
|
283
|
+
mirrorOnly: false,
|
|
284
|
+
hybridWalletMode: options?.hybridWalletMode,
|
|
285
|
+
});
|
|
286
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
287
|
+
const normalized = normalizeIntentResponse(response);
|
|
288
|
+
normalized.idempotencyKey = payload.idempotencyKey;
|
|
289
|
+
return normalized;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
293
|
+
const skipPreflight = Boolean(options?.skipPreflight);
|
|
294
|
+
let preflight = null;
|
|
295
|
+
if (!skipPreflight) {
|
|
296
|
+
preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
297
|
+
runMode: 'agent_wallet',
|
|
298
|
+
requireFundingCheck: true,
|
|
299
|
+
preflightSeed: options?.managedPreflight,
|
|
300
|
+
});
|
|
301
|
+
if (!preflight.success) {
|
|
302
|
+
const normalized = normalizeIntentResponse({
|
|
303
|
+
success: false,
|
|
304
|
+
status: 'failed',
|
|
305
|
+
mode: 'agent_wallet',
|
|
306
|
+
blockedReason: preflight.blockedReason || 'PRECHECK_FAILED',
|
|
307
|
+
blockedStage: preflight.blockedStage || 'routing',
|
|
308
|
+
error: preflight.error || 'Managed wallet preflight failed',
|
|
309
|
+
preflightBlocked: true,
|
|
310
|
+
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
311
|
+
executionWalletPreflight: preflight,
|
|
312
|
+
});
|
|
313
|
+
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
314
|
+
normalized.executionWalletPreflight = preflight;
|
|
315
|
+
return normalized;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const payload = buildManagedExecutePayload(flags, {
|
|
319
|
+
resolvedAction: resolved.resolvedAction,
|
|
320
|
+
runMode: 'agent_wallet',
|
|
321
|
+
forceIdempotency: true,
|
|
322
|
+
});
|
|
323
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute', payload);
|
|
324
|
+
const normalized = normalizeIntentResponse({
|
|
325
|
+
success: Boolean(response?.success),
|
|
326
|
+
status: response?.success ? 'completed' : 'failed',
|
|
327
|
+
mode: 'agent_wallet',
|
|
328
|
+
txHash: response?.txHash || null,
|
|
329
|
+
blockedReason: response?.blockedReason || response?.data?.blockedReason || null,
|
|
330
|
+
blockedStage: response?.blockedStage || response?.data?.blockedStage || null,
|
|
331
|
+
error: response?.error || null,
|
|
332
|
+
executionWalletAddress: response?.executionWalletAddress || response?.data?.executionWalletAddress || null,
|
|
333
|
+
signerMethod: 'managed_wallet_server',
|
|
334
|
+
idempotencyKey: payload.executionIdempotencyKey || null,
|
|
335
|
+
executionWalletPreflight: preflight,
|
|
336
|
+
amountNormalization: response?.amountNormalization || response?.data?.amountNormalization || null,
|
|
337
|
+
raw: response,
|
|
338
|
+
});
|
|
339
|
+
normalized.executionWalletAddress = response?.executionWalletAddress || response?.data?.executionWalletAddress || null;
|
|
340
|
+
normalized.signerMethod = 'managed_wallet_server';
|
|
341
|
+
normalized.executionWalletPreflight = preflight;
|
|
342
|
+
normalized.amountNormalization = response?.amountNormalization || response?.data?.amountNormalization || null;
|
|
343
|
+
normalized.raw = response;
|
|
344
|
+
normalized.idempotencyKey = payload.executionIdempotencyKey || null;
|
|
345
|
+
return normalized;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
349
|
+
const skipPreflight = Boolean(options?.skipPreflight);
|
|
350
|
+
const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
|
|
351
|
+
if (!skipPreflight) {
|
|
352
|
+
const preflightPayload = buildIntentPayload(flags, {
|
|
353
|
+
validationOnly: true,
|
|
354
|
+
forceIdempotency: false,
|
|
355
|
+
resolvedAction: resolved.resolvedAction,
|
|
356
|
+
runMode: 'self',
|
|
357
|
+
mirrorOnly: false,
|
|
358
|
+
});
|
|
359
|
+
const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
|
|
360
|
+
const preflightNormalized = normalizeIntentResponse(preflight);
|
|
361
|
+
const blockedCount = preflightNormalized.blocked || 0;
|
|
362
|
+
if (!preflightNormalized.success || blockedCount > 0) {
|
|
363
|
+
preflightNormalized.success = false;
|
|
364
|
+
preflightNormalized.error = preflightNormalized.error || 'Preflight blocked self execution';
|
|
365
|
+
preflightNormalized.preflightBlocked = true;
|
|
366
|
+
return preflightNormalized;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!shouldUseBuildTxExecution(options?.runMode || 'self', resolved)) {
|
|
370
|
+
const normalized = normalizeIntentResponse({
|
|
371
|
+
success: false,
|
|
372
|
+
status: 'failed',
|
|
373
|
+
blockedReason: blockedReasonCode,
|
|
374
|
+
blockedStage: 'routing',
|
|
375
|
+
error: 'Selected action does not support build-tx self lane. Use --run-mode thirdfy for delegated CEX execution.',
|
|
376
|
+
});
|
|
377
|
+
normalized.preflightBlocked = true;
|
|
378
|
+
return normalized;
|
|
379
|
+
}
|
|
380
|
+
const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
|
|
381
|
+
const buildTx = await apiPost(ctx, '/api/v1/agent/build-tx', buildTxPayload);
|
|
382
|
+
return normalizeIntentResponse({
|
|
383
|
+
...buildTx,
|
|
384
|
+
status: buildTx?.status || 'ready',
|
|
385
|
+
mode: buildTx?.mode || 'self',
|
|
386
|
+
unsignedTx: buildTx?.unsignedTx || null,
|
|
387
|
+
estimatedGas: buildTx?.estimatedGas || null,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
392
|
+
const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
|
|
393
|
+
if (hybridWalletMode === 'agent_wallet') {
|
|
394
|
+
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
395
|
+
if (!managedResult.success) {
|
|
396
|
+
return normalizeIntentResponse({
|
|
397
|
+
...managedResult,
|
|
398
|
+
mode: 'hybrid',
|
|
399
|
+
hybridWalletMode,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
403
|
+
validationOnly: true,
|
|
404
|
+
forceIdempotency: true,
|
|
405
|
+
resolvedAction: resolved.resolvedAction,
|
|
406
|
+
runMode: 'hybrid',
|
|
407
|
+
mirrorOnly: true,
|
|
408
|
+
hybridWalletMode,
|
|
409
|
+
});
|
|
410
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
411
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
412
|
+
return normalizeIntentResponse({
|
|
413
|
+
success: Boolean(managedResult.success && mirrorNormalized.success),
|
|
414
|
+
status: managedResult.status || mirrorNormalized.status || 'queued',
|
|
415
|
+
mode: 'hybrid',
|
|
416
|
+
hybridWalletMode,
|
|
417
|
+
idempotencyKey: managedResult.idempotencyKey || mirrorPayload.idempotencyKey,
|
|
418
|
+
managed: managedResult,
|
|
419
|
+
mirror: mirrorNormalized,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
|
423
|
+
...(options || {}),
|
|
424
|
+
runMode: 'hybrid',
|
|
425
|
+
blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
|
|
426
|
+
});
|
|
427
|
+
if (!selfResult.success) return selfResult;
|
|
428
|
+
|
|
429
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
430
|
+
validationOnly: true,
|
|
431
|
+
forceIdempotency: true,
|
|
432
|
+
resolvedAction: resolved.resolvedAction,
|
|
433
|
+
runMode: 'hybrid',
|
|
434
|
+
mirrorOnly: true,
|
|
435
|
+
});
|
|
436
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
437
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
438
|
+
return normalizeIntentResponse({
|
|
439
|
+
success: Boolean(selfResult.success && mirrorNormalized.success),
|
|
440
|
+
status: mirrorNormalized.status || 'ready',
|
|
441
|
+
mode: 'hybrid',
|
|
442
|
+
hybridWalletMode,
|
|
443
|
+
idempotencyKey: mirrorPayload.idempotencyKey,
|
|
444
|
+
self: selfResult,
|
|
445
|
+
mirror: mirrorNormalized,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function resolveManagedExecutionPreflight(ctx, flags, resolved, options = {}) {
|
|
450
|
+
const chainId = Number(flags.chainId || 8453);
|
|
451
|
+
const runMode = String(options.runMode || 'agent_wallet').trim().toLowerCase();
|
|
452
|
+
const agentApiKey = resolveAgentApiKey(flags, runMode);
|
|
453
|
+
const userDid = resolveEffectiveUserDid(flags);
|
|
454
|
+
if (!userDid) {
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
blockedReason: 'MISSING_USER_DID',
|
|
458
|
+
blockedStage: 'routing',
|
|
459
|
+
error: 'Managed wallet execution requires --user-did, THIRDFY_USER_DID, or a saved userDid from `thirdfy-agent login email`.',
|
|
460
|
+
signerMethod: 'managed_wallet_server',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const tokenIn = resolveTokenInForFunding(getPreparedParams(flags));
|
|
464
|
+
const query = new URLSearchParams({
|
|
465
|
+
userDid,
|
|
466
|
+
chainId: String(chainId),
|
|
467
|
+
runMode,
|
|
468
|
+
});
|
|
469
|
+
if (tokenIn) query.set('tokenIn', tokenIn);
|
|
470
|
+
const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
|
|
471
|
+
? options.preflightSeed
|
|
472
|
+
: null;
|
|
473
|
+
if (preflightSeed && preflightSeed.success === false) {
|
|
474
|
+
return preflightSeed;
|
|
475
|
+
}
|
|
476
|
+
const response = preflightSeed
|
|
477
|
+
? {
|
|
478
|
+
executionWalletAddress: preflightSeed.executionWalletAddress || null,
|
|
479
|
+
signerMethod: preflightSeed.signerMethod || null,
|
|
480
|
+
fundingTokenBalance: preflightSeed.fundingTokenBalance || null,
|
|
481
|
+
}
|
|
482
|
+
: await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
|
|
483
|
+
'x-agent-api-key': agentApiKey,
|
|
484
|
+
});
|
|
485
|
+
const executionWalletAddress = String(response?.executionWalletAddress || '').trim().toLowerCase();
|
|
486
|
+
const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
|
|
487
|
+
const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
|
|
488
|
+
let parsedRawBalance = null;
|
|
489
|
+
if (rawBalance) {
|
|
490
|
+
try {
|
|
491
|
+
parsedRawBalance = BigInt(rawBalance);
|
|
492
|
+
} catch {
|
|
493
|
+
return {
|
|
494
|
+
success: false,
|
|
495
|
+
blockedReason: 'INVALID_FUNDING_BALANCE',
|
|
496
|
+
blockedStage: 'sizing',
|
|
497
|
+
error: `Execution wallet ${executionWalletAddress || 'unknown'} returned invalid funding token balance.`,
|
|
498
|
+
executionWalletAddress,
|
|
499
|
+
signerMethod,
|
|
500
|
+
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim().toLowerCase();
|
|
505
|
+
const allowWalletMismatch = parseBooleanFlag(flags.allowWalletMismatch, false);
|
|
506
|
+
|
|
507
|
+
if (expectedWallet && executionWalletAddress && expectedWallet !== executionWalletAddress && !allowWalletMismatch) {
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
blockedReason: 'EXECUTION_WALLET_MISMATCH',
|
|
511
|
+
blockedStage: 'routing',
|
|
512
|
+
error: `Funded wallet (${expectedWallet}) does not match execution wallet (${executionWalletAddress}). Use --allow-wallet-mismatch to override.`,
|
|
513
|
+
executionWalletAddress,
|
|
514
|
+
signerMethod,
|
|
515
|
+
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (options.requireFundingCheck && parsedRawBalance !== null && parsedRawBalance <= 0n) {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
blockedReason: 'INSUFFICIENT_FUNDS',
|
|
523
|
+
blockedStage: 'sizing',
|
|
524
|
+
error: `Execution wallet ${executionWalletAddress || 'unknown'} has insufficient tokenIn balance.`,
|
|
525
|
+
executionWalletAddress,
|
|
526
|
+
signerMethod,
|
|
527
|
+
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
executionWalletAddress,
|
|
534
|
+
signerMethod,
|
|
535
|
+
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
536
|
+
userDid,
|
|
537
|
+
chainId,
|
|
538
|
+
tokenIn: tokenIn || null,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
543
|
+
const txd = resolveUnsignedTx(selfResult);
|
|
544
|
+
if (!txd?.to || !txd?.data) {
|
|
545
|
+
throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
|
|
546
|
+
}
|
|
547
|
+
const chainId = Number(txd.chainId || flags.chainId || 8453);
|
|
548
|
+
const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
|
|
549
|
+
const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
|
|
550
|
+
const rpcUrl = String(
|
|
551
|
+
flags.rpcUrl || process.env.BASE_RPC_URL || process.env.BASE_RPC_HTTP_URL || process.env.RPC_URL || 'https://mainnet.base.org'
|
|
552
|
+
).trim();
|
|
553
|
+
if (!walletAddress) {
|
|
554
|
+
throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
|
|
555
|
+
}
|
|
556
|
+
if (!owsWalletName) {
|
|
557
|
+
throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const params = getPreparedParams(flags);
|
|
561
|
+
const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
|
|
562
|
+
const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
|
|
563
|
+
const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
|
|
564
|
+
const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
|
|
565
|
+
const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
|
|
566
|
+
|
|
567
|
+
let balancesBefore = null;
|
|
568
|
+
if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
|
|
569
|
+
balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let submission;
|
|
573
|
+
try {
|
|
574
|
+
submission = await signAndBroadcastWithOws({
|
|
575
|
+
txd,
|
|
576
|
+
chainId,
|
|
577
|
+
walletAddress,
|
|
578
|
+
owsWalletName,
|
|
579
|
+
rpcUrl,
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let receipt;
|
|
586
|
+
try {
|
|
587
|
+
receipt = await submission.sent.wait();
|
|
588
|
+
} catch (error) {
|
|
589
|
+
throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
|
|
590
|
+
}
|
|
591
|
+
const status = Number(receipt?.status ?? 0);
|
|
592
|
+
if (status !== 1) {
|
|
593
|
+
throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
|
|
594
|
+
receiptStatus: receipt?.status ?? null,
|
|
595
|
+
txHash: submission.sent.hash,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const effectCheck = await resolveEffectCheckResult({
|
|
600
|
+
mode: effectMode,
|
|
601
|
+
rpcUrl,
|
|
602
|
+
walletAddress,
|
|
603
|
+
tokenIn: effectTokenIn,
|
|
604
|
+
tokenOut: effectTokenOut,
|
|
605
|
+
balancesBefore,
|
|
606
|
+
retries: effectRetries,
|
|
607
|
+
waitMs: effectWaitMs,
|
|
608
|
+
});
|
|
609
|
+
if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
|
|
610
|
+
throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
|
|
611
|
+
txHash: submission.sent.hash,
|
|
612
|
+
effectCheck,
|
|
613
|
+
mode: effectMode,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
txHash: submission.sent.hash,
|
|
619
|
+
chainId,
|
|
620
|
+
blockNumber: receipt?.blockNumber ?? null,
|
|
621
|
+
status: receipt?.status ?? null,
|
|
622
|
+
unsignedTx: txd,
|
|
623
|
+
effectCheck,
|
|
624
|
+
lane: 'self',
|
|
625
|
+
signer: 'ows',
|
|
626
|
+
resolvedAction,
|
|
627
|
+
apiBase: ctx.apiBase,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function resolveEffectCheckResult({
|
|
632
|
+
mode,
|
|
633
|
+
rpcUrl,
|
|
634
|
+
walletAddress,
|
|
635
|
+
tokenIn,
|
|
636
|
+
tokenOut,
|
|
637
|
+
balancesBefore,
|
|
638
|
+
retries,
|
|
639
|
+
waitMs,
|
|
640
|
+
}) {
|
|
641
|
+
const baseResult = {
|
|
642
|
+
checked: true,
|
|
643
|
+
mode,
|
|
644
|
+
hasEffect: false,
|
|
645
|
+
inDeltaRaw: '0',
|
|
646
|
+
outDeltaRaw: '0',
|
|
647
|
+
attempts: 0,
|
|
648
|
+
retries: Math.max(0, Number(retries || 0)),
|
|
649
|
+
};
|
|
650
|
+
if (mode === 'off') {
|
|
651
|
+
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
|
|
652
|
+
}
|
|
653
|
+
if (!tokenIn || !tokenOut || !balancesBefore) {
|
|
654
|
+
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
|
|
655
|
+
}
|
|
656
|
+
const maxAttempts = Math.max(1, Number(retries || 0) + 1);
|
|
657
|
+
let last = null;
|
|
658
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
659
|
+
const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
|
|
660
|
+
const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
|
|
661
|
+
const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
|
|
662
|
+
const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
|
|
663
|
+
last = {
|
|
664
|
+
checked: true,
|
|
665
|
+
mode,
|
|
666
|
+
hasEffect,
|
|
667
|
+
inDeltaRaw,
|
|
668
|
+
outDeltaRaw,
|
|
669
|
+
attempts: attempt,
|
|
670
|
+
retries: maxAttempts - 1,
|
|
671
|
+
};
|
|
672
|
+
if (hasEffect) return last;
|
|
673
|
+
if (attempt < maxAttempts) await sleepMs(waitMs);
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
...(last || baseResult),
|
|
677
|
+
warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function submitSignedTx({ rpcUrl, chainId, signedTxHex }) {
|
|
682
|
+
const ethers = await loadEthers();
|
|
683
|
+
const { JsonRpcProvider } = ethers;
|
|
684
|
+
const provider = new JsonRpcProvider(rpcUrl, chainId);
|
|
685
|
+
const sent = await provider.broadcastTransaction(String(signedTxHex));
|
|
686
|
+
return sent?.hash || null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
applyExecutionFallbackHints,
|
|
691
|
+
runPreflightByMode,
|
|
692
|
+
runExecutionByMode,
|
|
693
|
+
executeThirdfyRun,
|
|
694
|
+
executeManagedWalletRun,
|
|
695
|
+
executeSelfRun,
|
|
696
|
+
executeHybridRun,
|
|
697
|
+
resolveManagedExecutionPreflight,
|
|
698
|
+
performSelfBroadcast,
|
|
699
|
+
resolveEffectCheckResult,
|
|
700
|
+
submitSignedTx,
|
|
701
|
+
};
|
|
702
|
+
}
|