Skip to content

Commit 4c94987

Browse files
chore(runway): cherry-pick feat(predict): add confirmation hook plumbing (#30079)
- feat(predict): add confirmation hook plumbing (#29914) ## **Description** This PR adds minimal Predict confirmation hook plumbing needed by the upcoming Polymarket Deposit Wallet migration. It wires TransactionController confirmation lifecycle hooks to PredictController while keeping Predict behavior as passthrough by default: - `beforePublish` delegates to `PredictController.beforePublish`, which currently returns `true`. - `publish` delegates to `PredictController.publish` before Transaction Pay / 7702 / Smart Transactions, and continues normal publishing when Predict returns no transaction hash. - If a future Predict publish implementation returns `{ transactionHash, isIntentComplete: true }`, TransactionController marks the latest transaction meta as `isIntentComplete` before returning the hash. This PR intentionally contains no Polymarket Deposit Wallet business logic. It is a small foundation PR for confirmation-team review. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: N/A — preparatory plumbing for the Predict Deposit Wallet migration. ## **Manual testing steps** ```gherkin Feature: Predict confirmation hook plumbing Scenario: non-Predict transactions continue through the normal publish flow Given a transaction is published through TransactionController And PredictController.publish returns no transaction hash When the publish hook runs Then Transaction Pay / 7702 / Smart Transaction publishing continues as before Scenario: Predict publish can complete a transaction intent Given PredictController.publish returns a transaction hash and isIntentComplete When the publish hook runs Then normal publishing is short-circuited And the latest TransactionController transaction meta is marked intent complete ``` Local validation run: ```bash yarn jest app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts app/components/UI/Predict/controllers/PredictController.test.ts --runInBand yarn lint:tsc ``` ## **Screenshots/Recordings** N/A — no UI changes. ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - N/A — hook plumbing only, no UI/runtime performance path manually exercised. - [x] I've tested with a power user scenario - N/A — hook plumbing only, no account/token rendering path changed. - [x] I've instrumented key operations with Sentry traces for production performance metrics - N/A — this PR only adds passthrough hook plumbing. For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches transaction publishing lifecycle by adding new hooks and a short-circuit path, which could affect submission ordering and integration with Pay/7702/Smart Transactions if miswired. Default behavior remains passthrough, reducing blast radius. > > **Overview** > Adds Predict-specific confirmation hook plumbing into the transaction submission lifecycle. TransactionController init now calls `PredictController:beforePublish` as a new `hooks.beforePublish`, and calls `PredictController:publish` at the start of `hooks.publish`, **short-circuiting** the rest of the publish pipeline when Predict returns a `transactionHash`. > > Updates PredictController to expose new messenger methods (`beforePublish`, `publish`) with default passthrough implementations, extends messenger action typings/permissions accordingly, and adds unit tests verifying delegation, call ordering, and the short-circuit behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3f9d618. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> [e5a8b17](e5a8b17) Co-authored-by: Luis Taniça <matallui@gmail.com>
1 parent 904008f commit 4c94987

6 files changed

Lines changed: 198 additions & 9 deletions

File tree

app/components/UI/Predict/controllers/PredictController-method-action-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,21 @@ export type PredictControllerPrepareWithdrawAction = {
248248
handler: PredictController['prepareWithdraw'];
249249
};
250250

251+
export type PredictControllerBeforePublishAction = {
252+
type: `PredictController:beforePublish`;
253+
handler: PredictController['beforePublish'];
254+
};
255+
251256
export type PredictControllerBeforeSignAction = {
252257
type: `PredictController:beforeSign`;
253258
handler: PredictController['beforeSign'];
254259
};
255260

261+
export type PredictControllerPublishAction = {
262+
type: `PredictController:publish`;
263+
handler: PredictController['publish'];
264+
};
265+
256266
export type PredictControllerClearWithdrawTransactionAction = {
257267
type: `PredictController:clearWithdrawTransaction`;
258268
handler: PredictController['clearWithdrawTransaction'];
@@ -300,5 +310,7 @@ export type PredictControllerMethodActions =
300310
| PredictControllerGetAccountStateAction
301311
| PredictControllerGetBalanceAction
302312
| PredictControllerPrepareWithdrawAction
313+
| PredictControllerBeforePublishAction
303314
| PredictControllerBeforeSignAction
315+
| PredictControllerPublishAction
304316
| PredictControllerClearWithdrawTransactionAction;

app/components/UI/Predict/controllers/PredictController.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6139,6 +6139,40 @@ describe('PredictController', () => {
61396139
});
61406140
});
61416141

6142+
describe('beforePublish', () => {
6143+
it('passes through by default', async () => {
6144+
await withController(async ({ controller }) => {
6145+
const result = await controller.beforePublish({
6146+
transactionMeta: {
6147+
id: 'tx-1',
6148+
txParams: {
6149+
from: MOCK_ADDRESS,
6150+
},
6151+
} as TransactionMeta,
6152+
});
6153+
6154+
expect(result).toBe(true);
6155+
});
6156+
});
6157+
});
6158+
6159+
describe('publish', () => {
6160+
it('passes through by default', async () => {
6161+
await withController(async ({ controller }) => {
6162+
const result = await controller.publish({
6163+
transactionMeta: {
6164+
id: 'tx-1',
6165+
txParams: {
6166+
from: MOCK_ADDRESS,
6167+
},
6168+
} as TransactionMeta,
6169+
});
6170+
6171+
expect(result).toEqual({ transactionHash: undefined });
6172+
});
6173+
});
6174+
});
6175+
61426176
describe('beforeSign', () => {
61436177
const mockTransactionMeta = {
61446178
id: 'tx-1',

app/components/UI/Predict/controllers/PredictController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ export interface PredictControllerOptions {
345345
}
346346

347347
const MESSENGER_EXPOSED_METHODS = [
348+
'beforePublish',
348349
'beforeSign',
349350
'claimWithConfirmation',
350351
'clearActiveOrder',
@@ -370,6 +371,7 @@ const MESSENGER_EXPOSED_METHODS = [
370371
'onPlaceOrderSuccess',
371372
'placeOrder',
372373
'prepareWithdraw',
374+
'publish',
373375
'previewOrder',
374376
'refreshEligibility',
375377
'selectPaymentToken',
@@ -2619,6 +2621,12 @@ export class PredictController extends BaseController<
26192621
}
26202622
}
26212623

2624+
public async beforePublish(_request: {
2625+
transactionMeta: TransactionMeta;
2626+
}): Promise<boolean> {
2627+
return true;
2628+
}
2629+
26222630
public async beforeSign(request: {
26232631
transactionMeta: TransactionMeta;
26242632
}): Promise<
@@ -2722,6 +2730,12 @@ export class PredictController extends BaseController<
27222730
};
27232731
}
27242732

2733+
public async publish(_request: {
2734+
transactionMeta: TransactionMeta;
2735+
}): Promise<{ transactionHash?: string }> {
2736+
return { transactionHash: undefined };
2737+
}
2738+
27252739
public clearWithdrawTransaction(): void {
27262740
this.update((state) => {
27272741
state.withdrawTransaction = null;
@@ -2730,6 +2744,7 @@ export class PredictController extends BaseController<
27302744
}
27312745

27322746
export type {
2747+
PredictControllerBeforePublishAction,
27332748
PredictControllerBeforeSignAction,
27342749
PredictControllerClaimWithConfirmationAction,
27352750
PredictControllerClearActiveOrderAction,
@@ -2754,6 +2769,7 @@ export type {
27542769
PredictControllerPlaceOrderAction,
27552770
PredictControllerPrepareWithdrawAction,
27562771
PredictControllerPreviewOrderAction,
2772+
PredictControllerPublishAction,
27572773
PredictControllerRefreshEligibilityAction,
27582774
PredictControllerSelectPaymentTokenAction,
27592775
PredictControllerSetSelectedPaymentTokenAction,

app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,20 @@ const MOCK_TRANSACTION_META = {
9292
* with the default mock.
9393
* @returns A mock NetworkController.
9494
*/
95+
type ControllerMock = NetworkController & {
96+
beforePublish: jest.Mock;
97+
beforeSign: jest.Mock;
98+
publish: jest.Mock;
99+
};
100+
95101
function buildControllerMock(
96-
partialMock?: Partial<NetworkController>,
97-
): NetworkController {
98-
const defaultControllerMocks = {};
102+
partialMock?: Partial<ControllerMock>,
103+
): ControllerMock {
104+
const defaultControllerMocks = {
105+
beforePublish: jest.fn().mockResolvedValue(true),
106+
beforeSign: jest.fn(),
107+
publish: jest.fn().mockResolvedValue({ transactionHash: undefined }),
108+
};
99109

100110
// @ts-expect-error Incomplete mock, just includes properties used by code-under-test.
101111
return {
@@ -112,22 +122,43 @@ function buildInitRequestMock(
112122
TransactionControllerInitMessenger
113123
>
114124
> {
125+
const {
126+
predictControllerMock: providedPredictControllerMock,
127+
...requestOverrides
128+
} = initRequestProperties;
129+
const predictControllerMock =
130+
(providedPredictControllerMock as ControllerMock | undefined) ??
131+
buildControllerMock();
115132
const initMessenger = new ExtendedMessenger<MockAnyNamespace>({
116133
namespace: MOCK_ANY_NAMESPACE,
117134
});
118135
const baseControllerMessenger = new ExtendedMessenger<MockAnyNamespace>({
119136
namespace: MOCK_ANY_NAMESPACE,
120137
});
138+
(initMessenger as unknown as { call: jest.Mock }).call = jest.fn(
139+
(actionType: string, params: unknown) => {
140+
if (actionType === 'PredictController:beforePublish') {
141+
return predictControllerMock.beforePublish(params);
142+
}
143+
144+
if (actionType === 'PredictController:publish') {
145+
return predictControllerMock.publish(params);
146+
}
147+
148+
throw new Error(`Unexpected init messenger action: ${actionType}`);
149+
},
150+
);
151+
121152
const requestMock = {
122153
...buildMessengerClientInitRequestMock(baseControllerMessenger),
123154
initMessenger:
124155
initMessenger as unknown as TransactionControllerInitMessenger,
125156
controllerMessenger:
126157
baseControllerMessenger as unknown as TransactionControllerMessenger,
127-
...initRequestProperties,
158+
...requestOverrides,
128159
};
129160

130-
if (!initRequestProperties.getMessengerClient) {
161+
if (!requestOverrides.getMessengerClient) {
131162
requestMock.getMessengerClient.mockReturnValue(buildControllerMock());
132163
}
133164

@@ -180,9 +211,11 @@ describe('Transaction Controller Init', () => {
180211
): TransactionControllerOptions[T] {
181212
const requestMock = buildInitRequestMock(initRequestProperties);
182213

183-
requestMock.getMessengerClient.mockReturnValue(
184-
buildControllerMock(dependencyProperties),
185-
);
214+
if (!initRequestProperties.getMessengerClient) {
215+
requestMock.getMessengerClient.mockReturnValue(
216+
buildControllerMock(dependencyProperties),
217+
);
218+
}
186219

187220
TransactionControllerInit(requestMock);
188221

@@ -320,6 +353,25 @@ describe('Transaction Controller Init', () => {
320353
expect(optionFn?.()).toBe(false);
321354
});
322355

356+
describe('beforePublish hook', () => {
357+
it('delegates to PredictController beforePublish', async () => {
358+
const predictControllerMock = buildControllerMock();
359+
const hooks = testConstructorOption(
360+
'hooks',
361+
{},
362+
{
363+
predictControllerMock,
364+
},
365+
);
366+
367+
await hooks?.beforePublish?.(MOCK_TRANSACTION_META);
368+
369+
expect(predictControllerMock.beforePublish).toHaveBeenCalledWith({
370+
transactionMeta: MOCK_TRANSACTION_META,
371+
});
372+
});
373+
});
374+
323375
describe('publish hook', () => {
324376
it('calls submitSmartTransactionHook', async () => {
325377
const hooks = testConstructorOption('hooks');
@@ -347,6 +399,53 @@ describe('Transaction Controller Init', () => {
347399
expect(payHookMock).toHaveBeenCalledTimes(1);
348400
});
349401

402+
it('calls Predict publish before pay and smart transaction hooks', async () => {
403+
const predictControllerMock = buildControllerMock();
404+
const hooks = testConstructorOption(
405+
'hooks',
406+
{},
407+
{
408+
predictControllerMock,
409+
},
410+
);
411+
412+
await hooks?.publish?.(MOCK_TRANSACTION_META);
413+
414+
expect(predictControllerMock.publish).toHaveBeenCalledWith({
415+
transactionMeta: MOCK_TRANSACTION_META,
416+
});
417+
expect(payHookMock).toHaveBeenCalledTimes(1);
418+
expect(
419+
(predictControllerMock.publish as jest.Mock).mock
420+
.invocationCallOrder[0],
421+
).toBeLessThan(payHookMock.mock.invocationCallOrder[0]);
422+
expect(
423+
(predictControllerMock.publish as jest.Mock).mock
424+
.invocationCallOrder[0],
425+
).toBeLessThan(
426+
submitSmartTransactionHookMock.mock.invocationCallOrder[0],
427+
);
428+
});
429+
430+
it('short-circuits publish when Predict returns a transaction hash', async () => {
431+
const predictControllerMock = buildControllerMock({
432+
publish: jest.fn().mockResolvedValue({ transactionHash: '0xpredict' }),
433+
} as unknown as Partial<NetworkController>);
434+
const hooks = testConstructorOption(
435+
'hooks',
436+
{},
437+
{
438+
predictControllerMock,
439+
},
440+
);
441+
442+
const result = await hooks?.publish?.(MOCK_TRANSACTION_META);
443+
444+
expect(result).toEqual({ transactionHash: '0xpredict' });
445+
expect(payHookMock).not.toHaveBeenCalled();
446+
expect(submitSmartTransactionHookMock).not.toHaveBeenCalled();
447+
});
448+
350449
it('passes isSmartTransaction returning false to pay hook when stxDisabled is true', async () => {
351450
selectMetaMaskPayFlagsMock.mockReturnValue({
352451
attemptsMax: 2,

app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export const TransactionControllerInit: MessengerClientInitFunction<
136136
transactions:
137137
_request.transactions as PublishBatchHookTransaction[],
138138
}),
139+
beforePublish: (transactionMeta: TransactionMeta) =>
140+
beforePublish(transactionMeta, initMessenger),
139141
beforeSign: (_request: { transactionMeta: TransactionMeta }) =>
140142
beforeSign(_request, request),
141143
},
@@ -226,6 +228,15 @@ async function publishHook({
226228
initMessenger: TransactionControllerInitMessenger;
227229
signedTransactionInHex: Hex;
228230
}): Promise<{ transactionHash?: string }> {
231+
const { transactionHash: predictTransactionHash } = await initMessenger.call(
232+
'PredictController:publish',
233+
{ transactionMeta },
234+
);
235+
236+
if (predictTransactionHash) {
237+
return { transactionHash: predictTransactionHash };
238+
}
239+
229240
const state = getState();
230241

231242
const { shouldUseSmartTransaction, featureFlags } =
@@ -441,6 +452,15 @@ function getControllers(
441452
};
442453
}
443454

455+
function beforePublish(
456+
transactionMeta: TransactionMeta,
457+
initMessenger: TransactionControllerInitMessenger,
458+
) {
459+
return initMessenger.call('PredictController:beforePublish', {
460+
transactionMeta,
461+
});
462+
}
463+
444464
function beforeSign(
445465
hookRequest: { transactionMeta: TransactionMeta },
446466
request: MessengerClientInitRequest<

app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ import {
5555
MessengerActions,
5656
MessengerEvents,
5757
} from '@metamask/messenger';
58+
import type {
59+
PredictControllerBeforePublishAction,
60+
PredictControllerPublishAction,
61+
} from '../../../../components/UI/Predict/controllers/PredictController-method-action-types';
5862

5963
export function getTransactionControllerMessenger(
6064
rootMessenger: RootMessenger,
@@ -114,7 +118,9 @@ type InitMessengerActions =
114118
| TransactionPayControllerGetDelegationTransactionAction
115119
| TransactionPayControllerGetStateAction
116120
| TransactionPayControllerGetStrategyAction
117-
| AnalyticsControllerActions;
121+
| AnalyticsControllerActions
122+
| PredictControllerBeforePublishAction
123+
| PredictControllerPublishAction;
118124

119125
type InitMessengerEvents =
120126
| BridgeStatusControllerEvents
@@ -173,6 +179,8 @@ export function getTransactionControllerInitMessenger(
173179
'TransactionPayController:getState',
174180
'TransactionPayController:getStrategy',
175181
'AnalyticsController:trackEvent',
182+
'PredictController:beforePublish',
183+
'PredictController:publish',
176184
],
177185
events: [
178186
'BridgeStatusController:stateChange',

0 commit comments

Comments
 (0)