Skip to content
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

Add transaction insight caveat for accessing transaction origin #902

Merged
merged 7 commits into from Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/snaps-controllers/src/snaps/endowments/index.ts
Expand Up @@ -7,7 +7,11 @@ import {
} from './cronjob';
import { longRunningEndowmentBuilder } from './long-running';
import { networkAccessEndowmentBuilder } from './network-access';
import { transactionInsightEndowmentBuilder } from './transaction-insight';
import {
getTransactionInsightCaveatMapper,
transactionInsightCaveatSpecifications,
transactionInsightEndowmentBuilder,
} from './transaction-insight';
import {
keyringEndowmentBuilder,
keyringCaveatSpecifications,
Expand All @@ -29,6 +33,7 @@ export const endowmentPermissionBuilders = {
export const endowmentCaveatSpecifications = {
...keyringCaveatSpecifications,
...cronjobCaveatSpecifications,
...transactionInsightCaveatSpecifications,
};

export const endowmentCaveatMappers: Record<
Expand All @@ -37,6 +42,8 @@ export const endowmentCaveatMappers: Record<
> = {
[keyringEndowmentBuilder.targetKey]: getKeyringCaveatMapper,
[cronjobEndowmentBuilder.targetKey]: getCronjobCaveatMapper,
[transactionInsightEndowmentBuilder.targetKey]:
getTransactionInsightCaveatMapper,
};

export * from './enum';
@@ -1,18 +1,180 @@
import { PermissionType } from '@metamask/controllers';
import { transactionInsightEndowmentBuilder } from './transaction-insight';
import { PermissionConstraint, PermissionType } from '@metamask/controllers';
import { SnapCaveatType } from '@metamask/snaps-utils';
import {
getTransactionInsightCaveatMapper,
getTransactionOriginCaveat,
transactionInsightCaveatSpecifications,
transactionInsightEndowmentBuilder,
} from './transaction-insight';
import { SnapEndowments } from '.';

describe('endowment:transaction-insight', () => {
const specification = transactionInsightEndowmentBuilder.specificationBuilder(
{},
);
it('builds the expected permission specification', () => {
const specification =
transactionInsightEndowmentBuilder.specificationBuilder({});
expect(specification).toStrictEqual({
permissionType: PermissionType.Endowment,
targetKey: SnapEndowments.TransactionInsight,
allowedCaveats: [SnapCaveatType.TransactionOrigin],
endowmentGetter: expect.any(Function),
allowedCaveats: null,
validator: expect.any(Function),
});

expect(specification.endowmentGetter()).toBeUndefined();
});

describe('validator', () => {
it('allows no caveats', () => {
expect(() =>
// @ts-expect-error Missing required permission types.
specification.validator({}),
).not.toThrow('Expected a single "transactionOrigin" caveat.');
FrederikBolding marked this conversation as resolved.
Show resolved Hide resolved
});

it('throws if the caveat is not a single "transactionOrigin"', () => {
expect(() =>
// @ts-expect-error Missing other required permission types.
specification.validator({
caveats: [{ type: 'foo', value: 'bar' }],
}),
).toThrow('Expected a single "transactionOrigin" caveat.');

expect(() =>
// @ts-expect-error Missing other required permission types.
specification.validator({
caveats: [
{ type: 'transactionOrigin', value: [] },
{ type: 'transactionOrigin', value: [] },
],
}),
).toThrow('Expected a single "transactionOrigin" caveat.');
});
});
});

describe('getTransactionOriginCaveat', () => {
it('returns the value from a transaction insight permission', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.TransactionOrigin,
value: true,
},
],
};

expect(getTransactionOriginCaveat(permission)).toStrictEqual(true);
});

it('returns null if the input is undefined', () => {
expect(getTransactionOriginCaveat(undefined)).toBeNull();
});

it('returns null if the permission does not have caveats', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: null,
};

expect(getTransactionOriginCaveat(permission)).toBeNull();
});

it('throws if the permission does not have exactly one caveat', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.TransactionOrigin,
value: true,
},
{
type: SnapCaveatType.TransactionOrigin,
value: true,
},
],
};

expect(() => getTransactionOriginCaveat(permission)).toThrow(
'Assertion failed',
);
});

it('throws if the first caveat is not a "snapKeyring" caveat', () => {
const permission: PermissionConstraint = {
date: 0,
parentCapability: 'foo',
invoker: 'bar',
id: 'baz',
caveats: [
{
type: SnapCaveatType.PermittedCoinTypes,
value: 'foo',
},
],
};

expect(() => getTransactionOriginCaveat(permission)).toThrow(
'Assertion failed',
);
});
});

describe('getTransactionInsightCaveatMapper', () => {
it('maps input to a caveat', () => {
expect(
getTransactionInsightCaveatMapper({
allowTransactionOrigin: true,
}),
).toStrictEqual({
caveats: [
{
type: 'transactionOrigin',
value: true,
},
],
});
});

it('does not include caveat if input is empty object', () => {
expect(getTransactionInsightCaveatMapper({})).toStrictEqual({
caveats: null,
});
});
});

