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

- Add route-based `confirmations_pay` strategy resolution ([#8282](https://github.com/MetaMask/core/pull/8282))

### Fixed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more suitable as an Added ?


- Support for perps deposit for Across ([#8334](https://github.com/MetaMask/core/pull/8334))

## [19.0.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getAcrossQuotes } from './across-quotes';
import { submitAcrossQuotes } from './across-submit';
import { AcrossStrategy } from './AcrossStrategy';
import type { AcrossQuote } from './types';
import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../../constants';
import type {
PayStrategyExecuteRequest,
PayStrategyGetQuotesRequest,
Expand Down Expand Up @@ -91,7 +92,7 @@ describe('AcrossStrategy', () => {
expect(strategy.supports(baseRequest)).toBe(false);
});

it('returns true for perps deposits when other constraints are met', () => {
it('returns true for supported perps direct deposits', () => {
const strategy = new AcrossStrategy();
expect(
strategy.supports({
Expand All @@ -100,23 +101,48 @@ describe('AcrossStrategy', () => {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsDeposit,
} as TransactionMeta,
requests: [
{
from: '0xabc' as Hex,
sourceBalanceRaw: '100',
sourceChainId: CHAIN_ID_ARBITRUM,
sourceTokenAddress: ARBITRUM_USDC_ADDRESS,
sourceTokenAmount: '100',
targetAmountMinimum: '100',
targetChainId: CHAIN_ID_ARBITRUM,
targetTokenAddress: ARBITRUM_USDC_ADDRESS,
},
],
}),
).toBe(true);
});

it('returns false for perps across deposits', () => {
it('returns false for unsupported perps deposits', () => {
const strategy = new AcrossStrategy();
expect(
strategy.supports({
...baseRequest,
transaction: {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsAcrossDeposit,
type: TransactionType.perpsDeposit,
} as TransactionMeta,
}),
).toBe(false);
});

it('applies generic cross-chain handling to perps across deposits', () => {
const strategy = new AcrossStrategy();
expect(
strategy.supports({
...baseRequest,
transaction: {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsAcrossDeposit,
} as TransactionMeta,
}),
).toBe(true);
});

it('returns false for same-chain swaps', () => {
const strategy = new AcrossStrategy();
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TransactionType } from '@metamask/transaction-controller';

import { getAcrossQuotes } from './across-quotes';
import { submitAcrossQuotes } from './across-submit';
import { isSupportedAcrossPerpsDepositRequest } from './perps';
import type { AcrossQuote } from './types';
import type {
PayStrategy,
Expand All @@ -19,8 +20,13 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {
return false;
}

if (request.transaction?.type === TransactionType.perpsAcrossDeposit) {
return false;
if (request.transaction?.type === TransactionType.perpsDeposit) {
return request.requests.every((singleRequest) =>
isSupportedAcrossPerpsDepositRequest(
singleRequest,
request.transaction?.type,
),
);
}

// Across doesn't support same-chain swaps (e.g. mUSD conversions).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { TransactionDescription } from '@ethersproject/abi';
import type { TransactionMeta } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';

import { isSupportedAcrossPerpsDepositRequest } from './perps';
import type { AcrossAction, AcrossActionArg } from './types';
import type { QuoteRequest } from '../../types';

Expand Down Expand Up @@ -77,6 +78,13 @@ export function getAcrossDestination(
transaction: TransactionMeta,
request: QuoteRequest,
): AcrossDestination {
if (isSupportedAcrossPerpsDepositRequest(request, transaction.type)) {
return {
actions: [],
recipient: request.from,
};
}

const { from } = request;
const destinationCalls = getDestinationCalls(transaction);
const swapRecipientTransferCallIndex = destinationCalls.findIndex((call) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import type { TransactionMeta } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';

import { getAcrossQuotes } from './across-quotes';
import { ACROSS_HYPERCORE_USDC_PERPS_ADDRESS } from './perps';
import * as acrossTransactions from './transactions';
import type { AcrossSwapApprovalResponse } from './types';
import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller';
import { TransactionPayStrategy } from '../../constants';
import {
ARBITRUM_USDC_ADDRESS,
CHAIN_ID_ARBITRUM,
CHAIN_ID_HYPERCORE,
TransactionPayStrategy,
} from '../../constants';
import { getMessengerMock } from '../../tests/messenger-mock';
import type { QuoteRequest } from '../../types';
import { getGasBuffer, getSlippage } from '../../utils/feature-flags';
Expand Down Expand Up @@ -383,6 +389,46 @@ describe('Across Quotes', () => {
expect(body.actions).toStrictEqual([]);
});

it('converts supported perps deposits to Across HyperCore direct deposits', async () => {
successfulFetchMock.mockResolvedValue({
json: async () => QUOTE_MOCK,
} as Response);

await getAcrossQuotes({
messenger,
requests: [
{
...QUOTE_REQUEST_MOCK,
targetAmountMinimum: '1000000',
targetChainId: CHAIN_ID_ARBITRUM,
targetTokenAddress: ARBITRUM_USDC_ADDRESS,
},
],
transaction: {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsDeposit,
txParams: {
from: FROM_MOCK,
to: ARBITRUM_USDC_ADDRESS,
data: buildTransferData(TRANSFER_RECIPIENT, 1),
},
} as TransactionMeta,
});

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

expect(params.get('amount')).toBe('100000000');
expect(params.get('destinationChainId')).toBe(
String(parseInt(CHAIN_ID_HYPERCORE, 16)),
);
expect(params.get('outputToken')).toBe(
ACROSS_HYPERCORE_USDC_PERPS_ADDRESS,
);
expect(params.get('recipient')).toBe(FROM_MOCK);
expect(getRequestBody().actions).toStrictEqual([]);
});

it('uses transfer recipient for token transfer transactions', async () => {
const transferData = buildTransferData(TRANSFER_RECIPIENT);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createModuleLogger } from '@metamask/utils';
import { BigNumber } from 'bignumber.js';

import { getAcrossDestination } from './across-actions';
import { normalizeAcrossRequest } from './perps';
import { getAcrossOrderedTransactions } from './transactions';
import type {
AcrossAction,
Expand Down Expand Up @@ -80,6 +81,7 @@ async function getSingleQuote(
fullRequest: PayStrategyGetQuotesRequest,
): Promise<TransactionPayQuote<AcrossQuote>> {
const { messenger, transaction } = fullRequest;
const normalizedRequest = normalizeAcrossRequest(request, transaction.type);
const {
from,
isMaxAmount,
Expand All @@ -89,7 +91,7 @@ async function getSingleQuote(
targetAmountMinimum,
targetChainId,
targetTokenAddress,
} = request;
} = normalizedRequest;

const config = getPayStrategiesConfig(messenger);
const slippageDecimal = getSlippage(
Expand Down Expand Up @@ -123,7 +125,7 @@ async function getSingleQuote(
},
};

return await normalizeQuote(originalQuote, request, fullRequest);
return await normalizeQuote(originalQuote, normalizedRequest, fullRequest);
}

type AcrossApprovalRequest = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { TransactionType } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';
import { BigNumber } from 'bignumber.js';

import {
ARBITRUM_USDC_ADDRESS,
CHAIN_ID_ARBITRUM,
CHAIN_ID_HYPERCORE,
HYPERCORE_USDC_DECIMALS,
USDC_DECIMALS,
} from '../../constants';
import type { QuoteRequest } from '../../types';

export const ACROSS_HYPERCORE_USDC_PERPS_ADDRESS =
'0x2100000000000000000000000000000000000000' as Hex;

/**
* Detect the quote-time parent transaction shape that Across can map to the
* new HyperCore USDC-PERPS direct-deposit route.
*
* The parent transaction remains `perpsDeposit` while quotes are being
* selected. `perpsAcrossDeposit` is only assigned later to the generated
* Across submission transaction(s).
*
* @param request - Transaction pay quote request.
* @param parentTransactionType - Parent transaction type before Across
* execution.
* @returns Whether the request matches the supported direct-deposit path.
*/
export function isSupportedAcrossPerpsDepositRequest(
request: Pick<
QuoteRequest,
'isPostQuote' | 'targetChainId' | 'targetTokenAddress'
>,
parentTransactionType?: TransactionType,
): boolean {
return (
parentTransactionType === TransactionType.perpsDeposit &&
request.isPostQuote !== true &&
request.targetChainId === CHAIN_ID_ARBITRUM &&
request.targetTokenAddress.toLowerCase() ===
ARBITRUM_USDC_ADDRESS.toLowerCase()
);
}

/**
* Convert the transaction-pay request into the Across route shape required for
* direct perps deposits.
*
* Transaction pay starts from the required on-chain asset identity
* (Arbitrum USDC, 6 decimals), while Across now expects the HyperCore
* USDC-PERPS destination token (8 decimals).
*
* @param request - Transaction pay quote request.
* @param parentTransactionType - Parent transaction type before Across
* execution.
* @returns Normalized request for Across quoting.
*/
export function normalizeAcrossRequest(
request: QuoteRequest,
parentTransactionType?: TransactionType,
): QuoteRequest {
if (!isSupportedAcrossPerpsDepositRequest(request, parentTransactionType)) {
return request;
}

return {
...request,
targetAmountMinimum: new BigNumber(request.targetAmountMinimum)
.shiftedBy(HYPERCORE_USDC_DECIMALS - USDC_DECIMALS)
.toFixed(0),
targetChainId: CHAIN_ID_HYPERCORE,
targetTokenAddress: ACROSS_HYPERCORE_USDC_PERPS_ADDRESS,
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TransactionType } from '@metamask/transaction-controller';

import { updateSourceAmounts } from './source-amounts';
import { getTokenFiatRate } from './token';
import { getTransaction } from './transaction';
Expand Down Expand Up @@ -53,7 +55,9 @@ describe('Source Amounts Utils', () => {

getTokenFiatRateMock.mockReturnValue({ fiatRate: '2.0', usdRate: '3.0' });
getStrategyMock.mockReturnValue(TransactionPayStrategy.Test);
getTransactionMock.mockReturnValue({ id: TRANSACTION_ID_MOCK } as never);
getTransactionMock.mockReturnValue({
id: TRANSACTION_ID_MOCK,
} as never);
});

describe('updateSourceAmounts', () => {
Expand Down Expand Up @@ -117,6 +121,34 @@ describe('Source Amounts Utils', () => {
expect(transactionData.sourceAmounts).toHaveLength(1);
});

it('does not return empty array if payment token matches but supported perps deposit and across strategy', () => {
getStrategyMock.mockReturnValue(TransactionPayStrategy.Across);
getTransactionMock.mockReturnValue({
id: TRANSACTION_ID_MOCK,
type: TransactionType.perpsDeposit,
} as never);

const transactionData: TransactionData = {
isLoading: false,
paymentToken: {
...PAYMENT_TOKEN_MOCK,
address: ARBITRUM_USDC_ADDRESS,
chainId: CHAIN_ID_ARBITRUM,
},
tokens: [
{
...TRANSACTION_TOKEN_MOCK,
address: ARBITRUM_USDC_ADDRESS,
chainId: CHAIN_ID_ARBITRUM,
},
],
};

updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger);

expect(transactionData.sourceAmounts).toHaveLength(1);
});

it('returns empty array if skipIfBalance and has balance', () => {
const transactionData: TransactionData = {
isLoading: false,
Expand Down
Loading
Loading