Skip to content
4 changes: 4 additions & 0 deletions packages/transaction-controller/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]

### Changed

- Add RPC method, chain ID, and endpoint type context to transaction provider errors, including raw transaction submission failures ([#9144](https://github.com/MetaMask/core/pull/9144))

## [68.0.0]

### Changed
Expand Down
29 changes: 11 additions & 18 deletions packages/transaction-controller/src/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2785,7 +2785,8 @@ describe('TransactionController', () => {
);
});

it('throws with data message if publish fails', async () => {
it('throws with contextual rpcRequest message if publish fails', async () => {
const rpcErrorMessage = `RPC 0x5 Custom eth_sendRawTransaction: ${ERROR_MESSAGE_MOCK}`;
const { controller } = setupController({
messengerOptions: {
addTransactionApprovalRequest: {
Expand All @@ -2796,12 +2797,7 @@ describe('TransactionController', () => {

rpcRequestMock.mockImplementation(async ({ method }) => {
if (method === 'eth_sendRawTransaction') {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw {
data: {
message: ERROR_MESSAGE_MOCK,
},
};
throw new Error(rpcErrorMessage);
}

if (method === 'eth_getBalance') {
Expand All @@ -2824,7 +2820,7 @@ describe('TransactionController', () => {
},
);

await expect(result).rejects.toThrow(ERROR_MESSAGE_MOCK);
await expect(result).rejects.toThrow(rpcErrorMessage);
});

it('throws with standard message if publish fails', async () => {
Expand Down Expand Up @@ -3774,7 +3770,7 @@ describe('TransactionController', () => {
rpcRequestMock.mockRejectedValueOnce(error);

await expect(controller.stopTransaction('2')).rejects.toThrow(
'RPC submit: Another reason',
'Another reason',
);

const sendRawTransactionCalls = rpcRequestMock.mock.calls.filter(
Expand Down Expand Up @@ -4136,7 +4132,7 @@ describe('TransactionController', () => {
rpcRequestMock.mockRejectedValueOnce(error);

await expect(controller.speedUpTransaction('2')).rejects.toThrow(
'RPC submit: Another reason',
'Another reason',
);

const sendRawTransactionCalls = rpcRequestMock.mock.calls.filter(
Expand All @@ -4146,11 +4142,10 @@ describe('TransactionController', () => {
expect(controller.state.transactions).toHaveLength(1);
});

it('extracts nested data.message and prefixes it with RPC submit', async () => {
const error = {
message: 'Outer message',
data: { message: 'Nested rpc error message' },
};
it('propagates contextual rpcRequest errors without masking the message', async () => {
const error = new Error(
'RPC 0x5 Custom eth_sendRawTransaction: Nested rpc error message',
);
const { controller } = setupController({
options: {
state: {
Expand All @@ -4174,9 +4169,7 @@ describe('TransactionController', () => {

rpcRequestMock.mockRejectedValueOnce(error);

await expect(controller.speedUpTransaction('2')).rejects.toThrow(
'RPC submit: Nested rpc error message',
);
await expect(controller.speedUpTransaction('2')).rejects.toBe(error);
});

it('creates additional transaction with increased gas', async () => {
Expand Down
42 changes: 14 additions & 28 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3139,38 +3139,24 @@ export class TransactionController extends BaseController<
transactionMeta: TransactionMeta,
{ skipSubmitHistory }: { skipSubmitHistory?: boolean } = {},
): Promise<string> {
try {
const { networkClientId, rawTx } = transactionMeta;

if (!rawTx) {
throw new Error('Missing raw transaction');
}

const transactionHash = (await rpcRequest({
messenger: this.messenger,
networkClientId,
method: 'eth_sendRawTransaction',
params: [rawTx],
})) as string;

if (skipSubmitHistory !== true) {
this.#updateSubmitHistory(transactionMeta, transactionHash);
}
const { networkClientId, rawTx } = transactionMeta;

return transactionHash;
} catch (error: unknown) {
const errorObject = error as
| {
data?: { message?: string };
message?: string;
}
| undefined;
if (!rawTx) {
throw new Error('Missing raw transaction');
}

const errorMessage =
errorObject?.data?.message ?? errorObject?.message ?? String(error);
const transactionHash = (await rpcRequest({
messenger: this.messenger,
networkClientId,
method: 'eth_sendRawTransaction',
params: [rawTx],
})) as string;

throw new Error(`RPC submit: ${errorMessage}`);
if (skipSubmitHistory !== true) {
this.#updateSubmitHistory(transactionMeta, transactionHash);
}

return transactionHash;
}

/**
Expand Down
64 changes: 63 additions & 1 deletion packages/transaction-controller/src/utils/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { NetworkClientId, Provider } from '@metamask/network-controller';
import { NetworkClientType } from '@metamask/network-controller';
import { RpcEndpointType } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';

import type { TransactionControllerMessenger } from '../TransactionController';
Expand All @@ -9,6 +11,7 @@ describe('provider utils', () => {
const mockProvider = {
request: requestMock,
} as unknown as Provider;
const chainIdMock = '0xa4b1' as Hex;

let messengerCallMock: jest.Mock;
let messengerMock: TransactionControllerMessenger;
Expand All @@ -20,7 +23,27 @@ describe('provider utils', () => {
.fn()
.mockImplementation((action: string, ...args: unknown[]) => {
if (action === 'NetworkController:getNetworkClientById') {
return { provider: mockProvider };
const networkClientId = args[0] as NetworkClientId;

return {
configuration: {
chainId: chainIdMock,
type:
networkClientId === 'infuraNetworkClientId'
? NetworkClientType.Infura
: NetworkClientType.Custom,
rpcEndpoints: [
{
networkClientId,
type:
networkClientId === 'infuraNetworkClientId'
? RpcEndpointType.Infura
: RpcEndpointType.Custom,
},
],
},
provider: mockProvider,
};
}

if (action === 'NetworkController:findNetworkClientIdByChainId') {
Expand Down Expand Up @@ -106,6 +129,45 @@ describe('provider utils', () => {
params: ['0x123', 'latest'],
}),
).rejects.toBe(error);
expect(error.message).toBe(
'RPC 0xa4b1 Custom eth_getBalance: RPC failed',
);
});

it('identifies Infura provider.request errors', async () => {
const error = new Error('Unauthorized.');
requestMock.mockRejectedValue(error);

await expect(
rpcRequest({
messenger: messengerMock,
networkClientId: 'infuraNetworkClientId' as NetworkClientId,
method: 'eth_getBalance',
params: ['0x123', 'latest'],
}),
).rejects.toBe(error);
expect(error.message).toBe(
'RPC 0xa4b1 Infura eth_getBalance: Unauthorized.',
);
});

it('uses nested RPC data messages when available', async () => {
const error = Object.assign(new Error('Outer message'), {
data: { message: 'Nested rpc error message' },
});
requestMock.mockRejectedValue(error);

await expect(
rpcRequest({
messenger: messengerMock,
networkClientId: 'networkClientIdA' as NetworkClientId,
method: 'eth_getBalance',
params: ['0x123', 'latest'],
}),
).rejects.toBe(error);
expect(error.message).toBe(
'RPC 0xa4b1 Custom eth_getBalance: Nested rpc error message',
);
});

it('works when params are undefined', async () => {
Expand Down
73 changes: 65 additions & 8 deletions packages/transaction-controller/src/utils/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { NetworkClientId, Provider } from '@metamask/network-controller';
import type {
NetworkClient,
NetworkClientId,
Provider,
} from '@metamask/network-controller';
import { NetworkClientType } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';

import { createModuleLogger, projectLogger } from '../logger';
Expand Down Expand Up @@ -32,11 +37,8 @@ export function getProvider({
chainId,
networkClientId,
});
return (
messenger.call('NetworkController:getNetworkClientById', resolvedId) as {
provider: Provider;
}
).provider;

return getNetworkClient(messenger, resolvedId).provider;
}

/**
Expand All @@ -63,9 +65,24 @@ export async function rpcRequest({
method: string;
params?: ProviderRequestParams;
}): Promise<unknown> {
const provider = getProvider({ messenger, chainId, networkClientId });
const resolvedNetworkClientId = getNetworkClientId({
messenger,
chainId,
networkClientId,
});

const response = await provider.request({ method, params });
const networkClient = getNetworkClient(messenger, resolvedNetworkClientId);
const { provider } = networkClient;

let response: unknown;
try {
response = await provider.request({ method, params });
} catch (error) {
throwWithRpcContext(error, {
method,
networkClient,
});
}

log(method, { params, response });

Expand Down Expand Up @@ -128,3 +145,43 @@ export function getChainId({
}
).configuration.chainId;
}

function getNetworkClient(
messenger: TransactionControllerMessenger,
networkClientId: NetworkClientId,
): NetworkClient {
return messenger.call(
'NetworkController:getNetworkClientById',
networkClientId,
) as NetworkClient;
}

function throwWithRpcContext(
error: unknown,
{
method,
networkClient,
}: {
method: string;
networkClient: NetworkClient;
},
): never {
const errorObject = error as {
data?: { message?: string };
message?: string;
};
const message = errorObject.data?.message ?? errorObject.message;
const prefix = `RPC ${networkClient.configuration.chainId} ${getEndpointLabel(
networkClient,
)} ${method}`;
const prefixedMessage = `${prefix}: ${message}`;

errorObject.message = prefixedMessage;
throw error;
}

function getEndpointLabel(networkClient: NetworkClient): 'Infura' | 'Custom' {
return networkClient.configuration.type === NetworkClientType.Infura
? 'Infura'
: 'Custom';
}
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/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]

### Changed

- Add RPC method, chain ID, and endpoint type context to transaction pay provider errors ([#9144](https://github.com/MetaMask/core/pull/9144))

## [23.8.0]

### Changed
Expand Down
Loading
Loading