-
-
Notifications
You must be signed in to change notification settings - Fork 95
feat: add RPC methods described in (revised) EIP-7715 #396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
f339980
131d06f
44280d9
52b0346
9986e92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import type { | ||
Json, | ||
JsonRpcRequest, | ||
PendingJsonRpcResponse, | ||
} from '@metamask/utils'; | ||
import { klona } from 'klona'; | ||
|
||
import type { | ||
ProcessRequestExecutionPermissionsHook, | ||
RequestExecutionPermissionsRequestParams, | ||
RequestExecutionPermissionsResult, | ||
} from './wallet-request-execution-permissions'; | ||
import { walletRequestExecutionPermissions } from './wallet-request-execution-permissions'; | ||
|
||
const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a'; | ||
const CHAIN_ID_MOCK = '0x1'; | ||
const CONTEXT_MOCK = '0x123abc'; | ||
|
||
const REQUEST_MOCK = { | ||
params: [ | ||
{ | ||
chainId: CHAIN_ID_MOCK, | ||
address: ADDRESS_MOCK, | ||
signer: { | ||
type: 'account', | ||
data: { | ||
address: ADDRESS_MOCK, | ||
}, | ||
}, | ||
permission: { | ||
type: 'test-permission', | ||
isAdjustmentAllowed: true, | ||
data: { key: 'value' }, | ||
}, | ||
rules: [ | ||
{ | ||
type: 'test-rule', | ||
isAdjustmentAllowed: false, | ||
data: { ruleKey: 'ruleValue' }, | ||
}, | ||
], | ||
}, | ||
], | ||
} as unknown as JsonRpcRequest; | ||
|
||
const RESULT_MOCK: RequestExecutionPermissionsResult = [ | ||
{ | ||
chainId: CHAIN_ID_MOCK, | ||
address: ADDRESS_MOCK, | ||
signer: { | ||
type: 'account', | ||
data: { address: ADDRESS_MOCK }, | ||
}, | ||
permission: { | ||
type: 'test-permission', | ||
isAdjustmentAllowed: true, | ||
data: { key: 'value' }, | ||
}, | ||
rules: [ | ||
{ | ||
type: 'test-rule', | ||
isAdjustmentAllowed: false, | ||
data: { ruleKey: 'ruleValue' }, | ||
}, | ||
], | ||
context: CONTEXT_MOCK, | ||
}, | ||
]; | ||
|
||
describe('wallet_requestExecutionPermissions', () => { | ||
let request: JsonRpcRequest; | ||
let params: RequestExecutionPermissionsRequestParams; | ||
let response: PendingJsonRpcResponse<Json>; | ||
let processRequestExecutionPermissionsMock: jest.MockedFunction<ProcessRequestExecutionPermissionsHook>; | ||
|
||
async function callMethod() { | ||
return walletRequestExecutionPermissions(request, response, { | ||
processRequestExecutionPermissions: | ||
processRequestExecutionPermissionsMock, | ||
}); | ||
} | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
|
||
request = klona(REQUEST_MOCK); | ||
params = request.params as RequestExecutionPermissionsRequestParams; | ||
response = {} as PendingJsonRpcResponse<Json>; | ||
|
||
processRequestExecutionPermissionsMock = jest.fn(); | ||
processRequestExecutionPermissionsMock.mockResolvedValue(RESULT_MOCK); | ||
}); | ||
|
||
it('calls hook', async () => { | ||
await callMethod(); | ||
expect(processRequestExecutionPermissionsMock).toHaveBeenCalledWith( | ||
params, | ||
request, | ||
); | ||
}); | ||
|
||
it('returns result from hook', async () => { | ||
await callMethod(); | ||
expect(response.result).toStrictEqual(RESULT_MOCK); | ||
}); | ||
|
||
it('supports null rules', async () => { | ||
params[0].rules = null as never; | ||
|
||
await callMethod(); | ||
|
||
expect(processRequestExecutionPermissionsMock).toHaveBeenCalledWith( | ||
params, | ||
request, | ||
); | ||
}); | ||
|
||
it('supports optional address', async () => { | ||
params[0].address = undefined as never; | ||
|
||
await callMethod(); | ||
|
||
expect(processRequestExecutionPermissionsMock).toHaveBeenCalledWith( | ||
params, | ||
request, | ||
); | ||
}); | ||
|
||
it('throws if no hook', async () => { | ||
await expect( | ||
walletRequestExecutionPermissions(request, response, {}), | ||
).rejects.toMatchInlineSnapshot( | ||
jeffsmale90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`[Error: wallet_requestExecutionPermissions - no middleware configured]`, | ||
); | ||
}); | ||
|
||
it('throws if no params', async () => { | ||
request.params = undefined; | ||
|
||
await expect(callMethod()).rejects.toMatchInlineSnapshot(` | ||
[Error: Invalid params | ||
|
||
Expected an array value, but received: undefined] | ||
`); | ||
}); | ||
|
||
it('throws if missing properties', async () => { | ||
params[0].chainId = undefined as never; | ||
params[0].signer = undefined as never; | ||
params[0].permission = undefined as never; | ||
|
||
await expect(callMethod()).rejects.toMatchInlineSnapshot(` | ||
[Error: Invalid params | ||
|
||
0 > chainId - Expected a string, but received: undefined | ||
0 > signer - Expected an object, but received: undefined | ||
0 > permission - Expected an object, but received: undefined] | ||
`); | ||
}); | ||
|
||
it('throws if wrong types', async () => { | ||
params[0].chainId = 123 as never; | ||
params[0].address = 123 as never; | ||
params[0].permission = '123' as never; | ||
params[0].signer = { | ||
// Make signer an object but invalid to ensure object-type error messages are stable | ||
type: 123 as never, | ||
data: '123' as never, | ||
} as never; | ||
params[0].rules = [{} as never]; | ||
|
||
await expect(callMethod()).rejects.toMatchInlineSnapshot(` | ||
[Error: Invalid params | ||
|
||
0 > chainId - Expected a string, but received: 123 | ||
0 > address - Expected a string, but received: 123 | ||
0 > signer > type - Expected the literal \`"account"\`, but received: 123 | ||
0 > signer > data - Expected an object, but received: "123" | ||
0 > permission - Expected an object, but received: "123" | ||
0 > rules - Expected the value to satisfy a union of \`array | literal\`, but received: [object Object] | ||
0 > rules > 0 > type - Expected a string, but received: undefined | ||
0 > rules > 0 > isAdjustmentAllowed - Expected a value of type \`boolean\`, but received: \`undefined\` | ||
0 > rules > 0 > data - Expected an object, but received: undefined | ||
0 > rules - Expected the literal \`null\`, but received: [object Object]] | ||
`); | ||
}); | ||
|
||
it('throws if not hex', async () => { | ||
params[0].chainId = '123' as never; | ||
params[0].address = '123' as never; | ||
params[0].signer.data.address = '123' as never; | ||
|
||
await expect(callMethod()).rejects.toMatchInlineSnapshot(` | ||
[Error: Invalid params | ||
|
||
0 > chainId - Expected a string matching \`/^0x[0-9a-f]+$/\` but received "123" | ||
0 > address - Expected a string matching \`/^0x[0-9a-fA-F]{40}$/\` but received "123" | ||
0 > signer > data > address - Expected a string matching \`/^0x[0-9a-fA-F]{40}$/\` but received "123"] | ||
`); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||||||||
import { rpcErrors } from '@metamask/rpc-errors'; | ||||||||||||
import type { Infer } from '@metamask/superstruct'; | ||||||||||||
import { | ||||||||||||
array, | ||||||||||||
boolean, | ||||||||||||
literal, | ||||||||||||
object, | ||||||||||||
optional, | ||||||||||||
record, | ||||||||||||
string, | ||||||||||||
union, | ||||||||||||
unknown, | ||||||||||||
} from '@metamask/superstruct'; | ||||||||||||
import { | ||||||||||||
HexChecksumAddressStruct, | ||||||||||||
type Hex, | ||||||||||||
type Json, | ||||||||||||
type JsonRpcRequest, | ||||||||||||
type PendingJsonRpcResponse, | ||||||||||||
StrictHexStruct, | ||||||||||||
} from '@metamask/utils'; | ||||||||||||
|
||||||||||||
import { validateParams } from '../utils/validation'; | ||||||||||||
|
||||||||||||
const PermissionStruct = object({ | ||||||||||||
type: string(), | ||||||||||||
jiexi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
isAdjustmentAllowed: boolean(), | ||||||||||||
data: record(string(), unknown()), | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const RuleStruct = object({ | ||||||||||||
type: string(), | ||||||||||||
jiexi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
isAdjustmentAllowed: boolean(), | ||||||||||||
data: record(string(), unknown()), | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const AccountSignerStruct = object({ | ||||||||||||
type: literal('account'), | ||||||||||||
data: object({ | ||||||||||||
address: HexChecksumAddressStruct, | ||||||||||||
}), | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const PermissionRequestStruct = object({ | ||||||||||||
jeffsmale90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
chainId: StrictHexStruct, | ||||||||||||
address: optional(HexChecksumAddressStruct), | ||||||||||||
signer: AccountSignerStruct, | ||||||||||||
permission: PermissionStruct, | ||||||||||||
rules: optional(union([array(RuleStruct), literal(null)])), | ||||||||||||
}); | ||||||||||||
|
||||||||||||
export const RequestExecutionPermissionsStruct = array(PermissionRequestStruct); | ||||||||||||
|
||||||||||||
// RequestExecutionPermissions API types | ||||||||||||
export type RequestExecutionPermissionsRequestParams = Infer< | ||||||||||||
typeof RequestExecutionPermissionsStruct | ||||||||||||
>; | ||||||||||||
|
||||||||||||
export type RequestExecutionPermissionsResult = Json & | ||||||||||||
(Infer<typeof PermissionRequestStruct> & { | ||||||||||||
context: Hex; | ||||||||||||
})[]; | ||||||||||||
|
||||||||||||
export type ProcessRequestExecutionPermissionsHook = ( | ||||||||||||
request: RequestExecutionPermissionsRequestParams, | ||||||||||||
req: JsonRpcRequest, | ||||||||||||
) => Promise<RequestExecutionPermissionsResult>; | ||||||||||||
|
||||||||||||
export async function walletRequestExecutionPermissions( | ||||||||||||
req: JsonRpcRequest, | ||||||||||||
res: PendingJsonRpcResponse<Json>, | ||||||||||||
{ | ||||||||||||
processRequestExecutionPermissions, | ||||||||||||
}: { | ||||||||||||
processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; | ||||||||||||
}, | ||||||||||||
): Promise<void> { | ||||||||||||
if (!processRequestExecutionPermissions) { | ||||||||||||
throw rpcErrors.resourceNotFound( | ||||||||||||
'wallet_requestExecutionPermissions - no middleware configured', | ||||||||||||
jiexi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
); | ||||||||||||
} | ||||||||||||
|
||||||||||||
const { params } = req; | ||||||||||||
|
||||||||||||
validateParams(params, RequestExecutionPermissionsStruct); | ||||||||||||
|
||||||||||||
res.result = await processRequestExecutionPermissions(params, req); | ||||||||||||
} | ||||||||||||
Comment on lines
+88
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe you'll need to pass
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess they aren't doing this in the 5792 methods you used as a model. Need to figure out why that's ok 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From
The async middleware function accepts a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these two type names be singular to stay consistent with the singular form of
wallet_revokeExecutionPermission
?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are absolutely correct!