diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 03a7bc55bf6..613fc19879e 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 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)) 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/constants.ts b/packages/transaction-pay-controller/src/constants.ts index dffbcdc9cb2..5a097ee85dc 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'; + +export const HYPERCORE_USDC_DECIMALS = 8; +export const USDC_DECIMALS = 6; + 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-max-gas-station.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-max-gas-station.ts index a318e02d3c6..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 @@ -5,7 +5,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, @@ -307,7 +307,10 @@ async function getGasCostFromQuoteOrGasStation( }; } - const firstStepData = quote.original.steps[0]?.items[0]?.data; + const firstTxStep = quote.original.steps.find( + (step): step is RelayTransactionStep => step.kind === 'transaction', + ); + const firstStepData = firstTxStep?.items[0]?.data; 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..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', }, }, @@ -1876,6 +1877,7 @@ describe('Relay Quotes Utils', () => { }, }, ], + kind: 'transaction', } as never); successfulFetchMock.mockResolvedValue({ @@ -2283,6 +2285,89 @@ 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('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, @@ -2848,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 ee794aad7a2..8ad148cbf13 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,15 +18,18 @@ 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, CHAIN_ID_HYPERCORE, CHAIN_ID_POLYGON, + HYPERCORE_USDC_ADDRESS, + HYPERCORE_USDC_DECIMALS, NATIVE_TOKEN_ADDRESS, + USDC_DECIMALS, STABLECOINS, } from '../../constants'; import { projectLogger } from '../../logger'; @@ -378,9 +382,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_DECIMALS - USDC_DECIMALS) .toString(10); log('Converting Arbitrum Hyperliquid deposit to direct deposit', { @@ -389,6 +393,18 @@ function normalizeRequest(request: QuoteRequest): QuoteRequest { }); } + // HyperLiquid withdrawal: source is HyperCore Perps USDC, not Arbitrum. + if (request.isHyperliquidSource) { + newRequest.sourceChainId = CHAIN_ID_HYPERCORE; + newRequest.sourceTokenAddress = HYPERCORE_USDC_ADDRESS; + + if (newRequest.sourceTokenAmount) { + newRequest.sourceTokenAmount = new BigNumber(newRequest.sourceTokenAmount) + .shiftedBy(HYPERCORE_USDC_DECIMALS - USDC_DECIMALS) + .toString(10); + } + } + return newRequest; } @@ -537,7 +553,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 +631,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 +819,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 +848,7 @@ async function calculateSourceNetworkGasLimit( } function toRelayQuoteGasTransaction( - singleParams: RelayQuote['steps'][0]['items'][0]['data'], + singleParams: RelayTransactionStep['items'][0]['data'], fromOverride?: Hex, ): QuoteGasTransaction { return { @@ -920,7 +962,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..9ae6846e53c 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,8 +311,14 @@ async function submitTransactions( messenger: TransactionPayControllerMessenger, ): Promise { const { steps } = quote.original; - const params = steps.flatMap((step) => step.items).map((item) => item.data); - const invalidKind = steps.find((step) => step.kind !== 'transaction')?.kind; + const txSteps = steps.filter( + (step): step is RelayTransactionStep => step.kind === 'transaction', + ); + const params = txSteps.flatMap((step) => step.items).map((item) => item.data); + 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 326f6cb9cc9..c12b886b1ae 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,81 @@ export type RelayQuote = { }; metamask: RelayQuoteMetamask; request: RelayQuoteRequest; - steps: { - id: string; - items: { - check: { + steps: ( + | RelayTransactionStep + | RelaySignatureStep + | RelayHyperliquidDepositStep + )[]; +}; + +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..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; @@ -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..ed5c122effb 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, @@ -254,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. @@ -266,6 +269,7 @@ function buildQuoteRequests({ from, isMaxAmount, isPostQuote, + isHyperliquidSource, paymentToken, refundTo, sourceAmounts, @@ -275,6 +279,7 @@ function buildQuoteRequests({ from: Hex; isMaxAmount: boolean; isPostQuote?: boolean; + isHyperliquidSource?: boolean; paymentToken: TransactionPaymentToken | undefined; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -286,11 +291,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, @@ -332,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). @@ -341,6 +346,7 @@ function buildQuoteRequests({ function buildPostQuoteRequests({ from, isMaxAmount, + isHyperliquidSource, destinationToken, refundTo, sourceAmounts, @@ -348,6 +354,7 @@ function buildPostQuoteRequests({ }: { from: Hex; isMaxAmount: boolean; + isHyperliquidSource?: boolean; destinationToken: TransactionPaymentToken; refundTo?: Hex; sourceAmounts: TransactionPaySourceAmount[] | undefined; @@ -376,13 +383,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,