Skip to content

Commit a1c41f2

Browse files
authored
Merge pull request #30657 from MetaMask/cherry-pick-solana-swap-bridge
fix: cherry pick solana swap bridge commits into v12.14.0
2 parents 3183b75 + 3bb9a7b commit a1c41f2

32 files changed

+884
-365
lines changed

app/scripts/controllers/bridge/bridge-controller.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -168,29 +168,27 @@ describe('BridgeController', function () {
168168
bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 });
169169
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
170170
srcChainId: 1,
171-
slippage: 0.5,
172171
srcTokenAddress: '0x0000000000000000000000000000000000000000',
173172
walletAddress: undefined,
174173
});
175174

176175
bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 });
177176
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
178177
destChainId: 10,
179-
slippage: 0.5,
180178
srcTokenAddress: '0x0000000000000000000000000000000000000000',
181179
walletAddress: undefined,
182180
});
183181

184182
bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined });
185183
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
186184
destChainId: undefined,
187-
slippage: 0.5,
188185
srcTokenAddress: '0x0000000000000000000000000000000000000000',
189186
walletAddress: undefined,
190187
});
191188

192189
bridgeController.updateBridgeQuoteRequestParams({
193190
srcTokenAddress: undefined,
191+
slippage: 0.5,
194192
});
195193
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
196194
slippage: 0.5,
@@ -217,14 +215,12 @@ describe('BridgeController', function () {
217215
srcTokenAddress: '0x2ABC',
218216
});
219217
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
220-
slippage: 0.5,
221218
srcTokenAddress: '0x2ABC',
222219
walletAddress: undefined,
223220
});
224221

