Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions wallets/core/src/namespaces/common/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
intoConnectionFinished,
recommended as afterRecommended,
} from './after.js';
export { ChangeAccountSubscriberBuilder } from './hooks/changeAccountSubscriber.js';
export {
connectAndUpdateStateForMultiNetworks,
connectAndUpdateStateForSingleNetwork,
Expand Down
17 changes: 2 additions & 15 deletions wallets/core/src/namespaces/solana/actions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import type { ProviderAPI, SolanaActions } from './types.js';
import type { Context } from '../../hub/namespaces/mod.js';
import type { CaipAccount } from '../../types/accounts.js';
import type { FunctionWithContext } from '../../types/actions.js';

import { AccountId } from 'caip';

import { recommended as commonRecommended } from '../common/actions.js';

import { CAIP_NAMESPACE, CAIP_SOLANA_CHAIN_ID } from './constants.js';
import { getAccounts } from './utils.js';
import { formatAccountsToCAIP, getAccounts } from './utils.js';

export const recommended = [...commonRecommended];

Expand All @@ -25,15 +21,6 @@ export function connect(
);
}

return result.accounts.map(
(account) =>
AccountId.format({
address: account,
chainId: {
namespace: CAIP_NAMESPACE,
reference: CAIP_SOLANA_CHAIN_ID,
},
}) as CaipAccount
);
return formatAccountsToCAIP(result.accounts);
};
}
1 change: 1 addition & 0 deletions wallets/core/src/namespaces/solana/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * as after from './after.js';
export * as and from './and.js';
export * as before from './before.js';
export * as builders from './builders.js';
export * as utils from './utils.js';

export type { ProviderAPI, SolanaActions } from './types.js';
export { CAIP_NAMESPACE, CAIP_SOLANA_CHAIN_ID } from './constants.js';
12 changes: 10 additions & 2 deletions wallets/provider-metamask/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@
"@rango-dev/signer-evm": "^0.40.0",
"@rango-dev/wallets-core": "^0.50.0",
"@rango-dev/wallets-shared": "^0.51.0",
"rango-types": "^0.1.89"
"@rango-dev/signer-solana": "^0.44.0",
"@wallet-standard/app": "^1.1.0",
"rango-types": "^0.1.89",
"bs58": "^5.0.0"
},
"devDependencies": {
"@wallet-standard/features": "^1.1.0",
"@wallet-standard/base": "^1.1.0",
"@solana/wallet-standard-features": "^1.3.0"
},
"publishConfig": {
"access": "public"
}
}
}
14 changes: 10 additions & 4 deletions wallets/provider-metamask/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ More about implementation status can be found [here](../readme.md).
## Implementation notes/limitations

### Group

#### 🚧 Solana
MetaMask supports both **EVM** and **Solana**, but this integration currently supports **only EVM** and **Solana** is under construction.
MetaMask supports both **EVM** and **Solana**.

### Feature

All implemented features are working correctly.
#### ⚠️ Switch Account

In MetaMask, you can have only one active account at a time, which may belong to either the Solana or EVM namespace. When you connect to MetaMask, it connects to the currently active account—this account could belong to either namespace. Additionally, MetaMask may also establish a random connection to another account from the opposite namespace.

When a user switches accounts in MetaMask, the wallet emits an update event tied to the currently active namespace—either EVM or Solana.
The switchAccount notification is scoped to that active namespace, meaning only the relevant provider will receive the update.

Regardless of which namespace triggers the event, MetaMask always executes transactions using the correct account for the selected chain.
This ensures consistent behavior and prevents cross-namespace conflicts between Solana and EVM contexts.

---

Expand Down
42 changes: 42 additions & 0 deletions wallets/provider-metamask/src/actions/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { WalletStandardSolanaInstance } from '../types.js';
import type { Context, FunctionWithContext } from '@rango-dev/wallets-core';

import {
type SolanaActions,
utils,
} from '@rango-dev/wallets-core/namespaces/solana';

