diff --git a/src/execution/utils.ts b/src/execution/utils.ts index f227bf32..3f1b1b0e 100644 --- a/src/execution/utils.ts +++ b/src/execution/utils.ts @@ -928,7 +928,14 @@ async function prepareTransactionAsIntent( config.endpointUrl, config.headers, ) - const intentRoute = await orchestrator.getIntentRoute(metaIntent) + const routeHeaders = + Object.keys(preClaimExecutions).length > 0 + ? { 'x-feature-flags': 'new-path-generation=new' } + : undefined + const intentRoute = await orchestrator.getIntentRoute( + metaIntent, + routeHeaders, + ) return intentRoute } diff --git a/src/modules/validators/smart-sessions.test.ts b/src/modules/validators/smart-sessions.test.ts index 852e5975..d85a54a8 100644 --- a/src/modules/validators/smart-sessions.test.ts +++ b/src/modules/validators/smart-sessions.test.ts @@ -136,15 +136,17 @@ describe('getPolicyData', () => { // --------------------------------------------------------------------------- describe('getSessionData', () => { - test('no actions → single sudoAction fallback', () => { + test('no actions → sudoAction fallback + dummy preclaimop action', () => { const data = getSessionData(baseSession) - expect(data.actions).toHaveLength(1) + expect(data.actions).toHaveLength(2) expect(data.actions[0].actionTarget).toBe( SMART_SESSIONS_FALLBACK_TARGET_FLAG, ) expect(data.actions[0].actionTargetSelector).toBe( SMART_SESSIONS_FALLBACK_TARGET_SELECTOR_FLAG, ) + expect(data.actions[1].actionTarget).toBe(DUMMY_PRECLAIMOP_TARGET) + expect(data.actions[1].actionTargetSelector).toBe(DUMMY_PRECLAIMOP_SELECTOR) }) test('explicit actions → user action + 3 injected (WETH deposit + intent-execution fallback + dummy preclaimop)', () => { @@ -177,25 +179,14 @@ describe('getSessionData', () => { expect(dummyAction!.actionPolicies[0].initData).toBe('0x') }) - test('no explicit actions → sudoAction fallback only (dummy injected via injectedActions path is not used)', () => { - // Sessions without explicit actions use the [sudoAction] fallback directly, - // which covers all (target, selector) pairs — no dummy action needed. - const data = getSessionData(baseSession) - expect(data.actions).toHaveLength(1) - expect(data.actions[0].actionTarget).toBe( - SMART_SESSIONS_FALLBACK_TARGET_FLAG, - ) - }) - - test('empty actions array → same sudoAction fallback as no actions', () => { - // actions: [] is truthy but has no elements — must be treated the same as - // actions: undefined so injectedActions are not appended. + test('empty actions array → same as no actions (sudoAction + dummy)', () => { const sessionWithEmptyActions: Session = { ...baseSession, actions: [] } const data = getSessionData(sessionWithEmptyActions) - expect(data.actions).toHaveLength(1) + expect(data.actions).toHaveLength(2) expect(data.actions[0].actionTarget).toBe( SMART_SESSIONS_FALLBACK_TARGET_FLAG, ) + expect(data.actions[1].actionTarget).toBe(DUMMY_PRECLAIMOP_TARGET) }) test('multiple policies on one action', () => { diff --git a/src/modules/validators/smart-sessions.ts b/src/modules/validators/smart-sessions.ts index c7796b43..4aa4bcfc 100644 --- a/src/modules/validators/smart-sessions.ts +++ b/src/modules/validators/smart-sessions.ts @@ -211,13 +211,12 @@ const SMART_SESSIONS_FALLBACK_TARGET_SELECTOR_FLAG_PERMITTED_TO_CALL_SMARTSESSIO // Dummy preclaimop action injected into every session so that the filler can trigger // verifyExecution (ENABLE mode) using an injected dummy preclaimop when there are no -// real preclaimops. Target 0x...0001 is the ecRecover precompile; calls to it fail -// silently because preclaimops are failure-tolerant. Selector 0x69123456 is -// intentionally uncommon. Note: this address is the same as -// SMART_SESSIONS_FALLBACK_TARGET_FLAG — that is harmless because they operate in -// different contexts (action matching vs. literal execution target). +// real preclaimops. Target 0x...0002 is the SHA256 precompile; calls succeed and it +// is distinct from SMART_SESSIONS_FALLBACK_TARGET_FLAG (0x...0001) which is explicitly +// rejected by the SmartSession contract's InvalidTarget check. Selector 0x69123456 is +// intentionally uncommon. const DUMMY_PRECLAIMOP_TARGET: Address = - '0x0000000000000000000000000000000000000001' + '0x0000000000000000000000000000000000000002' const DUMMY_PRECLAIMOP_SELECTOR: Hex = '0x69123456' const SPENDING_LIMITS_POLICY_ADDRESS: Address = @@ -704,26 +703,41 @@ function getSessionData( }, ] + // Map a list of actions (user-defined + injected) to the on-chain format. + const mapActions = (acts: Action[]) => + acts.map((action) => ({ + actionTargetSelector: + 'selector' in action + ? action.selector + : SMART_SESSIONS_FALLBACK_TARGET_SELECTOR_FLAG, + actionTarget: + 'target' in action + ? action.target + : SMART_SESSIONS_FALLBACK_TARGET_FLAG, + actionPolicies: action.policies?.map((policy) => + getPolicyData(policy, useDevContracts), + ) ?? [ + { + policy: SUDO_POLICY_ADDRESS, + initData: '0x' as Hex, + }, + ], + })) + const actions = session.actions?.length - ? [...session.actions, ...injectedActions].map((action) => ({ - actionTargetSelector: - 'selector' in action - ? action.selector - : SMART_SESSIONS_FALLBACK_TARGET_SELECTOR_FLAG, - actionTarget: - 'target' in action - ? action.target - : SMART_SESSIONS_FALLBACK_TARGET_FLAG, - actionPolicies: action.policies?.map((policy) => - getPolicyData(policy, useDevContracts), - ) ?? [ + ? mapActions([...session.actions, ...injectedActions]) + : // No explicit actions — still inject the dummy preclaimop action so the + // filler can call verifyExecution in ENABLE mode even for catch-all sessions. + [ + sudoAction, + ...mapActions([ { - policy: SUDO_POLICY_ADDRESS, - initData: '0x' as Hex, + target: DUMMY_PRECLAIMOP_TARGET, + selector: DUMMY_PRECLAIMOP_SELECTOR, + policies: [{ type: 'sudo' as const }], }, - ], - })) - : [sudoAction] + ]), + ] return { sessionValidator: validator.address, salt: zeroHash, diff --git a/src/orchestrator/client.ts b/src/orchestrator/client.ts index 7342df98..337031b9 100644 --- a/src/orchestrator/client.ts +++ b/src/orchestrator/client.ts @@ -119,11 +119,14 @@ export class Orchestrator { return portfolio } - async getIntentRoute(input: IntentInput): Promise { + async getIntentRoute( + input: IntentInput, + extraHeaders?: Record, + ): Promise { const body = convertBigIntFields(input) return await this.fetch(`${this.serverUrl}/intents/route`, { method: 'POST', - headers: this.getHeaders(), + headers: { ...this.getHeaders(), ...extraHeaders }, body: JSON.stringify(body), }) }