225222
bridgeController.resetState();
226223
expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({
227-
slippage: 0.5,
228224
srcTokenAddress: '0x0000000000000000000000000000000000000000',
229225
walletAddress: undefined,
230226
});
@@ -278,10 +274,11 @@ describe('BridgeController', function () {
278274
destTokenAddress: '0x123',
279275
srcTokenAmount: '1000000000000000000',
280276
walletAddress: '0x123',
277+
slippage: 0,
281278
};
282279
const quoteRequest = {
283280
...quoteParams,
284-
slippage: 0.5,
281+
slippage: 0,
285282
};
286283
await bridgeController.updateBridgeQuoteRequestParams(quoteParams);
287284

@@ -427,6 +424,7 @@ describe('BridgeController', function () {
427424
destTokenAddress: '0x123',
428425
srcTokenAmount: '1000000000000000000',
429426
walletAddress: '0x123',
427+
slippage: 0.5,
430428
};
431429
const quoteRequest = {
432430
...quoteParams,
@@ -526,6 +524,7 @@ describe('BridgeController', function () {
526524
destChainId: 10,
527525
srcTokenAddress: '0x0000000000000000000000000000000000000000',
528526
destTokenAddress: '0x123',
527+
slippage: 0.5,
529528
});
530529

531530
expect(stopAllPollingSpy).toHaveBeenCalledTimes(1);
@@ -620,6 +619,7 @@ describe('BridgeController', function () {
620619
destTokenAddress: '0x0000000000000000000000000000000000000000',
621620
srcTokenAmount: '991250000000000000',
622621
walletAddress: 'eip:id/id:id/0x123',
622+
slippage: 0.5,
623623
};
624624
const quoteRequest = {
625625
...quoteParams,

app/scripts/controllers/bridge/bridge-controller.ts

+91-29
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Web3Provider } from '@ethersproject/providers';
88
import { BigNumber } from '@ethersproject/bignumber';
99
import { TransactionParams } from '@metamask/transaction-controller';
1010
import type { ChainId } from '@metamask/controller-utils';
11+
import { HandlerType } from '@metamask/snaps-utils';
12+
import type { SnapId } from '@metamask/snaps-sdk';
1113
import {
1214
fetchBridgeFeatureFlags,
1315
fetchBridgeQuotes,
@@ -24,6 +26,7 @@ import {
2426
BridgeFeatureFlagsKey,
2527
RequestStatus,
2628
type GenericQuoteRequest,
29+
type SolanaFees,
2730
} from '../../../../shared/types/bridge';
2831
import { isValidQuoteRequest } from '../../../../shared/modules/bridge-utils/quote';
2932
import { hasSufficientBalance } from '../../../../shared/modules/bridge-utils/balance';
@@ -170,7 +173,7 @@ export default class BridgeController extends StaticIntervalPollingController<Br
170173
};
171174

172175
#hasSufficientBalance = async (quoteRequest: GenericQuoteRequest) => {
173-
const walletAddress = this.#getSelectedAccount().address;
176+
const walletAddress = this.#getMultichainSelectedAccount()?.address;
174177
const srcChainIdInHex = formatChainIdToHex(quoteRequest.srcChainId);
175178
const provider = this.#getSelectedNetworkClient()?.provider;
176179
const srcTokenAddressWithoutPrefix = formatAddressToString(
@@ -179,6 +182,7 @@ export default class BridgeController extends StaticIntervalPollingController<Br
179182

180183
return (
181184
provider &&
185+
walletAddress &&
182186
srcTokenAddressWithoutPrefix &&
183187
quoteRequest.srcTokenAmount &&
184188
srcChainIdInHex &&
@@ -253,11 +257,12 @@ export default class BridgeController extends StaticIntervalPollingController<Br
253257
);
254258

255259
const quotesWithL1GasFees = await this.#appendL1GasFees(quotes);
260+
const quotesWithSolanaFees = await this.#appendSolanaFees(quotes);
256261

257262
this.update((_state) => {
258263
_state.bridgeState = {
259264
..._state.bridgeState,
260-
quotes: quotesWithL1GasFees,
265+
quotes: quotesWithL1GasFees ?? quotesWithSolanaFees ?? quotes,
261266
quotesLoadingStatus: RequestStatus.FETCHED,
262267
};
263268
});
@@ -309,45 +314,101 @@ export default class BridgeController extends StaticIntervalPollingController<Br
309314

310315
#appendL1GasFees = async (
311316
quotes: QuoteResponse[],
312-
): Promise<(QuoteResponse & L1GasFees)[]> => {
317+
): Promise<(QuoteResponse & L1GasFees)[] | undefined> => {
318+
// Return undefined if some of the quotes are not for optimism or base
319+
if (
320+
quotes.some(({ quote }) => {
321+
const chainId = formatChainIdToCaip(quote.srcChainId);
322+
return ![CHAIN_IDS.OPTIMISM, CHAIN_IDS.BASE]
323+
.map(formatChainIdToCaip)
324+
.includes(chainId);
325+
})
326+
) {
327+
return undefined;
328+
}
329+
313330
return await Promise.all(
314331
quotes.map(async (quoteResponse) => {
315332
const { quote, trade, approval } = quoteResponse;
316333
const chainId = add0x(decimalToHex(quote.srcChainId)) as ChainId;
317-
if (
318-
[CHAIN_IDS.OPTIMISM.toString(), CHAIN_IDS.BASE.toString()].includes(
319-
chainId,
320-
)
321-
) {
322-
const getTxParams = (txData: TxData) => ({
323-
from: txData.from,
324-
to: txData.to,
325-
value: txData.value,
326-
data: txData.data,
327-
gasLimit: txData.gasLimit?.toString(),
328-
});
329-
const approvalL1GasFees = approval
330-
? await this.#getLayer1GasFee({
331-
transactionParams: getTxParams(approval),
332-
chainId,
333-
})
334-
: '0';
335-
const tradeL1GasFees = await this.#getLayer1GasFee({
336-
transactionParams: getTxParams(trade),
337-
chainId,
338-
});
334+
335+
const getTxParams = (txData: TxData) => ({
336+
from: txData.from,
337+
to: txData.to,
338+
value: txData.value,
339+
data: txData.data,
340+
gasLimit: txData.gasLimit?.toString(),
341+
});
342+
const approvalL1GasFees = approval
343+
? await this.#getLayer1GasFee({
344+
transactionParams: getTxParams(approval),
345+
chainId,
346+
})
347+
: '0';
348+
const tradeL1GasFees = await this.#getLayer1GasFee({
349+
transactionParams: getTxParams(trade),
350+
chainId,
351+
});
352+
return {
353+
...quoteResponse,
354+
l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees),
355+
};
356+
357+
return quoteResponse;
358+
}),
359+
);
360+
};
361+
362+
#appendSolanaFees = async (
363+
quotes: QuoteResponse[],
364+
): Promise<(QuoteResponse & SolanaFees)[] | undefined> => {
365+
// Return undefined if some of the quotes are not for solana
366+
if (
367+
quotes.some(({ quote }) => {
368+
return (
369+
formatChainIdToCaip(quote.srcChainId) !== MultichainNetworks.SOLANA
370+
);
371+
})
372+
) {
373+
return undefined;
374+
}
375+
376+
return await Promise.all(
377+
quotes.map(async (quoteResponse) => {
378+
const { trade } = quoteResponse;
379+
const selectedAccount = this.#getMultichainSelectedAccount();
380+
381+
if (selectedAccount?.metadata?.snap?.id && typeof trade === 'string') {
382+
const { value: fees } = (await this.messagingSystem.call(
383+
'SnapController:handleRequest',
384+
{
385+
snapId: selectedAccount.metadata.snap.id as SnapId,
386+
origin: 'metamask',
387+
handler: HandlerType.OnRpcRequest,
388+
request: {
389+
method: 'getFeeForTransaction',
390+
params: {
391+
transaction: trade,
392+
scope: selectedAccount.options.scope,
393+
},
394+
},
395+
},
396+
)) as { value: string };
397+
339398
return {
340399
...quoteResponse,
341-
l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees),
400+
solanaFeesInLamports: fees,
342401
};
343402
}
344403
return quoteResponse;
345404
}),
346405
);
347406
};
348407

