Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
011e821
feat(transaction-pay-controller): submit target calls as a second leg…
matthewwalsh0 Sep 28, 2026
92bb8c5
docs(transaction-pay-controller): add changelog entry for second-leg …
matthewwalsh0 Sep 28, 2026
dbff1af
refactor(transaction-pay-controller): rename server quote calls flags…
matthewwalsh0 Sep 29, 2026
1b9c6ed
refactor(transaction-pay-controller): submit every second leg through…
matthewwalsh0 Sep 29, 2026
4581806
feat(transaction-pay-controller): only defer server quote calls when …
matthewwalsh0 Sep 29, 2026
82bfa31
chore(transaction-pay-controller): realign lint suppressions for the …
matthewwalsh0 Sep 30, 2026
6a8ad96
refactor(transaction-pay-controller): move settlement hash resolution…
matthewwalsh0 Oct 3, 2026
ad29b46
refactor(transaction-pay-controller): make resolveSecondLegCalls priv…
matthewwalsh0 Oct 3, 2026
6c8dc66
docs(transaction-pay-controller): note settled amount fallback remova…
matthewwalsh0 Oct 3, 2026
3a48ee5
feat(transaction-pay-controller): accept non-atomic requests in the s…
matthewwalsh0 Oct 3, 2026
9578427
refactor(transaction-pay-controller): type server provider as a string
matthewwalsh0 Oct 3, 2026
055b974
fix(transaction-pay-controller): zero relay source network fees for p…
matthewwalsh0 Oct 7, 2026
e912f9f
test(transaction-pay-controller): pass from in polymarket fee test an…
matthewwalsh0 Oct 7, 2026
824d1b2
fix(transaction-pay-controller): fund the execution account and detec…
matthewwalsh0 Oct 8, 2026
8b5d406
fix(transaction-pay-controller): set isIntentComplete only after the …
matthewwalsh0 Oct 8, 2026
e2cc524
Merge remote-tracking branch 'origin/main' into feat/transaction-pay-…
matthewwalsh0 Oct 9, 2026
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
30 changes: 11 additions & 19 deletions oxlint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6588,13 +6588,13 @@
"count": 11
},
"typescript/no-unsafe-assignment": {
"count": 9
"count": 6
},
"typescript/no-unsafe-call": {
"count": 12
"count": 9
},
"typescript/no-unsafe-member-access": {
"count": 12
"count": 9
}
},
"packages/transaction-pay-controller/src/strategy/relay/RelayStrategy.test.ts": {
Expand Down Expand Up @@ -6660,13 +6660,13 @@
},
"packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts": {
"typescript/no-unsafe-assignment": {
"count": 269
"count": 268
},
"typescript/no-unsafe-call": {
"count": 83
"count": 84
},
"typescript/no-unsafe-member-access": {
"count": 217
"count": 216
},
"typescript/no-unsafe-return": {
"count": 2
Expand Down Expand Up @@ -6702,13 +6702,13 @@
"count": 1
},
"typescript/no-unsafe-assignment": {
"count": 36
"count": 34
},
"typescript/no-unsafe-call": {
"count": 19
"count": 17
},
"typescript/no-unsafe-member-access": {
"count": 28
"count": 26
}
},
"packages/transaction-pay-controller/src/strategy/relay/relay-validation.test.ts": {
Expand All @@ -6726,14 +6726,11 @@
}
},
"packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts": {
"no-unused-vars": {
"count": 1
},
"typescript/no-unnecessary-type-assertion": {
"count": 1
},
"typescript/no-unsafe-assignment": {
"count": 47
"count": 66
},
"typescript/no-unsafe-call": {
"count": 9
Expand All @@ -6758,7 +6755,7 @@
"count": 4
},
"typescript/no-unsafe-assignment": {
"count": 44
"count": 46
},
"typescript/no-unsafe-call": {
"count": 21
Expand Down Expand Up @@ -6821,11 +6818,6 @@
"count": 1
}
},
"packages/transaction-pay-controller/src/utils/ma-vault-deposit.test.ts": {
"no-useless-return": {
"count": 2
}
},
"packages/transaction-pay-controller/src/utils/provider.test.ts": {
"no-unused-vars": {
"count": 1
Expand Down
16 changes: 16 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support submitting target calls as a separate second leg when the quote provider cannot execute them ([#10501](https://github.com/MetaMask/core/pull/10501))
- Add `requiresSecondLeg` to `TransactionPayQuote`.
- `ServerStrategy` now supports non-atomic requests.
- Non-atomic quotes settle funds on the account that executes the calls.

### Changed

- Fail fiat and second-leg submissions when the settled amount cannot be read from an on-chain transaction, rather than falling back to `order.cryptoAmount` or the quote minimum ([#10501](https://github.com/MetaMask/core/pull/10501))

### Fixed

- Zero source network fees for Relay quotes on Polymarket deposit-wallet Predict withdraws, as the Polymarket relayer pays origin gas ([#10501](https://github.com/MetaMask/core/pull/10501))
- Set `isIntentComplete` on the parent transaction only after the non-atomic second leg of a Relay quote is submitted, so a failed second leg no longer leaves a failed transaction flagged as complete ([#10501](https://github.com/MetaMask/core/pull/10501))

## [30.0.2]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ describe('FiatStrategy', () => {

it('preserves nested Post-Ramp and Vault prefixes', async () => {
submitFiatQuotesMock.mockRejectedValue(
new Error('Post-Ramp: Direct mUSD: Vault: Missing transaction hash'),
new Error(
'Post-Ramp: Direct mUSD: Second leg: Vault: Missing transaction hash',
),
);

await expect(
Expand All @@ -102,7 +104,7 @@ describe('FiatStrategy', () => {
transaction: { txParams: { from: '0x1' } } as TransactionMeta,
}),
).rejects.toThrow(
'Fiat: Post-Ramp: Direct mUSD: Vault: Missing transaction hash',
'Fiat: Post-Ramp: Direct mUSD: Second leg: Vault: Missing transaction hash',
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import type {
} from '../../types.js';
import { prefixError } from '../../utils/error-prefix.js';
import { getFiatVaultDisabled } from '../../utils/feature-flags.js';
import { submitMoneyAccountVaultDeposit } from '../../utils/ma-vault-deposit.js';
import { submitSecondLeg } from '../../utils/second-leg.js';
import { buildCaipAssetType, getTokenInfo } from '../../utils/token.js';
import { MUSD_MONAD_FIAT_ASSET } from './constants.js';
import type { FiatQuote } from './types.js';
import {
getRampsQuote,
getRawSourceAmountFromOrderCryptoAmount,
resolveSourceAmountRaw,
validateOrderAsset,
} from './utils.js';

Expand Down Expand Up @@ -135,6 +134,7 @@ export async function submitDirectMusdAfterFiatCompletion({
request: PayStrategyExecuteRequest<FiatQuote>;
}): Promise<{ transactionHash?: Hex }> {
const { messenger, transaction } = request;
const moneyAccountAddress = transaction.txParams.from as Hex;

try {
validateOrderAsset({
Expand All @@ -143,20 +143,24 @@ export async function submitDirectMusdAfterFiatCompletion({
transactionId: transaction.id,
});

const { amountRaw: sourceAmountRaw, fromBlock } =
await resolveSourceAmountRaw({
messenger,
order,
fiatAsset: MUSD_MONAD_FIAT_ASSET,
walletAddress: transaction.txParams.from as Hex,
// Kill switch: leave the on-ramped mUSD in the Money Account rather than
// depositing it into the vault.
if (getFiatVaultDisabled(messenger)) {
log('Skipping vault deposit because the fiat vault is disabled', {
moneyAccountAddress,
transactionId: transaction.id,
});

return await submitMoneyAccountVaultDeposit({
fromBlock,
return { transactionHash: '0x' };
}

return await submitSecondLeg({
chainId: MUSD_MONAD_FIAT_ASSET.chainId,
from: moneyAccountAddress,
messenger,
sourceAmountRaw,
settlementHash: order.txHash as Hex | undefined,
tokenAddress: MUSD_MONAD_FIAT_ASSET.address,
transaction,
vaultDisabled: getFiatVaultDisabled(messenger),
});
} catch (error) {
throw prefixError(error, DIRECT_MUSD_ERROR_PREFIX);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { buildCaipAssetType } from '../../utils/token.js';
import {
collectTransactionIds,
getTransaction,
getTransferredAmountFromTxHash,
updateTransaction,
waitForTransactionConfirmed,
} from '../../utils/transaction.js';
Expand Down Expand Up @@ -142,15 +143,18 @@ function getFiatOrderMock({
cryptoAmount = '1',
cryptoCurrency,
status = RampsOrderStatus.Completed,
txHash,
}: {
cryptoAmount?: RampsOrder['cryptoAmount'];
cryptoCurrency?: RampsOrderCryptoCurrency;
status?: RampsOrderStatus;
txHash?: RampsOrder['txHash'];
} = {}): RampsOrder {
return {
cryptoAmount,
cryptoCurrency,
status,
txHash,
} as RampsOrder;
}

Expand Down Expand Up @@ -312,6 +316,9 @@ describe('submitFiatQuotes', () => {
const deriveFiatAssetForFiatPaymentMock = jest.mocked(
deriveFiatAssetForFiatPayment,
);
const getTransferredAmountFromTxHashMock = jest.mocked(
getTransferredAmountFromTxHash,
);
const resolveSourceAmountRawMock = jest.mocked(resolveSourceAmountRaw);
const updateTransactionMock = jest.mocked(updateTransaction);
const collectTransactionIdsMock = jest.mocked(collectTransactionIds);
Expand Down Expand Up @@ -345,10 +352,7 @@ describe('submitFiatQuotes', () => {
);
waitForTransactionConfirmedMock.mockResolvedValue();
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);
resolveSourceAmountRawMock.mockResolvedValue({
amountRaw: '1000000000000000000',
fromBlock: undefined,
});
resolveSourceAmountRawMock.mockResolvedValue('1000000000000000000');
fundFiatOrderFromTestSourceMock.mockResolvedValue(
getFiatOrderMock({
cryptoAmount: '1',
Expand All @@ -375,10 +379,7 @@ describe('submitFiatQuotes', () => {
},
status: RampsOrderStatus.Completed,
});
resolveSourceAmountRawMock.mockResolvedValue({
amountRaw: '1234500000000000000',
fromBlock: undefined,
});
resolveSourceAmountRawMock.mockResolvedValue('1234500000000000000');
const { callMock, request } = getRequest({ order });

const result = await submitFiatQuotes(request);
Expand Down Expand Up @@ -425,10 +426,7 @@ describe('submitFiatQuotes', () => {
},
status: RampsOrderStatus.Completed,
});
resolveSourceAmountRawMock.mockResolvedValue({
amountRaw: '1234500000000000000',
fromBlock: undefined,
});
resolveSourceAmountRawMock.mockResolvedValue('1234500000000000000');

const callMock = jest.fn((action: string) => {
if (action === 'TransactionPayController:getState') {
Expand Down Expand Up @@ -551,10 +549,7 @@ describe('submitFiatQuotes', () => {
],
} as unknown as TransactionMeta;

resolveSourceAmountRawMock.mockResolvedValue({
amountRaw: '1234500000000000000',
fromBlock: undefined,
});
resolveSourceAmountRawMock.mockResolvedValue('1234500000000000000');

const { callMock, request } = getRequest({
transaction: nestedTransaction,
Expand Down Expand Up @@ -1094,6 +1089,16 @@ describe('submitFiatQuotes', () => {
describe('direct mUSD to money account flow', () => {
const MONEY_ACCOUNT_ADDRESS =
'0x3333333333333333333333333333333333333333' as Hex;
const MUSD_SETTLEMENT_HASH_MOCK =
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890' as Hex;
const MUSD_SETTLED_AMOUNT_MOCK = '10000000';

beforeEach(() => {
getTransferredAmountFromTxHashMock.mockResolvedValue({
amountRaw: MUSD_SETTLED_AMOUNT_MOCK,
blockNumber: undefined,
});
});

const MUSD_QUOTE_REQUEST: QuoteRequest = {
from: WALLET_ADDRESS_MOCK,
Expand All @@ -1111,7 +1116,11 @@ describe('submitFiatQuotes', () => {
id: TRANSACTION_ID_MOCK,
nestedTransactions: [
{ data: '0xoldApprove', to: '0xapprove' },
{ data: '0xoldDeposit', to: '0xdeposit' },
{
data: '0xoldDeposit',
to: '0xdeposit',
type: TransactionType.moneyAccountDeposit,
},
],
txParams: { from: MONEY_ACCOUNT_ADDRESS },
type: 'batch',
Expand All @@ -1120,6 +1129,7 @@ describe('submitFiatQuotes', () => {
it('uses txParams.from as walletAddress when quote is direct mUSD', async () => {
const order = getFiatOrderMock({
status: RampsOrderStatus.Completed,
txHash: MUSD_SETTLEMENT_HASH_MOCK,
});
const { callMock, request } = getRequest({
order,
Expand Down Expand Up @@ -1149,6 +1159,7 @@ describe('submitFiatQuotes', () => {
symbol: 'MUSD',
},
status: RampsOrderStatus.Completed,
txHash: MUSD_SETTLEMENT_HASH_MOCK,
});
buildCaipAssetTypeMock.mockReturnValue(
'eip155:143/erc20:0xaca92e438df0b2401ff60da7e4337b687a2435da',
Expand All @@ -1170,6 +1181,7 @@ describe('submitFiatQuotes', () => {

it('submits a sponsored Money Account vault batch for direct pure-fiat mUSD', async () => {
const { callMock, request } = getRequest({
order: getFiatOrderMock({ txHash: MUSD_SETTLEMENT_HASH_MOCK }),
quotes: [
getFiatQuoteMock({
includeRelayQuote: false,
Expand All @@ -1192,13 +1204,13 @@ describe('submitFiatQuotes', () => {
expect(callMock).toHaveBeenCalledWith(
'TransactionPayController:getAmountData',
expect.objectContaining({
amount: '1000000000000000000',
amount: MUSD_SETTLED_AMOUNT_MOCK,
transaction: expect.objectContaining({ id: TRANSACTION_ID_MOCK }),
}),
);
expect(updateTransactionMock).toHaveBeenCalledWith(
expect.objectContaining({
note: 'Money Account vault deposit: update vault amount',
note: 'Second leg: update amount',
transactionId: TRANSACTION_ID_MOCK,
}),
expect.any(Function),
Expand Down Expand Up @@ -1226,7 +1238,7 @@ describe('submitFiatQuotes', () => {
);
expect(updateTransactionMock).toHaveBeenCalledWith(
expect.objectContaining({
note: 'Add required transaction ID from Money Account vault submission',
note: 'Add required transaction ID from second leg submission',
transactionId: TRANSACTION_ID_MOCK,
}),
expect.any(Function),
Expand All @@ -1247,6 +1259,7 @@ describe('submitFiatQuotes', () => {
: undefined,
);
const { request } = getRequest({
order: getFiatOrderMock({ txHash: MUSD_SETTLEMENT_HASH_MOCK }),
quotes: [
getFiatQuoteMock({
includeRelayQuote: false,
Expand All @@ -1257,7 +1270,7 @@ describe('submitFiatQuotes', () => {
});

await expect(submitFiatQuotes(request)).rejects.toThrow(
'Post-Ramp: Direct mUSD: Missing transaction hash',
'Post-Ramp: Direct mUSD: Second leg: Vault: Missing transaction hash',
);
});

Expand Down Expand Up @@ -1297,7 +1310,7 @@ describe('submitFiatQuotes', () => {
}

if (action === 'RampsController:getOrder') {
return getFiatOrderMock();
return getFiatOrderMock({ txHash: MUSD_SETTLEMENT_HASH_MOCK });
}

if (action === 'TransactionPayController:getAmountData') {
Expand All @@ -1322,7 +1335,7 @@ describe('submitFiatQuotes', () => {
});

await expect(submitFiatQuotes(request)).rejects.toThrow(
'Post-Ramp: Direct mUSD: Vault: batch failed',
'Post-Ramp: Direct mUSD: Second leg: Vault: batch failed',
);
});

Expand Down Expand Up @@ -1389,7 +1402,7 @@ describe('submitFiatQuotes', () => {
);
expect(updateTransactionMock).not.toHaveBeenCalledWith(
expect.objectContaining({
note: 'Add required transaction ID from Money Account vault submission',
note: 'Add required transaction ID from second leg submission',
}),
expect.any(Function),
);
Expand Down
Loading
Loading