Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/solana-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Emit `transaction_type` on the transaction lifecycle events. ([#401](https://github.com/MetaMask/internal-snaps/pull/401))

### Changed

- Render the confirmation "Estimated changes" section with the shared `EstimatedChanges` component from `@metamask/snap-networks-utils` ([#384](https://github.com/MetaMask/internal-snaps/pull/384))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TransactionType } from '@metamask/keyring-api';
import type { Logger } from '@metamask/snap-networks-utils';
import { InvalidParamsError } from '@metamask/snaps-sdk';
import type { JsonRpcRequest } from '@metamask/snaps-sdk';
Expand Down Expand Up @@ -1105,6 +1106,8 @@ describe('ClientRequestHandler', () => {
expect.any(String),
Network.Mainnet,
'metamask',
undefined,
TransactionType.TokenApprove,
);
expect(result).toStrictEqual({ signature: mockSignature });
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FeeType } from '@metamask/keyring-api';
import { FeeType, TransactionType } from '@metamask/keyring-api';
import { normalizeError } from '@metamask/snap-networks-utils';
import type { Logger } from '@metamask/snap-networks-utils';
import { InvalidParamsError, MethodNotFoundError } from '@metamask/snaps-sdk';
Expand Down Expand Up @@ -463,6 +463,8 @@ export class ClientRequestHandler {
base64EncodedTransaction,
network,
METAMASK_ORIGIN,
undefined,
TransactionType.TokenApprove,
);

const result = { signature };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TransactionType } from '@metamask/keyring-api';
import { UuidStruct } from '@metamask/snap-networks-utils';
import { InternalError } from '@metamask/snaps-sdk';
import type { OnCronjobHandler } from '@metamask/snaps-sdk';
import { assert, literal, object, string } from '@metamask/superstruct';
import { assert, enums, literal, object, string } from '@metamask/superstruct';

import { analyticsService, keyring } from '../../../../snapContext';
import logger from '../../../utils/logger';
Expand All @@ -17,6 +18,7 @@ export const OnTransactionAddedRequestStruct = object({
metadata: object({
scope: NetworkStruct,
origin: string(),
transactionType: enums(Object.values(TransactionType)),
}),
}),
});
Expand All @@ -42,6 +44,7 @@ export const onTransactionAdded: OnCronjobHandler = async ({ request }) => {
origin: metadata.origin,
accountType: account.type,
chainIdCaip: metadata.scope,
transactionType: metadata.transactionType,
});
} catch (error) {
logger.error(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TransactionType } from '@metamask/keyring-api';
import { UuidStruct } from '@metamask/snap-networks-utils';
import { InternalError } from '@metamask/snaps-sdk';
import type { OnCronjobHandler } from '@metamask/snaps-sdk';
import { assert, literal, object, string } from '@metamask/superstruct';
import { assert, enums, literal, object, string } from '@metamask/superstruct';

import { analyticsService, keyring } from '../../../../snapContext';
import logger from '../../../utils/logger';
Expand All @@ -17,6 +18,7 @@ export const OnTransactionApprovedRequestStruct = object({
metadata: object({
scope: NetworkStruct,
origin: string(),
transactionType: enums(Object.values(TransactionType)),
}),
}),
});
Expand All @@ -42,6 +44,7 @@ export const onTransactionApproved: OnCronjobHandler = async ({ request }) => {
origin: metadata.origin,
accountType: account.type,
chainIdCaip: metadata.scope,
transactionType: metadata.transactionType,
});
} catch (error) {
logger.error(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TransactionType } from '@metamask/keyring-api';

import { analyticsService, keyring } from '../../../../snapContext';
import { Network } from '../../../constants/solana';
import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../../test/mocks/solana-keyring-accounts';
Expand Down Expand Up @@ -53,6 +55,7 @@ describe('transaction background events', () => {
metadata: {
scope: Network.Mainnet,
origin,
transactionType: TransactionType.Send,
},
},
},
Expand All @@ -63,6 +66,7 @@ describe('transaction background events', () => {
origin,
accountType: account.type,
chainIdCaip: Network.Mainnet,
transactionType: TransactionType.Send,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TransactionType } from '@metamask/keyring-api';
import { UuidStruct } from '@metamask/snap-networks-utils';
import { InternalError } from '@metamask/snaps-sdk';
import type { OnCronjobHandler } from '@metamask/snaps-sdk';
import { assert, literal, object, string } from '@metamask/superstruct';
import { assert, enums, literal, object, string } from '@metamask/superstruct';

import { analyticsService, keyring } from '../../../../snapContext';
import logger from '../../../utils/logger';
Expand All @@ -17,6 +18,7 @@ export const OnTransactionRejectedRequestStruct = object({
metadata: object({
scope: NetworkStruct,
origin: string(),
transactionType: enums(Object.values(TransactionType)),
}),
}),
});
Expand All @@ -42,6 +44,7 @@ export const onTransactionRejected: OnCronjobHandler = async ({ request }) => {
origin: metadata.origin,
accountType: account.type,
chainIdCaip: metadata.scope,
transactionType: metadata.transactionType,
});
} catch (error) {
logger.error(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { SolMethod } from '@metamask/keyring-api';
import { SolMethod, TransactionType } from '@metamask/keyring-api';

import { render as renderConfirmSignIn } from '../../../features/confirmation/views/ConfirmSignIn/render';
import { render as renderConfirmSignMessage } from '../../../features/confirmation/views/ConfirmSignMessage/render';
import { render as renderConfirmTransactionRequest } from '../../../features/confirmation/views/ConfirmTransactionRequest/render';
import { METAMASK_ORIGIN } from '../../constants/solana';
import { ScheduleBackgroundEventMethod } from '../../handlers/onCronjob/backgroundEvents/ScheduleBackgroundEventMethod';
import type { SolanaKeyringRequest } from '../../handlers/onKeyringRequest/structs';
import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../test/mocks/solana-keyring-accounts';
Expand Down Expand Up @@ -59,6 +60,7 @@ type ScheduledEventCall = {
metadata: {
scope: SolanaKeyringRequest['scope'];
origin: string;
transactionType: TransactionType;
};
};
};
Expand Down Expand Up @@ -94,9 +96,15 @@ describe('ConfirmationHandler', () => {
* keeps `Added` covered exactly like the terminal events.
*
* @param method - The lifecycle event method name.
* @param origin - The origin carried in the event metadata.
* @param transactionType - The classification carried in the event metadata.
* @returns The expected `snap.request` call.
*/
const expectedScheduleCall = (method: string): ScheduledEventCall => ({
const expectedScheduleCall = (
method: string,
origin = MOCK_ORIGIN,
transactionType = TransactionType.Unknown,
): ScheduledEventCall => ({
method: 'snap_scheduleBackgroundEvent',
params: {
duration: 'PT1S',
Expand All @@ -106,7 +114,8 @@ describe('ConfirmationHandler', () => {
accountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id,
metadata: {
scope: mockTransactionRequest.scope,
origin: MOCK_ORIGIN,
origin,
transactionType,
},
},
},
Expand Down Expand Up @@ -219,6 +228,32 @@ describe('ConfirmationHandler', () => {
},
);

it('reports a MetaMask-originated transaction as a send', async () => {
mockRenderConfirmTransactionRequest.mockResolvedValue(true);

await confirmationHandler.handleKeyringRequest(
{ ...request, origin: METAMASK_ORIGIN },
MOCK_SOLANA_KEYRING_ACCOUNT_0,
);

expect(mockSnapRequest.mock.calls).toStrictEqual([
[
expectedScheduleCall(
ScheduleBackgroundEventMethod.OnTransactionAdded,
METAMASK_ORIGIN,
TransactionType.Send,
),
],
[
expectedScheduleCall(
ScheduleBackgroundEventMethod.OnTransactionApproved,
METAMASK_ORIGIN,
TransactionType.Send,
),
],
]);
});

it('does not schedule Approved when the user rejects', async () => {
mockRenderConfirmTransactionRequest.mockResolvedValue(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../../features/confirmation/views/ConfirmTransactionRequest/render';
import { ScheduleBackgroundEventMethod } from '../../handlers/onCronjob/backgroundEvents/ScheduleBackgroundEventMethod';
import type { SolanaKeyringRequest } from '../../handlers/onKeyringRequest/structs';
import { resolveTransactionType } from '../../utils/transactionType';
import {
SolanaSignAndSendTransactionRequestStruct,
SolanaSignTransactionRequestStruct,
Expand Down Expand Up @@ -82,6 +83,12 @@ export class ConfirmationHandler {
origin,
} = request;

/**
* Resolved once and carried by all three lifecycle events, so Added,
* Approved and Rejected always agree on the classification.
*/
const transactionType = resolveTransactionType({ origin });

// Trigger the side effects that need to happen when the transaction is shown in confirmation UI
await snap.request({
method: 'snap_scheduleBackgroundEvent',
Expand All @@ -94,6 +101,7 @@ export class ConfirmationHandler {
metadata: {
scope,
origin,
transactionType,
},
},
},
Expand Down Expand Up @@ -122,6 +130,7 @@ export class ConfirmationHandler {
metadata: {
scope,
origin,
transactionType,
},
},
},
Expand All @@ -143,6 +152,7 @@ export class ConfirmationHandler {
metadata: {
scope,
origin,
transactionType,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SolMethod } from '@metamask/keyring-api';
import { SolMethod, TransactionType } from '@metamask/keyring-api';
import type { AnalyticsService } from '@metamask/snap-networks-utils';

import { METAMASK_ORIGIN, Network } from '../../constants/solana';
Expand Down Expand Up @@ -396,9 +396,47 @@ describe('WalletService', () => {
accountType: fromAccount.type,
chainIdCaip: scope,
origin: 'https://metamask.io',
transactionType: TransactionType.Unknown,
});
});

it('reports a MetaMask-originated transaction as a send', async () => {
await service.signAndSendTransaction(
fromAccount,
transactionMessageBase64Encoded,
scope,
METAMASK_ORIGIN,
);

expect(
mockAnalyticsService.trackTransactionSubmitted,
).toHaveBeenCalledWith(
expect.objectContaining({
origin: METAMASK_ORIGIN,
transactionType: TransactionType.Send,
}),
);
});

it('uses a caller-supplied classification over the origin', async () => {
await service.signAndSendTransaction(
fromAccount,
transactionMessageBase64Encoded,
scope,
METAMASK_ORIGIN,
undefined,
TransactionType.TokenApprove,
);

expect(
mockAnalyticsService.trackTransactionSubmitted,
).toHaveBeenCalledWith(
expect.objectContaining({
transactionType: TransactionType.TokenApprove,
}),
);
});

it('saves a pending unconfirmed transaction after broadcasting', async () => {
await service.signAndSendTransaction(
fromAccount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SLIP10Node } from '@metamask/key-tree';
import { SolMethod } from '@metamask/keyring-api';
import type { TransactionType } from '@metamask/keyring-api';
import { normalizeError } from '@metamask/snap-networks-utils';
import type {
AnalyticsService,
Expand Down Expand Up @@ -38,6 +39,7 @@ import { trackError } from '../../utils/errors';
import { getSolanaCoinTypeNode } from '../../utils/getBip32Entropy';
import { getSolanaExplorerUrl } from '../../utils/getSolanaExplorerUrl';
import logger from '../../utils/logger';
import { resolveTransactionType } from '../../utils/transactionType';
import { Base58Struct, Base64Struct } from '../../validation/structs';
import type { SolanaConnection } from '../connection';
import type { Signer } from '../signer/Signer';
Expand Down Expand Up @@ -275,6 +277,7 @@ export class WalletService {
* @param options.preflightCommitment - The preflight commitment.
* @param options.maxRetries - The maximum number of retries.
* @param options.commitment - The commitment.
* @param transactionType - A classification already known by the caller. When omitted, it is derived from the origin.
* @returns A Promise that resolves to the signed transaction.
*/
async signAndSendTransaction(
Expand All @@ -283,6 +286,7 @@ export class WalletService {
scope: Network,
origin: string,
options?: SolanaSignAndSendTransactionOptions,
transactionType?: TransactionType,
): Promise<SolanaSignAndSendTransactionResponse> {
this.#logger.log('Signing and sending transaction', account);

Expand Down Expand Up @@ -344,6 +348,7 @@ export class WalletService {
origin,
accountType: account.type,
chainIdCaip: scope,
transactionType: resolveTransactionType({ origin, transactionType }),
});

// Immediately save and emit a pending transaction, so the client can show
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { TransactionType } from '@metamask/keyring-api';

import { METAMASK_ORIGIN } from '../constants/solana';
import { resolveTransactionType } from './transactionType';

describe('resolveTransactionType', () => {
it('uses the classification supplied by the caller when present', () => {
expect(
resolveTransactionType({
origin: METAMASK_ORIGIN,
transactionType: TransactionType.Receive,
}),
).toBe(TransactionType.Receive);
});

it('prefers the supplied classification over the origin-derived one', () => {
expect(
resolveTransactionType({
origin: 'https://example.com',
transactionType: TransactionType.Swap,
}),
).toBe(TransactionType.Swap);
});

it('reports a MetaMask-originated transaction as a send', () => {
expect(resolveTransactionType({ origin: METAMASK_ORIGIN })).toBe(
TransactionType.Send,
);
});

it('reports a dApp-originated transaction as unknown', () => {
expect(resolveTransactionType({ origin: 'https://example.com' })).toBe(
TransactionType.Unknown,
);
});

it('does not treat the MetaMask origin URL as the MetaMask origin', () => {
expect(resolveTransactionType({ origin: 'https://metamask.io' })).toBe(
TransactionType.Unknown,
);
});
});
Loading
Loading