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/bridge-status-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- No longer report `SUBMITTED` or finalized quote statuses to the quote status API for intent-based swaps, since the bridge backend observes intent settlement and owns those statuses ([#10171](https://github.com/MetaMask/core/pull/10171))
- Bump `@metamask/profile-sync-controller` from `^32.0.0` to `^32.1.0` ([#10184](https://github.com/MetaMask/core/pull/10184))

## [76.1.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ const setup = (options?: {
clientId?: BridgeClientId;
keyringType?: string;
mockTxHistory?: any;
isQuoteStatusManagerEnabled?: () => boolean;
onQuoteStatusManagerError?: (error: unknown) => void;
}) => {
const accountAddress = '0xAccount1' as const;
const { messenger, transactions } = createMessengerHarness(
Expand All @@ -312,6 +314,8 @@ const setup = (options?: {
fetchFn: (...args: any[]) => mockFetchFn(...args),
config: { customBridgeApiBaseUrl: 'http://localhost' },
traceFn: (_req: any, fn?: any): any => fn?.(),
isQuoteStatusManagerEnabled: options?.isQuoteStatusManagerEnabled,
onQuoteStatusManagerError: options?.onQuoteStatusManagerError as any,
});

const startPollingSpy = jest
Expand Down Expand Up @@ -745,6 +749,56 @@ describe('BridgeStatusController (intent swaps)', () => {
expect(stopPollingSpy).toHaveBeenCalledWith('poll-token-1');
});

it.each([
{ description: 'COMPLETED', orderStatus: IntentOrderStatus.COMPLETED },
{ description: 'EXPIRED', orderStatus: IntentOrderStatus.EXPIRED },
])(
'intent polling: does not report any quote status when the order reaches $description',
async ({ orderStatus }) => {
const onQuoteStatusManagerError = jest.fn();
const orderUid = 'order-uid-quote-status-1';
const { controller } = setup({
isQuoteStatusManagerEnabled: () => true,
onQuoteStatusManagerError,
mockTxHistory: {
[orderUid]: {
txMetaId: orderUid,
originalTransactionId: orderUid,
quoteId: 'intent-quote-1',
quote: minimalIntentQuoteResponse().quote,
account: '0xAccount1',
startTime: Date.now(),
status: {
status: StatusTypes.PENDING,
srcChain: { chainId: 1, txHash: '0xhash' },
},
},
},
});

jest
.spyOn(intentApi.IntentApiImpl.prototype, 'getOrderStatus')
.mockResolvedValue({
id: orderUid,
status: orderStatus,
txHash: '0xsettlementhash',
metadata: { txHashes: ['0xsettlementhash'] },
});

await controller._executePoll({ bridgeTxMetaId: orderUid });

// The backend owns the quote status of intent orders, so reaching a
// terminal order status must not create or update a tracking entry.
expect(controller.state.quoteUpdateStatusStore).toStrictEqual({});
expect(
controller.state.txHistory[orderUid]?.reportedSubmittedTxHash,
).toBeUndefined();
expect(onQuoteStatusManagerError).not.toHaveBeenCalled();

controller.stopAllPolling();
},
);

it('intent polling: stops polling when attempts reach MAX_ATTEMPTS', async () => {
const orderUid = 'order-uid-3';
const { controller, stopPollingSpy } = setup({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7334,6 +7334,153 @@ describe('BridgeStatusController', () => {
);
});
});

describe('intent-based swaps', () => {
const INTENT_TX_META_ID = 'intentTxMetaId1';
const INTENT_SRC_TX_HASH = '0xintentSrcTxHash1';

/**
* Builds a history item for an intent-based order. The quote carries
* `intent` data, which is what marks the trade as backend-tracked.
*
* @param startTime - When the trade started, used to decide whether
* startup seeding considers the item.
* @returns The intent txHistory keyed by history id.
*/
function buildIntentHistory(
startTime = 1729964825189,
): Record<string, BridgeHistoryItem> {
const item = MockTxHistory.getPending({
txMetaId: INTENT_TX_META_ID,
srcTxHash: INTENT_SRC_TX_HASH,
startTime,
})[INTENT_TX_META_ID];

return {
[INTENT_TX_META_ID]: {
...item,
quoteId: 'intent-quote-1',
quote: {
...item.quote,
intent: {
protocol: 'cowswap',
order: {
sellToken: '0x0000000000000000000000000000000000000001',
buyToken: '0x0000000000000000000000000000000000000002',
validTo: 1717027200,
appData: 'some-app-data',
appDataHash: '0xabcd',
feeAmount: '100',
kind: 'sell',
partiallyFillable: false,
sellAmount: '1000',
},
typedData: {
types: {},
primaryType: 'Order',
domain: {},
message: {},
},
},
},
},
};
}

const getIntentMessengerCall = () =>
jest.fn((...args: unknown[]) => {
const action = args[0] as string;
if (action === 'TransactionController:getState') {
return { transactions: [] };
}
if (action === 'AuthenticationController:getBearerToken') {
return Promise.resolve('auth-token');
}
return undefined;
});

it.each([
{
description: 'submitted',
status: TransactionStatus.submitted,
type: TransactionType.swap,
},
{
description: 'confirmed',
status: TransactionStatus.confirmed,
type: TransactionType.swap,
},
{
description: 'failed',
status: TransactionStatus.failed,
type: TransactionType.swap,
},
])(
'does not report any quote status when the intent tx is $description',
async ({ status, type }) => {
const onQuoteStatusManagerError = jest.fn();

await withController(
{
options: {
isQuoteStatusManagerEnabled: () => true,
onQuoteStatusManagerError,
state: { txHistory: buildIntentHistory() },
},
mockMessengerCall: getIntentMessengerCall(),
},
async ({ controller, rootMessenger }) => {
rootMessenger.publish(
'TransactionController:transactionStatusUpdated',
{
transactionMeta: {
chainId: CHAIN_IDS.ARBITRUM,
networkClientId: 'eth-id',
time: Date.now(),
txParams: {} as unknown as TransactionParams,
type,
status,
id: INTENT_TX_META_ID,
hash: INTENT_SRC_TX_HASH,
},
},
);

// The backend owns the quote status of intent orders, so the
// client neither creates a tracking entry nor marks the history
// item as reported.
expect(controller.state.quoteUpdateStatusStore).toStrictEqual({});
expect(
controller.state.txHistory[INTENT_TX_META_ID]
.reportedSubmittedTxHash,
).toBeUndefined();
// Finalizing an untracked quote surfaces a "entry was not found"
// error, so silence here proves finalization was never attempted.
expect(onQuoteStatusManagerError).not.toHaveBeenCalled();
},
);
},
);

it('does not seed a quote status entry for an intent order on startup', async () => {
await withController(
{
options: {
isQuoteStatusManagerEnabled: () => true,
state: { txHistory: buildIntentHistory(Date.now()) },
},
mockMessengerCall: getIntentMessengerCall(),
},
async ({ controller }) => {
expect(controller.state.quoteUpdateStatusStore).toStrictEqual({});
expect(
controller.state.txHistory[INTENT_TX_META_ID]
.reportedSubmittedTxHash,
).toBeUndefined();
},
);
});
});
});

describe('seeding quote status entries from history on startup', () => {
Expand Down
Loading