From 1fd17802fd6698c93c8d7b784f05f28feb6951fb Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:19:50 +0100 Subject: [PATCH 01/14] feat(transaction-pay-controller): add HyperLiquid source quote support for Perps Withdraw Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/TransactionPayController.test.ts | 13 +++ .../src/TransactionPayController.ts | 2 + .../strategy/relay/relay-max-gas-station.ts | 8 +- .../src/strategy/relay/relay-quotes.test.ts | 106 ++++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 69 ++++++++++-- .../src/strategy/relay/relay-submit.ts | 8 +- .../src/strategy/relay/types.ts | 87 +++++++++++--- .../transaction-pay-controller/src/types.ts | 13 +++ .../src/utils/quotes.test.ts | 20 ++++ .../src/utils/quotes.ts | 12 +- 10 files changed, 303 insertions(+), 35 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 09e1e3c99c8..f19bac3064e 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -149,6 +149,19 @@ describe('TransactionPayController', () => { ).toBe(true); }); + it('updates isHyperliquidSource in state', () => { + const controller = createController(); + + controller.setTransactionConfig(TRANSACTION_ID_MOCK, (config) => { + config.isHyperliquidSource = true; + }); + + expect( + controller.state.transactionData[TRANSACTION_ID_MOCK] + .isHyperliquidSource, + ).toBe(true); + }); + it('triggers source amounts and quotes update when only isPostQuote changes', () => { const controller = createController(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 96938df0cd1..37ce1a19744 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -117,6 +117,7 @@ export class TransactionPayController extends BaseController< const config = { isMaxAmount: transactionData.isMaxAmount, isPostQuote: transactionData.isPostQuote, + isHyperliquidSource: transactionData.isHyperliquidSource, refundTo: transactionData.refundTo, }; @@ -124,6 +125,7 @@ export class TransactionPayController extends BaseController< transactionData.isMaxAmount = config.isMaxAmount; transactionData.isPostQuote = config.isPostQuote; + transactionData.isHyperliquidSource = config.isHyperliquidSource; transactionData.refundTo = config.refundTo; }); } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts index a318e02d3c6..54b9c4ecff9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts @@ -1,3 +1,4 @@ +import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -307,7 +308,12 @@ async function getGasCostFromQuoteOrGasStation( }; } - const firstStepData = quote.original.steps[0]?.items[0]?.data; + const firstTxStep = quote.original.steps.find( + (step) => step.kind === 'transaction', + ); + const firstStepData = firstTxStep?.items[0]?.data as + | { data: Hex; to: Hex; value?: string } + | undefined; if (!firstStepData) { return undefined; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index fb016e2cbf0..0263e990da8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -1876,6 +1876,7 @@ describe('Relay Quotes Utils', () => { }, }, ], + kind: 'transaction', } as never); successfulFetchMock.mockResolvedValue({ @@ -2283,6 +2284,111 @@ describe('Relay Quotes Utils', () => { }); }); + describe('HyperLiquid source (isHyperliquidSource)', () => { + const HL_REQUEST: QuoteRequest = { + ...QUOTE_REQUEST_MOCK, + isHyperliquidSource: true, + isPostQuote: true, + sourceChainId: CHAIN_ID_ARBITRUM, + sourceTokenAddress: ARBITRUM_USDC_ADDRESS, + sourceTokenAmount: '100000000', + }; + + it('overrides source chain and token to HyperCore', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.originChainId).toBe(parseInt(CHAIN_ID_HYPERCORE, 16)); + expect(body.originCurrency).toBe( + '0x00000000000000000000000000000000', + ); + }); + + it('shifts source amount by 2 decimals (8→6)', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.amount).toBe('10000000000'); + }); + + it('adds protocolVersion v2 to request body', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.protocolVersion).toBe('v2'); + }); + + it('zeroes source network fees (gasless)', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); + + const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; + + expect(result[0].fees.sourceNetwork.estimate).toStrictEqual( + zeroAmount, + ); + expect(result[0].fees.sourceNetwork.max).toStrictEqual(zeroAmount); + }); + + it('uses Arbitrum USDC fiat rate for source', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [HL_REQUEST], + transaction: TRANSACTION_META_MOCK, + }); + + expect(getTokenFiatRateMock).toHaveBeenCalledWith( + expect.anything(), + ARBITRUM_USDC_ADDRESS, + CHAIN_ID_ARBITRUM, + ); + }); + }); + it('includes target network fee in quote', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index ee794aad7a2..108c30176be 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,6 +2,7 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; +import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -17,9 +18,9 @@ import type { RelayQuote, RelayQuoteMetamask, RelayQuoteRequest, + RelayTransactionStep, } from './types'; import { TransactionPayStrategy } from '../..'; -import type { TransactionMeta } from '../../../../transaction-controller/src'; import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM, @@ -247,13 +248,19 @@ async function getSingleQuote( body.refundTo = request.refundTo; } + if (request.isHyperliquidSource) { + body.protocolVersion = 'v2'; + } + log('Request body', body); const quote = await fetchRelayQuote(messenger, body); log('Fetched relay quote', quote); - return await normalizeQuote(quote, request, fullRequest); + const normalized = await normalizeQuote(quote, request, fullRequest); + + return normalized; } catch (error) { log('Error fetching relay quote', error); throw error; @@ -323,8 +330,9 @@ async function processTransactions( requestBody.authorizationList = normalizedAuthorizationList; requestBody.tradeType = 'EXACT_OUTPUT'; - const tokenTransferData = nestedTransactions?.find((nestedTx) => - nestedTx.data?.startsWith(TOKEN_TRANSFER_FOUR_BYTE), + const tokenTransferData = nestedTransactions?.find( + (nestedTx: { data?: Hex }) => + nestedTx.data?.startsWith(TOKEN_TRANSFER_FOUR_BYTE), )?.data; // If the transactions include a token transfer, change the recipient @@ -389,6 +397,20 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { }); } + // HyperLiquid withdrawal: source is HyperCore Perps USDC, not Arbitrum. + // Override source chain/token and set protocolVersion=v2 (required by Relay). + // Decimal shift: Perps USDC is 8 decimals, standard USDC is 6. + if (request.isHyperliquidSource) { + newRequest.sourceChainId = CHAIN_ID_HYPERCORE; + newRequest.sourceTokenAddress = '0x00000000000000000000000000000000'; + + if (newRequest.sourceTokenAmount) { + newRequest.sourceTokenAmount = new BigNumber(newRequest.sourceTokenAmount) + .shiftedBy(2) + .toString(10); + } + } + return newRequest; } @@ -537,7 +559,15 @@ function getFiatRates( sourceFiatRate: FiatRates; usdToFiatRate: BigNumber; } { - const { sourceChainId, sourceTokenAddress } = request; + // For HyperLiquid source, the normalized chain/token (HyperCore + Perps USDC) + // won't have a fiat rate entry. Use Arbitrum USDC instead since Perps USDC + // is pegged 1:1. + const sourceChainId = request.isHyperliquidSource + ? CHAIN_ID_ARBITRUM + : request.sourceChainId; + const sourceTokenAddress = request.isHyperliquidSource + ? ARBITRUM_USDC_ADDRESS + : request.sourceTokenAddress; const finalSourceTokenAddress = sourceChainId === CHAIN_ID_POLYGON && @@ -607,7 +637,25 @@ async function calculateSourceNetworkCost( }; } - const relayParams = quote.steps + // HyperLiquid withdrawals are gasless -- the "deposit" step is an HL + // sendAsset (off-chain signature), not an on-chain transaction. + if (request.isHyperliquidSource) { + log('Zeroing network fees for HyperLiquid withdrawal (gasless)'); + + const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; + + return { + estimate: zeroAmount, + max: zeroAmount, + gasLimits: [], + is7702: false, + }; + } + + const txSteps = quote.steps.filter( + (step): step is RelayTransactionStep => step.kind === 'transaction', + ); + const relayParams = txSteps .flatMap((step) => step.items) .map((item) => item.data); @@ -777,7 +825,7 @@ async function calculateSourceNetworkCost( * @returns Total gas estimates and per-transaction gas limits. */ async function calculateSourceNetworkGasLimit( - params: RelayQuote['steps'][0]['items'][0]['data'][], + params: RelayTransactionStep['items'][0]['data'][], messenger: TransactionPayControllerMessenger, fromOverride?: Hex, ): Promise<{ @@ -806,7 +854,7 @@ async function calculateSourceNetworkGasLimit( } function toRelayQuoteGasTransaction( - singleParams: RelayQuote['steps'][0]['items'][0]['data'], + singleParams: RelayTransactionStep['items'][0]['data'], fromOverride?: Hex, ): QuoteGasTransaction { return { @@ -848,7 +896,9 @@ function combinePostQuoteGas( gasLimits: number[]; is7702: boolean; } { - const nestedGas = transaction.nestedTransactions?.find((tx) => tx.gas)?.gas; + const nestedGas = transaction.nestedTransactions?.find( + (tx: { gas?: string }) => tx.gas, + )?.gas; const rawGas = nestedGas ?? transaction.txParams.gas; const originalTxGas = rawGas ? new BigNumber(rawGas).toNumber() : undefined; @@ -920,7 +970,6 @@ function getTransferRecipient(data: Hex): Hex { .decodeFunctionData('transfer', data) .to.toLowerCase(); } - function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { const subsidizedFee = quote.fees?.subsidized; const amountUsd = new BigNumber(subsidizedFee?.amountUsd ?? '0'); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index f4b6c396ee9..2e1d784f929 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -19,6 +19,7 @@ import type { RelayExecuteRequest, RelayQuote, RelayStatusResponse, + RelayTransactionStep, } from './types'; import { projectLogger } from '../../logger'; import type { @@ -216,7 +217,7 @@ async function waitForRelayCompletion( * @returns Normalized transaction parameters. */ function normalizeParams( - params: RelayQuote['steps'][0]['items'][0]['data'], + params: RelayTransactionStep['items'][0]['data'], messenger: TransactionPayControllerMessenger, ): TransactionParams { const featureFlags = getFeatureFlags(messenger); @@ -310,7 +311,10 @@ async function submitTransactions( messenger: TransactionPayControllerMessenger, ): Promise { const { steps } = quote.original; - const params = steps.flatMap((step) => step.items).map((item) => item.data); + const txSteps = steps.filter( + (step): step is RelayTransactionStep => step.kind === 'transaction', + ); + const params = txSteps.flatMap((step) => step.items).map((item) => item.data); const invalidKind = steps.find((step) => step.kind !== 'transaction')?.kind; if (invalidKind) { diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 326f6cb9cc9..b70df29c983 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -15,6 +15,8 @@ export type RelayQuoteRequest = { originChainId: number; originCurrency: Hex; originGasOverhead?: string; + /** Required for HyperLiquid withdrawals (value: 'v2'). */ + protocolVersion?: string; recipient: Hex; refundTo?: Hex; slippageTolerance?: string; @@ -74,28 +76,77 @@ export type RelayQuote = { }; metamask: RelayQuoteMetamask; request: RelayQuoteRequest; - steps: { - id: string; - items: { - check: { + steps: (RelayTransactionStep | RelaySignatureStep)[]; +}; + +export type RelayTransactionStep = { + id: string; + items: { + check: { + endpoint: string; + method: 'GET' | 'POST'; + }; + data: { + chainId: number; + data: Hex; + from: Hex; + gas?: string; + maxFeePerGas: string; + maxPriorityFeePerGas: string; + to: Hex; + value?: string; + }; + status: 'complete' | 'incomplete'; + }[]; + kind: 'transaction'; + requestId: string; +}; + +export type RelaySignatureStep = { + id: string; + items: { + data: { + sign: { + signatureKind: string; + domain: Record; + types: Record; + value: Record; + primaryType: string; + }; + post: { endpoint: string; - method: 'GET' | 'POST'; + method: 'POST'; + body: Record; }; - data: { - chainId: number; - data: Hex; - from: Hex; - gas?: string; - maxFeePerGas: string; - maxPriorityFeePerGas: string; - to: Hex; - value?: string; + }; + status: 'complete' | 'incomplete'; + }[]; + kind: 'signature'; + requestId: string; +}; + +/** HyperLiquid deposit step (sendAsset to Relay solver). */ +export type RelayHyperliquidDepositStep = { + id: string; + items: { + check: { + endpoint: string; + method: 'GET' | 'POST'; + }; + data: { + action: { + type: string; + parameters: Record; }; - status: 'complete' | 'incomplete'; - }[]; - kind: 'transaction'; - requestId: string; + nonce: number; + eip712Types: Record; + eip712PrimaryType: string; + }; + status: 'complete' | 'incomplete'; }[]; + kind: 'transaction'; + requestId: string; + depositAddress?: string; }; type RelayQuoteMetamaskBase = { diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 486cbaa52db..879b95bf629 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -83,6 +83,13 @@ export type TransactionConfig = { */ isPostQuote?: boolean; + /** + * Whether the source of funds is HyperLiquid (HyperCore). + * When true, the Relay strategy uses the HyperLiquid 2-step withdrawal + * flow: (1) authorize nonce-mapping, (2) sendAsset to Relay solver. + */ + isHyperliquidSource?: boolean; + /** * Optional address to receive refunds if the Relay transaction fails. * When set, overrides the default refund recipient (EOA) in the Relay quote @@ -164,6 +171,9 @@ export type TransactionData = { */ isPostQuote?: boolean; + /** Whether the source of funds is HyperLiquid (HyperCore). */ + isHyperliquidSource?: boolean; + /** * Optional address to receive refunds if the Relay transaction fails. * When set, overrides the default refund recipient (EOA) in the Relay quote @@ -324,6 +334,9 @@ export type QuoteRequest = { /** Whether this is a post-quote flow. */ isPostQuote?: boolean; + /** Whether the source of funds is HyperLiquid (HyperCore). */ + isHyperliquidSource?: boolean; + /** * Optional address to receive refunds if the Relay transaction fails. * When set, overrides the default refund recipient (EOA) in the Relay quote diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index f582781019b..7e0953e7cc0 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -931,6 +931,26 @@ describe('Quotes Utils', () => { ); }); + it('passes isHyperliquidSource through to post-quote request', async () => { + await run({ + transactionData: { + ...POST_QUOTE_TRANSACTION_DATA, + isHyperliquidSource: true, + }, + }); + + expect(getQuotesMock).toHaveBeenCalledWith( + expect.objectContaining({ + requests: [ + expect.objectContaining({ + isPostQuote: true, + isHyperliquidSource: true, + }), + ], + }), + ); + }); + it('does not fetch quotes when sourceAmounts is empty (same-token filtered in source-amounts)', async () => { const sameTokenData: TransactionData = { ...POST_QUOTE_TRANSACTION_DATA, diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 1621076e193..143f4b866fc 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -70,6 +70,7 @@ export async function updateQuotes( const { isMaxAmount, isPostQuote, + isHyperliquidSource, paymentToken: originalPaymentToken, refundTo, sourceAmounts, @@ -95,6 +96,7 @@ export async function updateQuotes( from, isMaxAmount: isMaxAmount ?? false, isPostQuote, + isHyperliquidSource, paymentToken, refundTo, sourceAmounts, @@ -266,6 +268,7 @@ function buildQuoteRequests({ from, isMaxAmount, isPostQuote, + isHyperliquidSource, paymentToken, refundTo, sourceAmounts, @@ -275,6 +278,7 @@ function buildQuoteRequests({ from: Hex; isMaxAmount: boolean; isPostQuote?: boolean; + isHyperliquidSource?: boolean; paymentToken: TransactionPaymentToken | undefined; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -286,11 +290,10 @@ function buildQuoteRequests({ } if (isPostQuote) { - // Post-quote flow: source = transaction's required token, target = paymentToken (destination) - // The user wants to receive the transaction output in paymentToken return buildPostQuoteRequests({ from, isMaxAmount, + isHyperliquidSource, destinationToken: paymentToken, refundTo, sourceAmounts, @@ -341,6 +344,7 @@ function buildQuoteRequests({ function buildPostQuoteRequests({ from, isMaxAmount, + isHyperliquidSource, destinationToken, refundTo, sourceAmounts, @@ -348,6 +352,7 @@ function buildPostQuoteRequests({ }: { from: Hex; isMaxAmount: boolean; + isHyperliquidSource?: boolean; destinationToken: TransactionPaymentToken; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -376,13 +381,12 @@ function buildPostQuoteRequests({ from, isMaxAmount, isPostQuote: true, + isHyperliquidSource, refundTo, sourceBalanceRaw: sourceAmount.sourceBalanceRaw, sourceTokenAmount: sourceAmount.sourceAmountRaw, sourceChainId: sourceAmount.sourceChainId, sourceTokenAddress: sourceAmount.sourceTokenAddress, - // For post-quote flows, use EXACT_INPUT - user specifies how much to send, - // and we show them how much they'll receive after fees targetAmountMinimum: '0', targetChainId: destinationToken.chainId, targetTokenAddress: destinationToken.address, From 020736f7806379709ef7a1f2f34259de2b691b3d Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:36:44 +0100 Subject: [PATCH 02/14] Refactoring Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../transaction-pay-controller/src/constants.ts | 7 ++++++- .../src/strategy/relay/relay-quotes.ts | 15 +++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index dffbcdc9cb2..801bc751baa 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -14,6 +14,11 @@ export const ARBITRUM_USDC_ADDRESS = export const POLYGON_USDCE_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex; +export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000'; + +// HyperCore Perps USDC uses 8 decimals vs standard USDC's 6. +export const HYPERCORE_USDC_DECIMAL_SHIFT = 2; + export const STABLECOINS: Record = { // Mainnet '0x1': [ @@ -29,7 +34,7 @@ export const STABLECOINS: Record = { '0xa219439258ca9da29e9cc4ce5596924745e12b93', // USDT ], [CHAIN_ID_POLYGON]: [POLYGON_USDCE_ADDRESS.toLowerCase() as Hex], - [CHAIN_ID_HYPERCORE]: ['0x00000000000000000000000000000000'], // USDC + [CHAIN_ID_HYPERCORE]: [HYPERCORE_USDC_ADDRESS], // USDC }; export enum TransactionPayStrategy { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 108c30176be..49c85220cec 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -26,6 +26,8 @@ import { CHAIN_ID_ARBITRUM, CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, + HYPERCORE_USDC_ADDRESS, + HYPERCORE_USDC_DECIMAL_SHIFT, NATIVE_TOKEN_ADDRESS, STABLECOINS, } from '../../constants'; @@ -258,9 +260,7 @@ async function getSingleQuote( log('Fetched relay quote', quote); - const normalized = await normalizeQuote(quote, request, fullRequest); - - return normalized; + return await normalizeQuote(quote, request, fullRequest); } catch (error) { log('Error fetching relay quote', error); throw error; @@ -386,9 +386,9 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { if (isHyperliquidDeposit) { newRequest.targetChainId = CHAIN_ID_HYPERCORE; - newRequest.targetTokenAddress = '0x00000000000000000000000000000000'; + newRequest.targetTokenAddress = HYPERCORE_USDC_ADDRESS; newRequest.targetAmountMinimum = new BigNumber(request.targetAmountMinimum) - .shiftedBy(2) + .shiftedBy(HYPERCORE_USDC_DECIMAL_SHIFT) .toString(10); log('Converting Arbitrum Hyperliquid deposit to direct deposit', { @@ -399,14 +399,13 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { // HyperLiquid withdrawal: source is HyperCore Perps USDC, not Arbitrum. // Override source chain/token and set protocolVersion=v2 (required by Relay). - // Decimal shift: Perps USDC is 8 decimals, standard USDC is 6. if (request.isHyperliquidSource) { newRequest.sourceChainId = CHAIN_ID_HYPERCORE; - newRequest.sourceTokenAddress = '0x00000000000000000000000000000000'; + newRequest.sourceTokenAddress = HYPERCORE_USDC_ADDRESS; if (newRequest.sourceTokenAmount) { newRequest.sourceTokenAmount = new BigNumber(newRequest.sourceTokenAmount) - .shiftedBy(2) + .shiftedBy(HYPERCORE_USDC_DECIMAL_SHIFT) .toString(10); } } From 80deaf39d1316ca86fdbea445bdcf4b4f132ad91 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:37:07 +0100 Subject: [PATCH 03/14] Add casting Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/strategy/relay/relay-max-gas-station.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts index 54b9c4ecff9..1724e7785e6 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts @@ -6,7 +6,7 @@ import { getGasStationEligibility, getGasStationCostInSourceTokenRaw, } from './gas-station'; -import type { RelayQuote } from './types'; +import type { RelayQuote, RelayTransactionStep } from './types'; import { projectLogger } from '../../logger'; import type { PayStrategyGetQuotesRequest, @@ -309,11 +309,9 @@ async function getGasCostFromQuoteOrGasStation( } const firstTxStep = quote.original.steps.find( - (step) => step.kind === 'transaction', + (step): step is RelayTransactionStep => step.kind === 'transaction', ); - const firstStepData = firstTxStep?.items[0]?.data as - | { data: Hex; to: Hex; value?: string } - | undefined; + const firstStepData = firstTxStep?.items[0]?.data; if (!firstStepData) { return undefined; From 7448595b6652c5c1afc173b8dead90ed745bda3e Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:41:02 +0100 Subject: [PATCH 04/14] Remove protocolVersion v2 Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/strategy/relay/relay-quotes.test.ts | 18 ------------------ .../src/strategy/relay/relay-quotes.ts | 4 ---- 2 files changed, 22 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 0263e990da8..5dc19613758 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -2333,24 +2333,6 @@ describe('Relay Quotes Utils', () => { expect(body.amount).toBe('10000000000'); }); - it('adds protocolVersion v2 to request body', async () => { - successfulFetchMock.mockResolvedValue({ - json: async () => QUOTE_MOCK, - } as never); - - await getRelayQuotes({ - messenger, - requests: [HL_REQUEST], - transaction: TRANSACTION_META_MOCK, - }); - - const body = JSON.parse( - successfulFetchMock.mock.calls[0][1]?.body as string, - ); - - expect(body.protocolVersion).toBe('v2'); - }); - it('zeroes source network fees (gasless)', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 49c85220cec..a42dba748c1 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -250,10 +250,6 @@ async function getSingleQuote( body.refundTo = request.refundTo; } - if (request.isHyperliquidSource) { - body.protocolVersion = 'v2'; - } - log('Request body', body); const quote = await fetchRelayQuote(messenger, body); From ede9dfccce667f24451fa8fab0a0d1c463e7b387 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:43:30 +0100 Subject: [PATCH 05/14] Code cleanup Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/strategy/relay/relay-quotes.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index a42dba748c1..3bf3cf1e610 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -326,9 +326,8 @@ async function processTransactions( requestBody.authorizationList = normalizedAuthorizationList; requestBody.tradeType = 'EXACT_OUTPUT'; - const tokenTransferData = nestedTransactions?.find( - (nestedTx: { data?: Hex }) => - nestedTx.data?.startsWith(TOKEN_TRANSFER_FOUR_BYTE), + const tokenTransferData = nestedTransactions?.find((nestedTx) => + nestedTx.data?.startsWith(TOKEN_TRANSFER_FOUR_BYTE), )?.data; // If the transactions include a token transfer, change the recipient @@ -891,9 +890,7 @@ function combinePostQuoteGas( gasLimits: number[]; is7702: boolean; } { - const nestedGas = transaction.nestedTransactions?.find( - (tx: { gas?: string }) => tx.gas, - )?.gas; + const nestedGas = transaction.nestedTransactions?.find((tx) => tx.gas)?.gas; const rawGas = nestedGas ?? transaction.txParams.gas; const originalTxGas = rawGas ? new BigNumber(rawGas).toNumber() : undefined; From 7cd1b8e35ebc1751678c2aeff183b0b157bf80a1 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:49:36 +0100 Subject: [PATCH 06/14] Define HYPERCORE_USDC_DECIMALS and USDC_DECIMALS, use it for a decimal shift Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- packages/transaction-pay-controller/src/constants.ts | 4 ++-- .../src/strategy/relay/relay-quotes.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 801bc751baa..5a097ee85dc 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -16,8 +16,8 @@ export const POLYGON_USDCE_ADDRESS = export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000'; -// HyperCore Perps USDC uses 8 decimals vs standard USDC's 6. -export const HYPERCORE_USDC_DECIMAL_SHIFT = 2; +export const HYPERCORE_USDC_DECIMALS = 8; +export const USDC_DECIMALS = 6; export const STABLECOINS: Record = { // Mainnet diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 3bf3cf1e610..92b5c33642c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -27,8 +27,9 @@ import { CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, HYPERCORE_USDC_ADDRESS, - HYPERCORE_USDC_DECIMAL_SHIFT, + HYPERCORE_USDC_DECIMALS, NATIVE_TOKEN_ADDRESS, + USDC_DECIMALS, STABLECOINS, } from '../../constants'; import { projectLogger } from '../../logger'; @@ -383,7 +384,7 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { newRequest.targetChainId = CHAIN_ID_HYPERCORE; newRequest.targetTokenAddress = HYPERCORE_USDC_ADDRESS; newRequest.targetAmountMinimum = new BigNumber(request.targetAmountMinimum) - .shiftedBy(HYPERCORE_USDC_DECIMAL_SHIFT) + .shiftedBy(HYPERCORE_USDC_DECIMALS - USDC_DECIMALS) .toString(10); log('Converting Arbitrum Hyperliquid deposit to direct deposit', { @@ -400,7 +401,7 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { if (newRequest.sourceTokenAmount) { newRequest.sourceTokenAmount = new BigNumber(newRequest.sourceTokenAmount) - .shiftedBy(HYPERCORE_USDC_DECIMAL_SHIFT) + .shiftedBy(HYPERCORE_USDC_DECIMALS - USDC_DECIMALS) .toString(10); } } From 715baf6e63b9058bf137356efcf0cdc1b8398a80 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:50:59 +0100 Subject: [PATCH 07/14] Update the steps type Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- packages/transaction-pay-controller/src/strategy/relay/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index b70df29c983..78b32277f68 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -76,7 +76,7 @@ export type RelayQuote = { }; metamask: RelayQuoteMetamask; request: RelayQuoteRequest; - steps: (RelayTransactionStep | RelaySignatureStep)[]; + steps: (RelayTransactionStep | RelaySignatureStep | RelayHyperliquidDepositStep)[]; }; export type RelayTransactionStep = { From e03373c9a8fb9d6f5e9eaa8bff82ff78571b5d6c Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:52:52 +0100 Subject: [PATCH 08/14] Alphabetical order Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- packages/transaction-pay-controller/src/types.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 879b95bf629..723150c1da2 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -73,6 +73,13 @@ export type TransactionPayControllerGetStateAction = ControllerGetStateAction< /** Configurable properties of a transaction. */ export type TransactionConfig = { + /** + * Whether the source of funds is HyperLiquid (HyperCore). + * When true, the Relay strategy uses the HyperLiquid 2-step withdrawal + * flow: (1) authorize nonce-mapping, (2) sendAsset to Relay solver. + */ + isHyperliquidSource?: boolean; + /** Whether the user has selected the maximum amount. */ isMaxAmount?: boolean; @@ -83,13 +90,6 @@ export type TransactionConfig = { */ isPostQuote?: boolean; - /** - * Whether the source of funds is HyperLiquid (HyperCore). - * When true, the Relay strategy uses the HyperLiquid 2-step withdrawal - * flow: (1) authorize nonce-mapping, (2) sendAsset to Relay solver. - */ - isHyperliquidSource?: boolean; - /** * Optional address to receive refunds if the Relay transaction fails. * When set, overrides the default refund recipient (EOA) in the Relay quote From d9825b91fd58a264d81380720b93af2f051d61a3 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:01:07 +0100 Subject: [PATCH 09/14] =?UTF-8?q?Allow=20=E2=80=9Csignature=E2=80=9D=20as?= =?UTF-8?q?=20a=20valid=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/strategy/relay/relay-submit.ts | 5 ++++- .../transaction-pay-controller/src/strategy/relay/types.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 2e1d784f929..9ae6846e53c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -315,7 +315,10 @@ async function submitTransactions( (step): step is RelayTransactionStep => step.kind === 'transaction', ); const params = txSteps.flatMap((step) => step.items).map((item) => item.data); - const invalidKind = steps.find((step) => step.kind !== 'transaction')?.kind; + const SUPPORTED_STEP_KINDS = ['transaction', 'signature']; + const invalidKind = steps.find( + (step) => !SUPPORTED_STEP_KINDS.includes(step.kind), + )?.kind; if (invalidKind) { throw new Error(`Unsupported step kind: ${invalidKind}`); diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 78b32277f68..c12b886b1ae 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -76,7 +76,11 @@ export type RelayQuote = { }; metamask: RelayQuoteMetamask; request: RelayQuoteRequest; - steps: (RelayTransactionStep | RelaySignatureStep | RelayHyperliquidDepositStep)[]; + steps: ( + | RelayTransactionStep + | RelaySignatureStep + | RelayHyperliquidDepositStep + )[]; }; export type RelayTransactionStep = { From c489044cb79dfd7d6d3eedf0f5e6dbcc9b61a6a1 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:04:57 +0100 Subject: [PATCH 10/14] Fix linting Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../src/strategy/relay/relay-quotes.test.ts | 8 ++------ packages/transaction-pay-controller/src/utils/quotes.ts | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 5dc19613758..1dde69160fc 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -2310,9 +2310,7 @@ describe('Relay Quotes Utils', () => { ); expect(body.originChainId).toBe(parseInt(CHAIN_ID_HYPERCORE, 16)); - expect(body.originCurrency).toBe( - '0x00000000000000000000000000000000', - ); + expect(body.originCurrency).toBe('0x00000000000000000000000000000000'); }); it('shifts source amount by 2 decimals (8→6)', async () => { @@ -2346,9 +2344,7 @@ describe('Relay Quotes Utils', () => { const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; - expect(result[0].fees.sourceNetwork.estimate).toStrictEqual( - zeroAmount, - ); + expect(result[0].fees.sourceNetwork.estimate).toStrictEqual(zeroAmount); expect(result[0].fees.sourceNetwork.max).toStrictEqual(zeroAmount); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 143f4b866fc..ed5c122effb 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -256,6 +256,7 @@ export async function refreshQuotes( * @param request - Request parameters. * @param request.from - Address from which the transaction is sent. * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. + * @param request.isHyperliquidSource - Whether the source of funds is HyperLiquid. * @param request.isPostQuote - Whether this is a post-quote flow. * @param request.paymentToken - Payment token (source for standard flows, destination for post-quote). * @param request.refundTo - Optional address to receive refunds if the Relay transaction fails. @@ -335,6 +336,7 @@ function buildQuoteRequests({ * @param request - Request parameters. * @param request.from - Address from which the transaction is sent. * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. + * @param request.isHyperliquidSource - Whether the source of funds is HyperLiquid. * @param request.destinationToken - Destination token (paymentToken in post-quote mode). * @param request.refundTo - Optional address to receive refunds if the Relay transaction fails. * @param request.sourceAmounts - Source amounts for the transaction (includes source token info). From 6d53ed69a874d3049d0920509d56a56fcf3ec42d Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:55:35 +0100 Subject: [PATCH 11/14] Resolve Bugbot comment and linting Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- .../strategy/relay/relay-max-gas-station.ts | 1 - .../src/strategy/relay/relay-quotes.test.ts | 73 ++++++++++--------- .../src/strategy/relay/relay-quotes.ts | 1 - 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts index 1724e7785e6..1abbaad082c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts @@ -1,4 +1,3 @@ -import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 1dde69160fc..85ca5bfa33e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -8,7 +8,7 @@ import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; import { getRelayQuotes } from './relay-quotes'; -import type { RelayQuote } from './types'; +import type { RelayQuote, RelayTransactionStep } from './types'; import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { ARBITRUM_USDC_ADDRESS, @@ -87,6 +87,31 @@ const QUOTE_REQUEST_MOCK: QuoteRequest = { targetTokenAddress: '0x1234567890123456789012345678901234567890', }; +const STEP_MOCK: RelayTransactionStep = { + id: 'swap', + requestId: '0x1', + kind: 'transaction', + items: [ + { + check: { + endpoint: '/test', + method: 'GET', + }, + data: { + chainId: 1, + data: '0x123' as Hex, + from: FROM_MOCK, + gas: '21000', + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '2000000000', + to: '0x2' as Hex, + value: '300000', + }, + status: 'complete', + }, + ], +}; + const QUOTE_MOCK = { details: { currencyIn: { @@ -117,32 +142,8 @@ const QUOTE_MOCK = { gasLimits: [21000], is7702: false, }, - steps: [ - { - id: 'swap', - items: [ - { - check: { - endpoint: '/test', - method: 'GET', - }, - data: { - chainId: 1, - data: '0x123' as Hex, - from: FROM_MOCK, - gas: '21000', - maxFeePerGas: '1000000000', - maxPriorityFeePerGas: '2000000000', - to: '0x2' as Hex, - value: '300000', - }, - status: 'complete', - }, - ], - kind: 'transaction', - }, - ], -} as RelayQuote; + steps: [STEP_MOCK], +} as RelayQuote & { steps: RelayTransactionStep[] }; const DELEGATION_RESULT_MOCK = { authorizationList: [ @@ -897,13 +898,13 @@ describe('Relay Quotes Utils', () => { ...QUOTE_MOCK, steps: [ { - ...QUOTE_MOCK.steps[0], + ...STEP_MOCK, items: [ - QUOTE_MOCK.steps[0].items[0], + STEP_MOCK.items[0], { - ...QUOTE_MOCK.steps[0].items[0], + ...STEP_MOCK.items[0], data: { - ...QUOTE_MOCK.steps[0].items[0].data, + ...STEP_MOCK.items[0].data, gas: '30000', }, }, @@ -956,13 +957,13 @@ describe('Relay Quotes Utils', () => { ...QUOTE_MOCK, steps: [ { - ...QUOTE_MOCK.steps[0], + ...STEP_MOCK, items: [ - QUOTE_MOCK.steps[0].items[0], + STEP_MOCK.items[0], { - ...QUOTE_MOCK.steps[0].items[0], + ...STEP_MOCK.items[0], data: { - ...QUOTE_MOCK.steps[0].items[0].data, + ...STEP_MOCK.items[0].data, gas: '30000', }, }, @@ -2932,7 +2933,7 @@ describe('Relay Quotes Utils', () => { quoteMock.steps[0].items = [ { ...quoteMock.steps[0].items[0], - data: {}, + data: {} as RelayTransactionStep['items'][0]['data'], }, ]; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 92b5c33642c..8ad148cbf13 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -394,7 +394,6 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { } // HyperLiquid withdrawal: source is HyperCore Perps USDC, not Arbitrum. - // Override source chain/token and set protocolVersion=v2 (required by Relay). if (request.isHyperliquidSource) { newRequest.sourceChainId = CHAIN_ID_HYPERCORE; newRequest.sourceTokenAddress = HYPERCORE_USDC_ADDRESS; From f38d4c8a4f02772fc32bcfe254850532156bc76b Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:19:58 +0100 Subject: [PATCH 12/14] Linting Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- eslint-suppressions.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 03a7bc55bf6..f02e6062810 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1734,11 +1734,6 @@ "count": 2 } }, - "packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts": { - "import-x/no-relative-packages": { - "count": 1 - } - }, "packages/transaction-pay-controller/src/utils/source-amounts.ts": { "import-x/no-relative-packages": { "count": 1 @@ -1848,4 +1843,4 @@ "count": 1 } } -} +} \ No newline at end of file From 36571e1164be8275622ce256586d1df2f73a16ad Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:38:47 +0100 Subject: [PATCH 13/14] Linting Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f02e6062810..613fc19879e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1843,4 +1843,4 @@ "count": 1 } } -} \ No newline at end of file +} From 581aa2041d62e79fc6404fc831aebf6eed3ed464 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:00:15 +0100 Subject: [PATCH 14/14] Update changelog Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 9e639bc0db9..b7504fd7480 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add HyperLiquid source quote support for Relay strategy ([#8285](https://github.com/MetaMask/core/pull/8285)) + ### Changed - Bump `@metamask/assets-controller` from `^3.1.0` to `^3.1.1` ([#8298](https://github.com/MetaMask/core/pull/8298))