Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/snaps-controllers/src/snaps/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { HandlerType } from '@metamask/snaps-utils';

// These permissions are allowed without being on the allowlist.
export const ALLOWED_PERMISSIONS = Object.freeze([
'snap_confirmTransaction',
'snap_updateConfirmTransaction',
'snap_dialog',
'snap_manageState',
'snap_notify',
Expand Down
205 changes: 205 additions & 0 deletions packages/snaps-rpc-methods/src/restricted/confirmTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import type {
PermissionSpecificationBuilder,
RestrictedMethodOptions,
ValidPermissionSpecification,
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import {
create,
object,
optional,
record,
string,
} from '@metamask/superstruct';
import type {
CaipAssetType,
CaipChainId,
Json,
NonEmptyArray,
} from '@metamask/utils';
import {
CaipAssetTypeStruct,
CaipChainIdStruct,
isObject,
JsonStruct,
} from '@metamask/utils';

import type { MethodHooksObject } from '../utils';

const methodName = 'snap_confirmTransaction';

export type ConfirmTransactionParams = {
id?: string;
chainId: CaipChainId;
accountId: string;
to: string;
amount: string;
assetId?: CaipAssetType;
fee?: {
amount: string;
assetId?: CaipAssetType;
};
custom?: Record<string, Json>;
};

const ConfirmTransactionParametersStruct = object({
id: optional(string()),
chainId: CaipChainIdStruct,
accountId: string(),
to: string(),
amount: string(),
assetId: optional(CaipAssetTypeStruct),
fee: optional(
object({
amount: string(),
assetId: optional(CaipAssetTypeStruct),
}),
),
custom: optional(record(string(), JsonStruct)),
});

export type ConfirmTransactionMethodHooks = {
showUniversalTransactionConfirmation: (
snapId: string,
params: ConfirmTransactionParams,
) => Promise<boolean>;
};

type ConfirmTransactionSpecificationBuilderOptions = {
allowedCaveats?: Readonly<NonEmptyArray<string>> | null;
methodHooks: ConfirmTransactionMethodHooks;
};

type ConfirmTransactionSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.RestrictedMethod;
targetName: typeof methodName;
methodImplementation: ReturnType<typeof getConfirmTransactionImplementation>;
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
}>;

/**
* The specification builder for the `snap_confirmTransaction` permission.
* `snap_confirmTransaction` lets the Snap request user confirmation for a
* transaction on a supported non-EVM chain.
*
* @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_confirmTransaction` permission.
*/
const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.RestrictedMethod,
ConfirmTransactionSpecificationBuilderOptions,
ConfirmTransactionSpecification
> = ({
allowedCaveats = null,
methodHooks,
}: ConfirmTransactionSpecificationBuilderOptions) => {
return {
permissionType: PermissionType.RestrictedMethod,
targetName: methodName,
allowedCaveats,
methodImplementation: getConfirmTransactionImplementation({ methodHooks }),
subjectTypes: [SubjectType.Snap],
};
};

const methodHooks: MethodHooksObject<ConfirmTransactionMethodHooks> = {
showUniversalTransactionConfirmation: true,
};

/**
* Request user confirmation for a non-EVM transaction.
*
* @example
* ```json name="Manifest"
* {
* "initialPermissions": {
* "snap_confirmTransaction": {}
* }
* }
* ```
* ```ts name="Usage"
* const approved = await snap.request({
* method: 'snap_confirmTransaction',
* params: {
* chainId: 'solana:mainnet',
* accountId: 'solana:mainnet:account',
* to: 'to-address',
* amount: '1000000',
* fee: {
* amount: '5000',
* },
* },
* });
* ```
*/
export const confirmTransactionBuilder = Object.freeze({
targetName: methodName,
specificationBuilder,
methodHooks,
} as const);

/**
* Builds the method implementation for `snap_confirmTransaction`.
*
* @param options - The options.
* @param options.methodHooks - The RPC method hooks.
* @param options.methodHooks.showUniversalTransactionConfirmation - A function
* that shows the universal transaction confirmation UI.
* @returns The method implementation which returns `true` if approved, or
* `false` if rejected.
* @throws If the params are invalid, or the confirmation hook fails.
*/
export function getConfirmTransactionImplementation({
methodHooks: { showUniversalTransactionConfirmation },
}: ConfirmTransactionSpecificationBuilderOptions) {
return async function confirmTransactionImplementation(
args: RestrictedMethodOptions<ConfirmTransactionParams>,
): Promise<boolean> {
const {
params,
context: { origin: snapId },
} = args;

const validatedParams = getValidatedParams(params);

try {
return await showUniversalTransactionConfirmation(
snapId,
validatedParams,
);
} catch (error) {
throw rpcErrors.internal({
message: `Unable to confirm transaction: ${error.message}`,
});
}
};
}

/**
* Validates the confirm transaction method `params` and returns them cast to the
* correct type. Throws if validation fails.
*
* @param params - The unvalidated params object from the method request.
* @returns The validated confirm transaction method parameter object.
* @throws If the params are invalid.
*/
function getValidatedParams(params: unknown): ConfirmTransactionParams {
if (!isObject(params)) {
throw rpcErrors.invalidParams({
message: 'Invalid params: Expected params to be a single object.',
});
}

try {
return create(params, ConfirmTransactionParametersStruct);
} catch (error) {
throw rpcErrors.invalidParams({
message: `Invalid params: ${error.message}`,
});
}
}
10 changes: 10 additions & 0 deletions packages/snaps-rpc-methods/src/restricted/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
RestrictedMethodSpecificationConstraint,
} from '@metamask/permission-controller';

import type { ConfirmTransactionMethodHooks } from './confirmTransaction';
import { confirmTransactionBuilder } from './confirmTransaction';
import type { UpdateConfirmTransactionMethodHooks } from './updateConfirmTransaction';

Check failure on line 10 in packages/snaps-rpc-methods/src/restricted/index.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

`./updateConfirmTransaction` type import should occur after import of `./notify`
import { updateConfirmTransactionBuilder } from './updateConfirmTransaction';

Check failure on line 11 in packages/snaps-rpc-methods/src/restricted/index.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

`./updateConfirmTransaction` import should occur after import of `./notify`
import type { DialogMessengerActions } from './dialog';
import { dialogBuilder } from './dialog';
import type {
Expand Down Expand Up @@ -43,6 +47,8 @@

export { WALLET_SNAP_PERMISSION_KEY } from './invokeSnap';
export { getEncryptionEntropy } from './manageState';
export type { ConfirmTransactionParams } from './confirmTransaction';
export type { UpdateConfirmTransactionParams } from './updateConfirmTransaction';

export type RestrictedMethodActions =
| DialogMessengerActions
Expand All @@ -60,6 +66,8 @@
>;

export type RestrictedMethodHooks = GetBip32EntropyMethodHooks &
ConfirmTransactionMethodHooks &
UpdateConfirmTransactionMethodHooks &
GetBip32PublicKeyMethodHooks &
GetBip44EntropyMethodHooks &
GetEntropyHooks &
Expand All @@ -84,6 +92,8 @@
string,
RestrictedMethodPermissionBuilder
> = {
[confirmTransactionBuilder.targetName]: confirmTransactionBuilder,
[updateConfirmTransactionBuilder.targetName]: updateConfirmTransactionBuilder,
[dialogBuilder.targetName]: dialogBuilder,
[getBip32EntropyBuilder.targetName]: getBip32EntropyBuilder,
[getBip32PublicKeyBuilder.targetName]: getBip32PublicKeyBuilder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type {
PermissionSpecificationBuilder,
RestrictedMethodOptions,
ValidPermissionSpecification,
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import { create, object, optional, record, string } from '@metamask/superstruct';

Check failure on line 8 in packages/snaps-rpc-methods/src/restricted/updateConfirmTransaction.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `·create,·object,·optional,·record,·string·` with `⏎··create,⏎··object,⏎··optional,⏎··record,⏎··string,⏎`
import type { CaipAssetType, Json, NonEmptyArray } from '@metamask/utils';
import { CaipAssetTypeStruct, isObject, JsonStruct } from '@metamask/utils';

import type { MethodHooksObject } from '../utils';

const methodName = 'snap_updateConfirmTransaction';

export type UpdateConfirmTransactionParams = {
id: string;
fee?: {
amount: string;
assetId?: CaipAssetType;
};
custom?: Record<string, Json>;
};

const UpdateConfirmTransactionParametersStruct = object({
id: string(),
fee: optional(
object({
amount: string(),
assetId: optional(CaipAssetTypeStruct),
}),
),
custom: optional(record(string(), JsonStruct)),
});

export type UpdateConfirmTransactionMethodHooks = {
updateUniversalTransactionConfirmation: (
snapId: string,
params: UpdateConfirmTransactionParams,
) => Promise<void>;
};

type UpdateConfirmTransactionSpecificationBuilderOptions = {
allowedCaveats?: Readonly<NonEmptyArray<string>> | null;
methodHooks: UpdateConfirmTransactionMethodHooks;
};

type UpdateConfirmTransactionSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.RestrictedMethod;
targetName: typeof methodName;
methodImplementation: ReturnType<typeof getUpdateConfirmTransactionImplementation>;

Check failure on line 51 in packages/snaps-rpc-methods/src/restricted/updateConfirmTransaction.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `typeof·getUpdateConfirmTransactionImplementation` with `⏎····typeof·getUpdateConfirmTransactionImplementation⏎··`
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
}>;

const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.RestrictedMethod,
UpdateConfirmTransactionSpecificationBuilderOptions,
UpdateConfirmTransactionSpecification
> = ({
allowedCaveats = null,
methodHooks,
}: UpdateConfirmTransactionSpecificationBuilderOptions) => {
return {
permissionType: PermissionType.RestrictedMethod,
targetName: methodName,
allowedCaveats,
methodImplementation: getUpdateConfirmTransactionImplementation({
methodHooks,
}),
subjectTypes: [SubjectType.Snap],
};
};

const methodHooks: MethodHooksObject<UpdateConfirmTransactionMethodHooks> = {
updateUniversalTransactionConfirmation: true,
};

export const updateConfirmTransactionBuilder = Object.freeze({
targetName: methodName,
specificationBuilder,
methodHooks,
} as const);

export function getUpdateConfirmTransactionImplementation({

Check failure on line 84 in packages/snaps-rpc-methods/src/restricted/updateConfirmTransaction.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Missing JSDoc comment
methodHooks: { updateUniversalTransactionConfirmation },
}: UpdateConfirmTransactionSpecificationBuilderOptions) {
return async function updateConfirmTransactionImplementation(
args: RestrictedMethodOptions<UpdateConfirmTransactionParams>,
): Promise<null> {
const {
params,
context: { origin: snapId },
} = args;

const validatedParams = getValidatedParams(params);

try {
await updateUniversalTransactionConfirmation(snapId, validatedParams);
return null;
} catch (error) {
throw rpcErrors.internal({
message: `Unable to update transaction confirmation: ${error.message}`,
});
}
};
}

function getValidatedParams(params: unknown): UpdateConfirmTransactionParams {

Check failure on line 108 in packages/snaps-rpc-methods/src/restricted/updateConfirmTransaction.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Missing JSDoc comment
if (!isObject(params)) {
throw rpcErrors.invalidParams({
message: 'Invalid params: Expected params to be a single object.',
});
}

try {
return create(params, UpdateConfirmTransactionParametersStruct);
} catch (error) {
throw rpcErrors.invalidParams({
message: `Invalid params: ${error.message}`,
});
}
}
17 changes: 17 additions & 0 deletions packages/snaps-sdk/src/types/methods/confirm-transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { CaipAssetType, CaipChainId, Json } from '@metamask/utils';

export type ConfirmTransactionParams = {
id?: string;
chainId: CaipChainId;
accountId: string;
to: string;
amount: string;
assetId?: CaipAssetType;
fee?: {
amount: string;
assetId?: CaipAssetType;
};
custom?: Record<string, Json>;
};

export type ConfirmTransactionResult = boolean;
Loading
Loading