@thirdfy/agent-cli 0.4.1 → 0.4.3
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 +39 -0
- package/README.md +4 -7
- package/package.json +5 -3
- package/src/cli/help.mjs +1 -1
- package/src/cli/options/index.mjs +11 -11
- package/src/cli/options/onboarding/credentials.mjs +17 -0
- package/src/cli/program.mjs +1 -1
- package/src/commands/{agent.mjs → account/agent.mjs} +5 -5
- package/src/commands/{credits.mjs → account/credits.mjs} +13 -15
- package/src/commands/{wallet.mjs → account/wallet.mjs} +29 -33
- package/src/commands/{walletControl.mjs → account/walletControl.mjs} +3 -3
- package/src/commands/discovery.mjs +1 -1
- package/src/commands/doctor.mjs +5 -5
- package/src/commands/execute.mjs +4 -13
- package/src/commands/index.mjs +9 -9
- package/src/commands/{bootstrap.mjs → onboarding/bootstrap.mjs} +8 -8
- package/src/commands/{consoleMandate.mjs → onboarding/consoleMandate.mjs} +2 -2
- package/src/commands/{credentials.mjs → onboarding/credentials.mjs} +16 -29
- package/src/commands/{delegation.mjs → onboarding/delegation.mjs} +5 -5
- package/src/commands/{avantis.mjs → venues/avantis.mjs} +1 -1
- package/src/commands/{condor.mjs → venues/condor.mjs} +4 -4
- package/src/commands/{hummingbot.mjs → venues/hummingbot.mjs} +4 -4
- package/src/commands/{hyperliquid.mjs → venues/hyperliquid.mjs} +1 -1
- package/src/commands/{lighter.mjs → venues/lighter.mjs} +1 -1
- package/src/commands/{polymarket.mjs → venues/polymarket.mjs} +4 -4
- package/src/commands/{venue.mjs → venues/venue.mjs} +3 -3
- package/src/core/chainRegistry.mjs +11 -0
- package/src/core/constants.mjs +0 -19
- package/src/core/earnclawAttribution.mjs +1 -1
- package/src/core/{actionTimeouts.mjs → execution/actionTimeouts.mjs} +2 -2
- package/src/core/{walletExecuteChainId.mjs → execution/walletExecuteChainId.mjs} +1 -1
- package/src/core/http.mjs +1 -1
- package/src/runtime/actions/selection.mjs +0 -1
- package/src/runtime/execution/hostedRuntimeWriteGate.mjs +1 -1
- package/src/runtime/execution/payloads.mjs +4 -4
- package/src/runtime/execution/runners/agentWalletLane.mjs +109 -0
- package/src/runtime/execution/runners/createExecutionRunners.mjs +312 -0
- package/src/runtime/execution/runners/hybridLane.mjs +88 -0
- package/src/runtime/execution/runners/managedPreflight.mjs +253 -0
- package/src/runtime/execution/runners/managedResponse.mjs +119 -0
- package/src/runtime/execution/runners/selfLane.mjs +233 -0
- package/src/runtime/execution/runners/thirdfyLane.mjs +97 -0
- package/src/runtime/execution/svmLane.mjs +1 -1
- package/src/runtime/handlers.mjs +15 -20
- package/src/runtime/main.mjs +1 -11
- package/src/runtime/owsSigning.mjs +17 -16
- package/src/runtime/providerHints/cex.mjs +1 -52
- package/src/runtime/risk/notional.mjs +1 -1
- package/src/runtime/validation/actionParamsGate.mjs +1 -1
- package/src/cli/options/credentials.mjs +0 -15
- package/src/runtime/bitfinexProbe.mjs +0 -121
- package/src/runtime/execution/runners.mjs +0 -1134
- package/src/runtime/onboardingHints.mjs +0 -54
- /package/src/cli/options/{agent.mjs → account/agent.mjs} +0 -0
- /package/src/cli/options/{credits.mjs → account/credits.mjs} +0 -0
- /package/src/cli/options/{wallet.mjs → account/wallet.mjs} +0 -0
- /package/src/cli/options/{walletControl.mjs → account/walletControl.mjs} +0 -0
- /package/src/cli/options/{bootstrap.mjs → onboarding/bootstrap.mjs} +0 -0
- /package/src/cli/options/{delegation.mjs → onboarding/delegation.mjs} +0 -0
- /package/src/cli/options/{condor.mjs → venues/condor.mjs} +0 -0
- /package/src/cli/options/{hummingbot.mjs → venues/hummingbot.mjs} +0 -0
- /package/src/cli/options/{polymarket.mjs → venues/polymarket.mjs} +0 -0
- /package/src/cli/options/{venue.mjs → venues/venue.mjs} +0 -0
- /package/src/core/{executionContext.mjs → execution/executionContext.mjs} +0 -0
- /package/src/core/{fundingTokenIn.mjs → execution/fundingTokenIn.mjs} +0 -0
- /package/src/core/{hostedRuntimeIdempotency.mjs → execution/hostedRuntimeIdempotency.mjs} +0 -0
- /package/src/core/{readOnlyActions.mjs → execution/readOnlyActions.mjs} +0 -0
- /package/src/core/{signedTx.mjs → execution/signedTx.mjs} +0 -0
|
@@ -1,1134 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
import { pickNativeGasFields } from './nativeGas.mjs';
|
|
3
|
-
import {
|
|
4
|
-
ACTION_PREFLIGHT_VENUE_FIELDS,
|
|
5
|
-
appendActionPreflightQuery,
|
|
6
|
-
blockedActionPreflight,
|
|
7
|
-
blockedActionPreflightFromError,
|
|
8
|
-
describeExecutionVenue,
|
|
9
|
-
isApiActionBlock,
|
|
10
|
-
pickActionPreflightFields,
|
|
11
|
-
} from './actionPreflight.mjs';
|
|
12
|
-
import { requireFlag } from '../../core/args.mjs';
|
|
13
|
-
import { createCliError } from '../../core/envelope.mjs';
|
|
14
|
-
import {
|
|
15
|
-
normalizeRunMode,
|
|
16
|
-
normalizeManagedRunMode,
|
|
17
|
-
normalizeHybridWalletMode,
|
|
18
|
-
normalizeEffectCheckMode,
|
|
19
|
-
getEffectiveRunMode,
|
|
20
|
-
parseBooleanFlag,
|
|
21
|
-
} from '../../core/runMode.mjs';
|
|
22
|
-
import { apiGet, apiPost, responseHeadersFor } from '../../core/http.mjs';
|
|
23
|
-
import { idempotentReplayFields } from '../../core/hostedRuntimeIdempotency.mjs';
|
|
24
|
-
import { withActionTimeout } from '../../core/actionTimeouts.mjs';
|
|
25
|
-
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
26
|
-
import { resolveWalletExecuteChainId } from '../../core/walletExecuteChainId.mjs';
|
|
27
|
-
import { describeInsufficientNativeGas, isSvmChainId, resolveChainRpcUrl, sameWalletAddress } from '../../core/chainRegistry.mjs';
|
|
28
|
-
import { readWalletControl } from './walletControl.mjs';
|
|
29
|
-
import { actionNameForApi } from '../actions/selection.mjs';
|
|
30
|
-
import { blockedResultFields } from '../../core/errorEnvelope.mjs';
|
|
31
|
-
import { riskRefusalFields } from '../risk/riskRefusal.mjs';
|
|
32
|
-
import { isEmptyQueuedExecuteIntent, shouldFallbackManagedReadToExecuteIntent } from './managedReadExecuteRail.mjs';
|
|
33
|
-
import { createRequire } from 'module';
|
|
34
|
-
|
|
35
|
-
const require = createRequire(import.meta.url);
|
|
36
|
-
const { normalizeIntentResponse } = require('../../utils/cli/intentNormalization.cjs');
|
|
37
|
-
|
|
38
|
-
export function createExecutionRunners(deps) {
|
|
39
|
-
const {
|
|
40
|
-
buildIntentPayload,
|
|
41
|
-
buildBuildTxPayload,
|
|
42
|
-
buildManagedExecutePayload,
|
|
43
|
-
getPreparedParams,
|
|
44
|
-
resolveEffectiveUserDid,
|
|
45
|
-
resolveTokenInForFunding,
|
|
46
|
-
shouldUseBuildTxPreflight,
|
|
47
|
-
shouldUseBuildTxExecution,
|
|
48
|
-
buildBitfinexOnboardingCommand,
|
|
49
|
-
inferBitfinexPermissionProfileFromAction,
|
|
50
|
-
resolveAgentApiKey,
|
|
51
|
-
resolveAgentKey,
|
|
52
|
-
loadEthers,
|
|
53
|
-
signAndBroadcastWithOws,
|
|
54
|
-
readEffectBalances,
|
|
55
|
-
resolveUnsignedTx,
|
|
56
|
-
sleepMs,
|
|
57
|
-
} = deps;
|
|
58
|
-
|
|
59
|
-
function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
|
|
60
|
-
if (!normalized || typeof normalized !== 'object') return normalized;
|
|
61
|
-
const next = { ...normalized };
|
|
62
|
-
const reasons = new Set();
|
|
63
|
-
const topBlockedReason = String(next.blockedReason || '').trim();
|
|
64
|
-
if (topBlockedReason) reasons.add(topBlockedReason);
|
|
65
|
-
const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
|
|
66
|
-
for (const key of Object.keys(blockedByReason)) {
|
|
67
|
-
if (String(key || '').trim()) reasons.add(String(key).trim());
|
|
68
|
-
}
|
|
69
|
-
const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
|
|
70
|
-
const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
|
|
71
|
-
const permissionProfile = inferBitfinexPermissionProfileFromAction(resolvedActionLower);
|
|
72
|
-
const cexCredentialMissingSignals = new Set([
|
|
73
|
-
'CEX_CREDENTIAL_NOT_FOUND',
|
|
74
|
-
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
75
|
-
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
76
|
-
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
77
|
-
]);
|
|
78
|
-
const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
|
|
79
|
-
if (looksBitfinexAction && hasMissingCredentialSignal) {
|
|
80
|
-
next.onboardingHint = {
|
|
81
|
-
type: 'bitfinex_credentials',
|
|
82
|
-
message:
|
|
83
|
-
'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
|
|
84
|
-
command: buildBitfinexOnboardingCommand(permissionProfile),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (
|
|
88
|
-
looksBitfinexAction &&
|
|
89
|
-
runMode !== 'thirdfy' &&
|
|
90
|
-
runMode !== 'agent_wallet' &&
|
|
91
|
-
(topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
|
|
92
|
-
) {
|
|
93
|
-
next.onboardingHint = {
|
|
94
|
-
...(next.onboardingHint || {}),
|
|
95
|
-
runMode: 'thirdfy',
|
|
96
|
-
runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
return next;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isReadOnlyAction(resolved) {
|
|
103
|
-
return isReadOnlyResolvedAction(resolved);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
|
|
107
|
-
return shouldFallbackManagedReadToExecuteIntent({
|
|
108
|
-
isReadOnly: isReadOnlyAction(resolved),
|
|
109
|
-
errorText: String(response?.error || response?.message || response?.code || response?.blockedReason || ''),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Managed /execute often returns provider payloads as JSON strings under data.result. Pack parsers
|
|
114
|
-
// (and find_value_by_key) need a real object tree, so coerce strings that look like JSON.
|
|
115
|
-
function coerceJsonTreeValue(value) {
|
|
116
|
-
if (typeof value !== 'string') return value;
|
|
117
|
-
const trimmed = value.trim();
|
|
118
|
-
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return value;
|
|
119
|
-
try {
|
|
120
|
-
return JSON.parse(trimmed);
|
|
121
|
-
} catch {
|
|
122
|
-
return value;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function shapeManagedExecuteReadResponse(response) {
|
|
127
|
-
if (!response || typeof response !== 'object') return response;
|
|
128
|
-
const data =
|
|
129
|
-
response.data && typeof response.data === 'object' ? { ...response.data } : response.data;
|
|
130
|
-
if (data && typeof data === 'object' && Object.prototype.hasOwnProperty.call(data, 'result')) {
|
|
131
|
-
data.result = coerceJsonTreeValue(data.result);
|
|
132
|
-
}
|
|
133
|
-
return { ...response, data };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function normalizeManagedExecuteResponse(response, { preflight, payload, isRead }) {
|
|
137
|
-
const shaped = isRead ? shapeManagedExecuteReadResponse(response) : response;
|
|
138
|
-
const normalized = normalizeIntentResponse({
|
|
139
|
-
success: Boolean(shaped?.success),
|
|
140
|
-
status: shaped?.success ? 'completed' : 'failed',
|
|
141
|
-
mode: 'agent_wallet',
|
|
142
|
-
txHash: shaped?.txHash || null,
|
|
143
|
-
blockedReason: shaped?.blockedReason || shaped?.data?.blockedReason || null,
|
|
144
|
-
blockedStage: shaped?.blockedStage || shaped?.data?.blockedStage || null,
|
|
145
|
-
error: shaped?.error || null,
|
|
146
|
-
executionWalletAddress:
|
|
147
|
-
shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null,
|
|
148
|
-
signerMethod: 'managed_wallet_server',
|
|
149
|
-
idempotencyKey: payload.executionIdempotencyKey || null,
|
|
150
|
-
executionWalletPreflight: preflight,
|
|
151
|
-
amountNormalization:
|
|
152
|
-
shaped?.amountNormalization || shaped?.data?.amountNormalization || null,
|
|
153
|
-
// Reads must keep provider payload on the envelope. EarnClaw packs parse
|
|
154
|
-
// data.raw.data.result (see parse_thirdfy_action_result).
|
|
155
|
-
...(isRead && shaped?.data !== undefined ? { data: shaped.data } : {}),
|
|
156
|
-
raw: shaped,
|
|
157
|
-
});
|
|
158
|
-
normalized.executionWalletAddress =
|
|
159
|
-
shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null;
|
|
160
|
-
normalized.signerMethod = 'managed_wallet_server';
|
|
161
|
-
normalized.executionWalletPreflight = preflight;
|
|
162
|
-
normalized.amountNormalization =
|
|
163
|
-
shaped?.amountNormalization || shaped?.data?.amountNormalization || null;
|
|
164
|
-
normalized.raw = shaped;
|
|
165
|
-
normalized.idempotencyKey = payload.executionIdempotencyKey || null;
|
|
166
|
-
// A risk-policy refusal nests riskRefusal under data; lift it as thirdfy-mcp walletExecute does.
|
|
167
|
-
const refusal = shaped?.success ? undefined : riskRefusalFields(shaped || {});
|
|
168
|
-
if (refusal) Object.assign(normalized, refusal);
|
|
169
|
-
// A replay means nothing new ran; say so, or the caller reads it as a fresh fill.
|
|
170
|
-
if (!isRead) {
|
|
171
|
-
Object.assign(
|
|
172
|
-
normalized,
|
|
173
|
-
idempotentReplayFields(response, responseHeadersFor(response), 'executionIdempotencyKey'),
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return normalized;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const ROBINHOOD_MAINNET_CHAIN_ID = 4663;
|
|
180
|
-
|
|
181
|
-
/** Marker a read on the fast path carries instead of an execution-wallet preflight. */
|
|
182
|
-
const READ_FAST_PATH_PREFLIGHT = Object.freeze({ success: true, skipped: 'read_fast_path' });
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* agent_wallet reads skip the /execution-wallet preflight: the API resolves the agent's execution
|
|
186
|
-
* wallet itself, so the extra round trip only matters when a read uses its answer: a strict wallet
|
|
187
|
-
* match, a funding check, or the Robinhood delegated lane. An expected wallet equal to the agent key
|
|
188
|
-
* is not a separate wallet to match first; the wallet the API reports is checked after the read.
|
|
189
|
-
* Mirrors thirdfy-mcp `walletExecute` readFastPath.
|
|
190
|
-
*/
|
|
191
|
-
function shouldUseManagedReadFastPath(flags, resolved, options = {}) {
|
|
192
|
-
if (!isReadOnlyAction(resolved)) return false;
|
|
193
|
-
if (options.requireFundingCheck === true) return false;
|
|
194
|
-
const runMode = String(flags.runMode || 'agent_wallet').trim().toLowerCase();
|
|
195
|
-
if (runMode !== 'agent_wallet') return false;
|
|
196
|
-
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
197
|
-
const agentKey = String(typeof resolveAgentKey === 'function' ? resolveAgentKey(flags) : flags.agentKey || '').trim();
|
|
198
|
-
if (expectedWallet && !sameWalletAddress(expectedWallet, agentKey)) return false;
|
|
199
|
-
let chainId;
|
|
200
|
-
try {
|
|
201
|
-
chainId = resolveWalletExecuteChainId({
|
|
202
|
-
action: String(resolved?.resolvedAction || flags.action || '').trim(),
|
|
203
|
-
chainId: flags.chainId,
|
|
204
|
-
params: getPreparedParams(flags),
|
|
205
|
-
});
|
|
206
|
-
} catch {
|
|
207
|
-
// Unresolvable chain (e.g. a CCTP quote without sourceChainId): the preflight path reports it.
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
return Number(chainId) !== ROBINHOOD_MAINNET_CHAIN_ID;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
214
|
-
const resolvedAction = resolved.resolvedAction;
|
|
215
|
-
if (runMode === 'agent_wallet') {
|
|
216
|
-
const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
217
|
-
runMode,
|
|
218
|
-
// Reads carry no transaction; keep funding optional so preflight matches run.
|
|
219
|
-
requireFundingCheck: !isReadOnlyAction(resolved),
|
|
220
|
-
preflightSeed: options.managedPreflight,
|
|
221
|
-
// Ask the API to check policy and venue readiness for this action, not just the wallet.
|
|
222
|
-
evaluateAction: true,
|
|
223
|
-
});
|
|
224
|
-
const normalized = normalizeIntentResponse({
|
|
225
|
-
success: preflight.success,
|
|
226
|
-
status: preflight.success ? 'ready' : 'failed',
|
|
227
|
-
mode: 'agent_wallet',
|
|
228
|
-
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
229
|
-
signerMethod: preflight.signerMethod || 'managed_wallet_server',
|
|
230
|
-
blockedReason: preflight.blockedReason || null,
|
|
231
|
-
blockedStage: preflight.blockedStage || null,
|
|
232
|
-
error: preflight.success ? null : preflight.error || 'Managed wallet preflight failed',
|
|
233
|
-
preflightBlocked: !preflight.success,
|
|
234
|
-
fundingTokenBalance: preflight.fundingTokenBalance || null,
|
|
235
|
-
executionWalletPreflight: preflight,
|
|
236
|
-
});
|
|
237
|
-
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
238
|
-
normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
|
|
239
|
-
normalized.executionWalletPreflight = preflight;
|
|
240
|
-
normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
|
|
241
|
-
normalized.policyEvaluated = preflight.policyEvaluated === true;
|
|
242
|
-
for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'executionVenue']) {
|
|
243
|
-
if (preflight[key] != null) normalized[key] = preflight[key];
|
|
244
|
-
}
|
|
245
|
-
// Only an API refusal of the action is `blocked`; CLI-local checks keep `failed`.
|
|
246
|
-
const apiBlocked = isApiActionBlock(preflight);
|
|
247
|
-
if (apiBlocked) normalized.status = 'blocked';
|
|
248
|
-
return {
|
|
249
|
-
message: preflight.success
|
|
250
|
-
? 'Managed wallet preflight completed'
|
|
251
|
-
: preflight.blockedStage === 'venue_readiness'
|
|
252
|
-
? 'Managed wallet preflight blocked: venue not ready'
|
|
253
|
-
: apiBlocked
|
|
254
|
-
? 'Managed wallet preflight blocked by policy'
|
|
255
|
-
: 'Managed wallet preflight failed',
|
|
256
|
-
route: 'execution_wallet_preflight',
|
|
257
|
-
normalized,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
if (runMode === 'self') {
|
|
261
|
-
// Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
|
|
262
|
-
// operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
|
|
263
|
-
if (isReadOnlyAction(resolved)) {
|
|
264
|
-
const payload = buildIntentPayload(flags, {
|
|
265
|
-
validationOnly: true,
|
|
266
|
-
forceIdempotency: false,
|
|
267
|
-
resolvedAction,
|
|
268
|
-
apiAction: actionNameForApi(resolved),
|
|
269
|
-
runMode: 'self',
|
|
270
|
-
mirrorOnly: false,
|
|
271
|
-
});
|
|
272
|
-
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
273
|
-
const normalized = normalizeIntentResponse(response);
|
|
274
|
-
const blockedCount = normalized.blocked || 0;
|
|
275
|
-
if (!normalized.success || blockedCount > 0) {
|
|
276
|
-
normalized.success = false;
|
|
277
|
-
normalized.error = normalized.error || 'Preflight blocked self execution';
|
|
278
|
-
normalized.preflightBlocked = true;
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
message: normalized.success
|
|
282
|
-
? 'Self preflight completed'
|
|
283
|
-
: normalized.preflightBlocked
|
|
284
|
-
? 'Self preflight blocked by governance'
|
|
285
|
-
: 'Self preflight failed',
|
|
286
|
-
route: 'execute_intent_validation',
|
|
287
|
-
normalized,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
291
|
-
const normalized = await executeSelfRun(
|
|
292
|
-
ctx,
|
|
293
|
-
flags,
|
|
294
|
-
resolved,
|
|
295
|
-
{ skipPreflight: route === 'build_tx' }
|
|
296
|
-
);
|
|
297
|
-
const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
298
|
-
return {
|
|
299
|
-
message: blockedByGovernance
|
|
300
|
-
? 'Self preflight blocked by governance'
|
|
301
|
-
: normalized.success
|
|
302
|
-
? 'Self preflight completed (unsigned transaction ready)'
|
|
303
|
-
: 'Self preflight failed',
|
|
304
|
-
route,
|
|
305
|
-
normalized,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
if (runMode === 'hybrid') {
|
|
309
|
-
// Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
|
|
310
|
-
// execute-intent so dry-runs do not trigger a real catalog read (run still uses the read rail).
|
|
311
|
-
if (isReadOnlyAction(resolved)) {
|
|
312
|
-
const payload = buildIntentPayload(flags, {
|
|
313
|
-
validationOnly: true,
|
|
314
|
-
forceIdempotency: false,
|
|
315
|
-
resolvedAction,
|
|
316
|
-
apiAction: actionNameForApi(resolved),
|
|
317
|
-
runMode: 'hybrid',
|
|
318
|
-
mirrorOnly: false,
|
|
319
|
-
hybridWalletMode: options.hybridWalletMode,
|
|
320
|
-
});
|
|
321
|
-
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
322
|
-
const normalized = normalizeIntentResponse(response);
|
|
323
|
-
const blockedCount = normalized.blocked || 0;
|
|
324
|
-
if (!normalized.success || blockedCount > 0) {
|
|
325
|
-
normalized.success = false;
|
|
326
|
-
normalized.error = normalized.error || 'Preflight blocked hybrid execution';
|
|
327
|
-
normalized.preflightBlocked = true;
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
message: normalized.success
|
|
331
|
-
? 'Hybrid preflight completed'
|
|
332
|
-
: normalized.preflightBlocked
|
|
333
|
-
? 'Hybrid preflight blocked by governance'
|
|
334
|
-
: 'Hybrid preflight failed',
|
|
335
|
-
route: 'execute_intent_validation',
|
|
336
|
-
normalized,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
if (options.hybridWalletMode === 'agent_wallet') {
|
|
340
|
-
const managedPayload = buildIntentPayload(flags, {
|
|
341
|
-
validationOnly: true,
|
|
342
|
-
forceIdempotency: false,
|
|
343
|
-
resolvedAction,
|
|
344
|
-
apiAction: actionNameForApi(resolved),
|
|
345
|
-
runMode: 'agent_wallet',
|
|
346
|
-
mirrorOnly: false,
|
|
347
|
-
hybridWalletMode: 'agent_wallet',
|
|
348
|
-
});
|
|
349
|
-
const managedResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', managedPayload);
|
|
350
|
-
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
351
|
-
const mirrorPayload = buildIntentPayload(flags, {
|
|
352
|
-
validationOnly: true,
|
|
353
|
-
forceIdempotency: false,
|
|
354
|
-
resolvedAction,
|
|
355
|
-
apiAction: actionNameForApi(resolved),
|
|
356
|
-
runMode: 'hybrid',
|
|
357
|
-
mirrorOnly: true,
|
|
358
|
-
hybridWalletMode: 'agent_wallet',
|
|
359
|
-
});
|
|
360
|
-
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
361
|
-
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
362
|
-
return {
|
|
363
|
-
message:
|
|
364
|
-
managedNormalized.success && mirrorNormalized.success
|
|
365
|
-
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
366
|
-
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
367
|
-
route: 'execute_intent_validation_dual',
|
|
368
|
-
normalized: normalizeIntentResponse({
|
|
369
|
-
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
370
|
-
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
371
|
-
mode: 'hybrid',
|
|
372
|
-
hybridWalletMode: 'agent_wallet',
|
|
373
|
-
managed: managedNormalized,
|
|
374
|
-
mirror: mirrorNormalized,
|
|
375
|
-
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
376
|
-
}),
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const payload = buildIntentPayload(flags, {
|
|
381
|
-
validationOnly: true,
|
|
382
|
-
forceIdempotency: false,
|
|
383
|
-
resolvedAction,
|
|
384
|
-
apiAction: actionNameForApi(resolved),
|
|
385
|
-
runMode,
|
|
386
|
-
mirrorOnly: runMode === 'hybrid',
|
|
387
|
-
});
|
|
388
|
-
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
389
|
-
const normalized = normalizeIntentResponse(response);
|
|
390
|
-
return {
|
|
391
|
-
message: normalized.success
|
|
392
|
-
? runMode === 'hybrid'
|
|
393
|
-
? 'Hybrid mirror preflight completed'
|
|
394
|
-
: 'Preflight completed'
|
|
395
|
-
: runMode === 'hybrid'
|
|
396
|
-
? 'Hybrid mirror preflight failed'
|
|
397
|
-
: 'Preflight failed',
|
|
398
|
-
route: 'execute_intent_validation',
|
|
399
|
-
normalized,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
404
|
-
if (runMode === 'thirdfy') {
|
|
405
|
-
const normalized = await executeThirdfyRun(ctx, flags, resolved, options, runMode);
|
|
406
|
-
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
407
|
-
return {
|
|
408
|
-
code: normalized.success ? 'RUN_QUEUED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
409
|
-
message: normalized.success
|
|
410
|
-
? 'Execution intent queued'
|
|
411
|
-
: preflightBlocked
|
|
412
|
-
? 'Preflight blocked execution'
|
|
413
|
-
: 'Execution failed',
|
|
414
|
-
idempotencyKey: normalized.idempotencyKey || null,
|
|
415
|
-
normalized,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
if (runMode === 'agent_wallet') {
|
|
419
|
-
const normalized = await executeManagedWalletRun(ctx, flags, resolved, options);
|
|
420
|
-
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
421
|
-
return {
|
|
422
|
-
code: normalized.success ? 'WALLET_EXECUTED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
423
|
-
message: normalized.success
|
|
424
|
-
? 'Managed wallet execution completed'
|
|
425
|
-
: preflightBlocked
|
|
426
|
-
? 'Managed wallet preflight blocked execution'
|
|
427
|
-
: 'Managed wallet execution failed',
|
|
428
|
-
idempotencyKey: normalized.idempotencyKey || null,
|
|
429
|
-
normalized,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
if (runMode === 'self') {
|
|
433
|
-
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
434
|
-
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
435
|
-
const readOnlySuccess =
|
|
436
|
-
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
437
|
-
return {
|
|
438
|
-
code: normalized.success
|
|
439
|
-
? readOnlySuccess
|
|
440
|
-
? 'SELF_READ_COMPLETED'
|
|
441
|
-
: 'SELF_UNSIGNED_TX_READY'
|
|
442
|
-
: preflightBlocked
|
|
443
|
-
? 'PREFLIGHT_BLOCKED'
|
|
444
|
-
: 'SELF_EXECUTION_FAILED',
|
|
445
|
-
message: normalized.success
|
|
446
|
-
? readOnlySuccess
|
|
447
|
-
? 'Read-only action completed via delegated execute-intent'
|
|
448
|
-
: 'Unsigned transaction prepared for self-custody execution'
|
|
449
|
-
: preflightBlocked
|
|
450
|
-
? 'Preflight blocked self execution'
|
|
451
|
-
: 'Self-custody execution failed',
|
|
452
|
-
idempotencyKey: null,
|
|
453
|
-
normalized,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
const normalized = await executeHybridRun(ctx, flags, resolved, options);
|
|
457
|
-
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
458
|
-
const hybridWalletMode = normalizeHybridWalletMode(
|
|
459
|
-
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
460
|
-
);
|
|
461
|
-
const readOnlySuccess =
|
|
462
|
-
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
463
|
-
return {
|
|
464
|
-
code: normalized.success
|
|
465
|
-
? readOnlySuccess
|
|
466
|
-
? 'HYBRID_READ_COMPLETED'
|
|
467
|
-
: 'HYBRID_READY'
|
|
468
|
-
: preflightBlocked
|
|
469
|
-
? 'PREFLIGHT_BLOCKED'
|
|
470
|
-
: 'HYBRID_FAILED',
|
|
471
|
-
message: normalized.success
|
|
472
|
-
? readOnlySuccess
|
|
473
|
-
? 'Read-only action completed via delegated execute-intent'
|
|
474
|
-
: hybridWalletMode === 'agent_wallet'
|
|
475
|
-
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
476
|
-
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
477
|
-
: preflightBlocked
|
|
478
|
-
? 'Preflight blocked hybrid execution'
|
|
479
|
-
: 'Hybrid execution failed',
|
|
480
|
-
idempotencyKey: normalized.idempotencyKey || null,
|
|
481
|
-
normalized,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'thirdfy') {
|
|
486
|
-
const skipPreflight = Boolean(options?.skipPreflight);
|
|
487
|
-
const actionCtx = withActionTimeout(ctx, resolved);
|
|
488
|
-
if (!skipPreflight) {
|
|
489
|
-
const preflightPayload = buildIntentPayload(flags, {
|
|
490
|
-
validationOnly: true,
|
|
491
|
-
forceIdempotency: false,
|
|
492
|
-
resolvedAction: resolved.resolvedAction,
|
|
493
|
-
apiAction: actionNameForApi(resolved),
|
|
494
|
-
runMode,
|
|
495
|
-
mirrorOnly: false,
|
|
496
|
-
hybridWalletMode: options?.hybridWalletMode,
|
|
497
|
-
});
|
|
498
|
-
// validationOnly never writes, so a timed-out preflight is safe to resend.
|
|
499
|
-
const preflight = await apiPost(
|
|
500
|
-
{ ...actionCtx, writeAction: false },
|
|
501
|
-
'/api/v1/agent/execute-intent',
|
|
502
|
-
preflightPayload,
|
|
503
|
-
);
|
|
504
|
-
const preflightNormalized = normalizeIntentResponse(preflight);
|
|
505
|
-
const blockedCount = preflightNormalized.blocked || 0;
|
|
506
|
-
if (!preflightNormalized.success || blockedCount > 0) {
|
|
507
|
-
preflightNormalized.success = false;
|
|
508
|
-
preflightNormalized.error = preflightNormalized.error || 'Preflight blocked execution';
|
|
509
|
-
preflightNormalized.preflightBlocked = true;
|
|
510
|
-
return preflightNormalized;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const payload = buildIntentPayload(flags, {
|
|
514
|
-
validationOnly: false,
|
|
515
|
-
forceIdempotency: true,
|
|
516
|
-
resolvedAction: resolved.resolvedAction,
|
|
517
|
-
apiAction: actionNameForApi(resolved),
|
|
518
|
-
runMode,
|
|
519
|
-
mirrorOnly: false,
|
|
520
|
-
hybridWalletMode: options?.hybridWalletMode,
|
|
521
|
-
});
|
|
522
|
-
const response = await apiPost(actionCtx, '/api/v1/agent/execute-intent', payload);
|
|
523
|
-
const normalized = normalizeIntentResponse(response);
|
|
524
|
-
normalized.idempotencyKey = payload.idempotencyKey;
|
|
525
|
-
if (!isReadOnlyAction(resolved)) {
|
|
526
|
-
Object.assign(normalized, idempotentReplayFields(response, responseHeadersFor(response), 'idempotencyKey'));
|
|
527
|
-
}
|
|
528
|
-
return normalized;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight, reportedMode = 'agent_wallet') {
|
|
532
|
-
const userDid = resolveEffectiveUserDid(flags, {
|
|
533
|
-
runMode: 'agent_wallet',
|
|
534
|
-
hybridWalletMode: options?.hybridWalletMode,
|
|
535
|
-
preferDelegationIdentity: options?.preferDelegationIdentity,
|
|
536
|
-
});
|
|
537
|
-
const intentResult = await executeThirdfyRun(
|
|
538
|
-
ctx,
|
|
539
|
-
{
|
|
540
|
-
...flags,
|
|
541
|
-
chainId: Number(flags.chainId || 8453),
|
|
542
|
-
runMode: 'thirdfy',
|
|
543
|
-
userDid: userDid || flags.userDid,
|
|
544
|
-
executionScope: flags.executionScope || 'solo_owner_mirror',
|
|
545
|
-
},
|
|
546
|
-
resolved,
|
|
547
|
-
{ ...options, skipPreflight: true },
|
|
548
|
-
'thirdfy'
|
|
549
|
-
);
|
|
550
|
-
intentResult.mode = reportedMode;
|
|
551
|
-
intentResult.routeFallback = 'execute_intent_read_only';
|
|
552
|
-
if (preflight) {
|
|
553
|
-
intentResult.executionWalletPreflight = preflight;
|
|
554
|
-
intentResult.executionWalletAddress = preflight.executionWalletAddress || intentResult.executionWalletAddress || null;
|
|
555
|
-
}
|
|
556
|
-
intentResult.signerMethod = 'managed_wallet_server';
|
|
557
|
-
return intentResult;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
561
|
-
// Market-data reads under agent_wallet must use /execute and keep the provider payload on the
|
|
562
|
-
// envelope (raw + parsed data.result). Routing them only through /execute-intent was the 0.2.34
|
|
563
|
-
// attempt to avoid a burial bug, but solo Hermes agent_wallet intents come back as an empty
|
|
564
|
-
// queued envelope with no universe. Live /execute still returns the Hyperliquid meta payload
|
|
565
|
-
// (often as a JSON string under data.result). Reads skip funding gates; routing still fails closed.
|
|
566
|
-
const isRead = isReadOnlyAction(resolved);
|
|
567
|
-
const skipPreflight = Boolean(options?.skipPreflight);
|
|
568
|
-
// A seeded preflight (Hyperliquid/Lighter/Veranta address defaults) already ran; reuse it.
|
|
569
|
-
const readFastPath =
|
|
570
|
-
!skipPreflight && !options?.managedPreflight && shouldUseManagedReadFastPath(flags, resolved, options);
|
|
571
|
-
let preflight = readFastPath ? READ_FAST_PATH_PREFLIGHT : null;
|
|
572
|
-
if (!skipPreflight && !readFastPath) {
|
|
573
|
-
preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
574
|
-
runMode: 'agent_wallet',
|
|
575
|
-
requireFundingCheck: !isRead,
|
|
576
|
-
preflightSeed: options?.managedPreflight,
|
|
577
|
-
});
|
|
578
|
-
if (!preflight.success) {
|
|
579
|
-
const normalized = normalizeIntentResponse({
|
|
580
|
-
success: false,
|
|
581
|
-
status: 'failed',
|
|
582
|
-
mode: 'agent_wallet',
|
|
583
|
-
blockedReason: preflight.blockedReason || 'PRECHECK_FAILED',
|
|
584
|
-
blockedStage: preflight.blockedStage || 'routing',
|
|
585
|
-
error: preflight.error || 'Managed wallet preflight failed',
|
|
586
|
-
preflightBlocked: true,
|
|
587
|
-
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
588
|
-
executionWalletPreflight: preflight,
|
|
589
|
-
});
|
|
590
|
-
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
591
|
-
normalized.executionWalletPreflight = preflight;
|
|
592
|
-
return normalized;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
const payload = buildManagedExecutePayload(flags, {
|
|
596
|
-
resolvedAction: resolved.resolvedAction,
|
|
597
|
-
apiAction: actionNameForApi(resolved),
|
|
598
|
-
runMode: 'agent_wallet',
|
|
599
|
-
forceIdempotency: true,
|
|
600
|
-
});
|
|
601
|
-
const actionCtx = withActionTimeout(ctx, resolved);
|
|
602
|
-
const response = await apiPost(actionCtx, '/api/v1/agent/execute', payload);
|
|
603
|
-
if (readFastPath) {
|
|
604
|
-
const mismatch = fastPathWalletMismatch(flags, response);
|
|
605
|
-
if (mismatch) return mismatch;
|
|
606
|
-
}
|
|
607
|
-
if (shouldFallbackAgentWalletToExecuteIntent(resolved, response)) {
|
|
608
|
-
// On the fast path the wallet comes from the /execute answer, not a preflight.
|
|
609
|
-
const fallbackPreflight = readFastPath
|
|
610
|
-
? {
|
|
611
|
-
...READ_FAST_PATH_PREFLIGHT,
|
|
612
|
-
executionWalletAddress: response?.executionWalletAddress || response?.data?.executionWalletAddress || null,
|
|
613
|
-
}
|
|
614
|
-
: preflight;
|
|
615
|
-
const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, fallbackPreflight);
|
|
616
|
-
if (isEmptyQueuedExecuteIntent(intentResult.raw || intentResult)) {
|
|
617
|
-
// A read that did not answer in time (often a cold provider cache): nothing changed, and the
|
|
618
|
-
// same read usually answers on the next try. Same fields as thirdfy-mcp walletExecute.
|
|
619
|
-
intentResult.success = false;
|
|
620
|
-
intentResult.status = 'failed';
|
|
621
|
-
intentResult.error = intentResult.error || 'MANAGED_READ_EMPTY_EXECUTE_INTENT';
|
|
622
|
-
Object.assign(intentResult, {
|
|
623
|
-
errorCode: 'MANAGED_READ_EMPTY_EXECUTE_INTENT',
|
|
624
|
-
retryable: true,
|
|
625
|
-
retryAfterSeconds: 3,
|
|
626
|
-
nextAction: 'retry_later',
|
|
627
|
-
hint:
|
|
628
|
-
intentResult.hint ||
|
|
629
|
-
'The read did not answer in time; nothing was changed. Repeat the same read in a few seconds. Do not treat the empty queued result as data.',
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
intentResult.executionWalletAddress =
|
|
633
|
-
preflight?.executionWalletAddress ||
|
|
634
|
-
response?.executionWalletAddress ||
|
|
635
|
-
response?.data?.executionWalletAddress ||
|
|
636
|
-
null;
|
|
637
|
-
return intentResult;
|
|
638
|
-
}
|
|
639
|
-
return normalizeManagedExecuteResponse(response, { preflight, payload, isRead });
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* The fast path only runs when the expected wallet is empty or equals the agent key. The preflight
|
|
644
|
-
* path refuses a wallet that differs from the execution wallet, so apply the same rule to the wallet
|
|
645
|
-
* the API reports for the read.
|
|
646
|
-
*/
|
|
647
|
-
function fastPathWalletMismatch(flags, response) {
|
|
648
|
-
// 0x addresses compare case-insensitively; Solana base58 is case-sensitive and kept as sent.
|
|
649
|
-
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
650
|
-
if (!expectedWallet || parseBooleanFlag(flags.allowWalletMismatch, false)) return null;
|
|
651
|
-
const apiWallet = String(response?.executionWalletAddress || response?.data?.executionWalletAddress || '').trim();
|
|
652
|
-
if (!apiWallet || sameWalletAddress(apiWallet, expectedWallet)) return null;
|
|
653
|
-
const normalized = normalizeIntentResponse({
|
|
654
|
-
success: false,
|
|
655
|
-
status: 'failed',
|
|
656
|
-
mode: 'agent_wallet',
|
|
657
|
-
blockedReason: 'EXECUTION_WALLET_MISMATCH',
|
|
658
|
-
blockedStage: 'routing',
|
|
659
|
-
error: `Funded wallet (${expectedWallet}) does not match execution wallet (${apiWallet}). Use --allow-wallet-mismatch to override.`,
|
|
660
|
-
preflightBlocked: true,
|
|
661
|
-
executionWalletAddress: apiWallet,
|
|
662
|
-
});
|
|
663
|
-
normalized.executionWalletAddress = apiWallet;
|
|
664
|
-
normalized.executionWalletPreflight = READ_FAST_PATH_PREFLIGHT;
|
|
665
|
-
return normalized;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
669
|
-
const skipPreflight = Boolean(options?.skipPreflight);
|
|
670
|
-
const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
|
|
671
|
-
// A read has no transaction to build or sign, so the self lane has nothing to do with it. Without
|
|
672
|
-
// this it either fails with SELF_MODE_REQUIRES_BUILD_TX or, when catalog metadata is missing, asks
|
|
673
|
-
// /build-tx to produce a transaction for a market-data query.
|
|
674
|
-
if (isReadOnlyAction(resolved)) {
|
|
675
|
-
return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, options?.runMode || 'self');
|
|
676
|
-
}
|
|
677
|
-
if (!skipPreflight) {
|
|
678
|
-
const preflightPayload = buildIntentPayload(flags, {
|
|
679
|
-
validationOnly: true,
|
|
680
|
-
forceIdempotency: false,
|
|
681
|
-
resolvedAction: resolved.resolvedAction,
|
|
682
|
-
apiAction: actionNameForApi(resolved),
|
|
683
|
-
runMode: 'self',
|
|
684
|
-
mirrorOnly: false,
|
|
685
|
-
});
|
|
686
|
-
const preflight = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', preflightPayload);
|
|
687
|
-
const preflightNormalized = normalizeIntentResponse(preflight);
|
|
688
|
-
const blockedCount = preflightNormalized.blocked || 0;
|
|
689
|
-
if (!preflightNormalized.success || blockedCount > 0) {
|
|
690
|
-
preflightNormalized.success = false;
|
|
691
|
-
preflightNormalized.error = preflightNormalized.error || 'Preflight blocked self execution';
|
|
692
|
-
preflightNormalized.preflightBlocked = true;
|
|
693
|
-
return preflightNormalized;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
if (!shouldUseBuildTxExecution(options?.runMode || 'self', resolved)) {
|
|
697
|
-
const normalized = normalizeIntentResponse({
|
|
698
|
-
success: false,
|
|
699
|
-
status: 'failed',
|
|
700
|
-
blockedReason: blockedReasonCode,
|
|
701
|
-
blockedStage: 'routing',
|
|
702
|
-
error: 'Selected action does not support build-tx self lane. Use --run-mode thirdfy for delegated CEX execution.',
|
|
703
|
-
});
|
|
704
|
-
normalized.preflightBlocked = true;
|
|
705
|
-
return normalized;
|
|
706
|
-
}
|
|
707
|
-
const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction, actionNameForApi(resolved));
|
|
708
|
-
// build-tx returns an unsigned transaction; nothing is broadcast, so a timeout may be resent.
|
|
709
|
-
const buildTx = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/build-tx', buildTxPayload);
|
|
710
|
-
return normalizeIntentResponse({
|
|
711
|
-
...buildTx,
|
|
712
|
-
status: buildTx?.status || 'ready',
|
|
713
|
-
mode: buildTx?.mode || 'self',
|
|
714
|
-
unsignedTx: buildTx?.unsignedTx || null,
|
|
715
|
-
estimatedGas: buildTx?.estimatedGas || null,
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
720
|
-
const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
|
|
721
|
-
// Reads have nothing to mirror and need no wallet, whichever hybrid wallet mode is selected.
|
|
722
|
-
// Answering from the read rail also keeps the payload at the top level, instead of nesting it under
|
|
723
|
-
// `managed` where callers that read `results` cannot find it.
|
|
724
|
-
if (isReadOnlyAction(resolved)) {
|
|
725
|
-
const readResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, 'hybrid');
|
|
726
|
-
readResult.hybridWalletMode = hybridWalletMode;
|
|
727
|
-
return readResult;
|
|
728
|
-
}
|
|
729
|
-
if (hybridWalletMode === 'agent_wallet') {
|
|
730
|
-
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
731
|
-
if (!managedResult.success) {
|
|
732
|
-
return normalizeIntentResponse({
|
|
733
|
-
...managedResult,
|
|
734
|
-
mode: 'hybrid',
|
|
735
|
-
hybridWalletMode,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
const mirrorPayload = buildIntentPayload(flags, {
|
|
739
|
-
validationOnly: true,
|
|
740
|
-
forceIdempotency: true,
|
|
741
|
-
resolvedAction: resolved.resolvedAction,
|
|
742
|
-
apiAction: actionNameForApi(resolved),
|
|
743
|
-
runMode: 'hybrid',
|
|
744
|
-
mirrorOnly: true,
|
|
745
|
-
hybridWalletMode,
|
|
746
|
-
});
|
|
747
|
-
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
748
|
-
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
749
|
-
return normalizeIntentResponse({
|
|
750
|
-
success: Boolean(managedResult.success && mirrorNormalized.success),
|
|
751
|
-
status: managedResult.status || mirrorNormalized.status || 'queued',
|
|
752
|
-
mode: 'hybrid',
|
|
753
|
-
hybridWalletMode,
|
|
754
|
-
idempotencyKey: managedResult.idempotencyKey || mirrorPayload.idempotencyKey,
|
|
755
|
-
managed: managedResult,
|
|
756
|
-
mirror: mirrorNormalized,
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
|
760
|
-
...(options || {}),
|
|
761
|
-
runMode: 'hybrid',
|
|
762
|
-
blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
|
|
763
|
-
});
|
|
764
|
-
if (!selfResult.success) return selfResult;
|
|
765
|
-
|
|
766
|
-
const mirrorPayload = buildIntentPayload(flags, {
|
|
767
|
-
validationOnly: true,
|
|
768
|
-
forceIdempotency: true,
|
|
769
|
-
resolvedAction: resolved.resolvedAction,
|
|
770
|
-
apiAction: actionNameForApi(resolved),
|
|
771
|
-
runMode: 'hybrid',
|
|
772
|
-
mirrorOnly: true,
|
|
773
|
-
});
|
|
774
|
-
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
775
|
-
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
776
|
-
return normalizeIntentResponse({
|
|
777
|
-
success: Boolean(selfResult.success && mirrorNormalized.success),
|
|
778
|
-
status: mirrorNormalized.status || 'ready',
|
|
779
|
-
mode: 'hybrid',
|
|
780
|
-
hybridWalletMode,
|
|
781
|
-
idempotencyKey: mirrorPayload.idempotencyKey,
|
|
782
|
-
self: selfResult,
|
|
783
|
-
mirror: mirrorNormalized,
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
async function resolveManagedExecutionPreflight(ctx, flags, resolved, options = {}) {
|
|
788
|
-
const action = String(resolved?.resolvedAction || flags.action || '').trim();
|
|
789
|
-
const params = getPreparedParams(flags);
|
|
790
|
-
const chainId = resolveWalletExecuteChainId({
|
|
791
|
-
action,
|
|
792
|
-
chainId: flags.chainId,
|
|
793
|
-
params,
|
|
794
|
-
});
|
|
795
|
-
const runMode = String(options.runMode || 'agent_wallet').trim().toLowerCase();
|
|
796
|
-
const agentApiKey = resolveAgentApiKey(flags, runMode);
|
|
797
|
-
const userDid = resolveEffectiveUserDid(flags, {
|
|
798
|
-
runMode,
|
|
799
|
-
hybridWalletMode: options.hybridWalletMode,
|
|
800
|
-
preferDelegationIdentity: options.preferDelegationIdentity,
|
|
801
|
-
});
|
|
802
|
-
if (!userDid) {
|
|
803
|
-
return {
|
|
804
|
-
success: false,
|
|
805
|
-
blockedReason: 'MISSING_USER_DID',
|
|
806
|
-
blockedStage: 'routing',
|
|
807
|
-
error:
|
|
808
|
-
runMode === 'agent_wallet'
|
|
809
|
-
? 'Solo agent_wallet requires --agent-key, THIRDFY_AGENT_KEY, config.agent.agentKey, or --user-did did:wallet:<agentKey>. Subscriber Privy DIDs require delegation; use did:wallet for direct agent execution.'
|
|
810
|
-
: 'Managed wallet execution requires --user-did, THIRDFY_USER_DID, or a saved userDid from `thirdfy-agent login email`.',
|
|
811
|
-
signerMethod: 'managed_wallet_server',
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
const tokenIn = resolveTokenInForFunding(params, action, resolved?.resolvedActionMeta?.behavior?.spend);
|
|
815
|
-
const query = new URLSearchParams({
|
|
816
|
-
userDid,
|
|
817
|
-
chainId: String(chainId),
|
|
818
|
-
runMode,
|
|
819
|
-
});
|
|
820
|
-
if (tokenIn) query.set('tokenIn', tokenIn);
|
|
821
|
-
const evaluateAction = options.evaluateAction === true;
|
|
822
|
-
if (evaluateAction) appendActionPreflightQuery(query, { action, apiAction: actionNameForApi(resolved), params, flags });
|
|
823
|
-
const executionVenue = describeExecutionVenue(action, chainId);
|
|
824
|
-
const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
|
|
825
|
-
? options.preflightSeed
|
|
826
|
-
: null;
|
|
827
|
-
if (preflightSeed && preflightSeed.success === false) {
|
|
828
|
-
return preflightSeed;
|
|
829
|
-
}
|
|
830
|
-
// A seed (address default lookup) carries no policy answer; an action preflight asks the API again.
|
|
831
|
-
const useSeed = Boolean(preflightSeed) && !evaluateAction;
|
|
832
|
-
let response;
|
|
833
|
-
if (useSeed) {
|
|
834
|
-
response = {
|
|
835
|
-
executionWalletAddress: preflightSeed.executionWalletAddress || null,
|
|
836
|
-
signerMethod: preflightSeed.signerMethod || null,
|
|
837
|
-
fundingTokenBalance: preflightSeed.fundingTokenBalance || null,
|
|
838
|
-
...pickNativeGasFields(preflightSeed),
|
|
839
|
-
};
|
|
840
|
-
} else {
|
|
841
|
-
try {
|
|
842
|
-
response = await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
|
|
843
|
-
'x-agent-api-key': agentApiKey,
|
|
844
|
-
});
|
|
845
|
-
} catch (error) {
|
|
846
|
-
const blocked = evaluateAction ? blockedActionPreflightFromError(error) : null;
|
|
847
|
-
if (!blocked) throw error;
|
|
848
|
-
return {
|
|
849
|
-
...blocked,
|
|
850
|
-
userDid,
|
|
851
|
-
chainId,
|
|
852
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
// An action preflight the API answered 2xx but refused is a block too (thirdfy-mcp previewOrder).
|
|
857
|
-
if (evaluateAction && response?.success === false) {
|
|
858
|
-
return {
|
|
859
|
-
...blockedActionPreflight(response, {
|
|
860
|
-
errorFields: blockedResultFields(String(response.blockedReason || 'PREFLIGHT_BLOCKED')),
|
|
861
|
-
}),
|
|
862
|
-
userDid,
|
|
863
|
-
chainId,
|
|
864
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
const actionFields = evaluateAction ? pickActionPreflightFields(response) : {};
|
|
868
|
-
// API 4.0 wallet control, as thirdfy-mcp reports it: the API refuses writes itself (before
|
|
869
|
-
// signing), so preflight only reports it. Solana is exempt from the API's gate.
|
|
870
|
-
const walletControlFields = readWalletControl(response, { svm: isSvmChainId(chainId) });
|
|
871
|
-
// Keep the API's case: Solana base58 addresses are case-sensitive (lowercasing corrupts them).
|
|
872
|
-
const executionWalletAddress = String(response?.executionWalletAddress || '').trim();
|
|
873
|
-
const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
|
|
874
|
-
const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
|
|
875
|
-
let parsedRawBalance = null;
|
|
876
|
-
if (rawBalance) {
|
|
877
|
-
try {
|
|
878
|
-
parsedRawBalance = BigInt(rawBalance);
|
|
879
|
-
} catch {
|
|
880
|
-
// Reads pass requireFundingCheck: false; a malformed balance must not block them.
|
|
881
|
-
// Writes still fail closed on unparseable fundingTokenBalance.raw.
|
|
882
|
-
if (options.requireFundingCheck) {
|
|
883
|
-
return {
|
|
884
|
-
success: false,
|
|
885
|
-
blockedReason: 'INVALID_FUNDING_BALANCE',
|
|
886
|
-
blockedStage: 'sizing',
|
|
887
|
-
error: `Execution wallet ${executionWalletAddress || 'unknown'} returned invalid funding token balance.`,
|
|
888
|
-
executionWalletAddress,
|
|
889
|
-
signerMethod,
|
|
890
|
-
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
891
|
-
...actionFields,
|
|
892
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
893
|
-
...walletControlFields,
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
|
|
899
|
-
const allowWalletMismatch = parseBooleanFlag(flags.allowWalletMismatch, false);
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
expectedWallet
|
|
903
|
-
&& executionWalletAddress
|
|
904
|
-
&& !sameWalletAddress(expectedWallet, executionWalletAddress)
|
|
905
|
-
&& !allowWalletMismatch
|
|
906
|
-
) {
|
|
907
|
-
return {
|
|
908
|
-
success: false,
|
|
909
|
-
blockedReason: 'EXECUTION_WALLET_MISMATCH',
|
|
910
|
-
...blockedResultFields('EXECUTION_WALLET_MISMATCH'),
|
|
911
|
-
blockedStage: 'routing',
|
|
912
|
-
error: `Funded wallet (${expectedWallet}) does not match execution wallet (${executionWalletAddress}). Use --allow-wallet-mismatch to override.`,
|
|
913
|
-
executionWalletAddress,
|
|
914
|
-
signerMethod,
|
|
915
|
-
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
916
|
-
...actionFields,
|
|
917
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
918
|
-
...walletControlFields,
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (options.requireFundingCheck && parsedRawBalance !== null && parsedRawBalance <= 0n) {
|
|
923
|
-
return {
|
|
924
|
-
success: false,
|
|
925
|
-
blockedReason: 'INSUFFICIENT_FUNDS',
|
|
926
|
-
...blockedResultFields('INSUFFICIENT_FUNDS'),
|
|
927
|
-
blockedStage: 'sizing',
|
|
928
|
-
error: `Execution wallet ${executionWalletAddress || 'unknown'} has insufficient tokenIn balance.`,
|
|
929
|
-
executionWalletAddress,
|
|
930
|
-
signerMethod,
|
|
931
|
-
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
932
|
-
...actionFields,
|
|
933
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
934
|
-
...walletControlFields,
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// User-pays-gas chains (DogeOS, Solana): the API reports native gas readiness (API 3.18.2+). Keep
|
|
939
|
-
// it, and stop a write before /execute when the API says the wallet cannot pay gas (gasReady false).
|
|
940
|
-
const nativeGas = pickNativeGasFields(response);
|
|
941
|
-
if (options.requireFundingCheck && nativeGas.gasReady === false) {
|
|
942
|
-
return {
|
|
943
|
-
success: false,
|
|
944
|
-
blockedReason: 'INSUFFICIENT_NATIVE_GAS',
|
|
945
|
-
...blockedResultFields('INSUFFICIENT_NATIVE_GAS'),
|
|
946
|
-
blockedStage: 'sizing',
|
|
947
|
-
error: nativeGas.nativeGasWarning?.message || describeInsufficientNativeGas(executionWalletAddress, chainId),
|
|
948
|
-
executionWalletAddress,
|
|
949
|
-
signerMethod,
|
|
950
|
-
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
951
|
-
...actionFields,
|
|
952
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
953
|
-
...walletControlFields,
|
|
954
|
-
...nativeGas,
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
return {
|
|
959
|
-
success: true,
|
|
960
|
-
executionWalletAddress,
|
|
961
|
-
signerMethod,
|
|
962
|
-
fundingTokenBalance: response?.fundingTokenBalance || null,
|
|
963
|
-
userDid,
|
|
964
|
-
chainId,
|
|
965
|
-
tokenIn: tokenIn || null,
|
|
966
|
-
...nativeGas,
|
|
967
|
-
...actionFields,
|
|
968
|
-
...(executionVenue ? { executionVenue } : {}),
|
|
969
|
-
...walletControlFields,
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
|
|
974
|
-
const txd = resolveUnsignedTx(selfResult);
|
|
975
|
-
if (!txd?.to || !txd?.data) {
|
|
976
|
-
throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
|
|
977
|
-
}
|
|
978
|
-
const chainId = Number(txd.chainId || flags.chainId || 8453);
|
|
979
|
-
const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
|
|
980
|
-
const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
|
|
981
|
-
const rpcUrl = resolveChainRpcUrl({ chainId, rpcUrl: flags.rpcUrl });
|
|
982
|
-
if (!rpcUrl) {
|
|
983
|
-
throw createCliError('RPC_URL_REQUIRED', `self-exec has no known RPC for chain ${chainId}; pass --rpc-url <url> for that chain`);
|
|
984
|
-
}
|
|
985
|
-
if (!walletAddress) {
|
|
986
|
-
throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
|
|
987
|
-
}
|
|
988
|
-
if (!owsWalletName) {
|
|
989
|
-
throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const params = getPreparedParams(flags);
|
|
993
|
-
const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
|
|
994
|
-
const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
|
|
995
|
-
const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
|
|
996
|
-
const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
|
|
997
|
-
const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
|
|
998
|
-
|
|
999
|
-
let balancesBefore = null;
|
|
1000
|
-
if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
|
|
1001
|
-
balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
let submission;
|
|
1005
|
-
try {
|
|
1006
|
-
submission = await signAndBroadcastWithOws({
|
|
1007
|
-
txd,
|
|
1008
|
-
chainId,
|
|
1009
|
-
walletAddress,
|
|
1010
|
-
owsWalletName,
|
|
1011
|
-
rpcUrl,
|
|
1012
|
-
});
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
let receipt;
|
|
1018
|
-
try {
|
|
1019
|
-
receipt = await submission.sent.wait();
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
|
|
1022
|
-
}
|
|
1023
|
-
const status = Number(receipt?.status ?? 0);
|
|
1024
|
-
if (status !== 1) {
|
|
1025
|
-
throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
|
|
1026
|
-
receiptStatus: receipt?.status ?? null,
|
|
1027
|
-
txHash: submission.sent.hash,
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const effectCheck = await resolveEffectCheckResult({
|
|
1032
|
-
mode: effectMode,
|
|
1033
|
-
rpcUrl,
|
|
1034
|
-
walletAddress,
|
|
1035
|
-
tokenIn: effectTokenIn,
|
|
1036
|
-
tokenOut: effectTokenOut,
|
|
1037
|
-
balancesBefore,
|
|
1038
|
-
retries: effectRetries,
|
|
1039
|
-
waitMs: effectWaitMs,
|
|
1040
|
-
});
|
|
1041
|
-
if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
|
|
1042
|
-
throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
|
|
1043
|
-
txHash: submission.sent.hash,
|
|
1044
|
-
effectCheck,
|
|
1045
|
-
mode: effectMode,
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
return {
|
|
1050
|
-
txHash: submission.sent.hash,
|
|
1051
|
-
chainId,
|
|
1052
|
-
blockNumber: receipt?.blockNumber ?? null,
|
|
1053
|
-
status: receipt?.status ?? null,
|
|
1054
|
-
unsignedTx: txd,
|
|
1055
|
-
effectCheck,
|
|
1056
|
-
lane: 'self',
|
|
1057
|
-
signer: 'ows',
|
|
1058
|
-
resolvedAction,
|
|
1059
|
-
apiBase: ctx.apiBase,
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
async function resolveEffectCheckResult({
|
|
1064
|
-
mode,
|
|
1065
|
-
rpcUrl,
|
|
1066
|
-
walletAddress,
|
|
1067
|
-
tokenIn,
|
|
1068
|
-
tokenOut,
|
|
1069
|
-
balancesBefore,
|
|
1070
|
-
retries,
|
|
1071
|
-
waitMs,
|
|
1072
|
-
}) {
|
|
1073
|
-
const baseResult = {
|
|
1074
|
-
checked: true,
|
|
1075
|
-
mode,
|
|
1076
|
-
hasEffect: false,
|
|
1077
|
-
inDeltaRaw: '0',
|
|
1078
|
-
outDeltaRaw: '0',
|
|
1079
|
-
attempts: 0,
|
|
1080
|
-
retries: Math.max(0, Number(retries || 0)),
|
|
1081
|
-
};
|
|
1082
|
-
if (mode === 'off') {
|
|
1083
|
-
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
|
|
1084
|
-
}
|
|
1085
|
-
if (!tokenIn || !tokenOut || !balancesBefore) {
|
|
1086
|
-
return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
|
|
1087
|
-
}
|
|
1088
|
-
const maxAttempts = Math.max(1, Number(retries || 0) + 1);
|
|
1089
|
-
let last = null;
|
|
1090
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1091
|
-
const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
|
|
1092
|
-
const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
|
|
1093
|
-
const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
|
|
1094
|
-
const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
|
|
1095
|
-
last = {
|
|
1096
|
-
checked: true,
|
|
1097
|
-
mode,
|
|
1098
|
-
hasEffect,
|
|
1099
|
-
inDeltaRaw,
|
|
1100
|
-
outDeltaRaw,
|
|
1101
|
-
attempts: attempt,
|
|
1102
|
-
retries: maxAttempts - 1,
|
|
1103
|
-
};
|
|
1104
|
-
if (hasEffect) return last;
|
|
1105
|
-
if (attempt < maxAttempts) await sleepMs(waitMs);
|
|
1106
|
-
}
|
|
1107
|
-
return {
|
|
1108
|
-
...(last || baseResult),
|
|
1109
|
-
warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
async function submitSignedTx({ rpcUrl, chainId, signedTxHex }) {
|
|
1114
|
-
const ethers = await loadEthers();
|
|
1115
|
-
const { JsonRpcProvider } = ethers;
|
|
1116
|
-
const provider = new JsonRpcProvider(rpcUrl, chainId);
|
|
1117
|
-
const sent = await provider.broadcastTransaction(String(signedTxHex));
|
|
1118
|
-
return sent?.hash || null;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
return {
|
|
1122
|
-
applyExecutionFallbackHints,
|
|
1123
|
-
runPreflightByMode,
|
|
1124
|
-
runExecutionByMode,
|
|
1125
|
-
executeThirdfyRun,
|
|
1126
|
-
executeManagedWalletRun,
|
|
1127
|
-
executeSelfRun,
|
|
1128
|
-
executeHybridRun,
|
|
1129
|
-
resolveManagedExecutionPreflight,
|
|
1130
|
-
performSelfBroadcast,
|
|
1131
|
-
resolveEffectCheckResult,
|
|
1132
|
-
submitSignedTx,
|
|
1133
|
-
};
|
|
1134
|
-
}
|