Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Use distinct error message when failing incomplete transactions at startup whose required transactions are all confirmed ([#8189](https://github.com/MetaMask/core/pull/8189))
- Bump `@metamask/core-backend` from `^6.1.0` to `^6.1.1` ([#8162](https://github.com/MetaMask/core/pull/8162))

### Fixed
Expand Down
158 changes: 158 additions & 0 deletions packages/transaction-controller/src/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,164 @@ describe('TransactionController', () => {
]);
});

it('uses alternate error message when all required transactions are confirmed', async () => {
const mockTransactionMeta = {
from: ACCOUNT_MOCK,
txParams: {
from: ACCOUNT_MOCK,
to: ACCOUNT_2_MOCK,
},
};

const mockedTransactions = [
{
id: '123',
chainId: toHex(5),
status: TransactionStatus.approved,
requiredTransactionIds: ['222', '333'],
...mockTransactionMeta,
},
{
id: '222',
chainId: toHex(1),
status: TransactionStatus.confirmed,
...mockTransactionMeta,
},
{
id: '333',
chainId: toHex(16),
status: TransactionStatus.confirmed,
...mockTransactionMeta,
},
];

const mockedControllerState = {
transactions: mockedTransactions,
methodData: {},
lastFetchedBlockNumbers: {},
};

const { controller } = setupController({
options: {
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: mockedControllerState as any,
},
});

await flushPromises();

const { transactions } = controller.state;

const incompleteTransaction = transactions.find((tx) => tx.id === '123');

expect(incompleteTransaction?.status).toBe(TransactionStatus.failed);
expect(incompleteTransaction?.error?.message).toBe(
'Transaction incomplete at startup with all required transactions confirmed',
);
});

it('uses default error message when not all required transactions are confirmed', async () => {
const mockTransactionMeta = {
from: ACCOUNT_MOCK,
txParams: {
from: ACCOUNT_MOCK,
to: ACCOUNT_2_MOCK,
},
};

const mockedTransactions = [
{
id: '123',
chainId: toHex(5),
status: TransactionStatus.approved,
requiredTransactionIds: ['222', '333'],
...mockTransactionMeta,
},
{
id: '222',
chainId: toHex(1),
status: TransactionStatus.confirmed,
...mockTransactionMeta,
},
{
id: '333',
chainId: toHex(16),
status: TransactionStatus.submitted,
...mockTransactionMeta,
},
];

const mockedControllerState = {
transactions: mockedTransactions,
methodData: {},
lastFetchedBlockNumbers: {},
};

const { controller } = setupController({
options: {
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: mockedControllerState as any,
},
});

await flushPromises();

const { transactions } = controller.state;

const incompleteTransaction = transactions.find((tx) => tx.id === '123');

expect(incompleteTransaction?.status).toBe(TransactionStatus.failed);
expect(incompleteTransaction?.error?.message).toBe(
'Transaction incomplete at startup',
);
});

it('uses default error message when transaction has no required transaction IDs', async () => {
const mockTransactionMeta = {
from: ACCOUNT_MOCK,
txParams: {
from: ACCOUNT_MOCK,
to: ACCOUNT_2_MOCK,
},
};

const mockedTransactions = [
{
id: '123',
chainId: toHex(5),
status: TransactionStatus.approved,
...mockTransactionMeta,
},
];

const mockedControllerState = {
transactions: mockedTransactions,
methodData: {},
lastFetchedBlockNumbers: {},
};

const { controller } = setupController({
options: {
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: mockedControllerState as any,
},
});

await flushPromises();

const { transactions } = controller.state;

const incompleteTransaction = transactions.find((tx) => tx.id === '123');

expect(incompleteTransaction?.status).toBe(TransactionStatus.failed);
expect(incompleteTransaction?.error?.message).toBe(
'Transaction incomplete at startup',
);
});

it('removes unapproved transactions', async () => {
const mockTransactionMeta = {
from: ACCOUNT_MOCK,
Expand Down
18 changes: 13 additions & 5 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3046,14 +3046,22 @@ export class TransactionController extends BaseController<
);

for (const transactionMeta of incompleteTransactions) {
this.#failTransaction(
transactionMeta,
new Error('Transaction incomplete at startup'),
);

const requiredTransactionIds =
transactionMeta.requiredTransactionIds ?? [];

const allRequiredConfirmed =
requiredTransactionIds.length > 0 &&
requiredTransactionIds.every((id) => {
const tx = this.#getTransaction(id);
return tx?.status === TransactionStatus.confirmed;
});

const message = allRequiredConfirmed
? 'Transaction incomplete at startup with all required transactions confirmed'
: 'Transaction incomplete at startup';

this.#failTransaction(transactionMeta, new Error(message));

for (const requiredTransactionId of requiredTransactionIds) {
const requiredTransactionMeta = this.#getTransaction(
requiredTransactionId,
Expand Down
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Zero out source network fees in Relay strategy when quote indicates execute flow ([#8181](https://github.com/MetaMask/core/pull/8181))
- Harden relay status polling ([#8189](https://github.com/MetaMask/core/pull/8189))
- Throw if status not recognised.
- Support feature flags for polling interval and timeout.
- Ignore transient network errors.

## [16.5.0]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { TransactionType } from '@metamask/transaction-controller';

import type { RelayStatus } from './types';

export const RELAY_URL_BASE = 'https://api.relay.link';
export const RELAY_EXECUTE_URL = `${RELAY_URL_BASE}/execute`;
export const RELAY_QUOTE_URL = `${RELAY_URL_BASE}/quote`;
export const RELAY_STATUS_URL = `${RELAY_URL_BASE}/intents/status/v3`;
export const RELAY_POLLING_INTERVAL = 1000; // 1 Second
export const TOKEN_TRANSFER_FOUR_BYTE = '0xa9059cbb';

export const RELAY_FAILURE_STATUSES: RelayStatus[] = [
'failure',
'refund',
'refunded',
];

export const RELAY_PENDING_STATUSES: RelayStatus[] = [
'delayed',
'depositing',
'pending',
'submitted',
'waiting',
];

export const RELAY_DEPOSIT_TYPES: Record<string, TransactionType> = {
[TransactionType.predictDeposit]: TransactionType.predictRelayDeposit,
[TransactionType.perpsDeposit]: TransactionType.perpsRelayDeposit,
Expand Down
Loading
Loading