Skip to content

Commit

Permalink
Add transaction insight caveat for accessing transaction origin (#902)
Browse files Browse the repository at this point in the history
* Add transaction insight caveat for accessing transaction origin

* Fix tests

* Simplify caveat

* Fix a validation issue

* Fix tests

* Update packages/snaps-controllers/src/snaps/endowments/transaction-insight.test.ts

Co-authored-by: Maarten Zuidhoorn <maarten@zuidhoorn.com>

Co-authored-by: Maarten Zuidhoorn <maarten@zuidhoorn.com>
  • Loading branch information
FrederikBolding and Mrtenz committed Nov 16, 2022
1 parent ff21440 commit d8ae445
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 16 deletions.
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();
});

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"');
});
});
});
117 changes: 115 additions & 2 deletions packages/snaps-controllers/src/snaps/endowments/transaction-insight.ts
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

0 comments on commit d8ae445

Please sign in to comment.