function connect(
getInstance: () => WalletStandardSolanaInstance
): FunctionWithContext<SolanaActions['connect'], Context> {
return async () => {
const solanaInstance = getInstance();
const connectResult = await solanaInstance.features[
'standard:connect'
].connect();
return utils.formatAccountsToCAIP(
connectResult.accounts.map((account) => account.address)
);
};
}
function canEagerConnect(getInstance: () => WalletStandardSolanaInstance) {
return async () => {
const solanaInstance = getInstance();

if (!solanaInstance) {
throw new Error(
'Trying to eagerly connect to your Solana wallet, but it seems that its instance is not available.'
);
}

try {
const result = await solanaInstance.features['standard:connect'].connect({
silent: true,
});
return !!result.accounts.length;
} catch {
return false;
}
};
}
export const solanaActions = { connect, canEagerConnect };
27 changes: 27 additions & 0 deletions wallets/provider-metamask/src/builders/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { WalletStandardSolanaInstance } from '../types.js';
import type { StandardEventsChangeProperties } from '@wallet-standard/features';

import { ChangeAccountSubscriberBuilder } from '@rango-dev/wallets-core/namespaces/common';
import { utils } from '@rango-dev/wallets-core/namespaces/solana';

// Hooks
const changeAccountSubscriber = (
getInstance: () => WalletStandardSolanaInstance
) =>
new ChangeAccountSubscriberBuilder<
StandardEventsChangeProperties,
WalletStandardSolanaInstance
>()
.getInstance(getInstance)
.validateEventPayload((accounts) => !!accounts.accounts?.length)
.format(async (_, event) =>
utils.formatAccountsToCAIP(
event.accounts!.map((account) => account.address)
)
)
.addEventListener((instance, callback) => {
instance.features['standard:events'].on('change', callback);
})
.removeEventListener((_, __) => {});

export const solanaBuilders = { changeAccountSubscriber };
16 changes: 14 additions & 2 deletions wallets/provider-metamask/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { type ProviderMetadata } from '@rango-dev/wallets-core';
import { type BlockchainMeta, evmBlockchains } from 'rango-types';
import {
type BlockchainMeta,
evmBlockchains,
solanaBlockchain,
} from 'rango-types';

import getSigners from './signer.js';
import { getInstanceOrThrow } from './utils.js';

export const WALLET_ID = 'metamask';

export const WALLET_STANDARD_NAME = 'MetaMask';
export const SOLANA_WALLET_STANDARD_MAINNET = 'solana:mainnet';
export const metadata: ProviderMetadata = {
name: 'MetaMask',
icon: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/metamask/icon.svg',
Expand All @@ -31,6 +36,13 @@ export const metadata: ProviderMetadata = {
getSupportedChains: (allBlockchains: BlockchainMeta[]) =>
evmBlockchains(allBlockchains),
},
{
label: 'Solana',
value: 'Solana',
id: 'SOLANA',
getSupportedChains: (allBlockchains: BlockchainMeta[]) =>
solanaBlockchain(allBlockchains),
},
],
},
},
Expand Down
5 changes: 3 additions & 2 deletions wallets/provider-metamask/src/legacy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import { evmBlockchains } from 'rango-types';

import signer from '../signer.js';
import { metamask as metamask_instance, type Provider } from '../utils.js';
import { metamask as metamask_instance } from '../utils.js';

const WALLET = WalletTypes.META_MASK;

Expand Down Expand Up @@ -49,7 +49,8 @@ export const subscribe: Subscribe = subscribeToEvm;
export const switchNetwork: SwitchNetwork = switchNetworkForEvm;

export const canSwitchNetworkTo: CanSwitchNetwork = canSwitchNetworkToEvm;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Provider = any;
export const getSigners: (provider: Provider) => Promise<SignerFactory> =
signer;

Expand Down
22 changes: 19 additions & 3 deletions wallets/provider-metamask/src/namespaces/evm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { EvmActions } from '@rango-dev/wallets-core/namespaces/evm';

