Skip to content

Commit 65637b5

Browse files
refactor: add confirmation refresher
1 parent 9c43641 commit 65637b5

11 files changed

Lines changed: 1089 additions & 11 deletions

File tree

packages/snap/src/context.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { ComputeFeeHandler } from './handlers/clientRequest/computeFee';
1414
import { SignAndSendTransactionHandler } from './handlers/clientRequest/signAndSendTransaction';
1515
import type { ICronjobRequestHandler } from './handlers/cronjob/api';
1616
import { BackgroundEventMethod } from './handlers/cronjob/api';
17+
import {
18+
ConfirmationPriceRefresher,
19+
RefreshConfirmationContextHandler,
20+
} from './handlers/cronjob/refreshConfirmationContext';
1721
import { RefreshConfirmationPricesHandler } from './handlers/cronjob/refreshConfirmationPrices';
1822
import { SyncAccountsHandler } from './handlers/cronjob/syncAccounts';
1923
import { TrackTransactionHandler } from './handlers/cronjob/trackTransaction';
@@ -166,6 +170,19 @@ const refreshConfirmationPricesHandler = new RefreshConfirmationPricesHandler({
166170
confirmationUIController,
167171
});
168172

173+
const confirmationPriceRefresher = new ConfirmationPriceRefresher({
174+
logger,
175+
priceService,
176+
});
177+
178+
const refreshConfirmationContextHandler = new RefreshConfirmationContextHandler(
179+
{
180+
logger,
181+
confirmationUIController,
182+
refreshers: [confirmationPriceRefresher],
183+
},
184+
);
185+
169186
const trackTransactionHandler = new TrackTransactionHandler({
170187
logger,
171188
networkService,
@@ -186,6 +203,8 @@ const cronjobMethodHandlers: Record<
186203
> = {
187204
[BackgroundEventMethod.RefreshConfirmationPrices]:
188205
refreshConfirmationPricesHandler,
206+
[BackgroundEventMethod.RefreshConfirmationContext]:
207+
refreshConfirmationContextHandler,
189208
[BackgroundEventMethod.TrackTransaction]: trackTransactionHandler,
190209
[BackgroundEventMethod.SynchronizeAccounts]: syncAccountsHandler,
191210
};

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BackgroundEventMethod,
55
BackgroundEventMethodStruct,
66
CronjobJsonRpcRequestStruct,
7+
RefreshConfirmationContextJsonRpcRequestStruct,
78
RefreshConfirmationPricesJsonRpcRequestStruct,
89
SyncAccountJsonRpcRequestStruct,
910
SyncAccountParamsStruct,
@@ -150,6 +151,32 @@ describe('Cronjob API structs', () => {
150151
});
151152
});
152153

154+
describe('RefreshConfirmationContextJsonRpcRequestStruct', () => {
155+
it('accepts refresh confirmation context requests', () => {
156+
const value = {
157+
...jsonRpcBase,
158+
method: BackgroundEventMethod.RefreshConfirmationContext,
159+
params: {
160+
scope: KnownCaip2ChainId.Mainnet,
161+
interfaceId: 'interface-id',
162+
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
163+
refresherKeys: ['prices'],
164+
},
165+
};
166+
assert(value, RefreshConfirmationContextJsonRpcRequestStruct);
167+
expect(value).toStrictEqual({
168+
...jsonRpcBase,
169+
method: BackgroundEventMethod.RefreshConfirmationContext,
170+
params: {
171+
scope: KnownCaip2ChainId.Mainnet,
172+
interfaceId: 'interface-id',
173+
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
174+
refresherKeys: ['prices'],
175+
},
176+
});
177+
});
178+
});
179+
153180
describe('TrackTransactionJsonRpcRequestStruct', () => {
154181
it('accepts track transaction requests', () => {
155182
const value = {

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

Lines changed: 26 additions & 0 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
/**
@@ -34,6 +35,7 @@ export enum BackgroundEventMethod {
3435
SynchronizeAccounts = 'synchronizeAccounts',
3536
RefreshConfirmationPrices = 'refreshConfirmationPrices',
3637
TrackTransaction = 'trackTransaction',
38+
RefreshConfirmationContext = 'refreshConfirmationContext',
3739
}
3840

3941
export const BackgroundEventMethodStruct = enums(
@@ -46,6 +48,22 @@ export const RefreshConfirmationPricesParamsStruct = type({
4648
interfaceKey: ConfirmationInterfaceKeyStruct,
4749
});
4850

51+
export const RefreshConfirmationContextParamsStruct = type({
52+
scope: KnownCaip2ChainIdStruct,
53+
interfaceId: nonempty(string()),
54+
interfaceKey: ConfirmationInterfaceKeyStruct,
55+
/** Refresher keys to run; omitted keys are skipped for this cycle. */
56+
refresherKeys: nonempty(array(ConfirmationContextRefresherKeyStruct)),
57+
});
58+
59+
export const RefreshConfirmationContextJsonRpcRequestStruct = assign(
60+
JsonRpcRequestStruct,
61+
object({
62+
method: literal(BackgroundEventMethod.RefreshConfirmationContext),
63+
params: RefreshConfirmationContextParamsStruct,
64+
}),
65+
);
66+
4967
export const TrackTransactionParamsStruct = type({
5068
txId: nonempty(string()),
5169
scope: KnownCaip2ChainIdStruct,
@@ -112,3 +130,11 @@ export type SyncAccountJsonRpcRequest = Infer<
112130
>;
113131

114132
export type SyncAccountParams = Infer<typeof SyncAccountParamsStruct>;
133+
134+
export type RefreshConfirmationContextJsonRpcRequest = Infer<
135+
typeof RefreshConfirmationContextJsonRpcRequestStruct
136+
>;
137+
138+
export type RefreshConfirmationContextParams = Infer<
139+
typeof RefreshConfirmationContextParamsStruct
140+
>;
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)