@thirdfy/agent-cli 0.4.0 → 0.4.2

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +5 -8
  3. package/package.json +3 -3
  4. package/scripts/validate-cli-contract-schemas.mjs +1 -0
  5. package/src/cli/help.mjs +1 -1
  6. package/src/cli/manifest.mjs +8 -1
  7. package/src/cli/options/index.mjs +11 -11
  8. package/src/cli/program.mjs +1 -1
  9. package/src/commands/{agent.mjs → account/agent.mjs} +5 -5
  10. package/src/commands/{credits.mjs → account/credits.mjs} +73 -17
  11. package/src/commands/{wallet.mjs → account/wallet.mjs} +136 -21
  12. package/src/commands/{walletControl.mjs → account/walletControl.mjs} +3 -3
  13. package/src/commands/discovery.mjs +9 -9
  14. package/src/commands/execute.mjs +2 -2
  15. package/src/commands/index.mjs +9 -9
  16. package/src/commands/{bootstrap.mjs → onboarding/bootstrap.mjs} +8 -8
  17. package/src/commands/{consoleMandate.mjs → onboarding/consoleMandate.mjs} +2 -2
  18. package/src/commands/{credentials.mjs → onboarding/credentials.mjs} +6 -6
  19. package/src/commands/{delegation.mjs → onboarding/delegation.mjs} +5 -5
  20. package/src/commands/{avantis.mjs → venues/avantis.mjs} +1 -1
  21. package/src/commands/{condor.mjs → venues/condor.mjs} +4 -4
  22. package/src/commands/{hummingbot.mjs → venues/hummingbot.mjs} +4 -4
  23. package/src/commands/{hyperliquid.mjs → venues/hyperliquid.mjs} +1 -1
  24. package/src/commands/{lighter.mjs → venues/lighter.mjs} +1 -1
  25. package/src/commands/{polymarket.mjs → venues/polymarket.mjs} +4 -4
  26. package/src/commands/{venue.mjs → venues/venue.mjs} +3 -3
  27. package/src/contracts/cli/schemas/wallet-profile.response.schema.json +127 -0
  28. package/src/core/context.mjs +20 -0
  29. package/src/core/earnclawAttribution.mjs +1 -1
  30. package/src/core/{actionTimeouts.mjs → execution/actionTimeouts.mjs} +2 -2
  31. package/src/core/{walletExecuteChainId.mjs → execution/walletExecuteChainId.mjs} +1 -1
  32. package/src/core/http.mjs +1 -1
  33. package/src/runtime/execution/hostedRuntimeWriteGate.mjs +1 -1
  34. package/src/runtime/execution/payloads.mjs +4 -4
  35. package/src/runtime/execution/runners/agentWalletLane.mjs +109 -0
  36. package/src/runtime/execution/runners/createExecutionRunners.mjs +315 -0
  37. package/src/runtime/execution/runners/fallbackHints.mjs +54 -0
  38. package/src/runtime/execution/runners/hybridLane.mjs +88 -0
  39. package/src/runtime/execution/runners/managedPreflight.mjs +253 -0
  40. package/src/runtime/execution/runners/managedResponse.mjs +119 -0
  41. package/src/runtime/execution/runners/selfLane.mjs +232 -0
  42. package/src/runtime/execution/runners/thirdfyLane.mjs +97 -0
  43. package/src/runtime/execution/svmLane.mjs +1 -1
  44. package/src/runtime/handlers.mjs +17 -16
  45. package/src/runtime/main.mjs +1 -1
  46. package/src/runtime/risk/notional.mjs +1 -1
  47. package/src/runtime/validation/actionParamsGate.mjs +1 -1
  48. package/src/runtime/execution/runners.mjs +0 -1134
  49. /package/src/cli/options/{agent.mjs → account/agent.mjs} +0 -0
  50. /package/src/cli/options/{credits.mjs → account/credits.mjs} +0 -0
  51. /package/src/cli/options/{wallet.mjs → account/wallet.mjs} +0 -0
  52. /package/src/cli/options/{walletControl.mjs → account/walletControl.mjs} +0 -0
  53. /package/src/cli/options/{bootstrap.mjs → onboarding/bootstrap.mjs} +0 -0
  54. /package/src/cli/options/{credentials.mjs → onboarding/credentials.mjs} +0 -0
  55. /package/src/cli/options/{delegation.mjs → onboarding/delegation.mjs} +0 -0
  56. /package/src/cli/options/{condor.mjs → venues/condor.mjs} +0 -0
  57. /package/src/cli/options/{hummingbot.mjs → venues/hummingbot.mjs} +0 -0
  58. /package/src/cli/options/{polymarket.mjs → venues/polymarket.mjs} +0 -0
  59. /package/src/cli/options/{venue.mjs → venues/venue.mjs} +0 -0
  60. /package/src/core/{executionContext.mjs → execution/executionContext.mjs} +0 -0
  61. /package/src/core/{fundingTokenIn.mjs → execution/fundingTokenIn.mjs} +0 -0
  62. /package/src/core/{hostedRuntimeIdempotency.mjs → execution/hostedRuntimeIdempotency.mjs} +0 -0
  63. /package/src/core/{readOnlyActions.mjs → execution/readOnlyActions.mjs} +0 -0
  64. /package/src/core/{signedTx.mjs → execution/signedTx.mjs} +0 -0
