-
Notifications
You must be signed in to change notification settings - Fork 542
/
manageState.ts
210 lines (190 loc) · 6.27 KB
/
manageState.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import {
PermissionSpecificationBuilder,
PermissionType,
RestrictedMethodOptions,
ValidPermissionSpecification,
} from '@metamask/permission-controller';
import {
Json,
NonEmptyArray,
isObject,
validateJsonAndGetSize,
} from '@metamask/utils';
import { ethErrors } from 'eth-rpc-errors';
const methodName = 'snap_manageState';
export type ManageStateMethodHooks = {
/**
* A function that clears the state of the requesting Snap.
*/
clearSnapState: (snapId: string) => Promise<void>;
/**
* A function that gets the state of the requesting Snap.
*
* @returns The current state of the Snap.
*/
getSnapState: (snapId: string) => Promise<Record<string, Json>>;
/**
* A function that updates the state of the requesting Snap.
*
* @param newState - The new state of the Snap.
*/
updateSnapState: (
snapId: string,
newState: Record<string, Json>,
) => Promise<void>;
};
type ManageStateSpecificationBuilderOptions = {
allowedCaveats?: Readonly<NonEmptyArray<string>> | null;
methodHooks: ManageStateMethodHooks;
};
type ManageStateSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.RestrictedMethod;
targetKey: typeof methodName;
methodImplementation: ReturnType<typeof getManageStateImplementation>;
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
}>;
/**
* The specification builder for the `snap_manageState` permission.
* `snap_manageState` lets the Snap store and manage some of its state on
* your device.
*
* @param options - The specification builder options.
* @param options.allowedCaveats - The optional allowed caveats for the permission.
* @param options.methodHooks - The RPC method hooks needed by the method implementation.
* @returns The specification for the `snap_manageState` permission.
*/
export const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.RestrictedMethod,
ManageStateSpecificationBuilderOptions,
ManageStateSpecification
> = ({
allowedCaveats = null,
methodHooks,
}: ManageStateSpecificationBuilderOptions) => {
return {
permissionType: PermissionType.RestrictedMethod,
targetKey: methodName,
allowedCaveats,
methodImplementation: getManageStateImplementation(methodHooks),
};
};
export const manageStateBuilder = Object.freeze({
targetKey: methodName,
specificationBuilder,
methodHooks: {
clearSnapState: true,
getSnapState: true,
updateSnapState: true,
},
} as const);
export enum ManageStateOperation {
ClearState = 'clear',
GetState = 'get',
UpdateState = 'update',
}
export type ManageStateArgs = {
operation: ManageStateOperation;
newState?: Record<string, Json>;
};
export const STORAGE_SIZE_LIMIT = 104857600; // In bytes (100MB)
/**
* Builds the method implementation for `snap_manageState`.
*
* @param hooks - The RPC method hooks.
* @param hooks.clearSnapState - A function that clears the state stored for a snap.
* @param hooks.getSnapState - A function that fetches the persisted decrypted state for a snap.
* @param hooks.updateSnapState - A function that updates the state stored for a snap.
* @returns The method implementation which either returns `null` for a successful state update/deletion or returns the decrypted state.
* @throws If the params are invalid.
*/
export function getManageStateImplementation({
clearSnapState,
getSnapState,
updateSnapState,
}: ManageStateMethodHooks) {
return async function manageState(
options: RestrictedMethodOptions<ManageStateArgs>,
): Promise<null | Record<string, Json>> {
const {
params = {},
method,
context: { origin },
} = options;
const { operation, newState } = getValidatedParams(params, method);
switch (operation) {
case ManageStateOperation.ClearState:
await clearSnapState(origin);
return null;
case ManageStateOperation.GetState:
return await getSnapState(origin);
case ManageStateOperation.UpdateState: {
await updateSnapState(origin, newState as Record<string, Json>);
return null;
}
default:
throw ethErrors.rpc.invalidParams(
`Invalid ${method} operation: "${operation as string}"`,
);
}
};
}
/**
* Validates the manageState method `params` and returns them cast to the correct
* type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @param method - RPC method name used for debugging errors.
* @param storageSizeLimit - Maximum allowed size (in bytes) of a new state object.
* @returns The validated method parameter object.
*/
export function getValidatedParams(
params: unknown,
method: string,
storageSizeLimit = STORAGE_SIZE_LIMIT,
): ManageStateArgs {
if (!isObject(params)) {
throw ethErrors.rpc.invalidParams({
message: 'Expected params to be a single object.',
});
}
const { operation, newState } = params;
if (
!operation ||
typeof operation !== 'string' ||
!(Object.values(ManageStateOperation) as string[]).includes(operation)
) {
throw ethErrors.rpc.invalidParams({
message: 'Must specify a valid manage state "operation".',
});
}
if (operation === ManageStateOperation.UpdateState) {
if (!isObject(newState)) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be a plain object.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}
const [isValid, plainTextSizeInBytes] = validateJsonAndGetSize(newState);
if (!isValid) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must be JSON serializable.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
} else if (plainTextSizeInBytes > storageSizeLimit) {
throw ethErrors.rpc.invalidParams({
message: `Invalid ${method} "updateState" parameter: The new state must not exceed ${storageSizeLimit} bytes in size.`,
data: {
receivedNewState:
typeof newState === 'undefined' ? 'undefined' : newState,
},
});
}
}
return params as ManageStateArgs;
}