Skip to content
Open
1 change: 1 addition & 0 deletions packages/transaction-pay-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

- Use `EXACT_INPUT` Relay quotes when no transaction is embedded, expose optional `TransactionPayQuote.isInputBased` and `TransactionPayTotals.isInputBased` flags, and preserve `EXACT_OUTPUT` for embedded transactions such as Money Account calls ([#9954](https://github.com/MetaMask/core/pull/9954))
- Bump `@metamask/sentinel-api-service` from `^1.0.0` to `^1.0.1` ([#9972](https://github.com/MetaMask/core/pull/9972))

## [27.0.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,15 @@ describe('Across Quotes', () => {
json: async () => QUOTE_MOCK,
} as Response);

await getAcrossQuotes({
const result = await getAcrossQuotes({
accountSupports7702: true,
messenger,
requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }],
transaction: TRANSACTION_META_MOCK,
});

expect(result[0].isInputBased).toBe(true);

const [url] = successfulFetchMock.mock.calls[0];
const params = new URL(url as string).searchParams;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ async function normalizeQuote(
sourceNetwork,
targetNetwork,
},
isInputBased: original.request.tradeType === 'exactInput',
original: {
...original,
metamask,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ describe('fiat-direct-musd', () => {
},
targetNetwork: { fiat: '0', usd: '0' },
}),
isInputBased: false,
original: { rampsQuote: RAMPS_QUOTE_MOCK, relayQuote: undefined },
request: expect.objectContaining({
from: MONEY_ACCOUNT_ADDRESS_MOCK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ function combineDirectMusdFiatQuote({
},
targetNetwork: { fiat: '0', usd: '0' },
},
isInputBased: false,
original: {
rampsQuote: fiatQuote,
relayQuote: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export async function buildPolymarketDepositWalletSimulation(
}

export async function applyPolymarketDepositWalletOverrides(
body: RelayQuoteRequest,
body: Omit<RelayQuoteRequest, 'amount' | 'tradeType'>,
request: QuoteRequest,
messenger: TransactionPayControllerMessenger,
): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,13 @@ describe('Relay Quotes Utils', () => {

expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.targetAmountMinimum,
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
destinationChainId: 2,
destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress,
originChainId: 1,
originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress,
recipient: QUOTE_REQUEST_MOCK.from,
tradeType: 'EXPECTED_OUTPUT',
tradeType: 'EXACT_INPUT',
user: QUOTE_REQUEST_MOCK.from,
}),
);
Expand Down Expand Up @@ -379,13 +379,84 @@ describe('Relay Quotes Utils', () => {
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
const result = await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }],
transaction: TRANSACTION_META_MOCK,
});

expect(result[0].isInputBased).toBe(true);

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
tradeType: 'EXACT_INPUT',
}),
);
});

it('sends request with EXACT_INPUT trade type for token transfers', async () => {
const quoteMock = cloneDeep(QUOTE_MOCK);
quoteMock.details.currencyOut.minimumAmount = '100';

successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => quoteMock,
} as never);

const result = await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
data: TOKEN_TRANSFER_DATA_MOCK,
},
type: TransactionType.simpleSend,
},
});

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
tradeType: 'EXACT_INPUT',
}),
);
expect(result[0].dust).toStrictEqual({ fiat: '0', usd: '0' });
});

it('sends request with EXACT_INPUT trade type for nested token transfers', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: {
...TRANSACTION_META_MOCK,
nestedTransactions: [
{
data: TOKEN_TRANSFER_DATA_MOCK,
type: TransactionType.simpleSend,
},
],
type: TransactionType.batch,
},
});

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);
Expand Down Expand Up @@ -440,6 +511,7 @@ describe('Relay Quotes Utils', () => {

expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.targetAmountMinimum,
authorizationList: [
{
chainId: 1,
Expand Down Expand Up @@ -540,14 +612,14 @@ describe('Relay Quotes Utils', () => {
});

expect(result[0].original.request).toStrictEqual({
amount: QUOTE_REQUEST_MOCK.targetAmountMinimum,
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
destinationChainId: 2,
destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress,
originChainId: 1,
originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress,
recipient: QUOTE_REQUEST_MOCK.from,
slippageTolerance: '50',
tradeType: 'EXPECTED_OUTPUT',
tradeType: 'EXACT_INPUT',
user: QUOTE_REQUEST_MOCK.from,
});
});
Expand Down Expand Up @@ -3878,7 +3950,7 @@ describe('Relay Quotes Utils', () => {
).rejects.toThrow(`Source token fiat rate not found`);
});

it('updates request if Arbitrum deposit to Hyperliquid', async () => {
it('requests exact input for Hyperliquid deposits without embedded transactions', async () => {
const arbitrumToHyperliquidRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
targetChainId: CHAIN_ID_ARBITRUM,
Expand Down Expand Up @@ -3906,17 +3978,15 @@ describe('Relay Quotes Utils', () => {

expect(body).toStrictEqual(
expect.objectContaining({
amount: '12300',
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
destinationChainId: 1337,
destinationCurrency: '0x00000000000000000000000000000000',
tradeType: 'EXACT_INPUT',
}),
);
});

// A HyperCore deposit funds an order that needs the whole target as margin.
// EXPECTED_OUTPUT only guarantees `target * (1 - slippage)`, which leaves the
// follow-on order short and it fails on insufficient margin.
it('requests an exact output for Hyperliquid deposits so the full margin is guaranteed', async () => {
it('requests exact input for Hyperliquid deposit-and-order flows without embedded transactions', async () => {
const arbitrumToHyperliquidRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
targetChainId: CHAIN_ID_ARBITRUM,
Expand All @@ -3942,10 +4012,15 @@ describe('Relay Quotes Utils', () => {
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body.tradeType).toBe('EXACT_OUTPUT');
expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
tradeType: 'EXACT_INPUT',
}),
);
});

it('still requests an expected output for non-Hyperliquid targets', async () => {
it('requests exact input for non-Hyperliquid targets without embedded transactions', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
Expand All @@ -3962,7 +4037,12 @@ describe('Relay Quotes Utils', () => {
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body.tradeType).toBe('EXPECTED_OUTPUT');
expect(body).toStrictEqual(
expect.objectContaining({
amount: QUOTE_REQUEST_MOCK.sourceTokenAmount,
tradeType: 'EXACT_INPUT',
}),
);
});

it('does not convert to Hyperliquid deposit when parent transaction is not a Perps deposit', async () => {
Expand Down
Loading