349-
#getSelectedAccount() {
350-
return this.messagingSystem.call('AccountsController:getSelectedAccount');
408+
#getMultichainSelectedAccount() {
409+
return this.messagingSystem.call(
410+
'AccountsController:getSelectedMultichainAccount',
411+
); // ?? this.messagingSystem.call('AccountsController:getSelectedAccount')
351412
}
352413

353414
#getSelectedNetworkClient() {
@@ -380,7 +441,8 @@ export default class BridgeController extends StaticIntervalPollingController<Br
380441

381442
const web3Provider = new Web3Provider(provider);
382443
const contract = new Contract(contractAddress, abiERC20, web3Provider);
383-
const { address: walletAddress } = this.#getSelectedAccount();
444+
const { address: walletAddress } =
445+
this.#getMultichainSelectedAccount() ?? {};
384446
const allowance = await contract.allowance(
385447
walletAddress,
386448
METABRIDGE_CHAIN_TO_ADDRESS_MAP[chainId],

app/scripts/controllers/bridge/constants.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { zeroAddress } from 'ethereumjs-util';
22
import type { Hex } from '@metamask/utils';
33
import {
4-
BRIDGE_DEFAULT_SLIPPAGE,
54
DEFAULT_MAX_REFRESH_COUNT,
65
METABRIDGE_ETHEREUM_ADDRESS,
76
REFRESH_INTERVAL_MS,
@@ -23,7 +22,6 @@ export const DEFAULT_BRIDGE_STATE: BridgeState = {
2322
quoteRequest: {
2423
walletAddress: undefined,
2524
srcTokenAddress: zeroAddress() as `0x${string}`,
26-
slippage: BRIDGE_DEFAULT_SLIPPAGE,
2725
},
2826
quotesInitialLoadTime: undefined,
2927
quotes: [],

app/scripts/controllers/bridge/types.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import {
22
ControllerStateChangeEvent,
33
RestrictedMessenger,
44
} from '@metamask/base-controller';
5-
import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller';
5+
import {
6+
// AccountsControllerGetSelectedAccountAction,
7+
AccountsControllerGetSelectedMultichainAccountAction,
8+
} from '@metamask/accounts-controller';
69
import {
710
NetworkControllerFindNetworkClientIdByChainIdAction,
811
NetworkControllerGetSelectedNetworkClientAction,
912
} from '@metamask/network-controller';
13+
import { HandleSnapRequest } from '@metamask/snaps-controllers';
1014
import type {
1115
BridgeBackgroundAction,
1216
BridgeControllerState,
@@ -33,7 +37,9 @@ type BridgeControllerEvents = ControllerStateChangeEvent<
3337
>;
3438

3539
type AllowedActions =
36-
| AccountsControllerGetSelectedAccountAction
40+
// | AccountsControllerGetSelectedAccountAction
41+
| AccountsControllerGetSelectedMultichainAccountAction
42+
| HandleSnapRequest
3743
| NetworkControllerGetSelectedNetworkClientAction
3844
| NetworkControllerFindNetworkClientIdByChainIdAction;
3945
type AllowedEvents = never;

app/scripts/metamask-controller.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1811,7 +1811,9 @@ export default class MetamaskController extends EventEmitter {
18111811
const bridgeControllerMessenger = this.controllerMessenger.getRestricted({
18121812
name: BRIDGE_CONTROLLER_NAME,
18131813
allowedActions: [
1814-
'AccountsController:getSelectedAccount',
1814+
// 'AccountsController:getSelectedAccount',
1815+
'AccountsController:getSelectedMultichainAccount',
1816+
'SnapController:handleRequest',
18151817
'NetworkController:getSelectedNetworkClient',
18161818
'NetworkController:findNetworkClientIdByChainId',
18171819
],

shared/constants/bridge.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
21
import { MultichainNetworks } from './multichain/networks';
3-
///: END:ONLY_INCLUDE_IF
42
import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './network';
53

64
// TODO read from feature flags
@@ -66,3 +64,13 @@ export const REFRESH_INTERVAL_MS = 30 * 1000;
6664
export const DEFAULT_MAX_REFRESH_COUNT = 5;
6765

6866
export const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io';
67+
68+
export const SOLANA_USDC_ASSET = {
69+
address:
70+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
71+
symbol: 'USDC',
72+
decimals: 6,
73+
image:
74+
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png',
75+
chainId: MultichainNetworks.SOLANA,
76+
};

shared/types/bridge.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export type ChainConfiguration = {
1616
export type L1GasFees = {
1717
l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller
1818
};
19+
20+
export type SolanaFees = {
21+
solanaFeesInLamports?: string; // solana fees in lamports, appended by controller
22+
};
23+
1924
// Values derived from the quote response
2025
// valueInCurrency values are calculated based on the user's selected currency
2126
export type TokenAmountValues = {
@@ -211,7 +216,7 @@ export type GenericQuoteRequest = QuoteRequest<
211216
export type BridgeState = {
212217
bridgeFeatureFlags: BridgeFeatureFlags;
213218
quoteRequest: Partial<GenericQuoteRequest>;
214-
quotes: (QuoteResponse & L1GasFees)[];
219+
quotes: (QuoteResponse & L1GasFees & SolanaFees)[];
215220
quotesInitialLoadTime?: number;
216221
quotesLastFetched?: number;
217222
quotesLoadingStatus?: RequestStatus;

test/e2e/tests/metrics/errors.spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const removedBackgroundFields = [
6161
'AppStateController.timeoutMinutes',
6262
'AppStateController.lastInteractedConfirmationInfo',
6363
'BridgeController.bridgeState.quoteRequest.walletAddress',
64+
'BridgeController.bridgeState.quoteRequest.slippage',
6465
'PPOMController.chainStatus.0x539.lastVisited',
6566
'PPOMController.versionInfo',
6667
// This property is timing-dependent
@@ -873,6 +874,7 @@ describe('Sentry errors', function () {
873874
srcChainId: true,
874875
srcTokenAmount: true,
875876
walletAddress: false,
877+
slippage: true,
876878
},
877879
quotesLastFetched: true,
878880
quotesLoadingStatus: true,

test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@
7676
}
7777
},
7878
"quoteRequest": {
79-
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
80-
"slippage": 0.5
79+
"srcTokenAddress": "0x0000000000000000000000000000000000000000"
8180
},
8281
"quotes": {},
8382
"quotesRefreshCount": 0

0 commit comments

Comments
 (0)