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: submit multichain tx #30416

Merged
merged 23 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4358371
chore: disable tx submission based on non-evm balance
micaelae Feb 19, 2025
54f91e3
chore: add SOL native token to isNativeAddress
micaelae Feb 19, 2025
9fd0dd9
chore: use multichain account in bridge-status controller
micaelae Feb 19, 2025
64a156a
feat: submit solana tx using snap wallet
micaelae Feb 19, 2025
6f9708e
chore: add comments and fix lint issues
micaelae Feb 21, 2025
93a4149
chore: add devnet solana rpc to privacy-snapshot
micaelae Feb 21, 2025
555da54
refactor: get multichain account address
micaelae Feb 21, 2025
be6df4d
chore: upgrade solana wallet snap to 1.7.0
micaelae Feb 19, 2025
be76ec8
feat: update solana-snap to 1.8.0, change from sendAndConfirm to sign…
ghgoodreau Feb 25, 2025
a4ca36f
chore: show cached balance in useLatestbalance when chainId is not evm
micaelae Feb 19, 2025
a7df7a5
chore: undo privacy snapshot update
micaelae Feb 25, 2025
bf3ed6a
fix: type errors
micaelae Feb 25, 2025
92ed651
fix: undo privacy snapshot deletion
micaelae Feb 25, 2025
3c72d02
fix: unit tests
micaelae Feb 25, 2025
72b1a01
Merge branch 'main' into mms1869-submit-sol-tx
ghgoodreau Feb 26, 2025
75be624
fix: fix balance amount
ghgoodreau Feb 26, 2025
2b294ad
feat: update solana-snap from 1.8 to 1.9
ghgoodreau Feb 26, 2025
a1d89dc
Merge branch 'main' into mms1869-submit-sol-tx
ghgoodreau Feb 26, 2025
9a51bbd
fix: increase timeouts in send flow e2e tests
ghgoodreau Feb 26, 2025
ad39507
feat: added getMinimumBalanceForRentExemption to solana e2e tests
javiergarciavera Feb 26, 2025
e2a1613
Merge branch 'main' into mms1869-submit-sol-tx
micaelae Feb 26, 2025
159da89
Merge branch 'main' into mms1869-submit-sol-tx
micaelae Feb 27, 2025
4af1020
Merge branch 'main' into mms1869-submit-sol-tx
ghgoodreau Feb 27, 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getMessengerMock = ({
} = {}) =>
({
call: jest.fn((method: string) => {
if (method === 'AccountsController:getSelectedAccount') {
if (method === 'AccountsController:getSelectedMultichainAccount') {
return { address: account };
} else if (method === 'NetworkController:findNetworkClientIdByChainId') {
return 'networkClientId';
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('BridgeStatusController', () => {
let getSelectedAccountCalledTimes = 0;
const messengerMock = {
call: jest.fn((method: string) => {
if (method === 'AccountsController:getSelectedAccount') {
if (method === 'AccountsController:getSelectedMultichainAccount') {
let account;
if (getSelectedAccountCalledTimes === 0) {
account = '0xaccount1';
Expand Down Expand Up @@ -399,7 +399,7 @@ describe('BridgeStatusController', () => {
jest.useFakeTimers();
const messengerMock = {
call: jest.fn((method: string) => {
if (method === 'AccountsController:getSelectedAccount') {
if (method === 'AccountsController:getSelectedMultichainAccount') {
return { address: '0xaccount1' };
} else if (
method === 'NetworkController:findNetworkClientIdByChainId'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl
targetContractAddress,
} = startPollingForBridgeTxStatusArgs;
const { bridgeStatusState } = this.state;
const { address: account } = this.#getSelectedAccount();
const accountAddress = this.#getMultichainSelectedAccountAddress();

// Write all non-status fields to state so we can reference the quote in Activity list without the Bridge API
// We know it's in progress but not the exact status yet
Expand All @@ -176,7 +176,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl
},
initialDestAssetBalance,
targetContractAddress,
account,
account: accountAddress,
status: {
// We always have a PENDING status when we start polling for a tx, don't need the Bridge API for that
// Also we know the bare minimum fields for status at this point in time
Expand Down Expand Up @@ -210,8 +210,14 @@ export default class BridgeStatusController extends StaticIntervalPollingControl
await this.#fetchBridgeTxStatus(pollingInput);
};

#getSelectedAccount() {
return this.messagingSystem.call('AccountsController:getSelectedAccount');
// Returns an empty string if no account is selected, but this will never happen since
// the multichain selected account defaults to the EVM account
#getMultichainSelectedAccountAddress() {
return (
this.messagingSystem.call(
'AccountsController:getSelectedMultichainAccount',
)?.address ?? ''
);
}

#fetchBridgeTxStatus = async ({
Expand Down
4 changes: 2 additions & 2 deletions app/scripts/controllers/bridge-status/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
NetworkControllerGetNetworkClientByIdAction,
NetworkControllerGetStateAction,
} from '@metamask/network-controller';
import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller';
import { AccountsControllerGetSelectedMultichainAccountAction } from '@metamask/accounts-controller';
import { TransactionControllerGetStateAction } from '@metamask/transaction-controller';
import {
BridgeHistoryItem,
Expand Down Expand Up @@ -63,7 +63,7 @@ type AllowedActions =
| NetworkControllerFindNetworkClientIdByChainIdAction
| NetworkControllerGetStateAction
| NetworkControllerGetNetworkClientByIdAction
| AccountsControllerGetSelectedAccountAction
| AccountsControllerGetSelectedMultichainAccountAction
| TransactionControllerGetStateAction;

/**
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,7 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger.getRestricted({
name: BRIDGE_STATUS_CONTROLLER_NAME,
allowedActions: [
'AccountsController:getSelectedAccount',
'AccountsController:getSelectedMultichainAccount',
'NetworkController:getNetworkClientById',
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getState',
Expand Down
8 changes: 8 additions & 0 deletions ui/hooks/accounts/useMultichainWalletSnapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export class MultichainWalletSnapSender implements Sender {
};
}

export function useMultichainWalletSnapSender(snapId: SnapId) {
const client = useMemo(() => {
return new MultichainWalletSnapSender(snapId);
}, [snapId]);

return client;
}

export class MultichainWalletSnapClient {
readonly #client: KeyringClient;

Expand Down
21 changes: 12 additions & 9 deletions ui/hooks/bridge/useIsTxSubmittable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,46 @@ import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps
import {
getBridgeQuotes,
getFromAmount,
getFromChain,
getFromToken,
getToChain,
getValidationErrors,
getToToken,
} from '../../ducks/bridge/selectors';
import { getMultichainCurrentChainId } from '../../selectors/multichain';
import { useMultichainSelector } from '../useMultichainSelector';
import { useIsMultichainSwap } from '../../pages/bridge/hooks/useIsMultichainSwap';
import useLatestBalance from './useLatestBalance';

export const useIsTxSubmittable = () => {
const fromToken = useSelector(getFromToken);
const toToken = useSelector(getToToken);
const fromChain = useSelector(getFromChain);
const fromChainId = useMultichainSelector(getMultichainCurrentChainId);
const toChain = useSelector(getToChain);
const fromAmount = useSelector(getFromAmount);
const { activeQuote } = useSelector(getBridgeQuotes);

const isSwap = useIsMultichainSwap();
const {
isInsufficientBalance,
isInsufficientGasBalance,
isInsufficientGasForQuote,
} = useSelector(getValidationErrors);

const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId);
const { balanceAmount: nativeAssetBalance } = useLatestBalance(
fromChain?.chainId
const balanceAmount = useLatestBalance(fromToken, fromChainId);
const nativeAssetBalance = useLatestBalance(
fromChainId
? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
fromChainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
]
: null,
fromChain?.chainId,
fromChainId,
);

return Boolean(
fromToken &&
toToken &&
fromChain &&
toChain &&
fromChainId &&
(isSwap || toChain) &&
fromAmount &&
activeQuote &&
!isInsufficientBalance(balanceAmount) &&
Expand Down
4 changes: 2 additions & 2 deletions ui/hooks/bridge/useLatestBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('useLatestBalance', () => {
);

await waitForNextUpdate();
expect(result.current.balanceAmount).toStrictEqual(new BigNumber('1'));
expect(result.current).toStrictEqual(new BigNumber('1'));

expect(mockGetBalance).toHaveBeenCalledTimes(1);
expect(mockGetBalance).toHaveBeenCalledWith(
Expand All @@ -76,7 +76,7 @@ describe('useLatestBalance', () => {
);

await waitForNextUpdate();
expect(result.current.balanceAmount).toStrictEqual(new BigNumber('15.39'));
expect(result.current).toStrictEqual(new BigNumber('15.39'));

expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1);
expect(mockFetchTokenBalance).toHaveBeenCalledWith(
Expand Down
63 changes: 47 additions & 16 deletions ui/hooks/bridge/useLatestBalance.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useSelector } from 'react-redux';
import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils';
import { Numeric } from '../../../shared/modules/Numeric';
import { getCurrentChainId } from '../../../shared/modules/selectors/networks';
import { useMemo } from 'react';
import { getSelectedInternalAccount } from '../../selectors';
import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance';
import { useAsyncResult } from '../useAsyncResult';
import { Numeric } from '../../../shared/modules/Numeric';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import { useMultichainSelector } from '../useMultichainSelector';
import {
getMultichainBalances,
getMultichainCurrentChainId,
} from '../../selectors/multichain';
import { MultichainNetworks } from '../../../shared/constants/multichain/networks';

/**
* Custom hook to fetch and format the latest balance of a given token or native asset.
Expand All @@ -19,15 +24,21 @@ const useLatestBalance = (
address: string;
decimals: number;
symbol: string;
string?: string;
} | null,
chainId?: Hex | CaipChainId,
) => {
const { address: selectedAddress } = useSelector(getSelectedInternalAccount);
const currentChainId = useSelector(getCurrentChainId);
const { address: selectedAddress, id } = useMultichainSelector(
getSelectedInternalAccount,
);
const currentChainId = useMultichainSelector(getMultichainCurrentChainId);

const nonEvmBalancesByAccountId = useMultichainSelector(
getMultichainBalances,
);
const nonEvmBalances = nonEvmBalancesByAccountId[id];

const { value: latestBalance } = useAsyncResult<
Numeric | undefined
>(async () => {
const value = useAsyncResult<Numeric | undefined>(async () => {
if (
token?.address &&
// TODO check whether chainId is EVM when MultichainNetworkController is integrated
Expand All @@ -42,23 +53,43 @@ const useLatestBalance = (
chainId,
);
}

// No need to fetch the balance for non-EVM tokens, use the balance provided by the
// multichain balances controller
if (
isCaipChainId(chainId) &&
chainId === MultichainNetworks.SOLANA &&
token?.decimals
) {
return Numeric.from(
nonEvmBalances?.[token.address]?.amount ?? token?.string,
10,
).shiftedBy(-1 * token.decimals);
}

return undefined;
}, [currentChainId, token?.address, selectedAddress]);
}, [
chainId,
currentChainId,
token,
selectedAddress,
global.ethereumProvider,
nonEvmBalances,
]);

if (token && !token.decimals) {
throw new Error(
`Failed to calculate latest balance - ${token.symbol} token is missing "decimals" value`,
);
}

const tokenDecimals = token?.decimals ? Number(token.decimals) : 1;

return {
balanceAmount:
token && latestBalance
? calcTokenAmount(latestBalance.toString(), tokenDecimals)
return useMemo(
() =>
value?.value
? calcTokenAmount(value.value.toString(), token?.decimals)
: undefined,
};
[value.value, token?.decimals],
);
};

export default useLatestBalance;
4 changes: 2 additions & 2 deletions ui/hooks/bridge/useQuoteFetchEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const useQuoteFetchEvents = () => {
const fromToken = useSelector(getFromToken);
const fromChain = useSelector(getFromChain);

const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId);
const { balanceAmount: nativeAssetBalance } = useLatestBalance(
const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId);
const nativeAssetBalance = useLatestBalance(
fromChain?.chainId
? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
Expand Down
Loading
Loading