Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: solana swap and bridge navigation #29705

Merged
merged 28 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
999a96e
feat: navigate to bridge with either swap or bridge button click
bfullam Jan 14, 2025
d9e1796
refactor: remove unnecessary isEqual check
bfullam Jan 14, 2025
5c940bf
chore: only enable swap and bridge buttons in dev environments
bfullam Jan 14, 2025
5176fc7
Revert "chore: only enable swap and bridge buttons in dev environments"
bfullam Jan 14, 2025
2335318
chore: only display swap and bridge buttons for dev environments
bfullam Jan 14, 2025
88a1cb0
Merge branch 'main' into MMS-1846-solana-swap-and-bridge-navigation
bfullam Jan 16, 2025
3260487
Add handleSwapOnClick back
bfullam Jan 16, 2025
ca8428f
chore: add solana as swappable and bridgeable chain
bfullam Jan 16, 2025
8e222c4
refactor: only open bridge if on Solana
bfullam Jan 16, 2025
bde868f
chore: lint
bfullam Jan 16, 2025
d6ee2d5
feat: add code fences
bfullam Jan 16, 2025
e05cb3a
chore: rearrange for linting
bfullam Jan 16, 2025
6cf2fdf
chore: add code fences to import statements
bfullam Jan 16, 2025
bd4f106
chore: addition code fencing for imports
bfullam Jan 16, 2025
61b07ef
Merge branch 'main' into MMS-1846-solana-swap-and-bridge-navigation
bfullam Jan 16, 2025
a089fa5
test: update tests
bfullam Jan 16, 2025
c22689a
test: update e2e tests
bfullam Jan 16, 2025
849b844
test: fix e2e test for bridge button enabled
bfullam Jan 16, 2025
53303f1
test: update bridge button definition
bfullam Jan 16, 2025
dab950d
test: update bridge button string
bfullam Jan 16, 2025
ebeae13
chore: use positive variable names
bfullam Jan 20, 2025
4ea6f00
refactor: move solana chain ID to prod lists with code fencing
bfullam Jan 20, 2025
cad5839
chore: remove unused chain id constant
bfullam Jan 20, 2025
4968353
chore: change UI text if using Solana
bfullam Jan 20, 2025
6346c9c
chore: lint
bfullam Jan 20, 2025
350efa3
chore: lint
bfullam Jan 21, 2025
38eb96e
chore: add multichain networks to name map
bfullam Jan 21, 2025
2b1dd86
refactor: add selector for getMultichainIsSolana
bfullam Jan 22, 2025
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
2 changes: 2 additions & 0 deletions builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ buildTypes:
- build-flask
- keyring-snaps
- solana
- solana-swaps
env:
- INFURA_FLASK_PROJECT_ID
- SEGMENT_FLASK_WRITE_KEY
Expand Down Expand Up @@ -139,6 +140,7 @@ features:
solana:
assets:
- ./{app,shared,ui}/**/solana/**
solana-swaps:
bfullam marked this conversation as resolved.
Show resolved Hide resolved

# Env variables that are required for all types of builds
#
Expand Down
13 changes: 13 additions & 0 deletions shared/constants/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
import { MultichainNetworks } from './multichain/networks';
///: END:ONLY_INCLUDE_IF
import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './network';

// TODO read from feature flags
Expand All @@ -11,6 +14,9 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [
CHAIN_IDS.ARBITRUM,
CHAIN_IDS.LINEA_MAINNET,
CHAIN_IDS.BASE,
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
MultichainNetworks.SOLANA,
///: END:ONLY_INCLUDE_IF
];

export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number];
Expand Down Expand Up @@ -45,6 +51,13 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record<
[CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM],
[CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era',
[CHAIN_IDS.BASE]: 'Base',
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
[MultichainNetworks.SOLANA]: 'Solana',
[MultichainNetworks.SOLANA_TESTNET]: 'Solana Testnet',
[MultichainNetworks.SOLANA_DEVNET]: 'Solana Devnet',
[MultichainNetworks.BITCOIN]: 'Bitcoin',
[MultichainNetworks.BITCOIN_TESTNET]: 'Bitcoin Testnet',
///: END:ONLY_INCLUDE_IF
};
export const BRIDGE_MM_FEE_RATE = 0.875;
export const REFRESH_INTERVAL_MS = 30 * 1000;
Expand Down
6 changes: 6 additions & 0 deletions shared/constants/swaps.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
import { MultichainNetworks } from './multichain/networks';
///: END:ONLY_INCLUDE_IF
import {
ETH_TOKEN_IMAGE_URL,
TEST_ETH_TOKEN_IMAGE_URL,
Expand Down Expand Up @@ -181,6 +184,9 @@ export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
CHAIN_IDS.ZKSYNC_ERA,
CHAIN_IDS.LINEA_MAINNET,
CHAIN_IDS.BASE,
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
MultichainNetworks.SOLANA,
///: END:ONLY_INCLUDE_IF
] as const;

export const ALLOWED_DEV_SWAPS_CHAIN_IDS = [
Expand Down
24 changes: 12 additions & 12 deletions test/e2e/flask/solana/send-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ describe('Send full flow of USD', function (this: Suite) {
);
assert.equal(
await homePage.check_ifSwapButtonIsClickable(),
false,
'Swap button is enabled and it shouldn`t',
true,
'Swap button is not enabled and it should',
);
assert.equal(
await homePage.check_ifBridgeButtonIsClickable(),
false,
'Bridge button is enabled and it should`t',
true,
'Bridge button is not enabled and it should',
);
await homePage.clickOnSendButton();
const sendSolanaPage = new SendSolanaPage(driver);
Expand Down Expand Up @@ -234,13 +234,13 @@ describe('Send full flow of SOL', function (this: Suite) {
);
assert.equal(
await homePage.check_ifSwapButtonIsClickable(),
false,
'Swap button is enabled and it shouldn`t',
true,
'Swap button is not enabled and it should',
);
assert.equal(
await homePage.check_ifBridgeButtonIsClickable(),
false,
'Bridge button is enabled and it should`t',
true,
'Bridge button is not enabled and it should',
);
await homePage.clickOnSendButton();
const sendSolanaPage = new SendSolanaPage(driver);
Expand Down Expand Up @@ -378,13 +378,13 @@ describe('Send flow flow', function (this: Suite) {
);
assert.equal(
await homePage.check_ifSwapButtonIsClickable(),
false,
'Swap button is enabled and it should`t',
true,
'Swap button is not enabled and it should',
);
assert.equal(
await homePage.check_ifBridgeButtonIsClickable(),
false,
'Bridge button is enabled and it should`t',
true,
'Bridge button is not enabled and it should',
);
await homePage.clickOnSendButton();
const sendSolanaPage = new SendSolanaPage(driver);
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/page-objects/pages/home/non-evm-homepage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class NonEvmHomepage extends HomePage {

protected readonly swapButton = '[data-testid="token-overview-button-swap"]';

protected readonly bridgeButton = '[data-testid="coin-overview-bridge"]';

/**
* Clicks the send button on the non-EVM account homepage.
*/
Expand Down
81 changes: 47 additions & 34 deletions ui/components/app/wallet-overview/coin-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,14 @@ import {
import { isMultichainWalletSnap } from '../../../../shared/lib/accounts/snaps';
///: END:ONLY_INCLUDE_IF
import {
getMultichainIsEvm,
getMultichainNativeCurrency,
getMultichainNetwork,
} from '../../../selectors/multichain';
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
import { getCurrentChainId } from '../../../../shared/modules/selectors/networks';
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
///: END:ONLY_INCLUDE_IF

type CoinButtonsProps = {
account: InternalAccount;
Expand Down Expand Up @@ -160,12 +163,15 @@ const CoinButtons = ({
// Initially, those events were using a "ETH" as `token_symbol`, so we keep this behavior
// for EVM, no matter the currently selected native token (e.g. SepoliaETH if you are on Sepolia
// network).
const isEvm = useMultichainSelector(getMultichainIsEvm, account);
const { isEvmNetwork, chainId: multichainChainId } = useMultichainSelector(
getMultichainNetwork,
account,
);
const multichainNativeToken = useMultichainSelector(
getMultichainNativeCurrency,
account,
);
const nativeToken = isEvm ? 'ETH' : multichainNativeToken;
const nativeToken = isEvmNetwork ? 'ETH' : multichainNativeToken;

const isExternalServicesEnabled = useSelector(getUseExternalServices);

Expand Down Expand Up @@ -331,7 +337,7 @@ const CoinButtons = ({
///: END:ONLY_INCLUDE_IF

const setCorrectChain = useCallback(async () => {
if (currentChainId !== chainId) {
if (currentChainId !== chainId && multichainChainId !== chainId) {
Copy link
Member

Choose a reason for hiding this comment

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

Similar to Heyse's comment - can this be written with less negations for better readability?

try {
const networkConfigurationId = networks[chainId];
await dispatch(setActiveNetworkWithError(networkConfigurationId));
Expand Down Expand Up @@ -404,7 +410,44 @@ const CoinButtons = ({
history.push(SEND_ROUTE);
}, [chainId, account, setCorrectChain]);

///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
const handleBuyAndSellOnClick = useCallback(() => {
openBuyCryptoInPdapp(getChainId());
trackEvent({
event: MetaMetricsEventName.NavBuyButtonClicked,
category: MetaMetricsEventCategory.Navigation,
properties: {
account_type: account.type,
location: 'Home',
text: 'Buy',
chain_id: chainId,
token_symbol: defaultSwapsToken,
...getSnapAccountMetaMetricsPropertiesIfAny(account),
},
});
}, [chainId, defaultSwapsToken]);

const handleBridgeOnClick = useCallback(async () => {
if (!defaultSwapsToken) {
return;
}
await setCorrectChain();
openBridgeExperience(
'Home',
defaultSwapsToken,
location.pathname.includes('asset') ? '&token=native' : '',
);
}, [defaultSwapsToken, location, openBridgeExperience]);
///: END:ONLY_INCLUDE_IF

const handleSwapOnClick = useCallback(async () => {
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
if (multichainChainId === MultichainNetworks.SOLANA) {
handleBridgeOnClick();
return;
}
///: END:ONLY_INCLUDE_IF

await setCorrectChain();
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
global.platform.openTab({
Expand Down Expand Up @@ -447,36 +490,6 @@ const CoinButtons = ({
///: END:ONLY_INCLUDE_IF
]);

///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
const handleBuyAndSellOnClick = useCallback(() => {
openBuyCryptoInPdapp(getChainId());
trackEvent({
event: MetaMetricsEventName.NavBuyButtonClicked,
category: MetaMetricsEventCategory.Navigation,
properties: {
account_type: account.type,
location: 'Home',
text: 'Buy',
chain_id: chainId,
token_symbol: defaultSwapsToken,
...getSnapAccountMetaMetricsPropertiesIfAny(account),
},
});
}, [chainId, defaultSwapsToken]);

const handleBridgeOnClick = useCallback(async () => {
if (!defaultSwapsToken) {
return;
}
await setCorrectChain();
openBridgeExperience(
'Home',
defaultSwapsToken,
location.pathname.includes('asset') ? '&token=native' : '',
);
}, [defaultSwapsToken, location, openBridgeExperience]);
///: END:ONLY_INCLUDE_IF

return (
<Box display={Display.Flex} justifyContent={JustifyContent.spaceEvenly}>
{
Expand Down
2 changes: 2 additions & 0 deletions ui/components/app/wallet-overview/non-evm-overview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ describe('NonEvmOverview', () => {
location: 'Home',
snap_id: mockNonEvmAccount.metadata.snap.id,
text: 'Buy',
// We use a `SwapsEthToken` in this case, so we're expecting an entire object here.
token_symbol: expect.any(Object),
},
micaelae marked this conversation as resolved.
Show resolved Hide resolved
});
});
Expand Down
22 changes: 19 additions & 3 deletions ui/components/app/wallet-overview/non-evm-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import {
import { getIsBitcoinBuyable } from '../../../ducks/ramps';
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
///: END:ONLY_INCLUDE_IF
import { getSelectedInternalAccount } from '../../../selectors';
import {
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
getIsSwapsChain,
getIsBridgeChain,
///: END:ONLY_INCLUDE_IF
getSelectedInternalAccount,
getSwapsDefaultToken,
} from '../../../selectors';
import { CoinOverview } from './coin-overview';

type NonEvmOverviewProps = {
Expand All @@ -37,6 +44,14 @@ const NonEvmOverview = ({ className }: NonEvmOverviewProps) => {
const isBtc = accountType === BtcAccountType.P2wpkh;
const isBuyableChain = isBtc ? isBtcBuyable && isBtcMainnetAccount : false;
///: END:ONLY_INCLUDE_IF
const defaultSwapsToken = useSelector(getSwapsDefaultToken);

let isSwapsChain = false;
let isBridgeChain = false;
///: BEGIN:ONLY_INCLUDE_IF(solana-swaps)
isSwapsChain = useSelector((state) => getIsSwapsChain(state, chainId));
isBridgeChain = useSelector((state) => getIsBridgeChain(state, chainId));
///: END:ONLY_INCLUDE_IF

return (
<CoinOverview
Expand All @@ -47,9 +62,10 @@ const NonEvmOverview = ({ className }: NonEvmOverviewProps) => {
className={className}
chainId={chainId}
isSigningEnabled={true}
isSwapsChain={false}
isSwapsChain={isSwapsChain}
defaultSwapsToken={defaultSwapsToken}
micaelae marked this conversation as resolved.
Show resolved Hide resolved
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
isBridgeChain={false}
isBridgeChain={isBridgeChain}
isBuyableChain={isBuyableChain}
///: END:ONLY_INCLUDE_IF
/>
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/bridge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates';
import { useBridgeExchangeRates } from '../../hooks/bridge/useBridgeExchangeRates';
import { useQuoteFetchEvents } from '../../hooks/bridge/useQuoteFetchEvents';
import { TextVariant } from '../../helpers/constants/design-system';
import { getMultichainIsSolana } from '../../selectors/multichain';
import PrepareBridgePage from './prepare/prepare-bridge-page';
import AwaitingSignaturesCancelButton from './awaiting-signatures/awaiting-signatures-cancel-button';
import AwaitingSignatures from './awaiting-signatures/awaiting-signatures';
bfullam marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -94,6 +95,8 @@ const CrossChainSwap = () => {

const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);

const isSolana = useSelector(getMultichainIsSolana);

return (
<Page className="bridge__container">
<Header
Expand All @@ -117,7 +120,7 @@ const CrossChainSwap = () => {
/>
}
>
{t('bridge')}
{isSolana ? t('swap') : t('bridge')}
</Header>
<Content padding={0}>
<Switch>
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/bridge/prepare/bridge-input-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { shortenString } from '../../../helpers/utils/util';
import type { BridgeToken } from '../../../../shared/types/bridge';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { MINUTE } from '../../../../shared/constants/time';
import { getMultichainIsSolana } from '../../../selectors/multichain';
import { BridgeAssetPickerButton } from './components/bridge-asset-picker-button';

export const BridgeInputGroup = ({
Expand Down Expand Up @@ -99,6 +100,8 @@ export const BridgeInputGroup = ({
}
}, [amountFieldProps?.value, isAmountReadOnly, token]);

const isSolana = useSelector(getMultichainIsSolana);

return (
<Column paddingInline={6} gap={1}>
<Row gap={4}>
Expand Down Expand Up @@ -172,7 +175,7 @@ export const BridgeInputGroup = ({
fontWeight={FontWeight.Normal}
style={{ whiteSpace: 'nowrap' }}
>
{t('bridgeTo')}
{isSolana ? t('swapSwapTo') : t('bridgeTo')}
</Button>
) : (
<BridgeAssetPickerButton
Expand Down
5 changes: 4 additions & 1 deletion ui/pages/bridge/prepare/prepare-bridge-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { getCurrentKeyring, getLocale } from '../../../selectors';
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import { SECOND } from '../../../../shared/constants/time';
import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge';
import { getMultichainIsSolana } from '../../../selectors/multichain';
import { BridgeInputGroup } from './bridge-input-group';
import { BridgeCTAButton } from './bridge-cta-button';

Expand Down Expand Up @@ -365,6 +366,8 @@ const PrepareBridgePage = () => {
}
}, [fromChain, fromToken, fromTokens, search]);

const isSolana = useSelector(getMultichainIsSolana);

return (
<Column className="prepare-bridge-page" gap={8}>
<BridgeInputGroup
Expand Down Expand Up @@ -522,7 +525,7 @@ const PrepareBridgePage = () => {
dispatch(setToChain(networkConfig.chainId));
dispatch(setToToken(null));
},
header: t('bridgeTo'),
header: isSolana ? t('swapSwapTo') : t('bridgeTo'),
shouldDisableNetwork: ({ chainId }) =>
chainId === fromChain?.chainId,
}}
Expand Down
10 changes: 10 additions & 0 deletions ui/selectors/multichain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ export function getMultichainIsBitcoin(
return !isEvm && symbol === 'BTC';
}

export function getMultichainIsSolana(
state: MultichainState,
account?: InternalAccount,
) {
const isEvm = getMultichainIsEvm(state, account);
const { symbol } = getMultichainDefaultToken(state, account);

return !isEvm && symbol === 'SOL';
}

/**
* Retrieves the provider configuration for a multichain network.
*
Expand Down
Loading
Loading