import { NamespaceBuilder } from '@rango-dev/wallets-core';
import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core';
import {
builders as commonBuilders,
connectAndUpdateStateForMultiNetworks,
intoConnecting,
intoConnectionFinished,
standardizeAndThrowError,
} from '@rango-dev/wallets-core/namespaces/common';
import {
Expand All @@ -26,12 +29,25 @@ const [changeAccountSubscriber, changeAccountCleanup] = builders
})
.build();

const connect = builders
.connect()
const connect = new ActionBuilder<EvmActions, 'connect'>('connect')
.action(actions.connect(evmMetamask))
/*
* Metamask Wallet's `connect` returns a list where the currently selected account
* is always the first item. We're directly taking this first item as the active account.
*
* ***NOTE***: Please keep it synced with `wallets/core/src/namespaces/solana/builders.ts`.
*
*/
.and((_, connectResult) => ({
...connectResult,
accounts: [connectResult.accounts[0]],
}))
.and(connectAndUpdateStateForMultiNetworks)
.before(intoConnecting)
.before(changeAccountSubscriber)
.or(changeAccountCleanup)
.or(standardizeAndThrowError)
.after(intoConnectionFinished)
.build();

const canEagerConnect = builders
Expand Down
45 changes: 45 additions & 0 deletions wallets/provider-metamask/src/namespaces/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core';
import {
builders as commonBuilders,
standardizeAndThrowError,
} from '@rango-dev/wallets-core/namespaces/common';
import {
builders,
type SolanaActions,
} from '@rango-dev/wallets-core/namespaces/solana';

import { solanaActions } from '../actions/solana.js';
import { solanaBuilders } from '../builders/solana.js';
import { WALLET_ID } from '../constants.js';
import { solanaMetamask } from '../utils.js';

const [changeAccountSubscriber, changeAccountCleanup] = solanaBuilders
.changeAccountSubscriber(solanaMetamask)
.build();

const connect = builders
.connect()
.action(solanaActions.connect(solanaMetamask))
.before(changeAccountSubscriber)
.or(changeAccountCleanup)
.or(standardizeAndThrowError)
.build();

const canEagerConnect = new ActionBuilder<SolanaActions, 'canEagerConnect'>(
'canEagerConnect'
)
.action(solanaActions.canEagerConnect(solanaMetamask))
.build();

const disconnect = commonBuilders
.disconnect<SolanaActions>()
.after(changeAccountCleanup)
.build();

const solana = new NamespaceBuilder<SolanaActions>('Solana', WALLET_ID)
.action(connect)
.action(disconnect)
.action(canEagerConnect)
.build();

export { solana };
2 changes: 2 additions & 0 deletions wallets/provider-metamask/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ProviderBuilder } from '@rango-dev/wallets-core';

import { metadata, WALLET_ID } from './constants.js';
import { evm } from './namespaces/evm.js';
import { solana } from './namespaces/solana.js';
import { metamask as metamask } from './utils.js';

const buildProvider = () =>
Expand All @@ -16,6 +17,7 @@ const buildProvider = () =>
})
.config('metadata', metadata)
.add('evm', evm)
.add('solana', solana)
.build();

export { buildProvider };
11 changes: 9 additions & 2 deletions wallets/provider-metamask/src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Provider } from './utils.js';
import type { Provider } from './types.js';
import type { SignerFactory } from 'rango-types';

import { LegacyNetworks as Networks } from '@rango-dev/wallets-core/legacy';
Expand All @@ -8,15 +8,22 @@ import {
} from '@rango-dev/wallets-shared';
import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types';

import { MetamaskSolanaSigner } from './signers/solana.js';

export default async function getSigners(
provider: Provider
): Promise<SignerFactory> {
const ethProvider = getNetworkInstance(provider, Networks.ETHEREUM);
const solanaProvider = getNetworkInstance(provider, Networks.SOLANA);

const signers = new DefaultSignerFactory();
const { DefaultEvmSigner } = await dynamicImportWithRefinedError(
async () => await import('@rango-dev/signer-evm')
);
signers.registerSigner(TxType.EVM, new DefaultEvmSigner(ethProvider));

signers.registerSigner(
TxType.SOLANA,
new MetamaskSolanaSigner(solanaProvider)
);
return signers;
}
Loading