Skip to content
6 changes: 6 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fix transaction with accountOverride and targeting Arbitrum USDC ([#8724](https://github.com/MetaMask/core/pull/8724))
- Deliver the Relay-acquired token to `transaction.txParams.from` (the delegator that executes the inner delegation), not `request.from`.
- Gate the Arbitrum-USDC → Hypercore quote rewrite on `transaction.type === TransactionType.perpsDeposit`.

## [22.0.2]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,68 @@ describe('Relay Quotes Utils', () => {
);
});

it('funds the delegator (transaction.txParams.from) rather than request.from when they differ', async () => {
const delegatorAddress =
'0xabcdef0000000000000000000000000000000001' as Hex;

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

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
from: delegatorAddress,
data: '0xabc' as Hex,
},
} as TransactionMeta,
});

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

expect(body.txs[0]).toStrictEqual({
to: QUOTE_REQUEST_MOCK.targetTokenAddress,
data: '0xa9059cbb000000000000000000000000abcdef0000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000007b',
value: '0x0',
});
});

it('falls back to request.from for the funding recipient when transaction.txParams.from is unset', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
data: '0xabc' as Hex,
},
} as TransactionMeta,
});

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

expect(body.txs[0]).toStrictEqual({
to: QUOTE_REQUEST_MOCK.targetTokenAddress,
data: '0xa9059cbb0000000000000000000000001234567890123456789012345678901234567891000000000000000000000000000000000000000000000000000000000000007b',
value: '0x0',
});
});

it('includes request in quote', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -2751,7 +2813,10 @@ describe('Relay Quotes Utils', () => {
accountSupports7702: true,
messenger,
requests: [arbitrumToHyperliquidRequest],
transaction: TRANSACTION_META_MOCK,
transaction: {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsDeposit,
},
});

const body = JSON.parse(
Expand All @@ -2767,6 +2832,37 @@ describe('Relay Quotes Utils', () => {
);
});

it('does not convert to Hyperliquid deposit when parent transaction is not a Perps deposit', async () => {
const arbitrumUsdcRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
targetChainId: CHAIN_ID_ARBITRUM,
targetTokenAddress: ARBITRUM_USDC_ADDRESS,
};

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

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [arbitrumUsdcRequest],
transaction: TRANSACTION_META_MOCK,
});

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

expect(body).toStrictEqual(
expect.objectContaining({
destinationChainId: Number(CHAIN_ID_ARBITRUM),
destinationCurrency: ARBITRUM_USDC_ADDRESS,
}),
);
});

it('does not convert to Hyperliquid deposit for post-quote requests targeting Arbitrum USDC', async () => {
const postQuoteRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Interface } from '@ethersproject/abi';
import { toHex } from '@metamask/controller-utils';
import { TransactionType } from '@metamask/transaction-controller';
import type { TransactionMeta } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';
import { createModuleLogger } from '@metamask/utils';
Expand Down Expand Up @@ -87,7 +88,9 @@ export async function getRelayQuotes(

return hasTargetMinimum || isPostQuote || isExactInputRequest;
})
.map((singleRequest) => normalizeRequest(singleRequest));
.map((singleRequest) =>
normalizeRequest(singleRequest, request.transaction),
);

log('Normalized requests', normalizedRequests);

Expand Down Expand Up @@ -346,10 +349,15 @@ async function processTransactions(
requestBody.refundTo = request.from;
}

const fundingRecipient = (transaction.txParams?.from as Hex) ?? request.from;

requestBody.txs = [
{
to: request.targetTokenAddress,
data: buildTokenTransferData(request.from, request.targetAmountMinimum),
data: buildTokenTransferData(
fundingRecipient,
request.targetAmountMinimum,
),
value: '0x0',
},
{
Expand All @@ -364,14 +372,22 @@ async function processTransactions(
* Normalizes requests for Relay.
*
* @param request - Quote request to normalize.
* @param transaction - Parent transaction metadata, used to gate
* Hyperliquid-specific rewrites on transaction type.
* @returns Normalized request.
*/
function normalizeRequest(request: QuoteRequest): QuoteRequest {
function normalizeRequest(
request: QuoteRequest,
transaction: TransactionMeta,
): QuoteRequest {
const newRequest = {
...request,
};

const isPerpsDeposit = transaction.type === TransactionType.perpsDeposit;

const isHyperliquidDeposit =
isPerpsDeposit &&
!request.isPostQuote &&
request.targetChainId === CHAIN_ID_ARBITRUM &&
request.targetTokenAddress.toLowerCase() ===
Expand Down
Loading