Skip to content

Commit 0dba8f8

Browse files
committed
feat: add solana support to metamask
1 parent 8030bf2 commit 0dba8f8

File tree

15 files changed

+396
-28
lines changed

15 files changed

+396
-28
lines changed

wallets/provider-metamask/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,17 @@
2424
"@rango-dev/signer-evm": "^0.40.0",
2525
"@rango-dev/wallets-core": "^0.50.0",
2626
"@rango-dev/wallets-shared": "^0.51.0",
27-
"rango-types": "^0.1.89"
27+
"@rango-dev/signer-solana": "^0.44.0",
28+
"@wallet-standard/app": "^1.1.0",
29+
"rango-types": "^0.1.89",
30+
"bs58": "^5.0.0"
31+
},
32+
"devDependencies": {
33+
"@wallet-standard/features": "^1.1.0",
34+
"@wallet-standard/base": "^1.1.0",
35+
"@solana/wallet-standard-features": "^1.3.0"
2836
},
2937
"publishConfig": {
3038
"access": "public"
3139
}
32-
}
40+
}

wallets/provider-metamask/readme.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ More about implementation status can be found [here](../readme.md).
77
## Implementation notes/limitations
88

99
### Group
10-
11-
#### 🚧 Solana
12-
MetaMask supports both **EVM** and **Solana**, but this integration currently supports **only EVM** and **Solana** is under construction.
10+
MetaMask supports both **EVM** and **Solana**.
1311

1412
### Feature
1513

16-
All implemented features are working correctly.
14+
#### ⚠️ Switch Account
15+
16+
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.
17+
18+
When a user switches accounts in MetaMask, the wallet emits an update event tied to the currently active namespace—either EVM or Solana.
19+
The switchAccount notification is scoped to that active namespace, meaning only the relevant provider will receive the update.
20+
21+
Regardless of which namespace triggers the event, MetaMask always executes transactions using the correct account for the selected chain.
22+
This ensures consistent behavior and prevents cross-namespace conflicts between Solana and EVM contexts.
1723

1824
---
1925

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { WalletStandardSolanaInstance } from '../types.js';
2+
import type { Context, FunctionWithContext } from '@rango-dev/wallets-core';
3+
4+
import {
5+
type SolanaActions,
6+
utils,
7+
} from '@rango-dev/wallets-core/namespaces/solana';
8+
9+
function connect(
10+
getInstance: () => WalletStandardSolanaInstance
11+
): FunctionWithContext<SolanaActions['connect'], Context> {
12+
return async () => {
13+
const solanaInstance = getInstance();
14+
const connectResult = await solanaInstance.features[
15+
'standard:connect'
16+
].connect();
17+
return utils.formatAccountsToCAIP(
18+
connectResult.accounts.map((account) => account.address)
19+
);
20+
};
21+
}
22+
function canEagerConnect(getInstance: () => WalletStandardSolanaInstance) {
23+
return async () => {
24+
const solanaInstance = getInstance();
25+
26+
if (!solanaInstance) {
27+
throw new Error(
28+
'Trying to eagerly connect to your Solana wallet, but it seems that its instance is not available.'
29+
);
30+
}
31+
32+
try {
33+
const result = await solanaInstance.features['standard:connect'].connect({
34+
silent: true,
35+
});
36+
return !!result.accounts.length;
37+
} catch {
38+
return false;
39+
}
40+
};
41+
}
42+
export const solanaActions = { connect, canEagerConnect };
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { WalletStandardSolanaInstance } from '../types.js';
2+
import type { StandardEventsChangeProperties } from '@wallet-standard/features';
3+
4+
import { ChangeAccountSubscriberBuilder } from '@rango-dev/wallets-core/namespaces/common';
5+
import { utils } from '@rango-dev/wallets-core/namespaces/solana';
6+
7+
// Hooks
8+
const changeAccountSubscriber = (
9+
getInstance: () => WalletStandardSolanaInstance
10+
) =>
11+
new ChangeAccountSubscriberBuilder<
12+
StandardEventsChangeProperties,
13+
WalletStandardSolanaInstance
14+
>()
15+
.getInstance(getInstance)
16+
.validateEventPayload((accounts) => !!accounts.accounts?.length)
17+
.format(async (_, event) =>
18+
utils.formatAccountsToCAIP(
19+
event.accounts!.map((account) => account.address)
20+
)
21+
)
22+
.addEventListener((instance, callback) => {
23+
instance.features['standard:events'].on('change', callback);
24+
})
25+
.removeEventListener((_, __) => {});
26+
27+
export const solanaBuilders = { changeAccountSubscriber };

wallets/provider-metamask/src/constants.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { type ProviderMetadata } from '@rango-dev/wallets-core';
2-
import { type BlockchainMeta, evmBlockchains } from 'rango-types';
2+
import {
3+
type BlockchainMeta,
4+
evmBlockchains,
5+
solanaBlockchain,
6+
} from 'rango-types';
37

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

711
export const WALLET_ID = 'metamask';
8-
12+
export const WALLET_STANDARD_NAME = 'MetaMask';
13+
export const SOLANA_WALLET_STANDARD_MAINNET = 'solana:mainnet';
914
export const metadata: ProviderMetadata = {
1015
name: 'MetaMask',
1116
icon: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/metamask/icon.svg',
@@ -31,6 +36,13 @@ export const metadata: ProviderMetadata = {
3136
getSupportedChains: (allBlockchains: BlockchainMeta[]) =>
3237
evmBlockchains(allBlockchains),
3338
},
39+
{
40+
label: 'Solana',
41+
value: 'Solana',
42+
id: 'SOLANA',
43+
getSupportedChains: (allBlockchains: BlockchainMeta[]) =>
44+
solanaBlockchain(allBlockchains),
45+
},
3446
],
3547
},
3648
},