describe('transactionInsightCaveatSpecifications', () => {
describe('validator', () => {
it('throws if the caveat values are invalid', () => {
expect(() =>
transactionInsightCaveatSpecifications[
SnapCaveatType.TransactionOrigin
].validator?.(
// @ts-expect-error Missing value type.
{
type: SnapCaveatType.TransactionOrigin,
},
),
).toThrow('Expected a plain object.');

expect(() =>
transactionInsightCaveatSpecifications[
SnapCaveatType.TransactionOrigin
].validator?.({
type: SnapCaveatType.TransactionOrigin,
value: undefined,
}),
).toThrow('Expected caveat value to have type "boolean"');
});
});
});
Expand Up @@ -3,7 +3,21 @@ import {
PermissionType,
EndowmentGetterParams,
ValidPermissionSpecification,
PermissionValidatorConstraint,
PermissionConstraint,
CaveatSpecificationConstraint,
Caveat,
} from '@metamask/controllers';
import {
assert,
hasProperty,
isObject,
isPlainObject,
Json,
NonEmptyArray,
} from '@metamask/utils';
import { SnapCaveatType } from '@metamask/snaps-utils';
import { ethErrors } from 'eth-rpc-errors';
import { SnapEndowments } from './enum';

const permissionName = SnapEndowments.TransactionInsight;
Expand All @@ -12,7 +26,8 @@ type TransactionInsightEndowmentSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.Endowment;
targetKey: typeof permissionName;
endowmentGetter: (_options?: EndowmentGetterParams) => undefined;
allowedCaveats: null;
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
validator: PermissionValidatorConstraint;
}>;

/**
Expand All @@ -30,12 +45,110 @@ const specificationBuilder: PermissionSpecificationBuilder<
return {
permissionType: PermissionType.Endowment,
targetKey: permissionName,
allowedCaveats: null,
allowedCaveats: [SnapCaveatType.TransactionOrigin],
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => undefined,
validator: ({ caveats }) => {
if (
(caveats !== null && caveats?.length > 1) ||
(caveats?.length === 1 &&
caveats[0].type !== SnapCaveatType.TransactionOrigin)
) {
throw ethErrors.rpc.invalidParams({
message: `Expected a single "${SnapCaveatType.TransactionOrigin}" caveat.`,
});
}
},
};
};

export const transactionInsightEndowmentBuilder = Object.freeze({
targetKey: permissionName,
specificationBuilder,
} as const);

/**
* Validates the type of the caveat value.
*
* @param caveat - The caveat to validate.
* @throws If the caveat value is invalid.
*/
function validateCaveat(caveat: Caveat<string, any>): void {
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat)) {
throw ethErrors.rpc.invalidParams({
message: 'Expected a plain object.',
});
}

const { value } = caveat;

assert(
typeof value === 'boolean',
'Expected caveat value to have type "boolean"',
);
}

/**
* Map a raw value from the `initialPermissions` to a caveat specification.
* Note that this function does not do any validation, that's handled by the
* PermissionsController when the permission is requested.
*
* @param value - The raw value from the `initialPermissions`.
* @returns The caveat specification.
*/
export function getTransactionInsightCaveatMapper(
value: Json,
): Pick<PermissionConstraint, 'caveats'> {
if (
!value ||
!isObject(value) ||
(isObject(value) && Object.keys(value).length === 0)
) {
return { caveats: null };
}
return {
caveats: [
{
type: SnapCaveatType.TransactionOrigin,
value:
hasProperty(value, 'allowTransactionOrigin') &&
value.allowTransactionOrigin,
},
],
};
}

/**
* Getter function to get the transaction origin caveat from a permission.
*
* This does basic validation of the caveat, but does not validate the type or
* value of the namespaces object itself, as this is handled by the
* `PermissionsController` when the permission is requested.
*
* @param permission - The permission to get the transaction origin caveat from.
* @returns The transaction origin, or `null` if the permission does not have a
* transaction origin caveat.
*/
export function getTransactionOriginCaveat(
permission?: PermissionConstraint,
): boolean | null {
if (!permission?.caveats) {
return null;
}

assert(permission.caveats.length === 1);
assert(permission.caveats[0].type === SnapCaveatType.TransactionOrigin);

const caveat = permission.caveats[0] as Caveat<string, boolean>;

return caveat.value ?? null;
}

export const transactionInsightCaveatSpecifications: Record<
SnapCaveatType.TransactionOrigin,
CaveatSpecificationConstraint
> = {
[SnapCaveatType.TransactionOrigin]: Object.freeze({
type: SnapCaveatType.TransactionOrigin,
validator: (caveat: Caveat<string, any>) => validateCaveat(caveat),
}),
};
4 changes: 2 additions & 2 deletions packages/snaps-execution-environments/jest.config.js
Expand Up @@ -7,8 +7,8 @@ module.exports = deepmerge(baseConfig, {
global: {
branches: 89.7,
functions: 90.43,
lines: 88.45,
statements: 88.45,
lines: 88.46,
statements: 88.46,
},
},
testEnvironment: '<rootDir>/jest.environment.js',
Expand Down
Expand Up @@ -853,7 +853,7 @@ describe('BaseSnapExecutor', () => {

it('supports onTransaction export', async () => {
const CODE = `
module.exports.onTransaction = ({ transaction, chainId }) => ({ transaction, chainId });
module.exports.onTransaction = ({ transaction, chainId, transactionOrigin }) => ({ transaction, chainId, transactionOrigin });
`;
const executor = new TestSnapExecutor();

Expand All @@ -869,7 +869,11 @@ describe('BaseSnapExecutor', () => {
// We also have to decide on the shape of that object.
const transaction = { maxFeePerGas: '0x' };

const params = { transaction, chainId: 'eip155:1' };
const params = {
transaction,
chainId: 'eip155:1',
transactionOrigin: null,
};

await executor.writeCommand({
jsonrpc: '2.0',
Expand Down