@@ -0,0 +1,253 @@
1
+ import { pickNativeGasFields } from '../nativeGas.mjs';
2
+ import {
3
+ appendActionPreflightQuery,
4
+ blockedActionPreflight,
5
+ blockedActionPreflightFromError,
6
+ describeExecutionVenue,
7
+ pickActionPreflightFields,
8
+ } from '../actionPreflight.mjs';
9
+ import { parseBooleanFlag } from '../../../core/runMode.mjs';
10
+ import { apiGet } from '../../../core/http.mjs';
11
+ import { resolveWalletExecuteChainId } from '../../../core/execution/walletExecuteChainId.mjs';
12
+ import {
13
+ describeInsufficientNativeGas,
14
+ isSvmChainId,
15
+ sameWalletAddress,
16
+ } from '../../../core/chainRegistry.mjs';
17
+ import { readWalletControl } from '../walletControl.mjs';
18
+ import { actionNameForApi } from '../../actions/selection.mjs';
19
+ import { blockedResultFields } from '../../../core/errorEnvelope.mjs';
20
+ import { isReadOnlyAction } from './managedResponse.mjs';
21
+
22
+ /** The agent_wallet preflight: the read fast path and the execution-wallet preflight. */
23
+ export function createManagedPreflight(deps) {
24
+ const {
25
+ getPreparedParams,
26
+ resolveEffectiveUserDid,
27
+ resolveTokenInForFunding,
28
+ resolveAgentApiKey,
29
+ resolveAgentKey,
30
+ } = deps;
31
+
32
+ const ROBINHOOD_MAINNET_CHAIN_ID = 4663;
33
+
34
+ /**
35
+ * agent_wallet reads skip the /execution-wallet preflight: the API resolves the agent's execution
36
+ * wallet itself, so the extra round trip only matters when a read uses its answer: a strict wallet
37
+ * match, a funding check, or the Robinhood delegated lane. An expected wallet equal to the agent key
38
+ * is not a separate wallet to match first; the wallet the API reports is checked after the read.
39
+ * Mirrors thirdfy-mcp `walletExecute` readFastPath.
40
+ */
41
+ function shouldUseManagedReadFastPath(flags, resolved, options = {}) {
42
+ if (!isReadOnlyAction(resolved)) return false;
43
+ if (options.requireFundingCheck === true) return false;
44
+ const runMode = String(flags.runMode || 'agent_wallet').trim().toLowerCase();
45
+ if (runMode !== 'agent_wallet') return false;
46
+ const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
47
+ const agentKey = String(typeof resolveAgentKey === 'function' ? resolveAgentKey(flags) : flags.agentKey || '').trim();
48
+ if (expectedWallet && !sameWalletAddress(expectedWallet, agentKey)) return false;
49
+ let chainId;
50
+ try {
51
+ chainId = resolveWalletExecuteChainId({
52
+ action: String(resolved?.resolvedAction || flags.action || '').trim(),
53
+ chainId: flags.chainId,
54
+ params: getPreparedParams(flags),
55
+ });
56
+ } catch {
57
+ // Unresolvable chain (e.g. a CCTP quote without sourceChainId): the preflight path reports it.
58
+ return false;
59
+ }
60
+ return Number(chainId) !== ROBINHOOD_MAINNET_CHAIN_ID;
61
+ }
62
+
63
+ async function resolveManagedExecutionPreflight(ctx, flags, resolved, options = {}) {
64
+ const action = String(resolved?.resolvedAction || flags.action || '').trim();
65
+ const params = getPreparedParams(flags);
66
+ const chainId = resolveWalletExecuteChainId({
67
+ action,
68
+ chainId: flags.chainId,
69
+ params,
70
+ });
71
+ const runMode = String(options.runMode || 'agent_wallet').trim().toLowerCase();
72
+ const agentApiKey = resolveAgentApiKey(flags, runMode);
73
+ const userDid = resolveEffectiveUserDid(flags, {
74
+ runMode,
75
+ hybridWalletMode: options.hybridWalletMode,
76
+ preferDelegationIdentity: options.preferDelegationIdentity,
77
+ });
78
+ if (!userDid) {
79
+ return {
80
+ success: false,
81
+ blockedReason: 'MISSING_USER_DID',
82
+ blockedStage: 'routing',
83
+ error:
84
+ runMode === 'agent_wallet'
85
+ ? '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.'
86
+ : 'Managed wallet execution requires --user-did, THIRDFY_USER_DID, or a saved userDid from `thirdfy-agent login email`.',
87
+ signerMethod: 'managed_wallet_server',
88
+ };
89
+ }
90
+ const tokenIn = resolveTokenInForFunding(params, action, resolved?.resolvedActionMeta?.behavior?.spend);
91
+ const query = new URLSearchParams({
92
+ userDid,
93
+ chainId: String(chainId),
94
+ runMode,
95
+ });
96
+ if (tokenIn) query.set('tokenIn', tokenIn);
97
+ const evaluateAction = options.evaluateAction === true;
98
+ if (evaluateAction) appendActionPreflightQuery(query, { action, apiAction: actionNameForApi(resolved), params, flags });
99
+ const executionVenue = describeExecutionVenue(action, chainId);
100
+ const preflightSeed = options.preflightSeed && typeof options.preflightSeed === 'object'
101
+ ? options.preflightSeed
102
+ : null;
103
+ if (preflightSeed && preflightSeed.success === false) {
104
+ return preflightSeed;
105
+ }
106
+ // A seed (address default lookup) carries no policy answer; an action preflight asks the API again.
107
+ const useSeed = Boolean(preflightSeed) && !evaluateAction;
108
+ let response;
109
+ if (useSeed) {
110
+ response = {
111
+ executionWalletAddress: preflightSeed.executionWalletAddress || null,
112
+ signerMethod: preflightSeed.signerMethod || null,
113
+ fundingTokenBalance: preflightSeed.fundingTokenBalance || null,
114
+ ...pickNativeGasFields(preflightSeed),
115
+ };
116
+ } else {
117
+ try {
118
+ response = await apiGet(ctx, `/api/v1/agent/execution-wallet?${query.toString()}`, {
119
+ 'x-agent-api-key': agentApiKey,
120
+ });
121
+ } catch (error) {
122
+ const blocked = evaluateAction ? blockedActionPreflightFromError(error) : null;
123
+ if (!blocked) throw error;
124
+ return {
125
+ ...blocked,
126
+ userDid,
127
+ chainId,
128
+ ...(executionVenue ? { executionVenue } : {}),
129
+ };
130
+ }
131
+ }
132
+ // An action preflight the API answered 2xx but refused is a block too (thirdfy-mcp previewOrder).
133
+ if (evaluateAction && response?.success === false) {
134
+ return {
135
+ ...blockedActionPreflight(response, {
136
+ errorFields: blockedResultFields(String(response.blockedReason || 'PREFLIGHT_BLOCKED')),
137
+ }),
138
+ userDid,
139
+ chainId,
140
+ ...(executionVenue ? { executionVenue } : {}),
141
+ };
142
+ }
143
+ const actionFields = evaluateAction ? pickActionPreflightFields(response) : {};
144
+ // API 4.0 wallet control, as thirdfy-mcp reports it: the API refuses writes itself (before
145
+ // signing), so preflight only reports it. Solana is exempt from the API's gate.
146
+ const walletControlFields = readWalletControl(response, { svm: isSvmChainId(chainId) });
147
+ // Keep the API's case: Solana base58 addresses are case-sensitive (lowercasing corrupts them).
148
+ const executionWalletAddress = String(response?.executionWalletAddress || '').trim();
149
+ const signerMethod = String(response?.signerMethod || 'managed_wallet_server').trim();
150
+ const rawBalance = String(response?.fundingTokenBalance?.raw || '').trim();
151
+ let parsedRawBalance = null;
152
+ if (rawBalance) {
153
+ try {
154
+ parsedRawBalance = BigInt(rawBalance);
155
+ } catch {
156
+ // Reads pass requireFundingCheck: false; a malformed balance must not block them.
157
+ // Writes still fail closed on unparseable fundingTokenBalance.raw.
158
+ if (options.requireFundingCheck) {
159
+ return {
160
+ success: false,
161
+ blockedReason: 'INVALID_FUNDING_BALANCE',
162
+ blockedStage: 'sizing',
163
+ error: `Execution wallet ${executionWalletAddress || 'unknown'} returned invalid funding token balance.`,
164
+ executionWalletAddress,
165
+ signerMethod,
166
+ fundingTokenBalance: response?.fundingTokenBalance || null,
167
+ ...actionFields,
168
+ ...(executionVenue ? { executionVenue } : {}),
169
+ ...walletControlFields,
170
+ };
171
+ }
172
+ }
173
+ }
174
+ const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
175
+ const allowWalletMismatch = parseBooleanFlag(flags.allowWalletMismatch, false);
176
+
177
+ if (
178
+ expectedWallet
179
+ && executionWalletAddress
180
+ && !sameWalletAddress(expectedWallet, executionWalletAddress)
181
+ && !allowWalletMismatch
182
+ ) {
183
+ return {
184
+ success: false,
185
+ blockedReason: 'EXECUTION_WALLET_MISMATCH',
186
+ ...blockedResultFields('EXECUTION_WALLET_MISMATCH'),
187
+ blockedStage: 'routing',
188
+ error: `Funded wallet (${expectedWallet}) does not match execution wallet (${executionWalletAddress}). Use --allow-wallet-mismatch to override.`,
189
+ executionWalletAddress,
190
+ signerMethod,
191
+ fundingTokenBalance: response?.fundingTokenBalance || null,
192
+ ...actionFields,
193
+ ...(executionVenue ? { executionVenue } : {}),
194
+ ...walletControlFields,
195
+ };
196
+ }
197
+
198
+ if (options.requireFundingCheck && parsedRawBalance !== null && parsedRawBalance <= 0n) {
199
+ return {
200
+ success: false,
201
+ blockedReason: 'INSUFFICIENT_FUNDS',
202
+ ...blockedResultFields('INSUFFICIENT_FUNDS'),
203
+ blockedStage: 'sizing',
204
+ error: `Execution wallet ${executionWalletAddress || 'unknown'} has insufficient tokenIn balance.`,
205
+ executionWalletAddress,
206
+ signerMethod,
207
+ fundingTokenBalance: response?.fundingTokenBalance || null,
208
+ ...actionFields,
209
+ ...(executionVenue ? { executionVenue } : {}),
210
+ ...walletControlFields,
211
+ };
212
+ }
213
+
214
+ // User-pays-gas chains (DogeOS, Solana): the API reports native gas readiness (API 3.18.2+). Keep
215
+ // it, and stop a write before /execute when the API says the wallet cannot pay gas (gasReady false).
216
+ const nativeGas = pickNativeGasFields(response);
217
+ if (options.requireFundingCheck && nativeGas.gasReady === false) {
218
+ return {
219
+ success: false,
220
+ blockedReason: 'INSUFFICIENT_NATIVE_GAS',
221
+ ...blockedResultFields('INSUFFICIENT_NATIVE_GAS'),
222
+ blockedStage: 'sizing',
223
+ error: nativeGas.nativeGasWarning?.message || describeInsufficientNativeGas(executionWalletAddress, chainId),
224
+ executionWalletAddress,
225
+ signerMethod,
226
+ fundingTokenBalance: response?.fundingTokenBalance || null,
227
+ ...actionFields,
228
+ ...(executionVenue ? { executionVenue } : {}),
229
+ ...walletControlFields,
230
+ ...nativeGas,
231
+ };
232
+ }
233
+
234
+ return {
235
+ success: true,
236
+ executionWalletAddress,
237
+ signerMethod,
238
+ fundingTokenBalance: response?.fundingTokenBalance || null,
239
+ userDid,
240
+ chainId,
241
+ tokenIn: tokenIn || null,
242
+ ...nativeGas,
243
+ ...actionFields,
244
+ ...(executionVenue ? { executionVenue } : {}),
245
+ ...walletControlFields,
246
+ };
247
+ }
248
+
249
+ return {
250
+ shouldUseManagedReadFastPath,
251
+ resolveManagedExecutionPreflight,
252
+ };
253
+ }
@@ -0,0 +1,119 @@
1
+ import { parseBooleanFlag } from '../../../core/runMode.mjs';
2
+ import { responseHeadersFor } from '../../../core/http.mjs';
3
+ import { idempotentReplayFields } from '../../../core/execution/hostedRuntimeIdempotency.mjs';
4
+ import { isReadOnlyResolvedAction } from '../../../core/execution/readOnlyActions.mjs';
5
+ import { sameWalletAddress } from '../../../core/chainRegistry.mjs';
6
+ import { riskRefusalFields } from '../../risk/riskRefusal.mjs';
7
+ import { shouldFallbackManagedReadToExecuteIntent } from '../managedReadExecuteRail.mjs';
8
+ import { createRequire } from 'module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
12
+
13
+ /** Managed (agent_wallet) execute answers: the read-only check, read-response shaping, and the read fast-path marker and wallet check. */
14
+
15
+ export function isReadOnlyAction(resolved) {
16
+ return isReadOnlyResolvedAction(resolved);
17
+ }
18
+
19
+ export function shouldFallbackAgentWalletToExecuteIntent(resolved, response) {
20
+ return shouldFallbackManagedReadToExecuteIntent({
21
+ isReadOnly: isReadOnlyAction(resolved),
22
+ errorText: String(response?.error || response?.message || response?.code || response?.blockedReason || ''),
23
+ });
24
+ }
25
+
26
+ // Managed /execute often returns provider payloads as JSON strings under data.result. Pack parsers
27
+ // (and find_value_by_key) need a real object tree, so coerce strings that look like JSON.
28
+ export function coerceJsonTreeValue(value) {
29
+ if (typeof value !== 'string') return value;
30
+ const trimmed = value.trim();
31
+ if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return value;
32
+ try {
33
+ return JSON.parse(trimmed);
34
+ } catch {
35
+ return value;
36
+ }
37
+ }
38
+
39
+ export function shapeManagedExecuteReadResponse(response) {
40
+ if (!response || typeof response !== 'object') return response;
41
+ const data =
42
+ response.data && typeof response.data === 'object' ? { ...response.data } : response.data;
43
+ if (data && typeof data === 'object' && Object.prototype.hasOwnProperty.call(data, 'result')) {
44
+ data.result = coerceJsonTreeValue(data.result);
45
+ }
46
+ return { ...response, data };
47
+ }
48
+
49
+ export function normalizeManagedExecuteResponse(response, { preflight, payload, isRead }) {
50
+ const shaped = isRead ? shapeManagedExecuteReadResponse(response) : response;
51
+ const normalized = normalizeIntentResponse({
52
+ success: Boolean(shaped?.success),
53
+ status: shaped?.success ? 'completed' : 'failed',
54
+ mode: 'agent_wallet',
55
+ txHash: shaped?.txHash || null,
56
+ blockedReason: shaped?.blockedReason || shaped?.data?.blockedReason || null,
57
+ blockedStage: shaped?.blockedStage || shaped?.data?.blockedStage || null,
58
+ error: shaped?.error || null,
59
+ executionWalletAddress:
60
+ shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null,
61
+ signerMethod: 'managed_wallet_server',
62
+ idempotencyKey: payload.executionIdempotencyKey || null,
63
+ executionWalletPreflight: preflight,
64
+ amountNormalization:
65
+ shaped?.amountNormalization || shaped?.data?.amountNormalization || null,
66
+ // Reads must keep provider payload on the envelope. EarnClaw packs parse
67
+ // data.raw.data.result (see parse_thirdfy_action_result).
68
+ ...(isRead && shaped?.data !== undefined ? { data: shaped.data } : {}),
69
+ raw: shaped,
70
+ });
71
+ normalized.executionWalletAddress =
72
+ shaped?.executionWalletAddress || shaped?.data?.executionWalletAddress || null;
73
+ normalized.signerMethod = 'managed_wallet_server';
74
+ normalized.executionWalletPreflight = preflight;
75
+ normalized.amountNormalization =
76
+ shaped?.amountNormalization || shaped?.data?.amountNormalization || null;
77
+ normalized.raw = shaped;
78
+ normalized.idempotencyKey = payload.executionIdempotencyKey || null;
79
+ // A risk-policy refusal nests riskRefusal under data; lift it as thirdfy-mcp walletExecute does.
80
+ const refusal = shaped?.success ? undefined : riskRefusalFields(shaped || {});
81
+ if (refusal) Object.assign(normalized, refusal);
82
+ // A replay means nothing new ran; say so, or the caller reads it as a fresh fill.
83
+ if (!isRead) {
84
+ Object.assign(
85
+ normalized,
86
+ idempotentReplayFields(response, responseHeadersFor(response), 'executionIdempotencyKey'),
87
+ );
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ /** Marker a read on the fast path carries instead of an execution-wallet preflight. */
93
+ export const READ_FAST_PATH_PREFLIGHT = Object.freeze({ success: true, skipped: 'read_fast_path' });
94
+
95
+ /**
96
+ * The fast path only runs when the expected wallet is empty or equals the agent key. The preflight
97
+ * path refuses a wallet that differs from the execution wallet, so apply the same rule to the wallet
98
+ * the API reports for the read.
99
+ */
100
+ export function fastPathWalletMismatch(flags, response) {
101
+ // 0x addresses compare case-insensitively; Solana base58 is case-sensitive and kept as sent.
102
+ const expectedWallet = String(flags.walletAddress || flags.executionWalletAddress || '').trim();
103
+ if (!expectedWallet || parseBooleanFlag(flags.allowWalletMismatch, false)) return null;
104
+ const apiWallet = String(response?.executionWalletAddress || response?.data?.executionWalletAddress || '').trim();
105
+ if (!apiWallet || sameWalletAddress(apiWallet, expectedWallet)) return null;
106
+ const normalized = normalizeIntentResponse({
107
+ success: false,
108
+ status: 'failed',
109
+ mode: 'agent_wallet',
110
+ blockedReason: 'EXECUTION_WALLET_MISMATCH',
111
+ blockedStage: 'routing',
112
+ error: `Funded wallet (${expectedWallet}) does not match execution wallet (${apiWallet}). Use --allow-wallet-mismatch to override.`,
113
+ preflightBlocked: true,
114
+ executionWalletAddress: apiWallet,
115
+ });
116
+ normalized.executionWalletAddress = apiWallet;
117
+ normalized.executionWalletPreflight = READ_FAST_PATH_PREFLIGHT;
118
+ return normalized;
119
+ }
@@ -0,0 +1,232 @@
1
+ import { createCliError } from '../../../core/envelope.mjs';
2
+ import { normalizeEffectCheckMode } from '../../../core/runMode.mjs';
3
+ import { apiPost } from '../../../core/http.mjs';
4
+ import { resolveChainRpcUrl } from '../../../core/chainRegistry.mjs';
5
+ import { actionNameForApi } from '../../actions/selection.mjs';
6
+ import { isReadOnlyAction } from './managedResponse.mjs';
7
+ import { createRequire } from 'module';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
11
+
12
+ /** The self lane: build the transaction, sign and broadcast locally, then check its effect. */
13
+ export function createSelfLane(deps, lanes) {
14
+ const {
15
+ buildIntentPayload,
16
+ buildBuildTxPayload,
17
+ getPreparedParams,
18
+ shouldUseBuildTxExecution,
19
+ loadEthers,
20
+ signAndBroadcastWithOws,
21
+ readEffectBalances,
22
+ resolveUnsignedTx,
23
+ sleepMs,
24
+ } = deps;
25
+ const { runReadOnlyActionViaIntentRail } = lanes;
26
+
27
+ async function executeSelfRun(ctx, flags, resolved, options) {
28
+ const skipPreflight = Boolean(options?.skipPreflight);
29
+ const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
30
+ // A read has no transaction to build or sign, so the self lane has nothing to do with it. Without
31
+ // this it either fails with SELF_MODE_REQUIRES_BUILD_TX or, when catalog metadata is missing, asks
32
+ // /build-tx to produce a transaction for a market-data query.
33
+ if (isReadOnlyAction(resolved)) {
34
+ return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, options?.runMode || 'self');
35
+ }
36
+ if (!skipPreflight) {
37
+ const preflightPayload = buildIntentPayload(flags, {
38
+ validationOnly: true,
39
+ forceIdempotency: false,
40
+ resolvedAction: resolved.resolvedAction,
41
+ apiAction: actionNameForApi(resolved),
42
+ runMode: 'self',
43
+ mirrorOnly: false,
44
+ });
45
+ const preflight = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', preflightPayload);
46
+ const preflightNormalized = normalizeIntentResponse(preflight);
47
+ const blockedCount = preflightNormalized.blocked || 0;
48
+ if (!preflightNormalized.success || blockedCount > 0) {
49
+ preflightNormalized.success = false;
50
+ preflightNormalized.error = preflightNormalized.error || 'Preflight blocked self execution';
51
+ preflightNormalized.preflightBlocked = true;
52
+ return preflightNormalized;
53
+ }
54
+ }
55
+ if (!shouldUseBuildTxExecution(options?.runMode || 'self', resolved)) {
56
+ const normalized = normalizeIntentResponse({
57
+ success: false,
58
+ status: 'failed',
59
+ blockedReason: blockedReasonCode,
60
+ blockedStage: 'routing',
61
+ error: 'Selected action does not support build-tx self lane. Use --run-mode thirdfy for delegated CEX execution.',
62
+ });
63
+ normalized.preflightBlocked = true;
64
+ return normalized;
65
+ }
66
+ const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction, actionNameForApi(resolved));
67
+ // build-tx returns an unsigned transaction; nothing is broadcast, so a timeout may be resent.
68
+ const buildTx = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/build-tx', buildTxPayload);
69
+ return normalizeIntentResponse({
70
+ ...buildTx,
71
+ status: buildTx?.status || 'ready',
72
+ mode: buildTx?.mode || 'self',
73
+ unsignedTx: buildTx?.unsignedTx || null,
74
+ estimatedGas: buildTx?.estimatedGas || null,
75
+ });
76
+ }
77
+
78
+ async function performSelfBroadcast(ctx, flags, resolvedAction, selfResult) {
79
+ const txd = resolveUnsignedTx(selfResult);
80
+ if (!txd?.to || !txd?.data) {
81
+ throw createCliError('SELF_UNSIGNED_TX_MISSING', 'Self execution did not return a valid unsigned transaction');
82
+ }
83
+ const chainId = Number(txd.chainId || flags.chainId || 8453);
84
+ const walletAddress = String(flags.walletAddress || process.env.MARKET_MAKER_WALLET_ADDRESS || '').trim();
85
+ const owsWalletName = String(flags.owsWalletName || process.env.OWS_WALLET_NAME || '').trim();
86
+ const rpcUrl = resolveChainRpcUrl({ chainId, rpcUrl: flags.rpcUrl });
87
+ if (!rpcUrl) {
88
+ throw createCliError('RPC_URL_REQUIRED', `self-exec has no known RPC for chain ${chainId}; pass --rpc-url <url> for that chain`);
89
+ }
90
+ if (!walletAddress) {
91
+ throw createCliError('SIGNING_FAILED', 'Missing --wallet-address (or MARKET_MAKER_WALLET_ADDRESS) for self-exec signing.');
92
+ }
93
+ if (!owsWalletName) {
94
+ throw createCliError('SIGNING_FAILED', 'Missing --ows-wallet-name (or OWS_WALLET_NAME) for self-exec signing.');
95
+ }
96
+
97
+ const params = getPreparedParams(flags);
98
+ const effectTokenIn = String(flags.effectTokenIn || params.tokenIn || '').trim();
99
+ const effectTokenOut = String(flags.effectTokenOut || params.tokenOut || '').trim();
100
+ const effectMode = normalizeEffectCheckMode(flags.effectCheck || process.env.THIRDFY_EFFECT_CHECK || 'relaxed');
101
+ const effectRetries = Number(flags.effectCheckRetries || process.env.THIRDFY_EFFECT_CHECK_RETRIES || 2);
102
+ const effectWaitMs = Number(flags.effectCheckWaitMs || process.env.THIRDFY_EFFECT_CHECK_WAIT_MS || 1200);
103
+
104
+ let balancesBefore = null;
105
+ if (effectMode !== 'off' && effectTokenIn && effectTokenOut) {
106
+ balancesBefore = await readEffectBalances(rpcUrl, walletAddress, effectTokenIn, effectTokenOut);
107
+ }
108
+
109
+ let submission;
110
+ try {
111
+ submission = await signAndBroadcastWithOws({
112
+ txd,
113
+ chainId,
114
+ walletAddress,
115
+ owsWalletName,
116
+ rpcUrl,
117
+ });
118
+ } catch (error) {
119
+ throw createCliError('SIGNING_FAILED', error?.message || 'OWS signing failed');
120
+ }
121
+
122
+ let receipt;
123
+ try {
124
+ receipt = await submission.sent.wait();
125
+ } catch (error) {
126
+ throw createCliError('BROADCAST_FAILED', error?.message || 'Broadcast wait failed');
127
+ }
128
+ const status = Number(receipt?.status ?? 0);
129
+ if (status !== 1) {
130
+ throw createCliError('BROADCAST_FAILED', 'Transaction receipt status is not success', {
131
+ receiptStatus: receipt?.status ?? null,
132
+ txHash: submission.sent.hash,
133
+ });
134
+ }
135
+
136
+ const effectCheck = await resolveEffectCheckResult({
137
+ mode: effectMode,
138
+ rpcUrl,
139
+ walletAddress,
140
+ tokenIn: effectTokenIn,
141
+ tokenOut: effectTokenOut,
142
+ balancesBefore,
143
+ retries: effectRetries,
144
+ waitMs: effectWaitMs,
145
+ });
146
+ if (effectCheck.checked && !effectCheck.hasEffect && effectMode === 'strict') {
147
+ throw createCliError('EFFECT_CHECK_FAILED', 'Transaction mined but did not produce observable token effect.', {
148
+ txHash: submission.sent.hash,
149
+ effectCheck,
150
+ mode: effectMode,
151
+ });
152
+ }
153
+
154
+ return {
155
+ txHash: submission.sent.hash,
156
+ chainId,
157
+ blockNumber: receipt?.blockNumber ?? null,
158
+ status: receipt?.status ?? null,
159
+ unsignedTx: txd,
160
+ effectCheck,
161
+ lane: 'self',
162
+ signer: 'ows',
163
+ resolvedAction,
164
+ apiBase: ctx.apiBase,
165
+ };
166
+ }
167
+
168
+ async function resolveEffectCheckResult({
169
+ mode,
170
+ rpcUrl,
171
+ walletAddress,
172
+ tokenIn,
173
+ tokenOut,
174
+ balancesBefore,
175
+ retries,
176
+ waitMs,
177
+ }) {
178
+ const baseResult = {
179
+ checked: true,
180
+ mode,
181
+ hasEffect: false,
182
+ inDeltaRaw: '0',
183
+ outDeltaRaw: '0',
184
+ attempts: 0,
185
+ retries: Math.max(0, Number(retries || 0)),
186
+ };
187
+ if (mode === 'off') {
188
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'disabled' };
189
+ }
190
+ if (!tokenIn || !tokenOut || !balancesBefore) {
191
+ return { checked: false, mode, hasEffect: null, inDeltaRaw: null, outDeltaRaw: null, reason: 'token_pair_missing' };
192
+ }
193
+ const maxAttempts = Math.max(1, Number(retries || 0) + 1);
194
+ let last = null;
195
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
196
+ const balancesAfter = await readEffectBalances(rpcUrl, walletAddress, tokenIn, tokenOut);
197
+ const inDeltaRaw = (BigInt(balancesBefore.tokenIn) - BigInt(balancesAfter.tokenIn)).toString();
198
+ const outDeltaRaw = (BigInt(balancesAfter.tokenOut) - BigInt(balancesBefore.tokenOut)).toString();
199
+ const hasEffect = BigInt(inDeltaRaw) > 0n || BigInt(outDeltaRaw) > 0n;
200
+ last = {
201
+ checked: true,
202
+ mode,
203
+ hasEffect,
204
+ inDeltaRaw,
205
+ outDeltaRaw,
206
+ attempts: attempt,
207
+ retries: maxAttempts - 1,
208
+ };
209
+ if (hasEffect) return last;
210
+ if (attempt < maxAttempts) await sleepMs(waitMs);
211
+ }
212
+ return {
213
+ ...(last || baseResult),
214
+ warning: 'No balance delta observed after retries; treating as non-fatal in relaxed mode.',
215
+ };
216
+ }
217
+
218
+ async function submitSignedTx({ rpcUrl, chainId, signedTxHex }) {
219
+ const ethers = await loadEthers();
220
+ const { JsonRpcProvider } = ethers;
221
+ const provider = new JsonRpcProvider(rpcUrl, chainId);
222
+ const sent = await provider.broadcastTransaction(String(signedTxHex));
223
+ return sent?.hash || null;
224
+ }
225
+
226
+ return {
227
+ executeSelfRun,
228
+ performSelfBroadcast,
229
+ resolveEffectCheckResult,
230
+ submitSignedTx,
231
+ };
232
+ }