wallets/provider-metamask/src/legacy/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { evmBlockchains } from 'rango-types';
2121

2222
import signer from '../signer.js';
23-
import { metamask as metamask_instance, type Provider } from '../utils.js';
23+
import { metamask as metamask_instance } from '../utils.js';
2424

2525
const WALLET = WalletTypes.META_MASK;
2626

@@ -49,7 +49,8 @@ export const subscribe: Subscribe = subscribeToEvm;
4949
export const switchNetwork: SwitchNetwork = switchNetworkForEvm;
5050

5151
export const canSwitchNetworkTo: CanSwitchNetwork = canSwitchNetworkToEvm;
52-
52+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
53+
type Provider = any;
5354
export const getSigners: (provider: Provider) => Promise<SignerFactory> =
5455
signer;
5556

wallets/provider-metamask/src/namespaces/evm.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { EvmActions } from '@rango-dev/wallets-core/namespaces/evm';
22

3-
import { NamespaceBuilder } from '@rango-dev/wallets-core';
3+
import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core';
44
import {
55
builders as commonBuilders,
6+
connectAndUpdateStateForMultiNetworks,
7+
intoConnecting,
8+
intoConnectionFinished,
69
standardizeAndThrowError,
710
} from '@rango-dev/wallets-core/namespaces/common';
811
import {
@@ -26,12 +29,25 @@ const [changeAccountSubscriber, changeAccountCleanup] = builders
2629
})
2730
.build();
2831

29-
const connect = builders
30-
.connect()
32+
const connect = new ActionBuilder<EvmActions, 'connect'>('connect')
3133
.action(actions.connect(evmMetamask))
34+
/*
35+
* Metamask Wallet's `connect` returns a list where the currently selected account
36+
* is always the first item. We're directly taking this first item as the active account.
37+
*
38+
* ***NOTE***: Please keep it synced with `wallets/core/src/namespaces/solana/builders.ts`.
39+
*
40+
*/
41+
.and((_, connectResult) => ({
42+
...connectResult,
43+
accounts: [connectResult.accounts[0]],
44+
}))
45+
.and(connectAndUpdateStateForMultiNetworks)
46+
.before(intoConnecting)
3247
.before(changeAccountSubscriber)
3348
.or(changeAccountCleanup)
3449
.or(standardizeAndThrowError)
50+
.after(intoConnectionFinished)
3551
.build();
3652

3753
const canEagerConnect = builders
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core';
2+
import {
3+
builders as commonBuilders,
4+
standardizeAndThrowError,
5+
} from '@rango-dev/wallets-core/namespaces/common';
6+
import {
7+
builders,
8+
type SolanaActions,
9+
} from '@rango-dev/wallets-core/namespaces/solana';
10+
11+
import { solanaActions } from '../actions/solana.js';
12+
import { solanaBuilders } from '../builders/solana.js';
13+
import { WALLET_ID } from '../constants.js';
14+
import { solanaMetamask } from '../utils.js';
15+
16+
const [changeAccountSubscriber, changeAccountCleanup] = solanaBuilders
17+
.changeAccountSubscriber(solanaMetamask)
18+
.build();
19+
20+
const connect = builders
21+
.connect()
22+
.action(solanaActions.connect(solanaMetamask))
23+
.before(changeAccountSubscriber)
24+
.or(changeAccountCleanup)
25+
.or(standardizeAndThrowError)
26+
.build();
27+
28+
const canEagerConnect = new ActionBuilder<SolanaActions, 'canEagerConnect'>(
29+
'canEagerConnect'
30+
)
31+
.action(solanaActions.canEagerConnect(solanaMetamask))
32+
.build();
33+
34+
const disconnect = commonBuilders
35+
.disconnect<SolanaActions>()
36+
.after(changeAccountCleanup)
37+
.build();
38+
39+
const solana = new NamespaceBuilder<SolanaActions>('Solana', WALLET_ID)
40+
.action(connect)
41+
.action(disconnect)
42+
.action(canEagerConnect)
43+
.build();
44+
45+
export { solana };

wallets/provider-metamask/src/provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ProviderBuilder } from '@rango-dev/wallets-core';
22

33
import { metadata, WALLET_ID } from './constants.js';
44
import { evm } from './namespaces/evm.js';
5+
import { solana } from './namespaces/solana.js';
56
import { metamask as metamask } from './utils.js';
67

78
const buildProvider = () =>
@@ -16,6 +17,7 @@ const buildProvider = () =>
1617
})
1718
.config('metadata', metadata)
1819
.add('evm', evm)
20+
.add('solana', solana)
1921
.build();
2022

2123
export { buildProvider };
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Provider } from './utils.js';
1+
import type { Provider } from './types.js';
22
import type { SignerFactory } from 'rango-types';
33

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

11+
import { MetamaskSolanaSigner } from './signers/solana.js';
12+
1113
export default async function getSigners(
1214
provider: Provider
1315
): Promise<SignerFactory> {
1416
const ethProvider = getNetworkInstance(provider, Networks.ETHEREUM);
17+
const solanaProvider = getNetworkInstance(provider, Networks.SOLANA);
18+
1519
const signers = new DefaultSignerFactory();
1620
const { DefaultEvmSigner } = await dynamicImportWithRefinedError(
1721
async () => await import('@rango-dev/signer-evm')
1822
);
1923
signers.registerSigner(TxType.EVM, new DefaultEvmSigner(ethProvider));
20-
24+
signers.registerSigner(
25+
TxType.SOLANA,
26+
new MetamaskSolanaSigner(solanaProvider)
27+
);
2128
return signers;
2229
}

0 commit comments

Comments
 (0)