Skip to content

Commit 315b63a

Browse files
pragmaximclaude
andcommitted
feat(wallet-core): record private sends and route their estimates via privatePending
Third of three stacked changes for the blockbook privatePending hint (trezor/blockbook#1639). First change with observable behavior: it fills the slice from the two flows whose signed nonce is already in scope, and routes their gas estimates. - isMevPrivateSend(symbol, isMevProtectionEnabled) predicate (wallet-utils) - the exact condition under which getMevProtectedTxData broadcasts through the relay; - record the private nonce+txid after a successful pushTransaction in the send flow (pushSendFormTransactionThunk) and in WalletConnect eth_sendTransaction, gated by isMevPrivateSend so public sends never declare; - adopt buildEvmEstimateSpecific at the send and allowance estimate sites, passing the account's private-pending nonces so the estimate simulates against the relay's pending-private state - presence-only, so a keystroke with nothing in flight never routes to the relay. Deferred (see PR description): staking / Merkl-claim / stablecoin-yield writes need signed-nonce capture first (PrecomposedTransactionFinal carries no nonce); until then those private flows fall back to blockbook's recentSenders heuristic, which is acceptable because #1639 keeps that heuristic as the fallback. IndexedDB persistence also deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec75d2a commit 315b63a

5 files changed

Lines changed: 86 additions & 13 deletions

File tree

suite-common/wallet-core/src/allowance/composeAllowanceTransactionThunk.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import {
77
type FeeLevelLabel,
88
type PrecomposedLevels,
99
} from '@suite-common/wallet-types';
10-
import { findToken, getAccountIdentity } from '@suite-common/wallet-utils';
10+
import {
11+
buildEvmEstimateSpecific,
12+
findToken,
13+
getAccountIdentity,
14+
} from '@suite-common/wallet-utils';
1115
import TrezorConnect from '@trezor/connect';
1216
import { BigNumber, typedObjectFromEntries } from '@trezor/utils';
1317

1418
import { ALLOWANCE_MODULE_PREFIX } from './allowanceConstants';
1519
import { buildAllowanceTransaction } from './buildAllowanceTransaction';
1620
import { ETHEREUM_ADJUST_GAS_LIMIT } from '../fees/feesUtils';
21+
import { selectAccountPrivatePendingNonces } from '../privatePending/privatePendingReducer';
1722
import { type ComposeFeeLevelsError } from '../send/sendFormTypes';
1823

1924
export interface ComposeAllowanceTransactionThunkParams {
@@ -36,7 +41,10 @@ export const composeAllowanceTransactionThunk = createThunk<
3641
{ rejectValue: ComposeFeeLevelsError }
3742
>(
3843
`${ALLOWANCE_MODULE_PREFIX}/composeAllowanceTransactionThunk`,
39-
async ({ feeInfo, account, contract, selectedFee, customFee, data }, { rejectWithValue }) => {
44+
async (
45+
{ feeInfo, account, contract, selectedFee, customFee, data },
46+
{ rejectWithValue, getState },
47+
) => {
4048
const token = findToken(account.tokens, contract);
4149

4250
if (!token) {
@@ -51,12 +59,17 @@ export const composeAllowanceTransactionThunk = createThunk<
5159
identity: getAccountIdentity(account),
5260
request: {
5361
blocks: [2],
54-
specific: {
55-
from: account.descriptor,
56-
to: contract,
57-
value: '0x0',
58-
data,
59-
},
62+
// Route the estimate to the relay's pending-private state when this account has
63+
// in-flight private txs (presence-only; no-op otherwise). See trezor/blockbook#1639.
64+
specific: buildEvmEstimateSpecific(
65+
{
66+
from: account.descriptor,
67+
to: contract,
68+
value: '0x0',
69+
data,
70+
},
71+
selectAccountPrivatePendingNonces(getState(), account.key),
72+
),
6073
},
6174
});
6275

suite-common/wallet-core/src/send/sendFormEthereumThunks.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import {
2424
asAmountSubunit,
2525
asAmountUnit,
26+
buildEvmEstimateSpecific,
2627
calculateMax,
2728
calculateTotal,
2829
calculateTotalGasCost,
@@ -57,6 +58,7 @@ import {
5758
type SignTransactionError,
5859
type SignTransactionThunkArguments,
5960
} from './sendFormTypes';
61+
import { selectAccountPrivatePendingNonces } from '../privatePending/privatePendingReducer';
6062
import { selectAddressDisplayType } from '../settings/walletSettingsReducer';
6163
import { selectAccountTransactions } from '../transactions/transactionsSelectors';
6264

@@ -319,10 +321,15 @@ export const composeEthereumTransactionFeeLevelsThunk = createThunk<
319321
identity: getAccountIdentity(account),
320322
request: {
321323
blocks: [2],
322-
specific: {
323-
from: account.descriptor,
324-
...ethereumEstimateFeeParams,
325-
},
324+
// Route the estimate to the relay's pending-private state when this account has
325+
// in-flight private txs (presence-only; no-op otherwise). See trezor/blockbook#1639.
326+
specific: buildEvmEstimateSpecific(
327+
{
328+
from: account.descriptor,
329+
...ethereumEstimateFeeParams,
330+
},
331+
selectAccountPrivatePendingNonces(getState(), account.key),
332+
),
326333
},
327334
});
328335

suite-common/wallet-core/src/send/sendFormThunks.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
isEvmApprovalTxByTextSignature,
3636
isEvmYieldTxByTextSignature,
3737
isExchangeTradingForm,
38+
isMevPrivateSend,
3839
isRbfCancelTransaction,
3940
subunitsToUnits,
4041
tryGetAccountIdentity,
@@ -84,6 +85,7 @@ import {
8485
import { accountsActions } from '../accounts/accountsActions';
8586
import { selectAccountByKey } from '../accounts/accountsSelectors';
8687
import { syncAccountsWithBlockchainThunk } from '../blockchain/blockchainThunks';
88+
import { privatePendingActions } from '../privatePending/privatePendingActions';
8789
import {
8890
selectAreSatsAmountUnit,
8991
selectBitcoinAmountUnit,
@@ -478,6 +480,24 @@ export const pushSendFormTransactionThunk = createThunk<
478480
ethereumNonce: resolvedEthereumNonce,
479481
}),
480482
);
483+
484+
// Record a genuinely-private (MEV-protected / relay-routed) EVM send so the account's
485+
// subsequent getAccountInfo / estimateFee requests declare it to blockbook as
486+
// privatePending (trezor/blockbook#1639). Gated by isMevPrivateSend so public sends never
487+
// declare; the entry is pruned once the nonce confirms (see privatePendingReducer).
488+
if (
489+
selectedAccount.networkType === 'ethereum' &&
490+
resolvedEthereumNonce !== undefined &&
491+
isMevPrivateSend(selectedAccount.symbol, isMevProtectionEnabled)
492+
) {
493+
dispatch(
494+
privatePendingActions.privatePendingAdded({
495+
accountKey: selectedAccount.key,
496+
nonce: Number(resolvedEthereumNonce),
497+
txid,
498+
}),
499+
);
500+
}
481501
} else {
482502
dispatch(
483503
notificationsActions.addToast({

suite-common/wallet-utils/src/sendFormUtils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,14 @@ export const getMevProtectedTxData = (
682682
return hex;
683683
};
684684

685+
// Whether a send is genuinely private, i.e. exactly the case where getMevProtectedTxData drops
686+
// disableAlternativeRPC and the tx is broadcast through the alternative relay (MEV protection on AND
687+
// a relay-capable network). This is the condition under which the tx must be recorded and declared
688+
// to blockbook as privatePending. isMevProtectionEnabled is expected to already fold in the
689+
// message-system feature flag, as the send and walletconnect flows do.
690+
export const isMevPrivateSend = (symbol: NetworkSymbol, isMevProtectionEnabled: boolean): boolean =>
691+
isMevProtectionEnabled && getNetwork(symbol).features.includes('mev-protection');
692+
685693
export const isExchangeTradingForm = (
686694
form: FormStateTrading | undefined,
687695
): form is FormStateTradingExchange => form?.activeSection === 'exchange';

suite-common/walletconnect/src/adapters/ethereum.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import { type Network, getNetwork, networksCollection } from '@suite-common/wall
99
import { ETH_CONTRACT_CALL_BACKUP_GAS_LIMIT } from '@suite-common/wallet-constants';
1010
import {
1111
ethereumGetCurrentNonceThunk,
12+
privatePendingActions,
1213
selectAccounts,
1314
selectIsMevProtectionEnabled,
1415
} from '@suite-common/wallet-core';
1516
import { type Account } from '@suite-common/wallet-types';
16-
import { getAccountIdentity, getMevProtectedTxData, sanitizeHex } from '@suite-common/wallet-utils';
17+
import {
18+
getAccountIdentity,
19+
getMevProtectedTxData,
20+
isMevPrivateSend,
21+
sanitizeHex,
22+
} from '@suite-common/wallet-utils';
1723
import TrezorConnect, {
1824
type CallMethodResponse,
1925
type EthereumSignTypedData,
@@ -219,6 +225,25 @@ const ethereumRequestThunk = createThunk<
219225
throw new Error('eth_sendTransaction push error');
220226
}
221227

228+
// Record a genuinely-private (MEV-protected / relay-routed) WalletConnect send so the
229+
// account's subsequent read requests declare it to blockbook as privatePending
230+
// (trezor/blockbook#1639); pruned once the nonce confirms. Uses the same combined MEV
231+
// condition as the broadcast above.
232+
if (
233+
isMevPrivateSend(
234+
account.symbol,
235+
isMevProtectionEnabled && isMevProtectionFeatureEnabled,
236+
)
237+
) {
238+
dispatch(
239+
privatePendingActions.privatePendingAdded({
240+
accountKey: account.key,
241+
nonce: Number(nonce),
242+
txid: pushResponse.payload.txid,
243+
}),
244+
);
245+
}
246+
222247
return pushResponse.payload.txid;
223248
}
224249
case 'wallet_switchEthereumChain': {

0 commit comments

Comments
 (0)