Skip to content

Commit 1795b7f

Browse files
refactor: add confirmation refresher (#74)
## Explanation This PR refactors confirmation-dialog background updates into a single “confirmation context refresh” cronjob that orchestrates one or more pluggable refreshers (starting with token spot prices), and wires it into the confirmation UX controller and snap cronjob dispatch. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them
1 parent 1114300 commit 1795b7f

12 files changed

Lines changed: 1057 additions & 240 deletions

File tree

packages/snap/src/context.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import { OnAmountInputHandler } from './handlers/clientRequest/onAmountInput';
1616
import { SignAndSendTransactionHandler } from './handlers/clientRequest/signAndSendTransaction';
1717
import type { ICronjobRequestHandler } from './handlers/cronjob/api';
1818
import { BackgroundEventMethod } from './handlers/cronjob/api';
19-
import { RefreshConfirmationPricesHandler } from './handlers/cronjob/refreshConfirmationPrices';
19+
import {
20+
ConfirmationPriceRefresher,
21+
RefreshConfirmationContextHandler,
22+
} from './handlers/cronjob/refreshConfirmationContext';
2023
import { SyncAccountsHandler } from './handlers/cronjob/syncAccounts';
2124
import { TrackTransactionHandler } from './handlers/cronjob/trackTransaction';
2225
import type { IKeyringRequestHandler } from './handlers/keyring';
@@ -161,13 +164,19 @@ const userInputHandler = new UserInputHandler({
161164
});
162165

163166
/** ------------------------------ Cronjob Handler ------------------------------ */
164-
165-
const refreshConfirmationPricesHandler = new RefreshConfirmationPricesHandler({
167+
const confirmationPriceRefresher = new ConfirmationPriceRefresher({
166168
logger,
167169
priceService,
168-
confirmationUIController,
169170
});
170171

172+
const refreshConfirmationContextHandler = new RefreshConfirmationContextHandler(
173+
{
174+
logger,
175+
confirmationUIController,
176+
refreshers: [confirmationPriceRefresher],
177+
},
178+
);
179+
171180
const trackTransactionHandler = new TrackTransactionHandler({
172181
logger,
173182
networkService,
@@ -186,8 +195,8 @@ const cronjobMethodHandlers: Record<
186195
BackgroundEventMethod,
187196
ICronjobRequestHandler
188197
> = {
189-
[BackgroundEventMethod.RefreshConfirmationPrices]:
190-
refreshConfirmationPricesHandler,
198+
[BackgroundEventMethod.RefreshConfirmationContext]:
199+
refreshConfirmationContextHandler,
191200
[BackgroundEventMethod.TrackTransaction]: trackTransactionHandler,
192201
[BackgroundEventMethod.SynchronizeAccounts]: syncAccountsHandler,
193202
};

packages/snap/src/handlers/cronjob/api.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
BackgroundEventMethod,
55
BackgroundEventMethodStruct,
66
CronjobJsonRpcRequestStruct,
7-
RefreshConfirmationPricesJsonRpcRequestStruct,
7+
RefreshConfirmationContextJsonRpcRequestStruct,
88
SyncAccountJsonRpcRequestStruct,
99
SyncAccountParamsStruct,
1010
TrackTransactionJsonRpcRequestStruct,
@@ -126,25 +126,27 @@ describe('Cronjob API structs', () => {
126126
});
127127
});
128128

129-
describe('RefreshConfirmationPricesJsonRpcRequestStruct', () => {
130-
it('accepts refresh confirmation prices requests', () => {
129+
describe('RefreshConfirmationContextJsonRpcRequestStruct', () => {
130+
it('accepts refresh confirmation context requests', () => {
131131
const value = {
132132
...jsonRpcBase,
133-
method: BackgroundEventMethod.RefreshConfirmationPrices,
133+
method: BackgroundEventMethod.RefreshConfirmationContext,
134134
params: {
135135
scope: KnownCaip2ChainId.Mainnet,
136136
interfaceId: 'interface-id',
137137
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
138+
refresherKeys: ['prices'],
138139
},
139140
};
140-
assert(value, RefreshConfirmationPricesJsonRpcRequestStruct);
141+
assert(value, RefreshConfirmationContextJsonRpcRequestStruct);
141142
expect(value).toStrictEqual({
142143
...jsonRpcBase,
143-
method: BackgroundEventMethod.RefreshConfirmationPrices,
144+
method: BackgroundEventMethod.RefreshConfirmationContext,
144145
params: {
145146
scope: KnownCaip2ChainId.Mainnet,
146147
interfaceId: 'interface-id',
147148
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
149+
refresherKeys: ['prices'],
148150
},
149151
});
150152
});

packages/snap/src/handlers/cronjob/api.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
KnownCaip2ChainIdStruct,
2222
UuidStruct,
2323
} from '../../api';
24+
import { ConfirmationContextRefresherKeyStruct } from './refreshConfirmationContext/api';
2425
import { ConfirmationInterfaceKeyStruct } from '../../ui/confirmation/api';
2526

2627
/**
@@ -32,20 +33,30 @@ export type ICronjobRequestHandler = {
3233

3334
export enum BackgroundEventMethod {
3435
SynchronizeAccounts = 'synchronizeAccounts',
35-
RefreshConfirmationPrices = 'refreshConfirmationPrices',
3636
TrackTransaction = 'trackTransaction',
37+
RefreshConfirmationContext = 'refreshConfirmationContext',
3738
}
3839

3940
export const BackgroundEventMethodStruct = enums(
4041
Object.values(BackgroundEventMethod),
4142
);
4243

43-
export const RefreshConfirmationPricesParamsStruct = type({
44+
export const RefreshConfirmationContextParamsStruct = type({
4445
scope: KnownCaip2ChainIdStruct,
4546
interfaceId: nonempty(string()),
4647
interfaceKey: ConfirmationInterfaceKeyStruct,
48+
/** Refresher keys to run; omitted keys are skipped for this cycle. */
49+
refresherKeys: nonempty(array(ConfirmationContextRefresherKeyStruct)),
4750
});
4851

52+
export const RefreshConfirmationContextJsonRpcRequestStruct = assign(
53+
JsonRpcRequestStruct,
54+
object({
55+
method: literal(BackgroundEventMethod.RefreshConfirmationContext),
56+
params: RefreshConfirmationContextParamsStruct,
57+
}),
58+
);
59+
4960
export const TrackTransactionParamsStruct = type({
5061
txId: nonempty(string()),
5162
scope: KnownCaip2ChainIdStruct,
@@ -61,14 +72,6 @@ export const SyncAccountParamsStruct = object({
6172
),
6273
});
6374

64-
export const RefreshConfirmationPricesJsonRpcRequestStruct = assign(
65-
JsonRpcRequestStruct,
66-
object({
67-
method: literal(BackgroundEventMethod.RefreshConfirmationPrices),
68-
params: RefreshConfirmationPricesParamsStruct,
69-
}),
70-
);
71-
7275
export const TrackTransactionJsonRpcRequestStruct = assign(
7376
JsonRpcRequestStruct,
7477
object({
@@ -93,14 +96,6 @@ export const CronjobJsonRpcRequestStruct = object({
9396

9497
export type CronjobJsonRpcRequest = Infer<typeof CronjobJsonRpcRequestStruct>;
9598

96-
export type RefreshConfirmationPricesJsonRpcRequest = Infer<
97-
typeof RefreshConfirmationPricesJsonRpcRequestStruct
98-
>;
99-
100-
export type RefreshConfirmationPricesParams = Infer<
101-
typeof RefreshConfirmationPricesParamsStruct
102-
>;
103-
10499
export type TrackTransactionJsonRpcRequest = Infer<
105100
typeof TrackTransactionJsonRpcRequestStruct
106101
>;
@@ -112,3 +107,11 @@ export type SyncAccountJsonRpcRequest = Infer<
112107
>;
113108

114109
export type SyncAccountParams = Infer<typeof SyncAccountParamsStruct>;
110+
111+
export type RefreshConfirmationContextJsonRpcRequest = Infer<
112+
typeof RefreshConfirmationContextJsonRpcRequestStruct
113+
>;
114+
115+
export type RefreshConfirmationContextParams = Infer<
116+
typeof RefreshConfirmationContextParamsStruct
117+
>;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { KnownCaip2ChainId } from '../../../../api';
2+
import {
3+
ConfirmationInterfaceKey,
4+
type ContextWithPrices,
5+
FetchStatus,
6+
} from '../../../../ui/confirmation/api';
7+
import { getSlip44AssetId } from '../../../../utils';
8+
import type { RefreshConfirmationContextParams } from '../../api';
9+
import {
10+
ConfirmationContextRefresherKey,
11+
type ConfirmationDataContext,
12+
} from '../api';
13+
14+
const scope = KnownCaip2ChainId.Testnet;
15+
const nativeAssetId = getSlip44AssetId(scope);
16+
17+
export const confirmationContextRequestParams: RefreshConfirmationContextParams =
18+
{
19+
scope,
20+
interfaceId: 'interface-id-1',
21+
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
22+
refresherKeys: [ConfirmationContextRefresherKey.Prices],
23+
};
24+
25+
/**
26+
* Builds a valid confirmation refresh context for tests.
27+
*
28+
* @param overrides - Partial fields to override on the default context.
29+
* @returns A context that satisfies {@link ContextWithPricesStruct}.
30+
*/
31+
export function createConfirmationDataContext(
32+
overrides: Partial<ConfirmationDataContext> = {},
33+
): ConfirmationDataContext {
34+
return {
35+
tokenPrices: {
36+
[nativeAssetId]: null,
37+
} as ContextWithPrices['tokenPrices'],
38+
tokenPricesFetchStatus: FetchStatus.Fetching,
39+
currency: 'usd',
40+
preferences: { useExternalPricingData: true },
41+
...overrides,
42+
};
43+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { enums } from '@metamask/superstruct';
2+
import type { Json } from '@metamask/utils';
3+
4+
import type { ContextWithPrices } from '../../../ui/confirmation/api';
5+
6+
/** Identifies which confirmation context refreshers to run for a cron cycle. */
7+
export enum ConfirmationContextRefresherKey {
8+
Prices = 'prices',
9+
/** TODO: Reserved for security scan; wire in context when implemented. */
10+
Scan = 'scan',
11+
}
12+
13+
export const ConfirmationContextRefresherKeyStruct = enums(
14+
Object.values(ConfirmationContextRefresherKey),
15+
);
16+
17+
/**
18+
* Context the handler passes to refreshers.
19+
*/
20+
export type ConfirmationDataContext = Record<string, Json> & ContextWithPrices;
21+
22+
/** Outcome of one refresher cycle. `null` means no work was needed. */
23+
export type ConfirmationContextRefreshResult = {
24+
result: Record<string, Json>;
25+
reschedule: boolean;
26+
} | null;
27+
28+
/**
29+
* Contract for a single background data source (prices, security scan, …).
30+
*/
31+
export type IConfirmationContextRefresher = {
32+
/** Stable id used in cron params to select this refresher. */
33+
readonly key: ConfirmationContextRefresherKey;
34+
35+
/**
36+
* Returns whether this cycle should call.
37+
* When false, the handler uses {@link IConfirmationContextRefresher.recoveryResult} instead.
38+
*/
39+
shouldFetch: (ctx: ConfirmationDataContext) => boolean;
40+
41+
/**
42+
* Patch applied when {@link IConfirmationContextRefresher.shouldFetch} is false
43+
* (e.g. clear a stuck loading state). Return `null` when the context is already settled.
44+
*/
45+
recoveryResult: (
46+
ctx: ConfirmationDataContext,
47+
) => ConfirmationContextRefreshResult;
48+
49+
/**
50+
* Fetches fresh data when {@link IConfirmationContextRefresher.shouldFetch} is true.
51+
*/
52+
refresh: (
53+
ctx: ConfirmationDataContext,
54+
) => Promise<ConfirmationContextRefreshResult>;
55+
56+
/**
57+
* Returns false when this refresher cannot safely read `ctx` (missing or
58+
* malformed fields). Only enabled refreshers (by key) are validated and run.
59+
*/
60+
isValidContext: (ctx: Record<string, Json>) => boolean;
61+
};
62+
63+
/** Composed refreshers passed into the confirmation context handler. */
64+
export type ConfirmationContextRefreshers =
65+
readonly IConfirmationContextRefresher[];

0 commit comments

Comments
 (0)