From 7f23be9a00d4c1771f9949b3c05ccd944aaa8c03 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 4 Nov 2024 15:40:20 +0100 Subject: [PATCH] chore: add Solana shared utilities and constants --- app/images/solana-logo.svg | 13 +++++ package.json | 6 ++- shared/constants/multichain/assets.ts | 5 ++ shared/constants/multichain/networks.ts | 70 ++++++++++++++++++++++++- shared/lib/multichain.test.ts | 17 +++++- shared/lib/multichain.ts | 25 +++++++++ yarn.lock | 45 +++++++++++++++- 7 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 app/images/solana-logo.svg diff --git a/app/images/solana-logo.svg b/app/images/solana-logo.svg new file mode 100644 index 000000000000..ed6f34d95f7e --- /dev/null +++ b/app/images/solana-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/package.json b/package.json index 4b30ab0948c8..e25065529d76 100644 --- a/package.json +++ b/package.json @@ -348,7 +348,7 @@ "@metamask/snaps-utils": "^8.4.1", "@metamask/transaction-controller": "^38.1.0", "@metamask/user-operation-controller": "^13.0.0", - "@metamask/utils": "^9.3.0", + "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.4.0", @@ -357,6 +357,7 @@ "@sentry/browser": "^8.33.1", "@sentry/types": "^8.33.1", "@sentry/utils": "^8.33.1", + "@solana/web3.js": "^1.95.4", "@swc/core": "1.4.11", "@trezor/connect-web": "^9.4.0", "@zxing/browser": "^0.1.4", @@ -745,7 +746,8 @@ "resolve-url-loader>es6-iterator>d>es5-ext": false, "resolve-url-loader>es6-iterator>d>es5-ext>esniff>es5-ext": false, "level>classic-level": false, - "jest-preview": false + "jest-preview": false, + "@solana/web3.js>bigint-buffer": false } }, "packageManager": "yarn@4.4.1" diff --git a/shared/constants/multichain/assets.ts b/shared/constants/multichain/assets.ts index 988a9fbad624..23462d57d05a 100644 --- a/shared/constants/multichain/assets.ts +++ b/shared/constants/multichain/assets.ts @@ -2,9 +2,14 @@ import { MultichainNetworks } from './networks'; export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = { BTC: `${MultichainNetworks.BITCOIN}/slip44:0`, + SOL: `${MultichainNetworks.SOLANA}/slip44:501`, } as const; export enum MultichainNativeAssets { BITCOIN = `${MultichainNetworks.BITCOIN}/slip44:0`, BITCOIN_TESTNET = `${MultichainNetworks.BITCOIN_TESTNET}/slip44:0`, + + SOLANA = `${MultichainNetworks.SOLANA}/slip44:501`, + SOLANA_DEVNET = `${MultichainNetworks.SOLANA_DEVNET}/slip44:501`, + SOLANA_TESTNET = `${MultichainNetworks.SOLANA_TESTNET}/slip44:501`, } diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index 5217394a5415..04ee1134c0b6 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,5 +1,9 @@ import { CaipChainId } from '@metamask/utils'; -import { isBtcMainnetAddress, isBtcTestnetAddress } from '../../lib/multichain'; +import { + isBtcMainnetAddress, + isBtcTestnetAddress, + isSolanaAddress, +} from '../../lib/multichain'; export type ProviderConfigWithImageUrl = { rpcUrl?: string; @@ -21,24 +25,39 @@ export type MultichainProviderConfig = ProviderConfigWithImageUrl & { export enum MultichainNetworks { BITCOIN = 'bip122:000000000019d6689c085ae165831e93', BITCOIN_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba', + + SOLANA = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z', } export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg'; +export const SOLANA_TOKEN_IMAGE_URL = './images/solana-logo.svg'; export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP = { [MultichainNetworks.BITCOIN]: 'https://blockstream.info/address', [MultichainNetworks.BITCOIN_TESTNET]: 'https://blockstream.info/testnet/address', + + [MultichainNetworks.SOLANA]: 'https://explorer.solana.com/', + [MultichainNetworks.SOLANA_DEVNET]: + 'https://explorer.solana.com/?cluster=devnet', + [MultichainNetworks.SOLANA_TESTNET]: + 'https://explorer.solana.com/?cluster=testnet', } as const; export const MULTICHAIN_TOKEN_IMAGE_MAP = { [MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL, + [MultichainNetworks.SOLANA]: SOLANA_TOKEN_IMAGE_URL, } as const; export const MULTICHAIN_PROVIDER_CONFIGS: Record< CaipChainId, MultichainProviderConfig > = { + /** + * Bitcoin + */ [MultichainNetworks.BITCOIN]: { chainId: MultichainNetworks.BITCOIN, rpcUrl: '', // not used @@ -69,4 +88,53 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< }, isAddressCompatible: isBtcTestnetAddress, }, + /** + * Solana + */ + [MultichainNetworks.SOLANA]: { + chainId: MultichainNetworks.SOLANA, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana', + id: 'solana-mainnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[MultichainNetworks.SOLANA], + }, + isAddressCompatible: isSolanaAddress, + }, + [MultichainNetworks.SOLANA_DEVNET]: { + chainId: MultichainNetworks.SOLANA_DEVNET, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana (devnet)', + id: 'solana-devnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[ + MultichainNetworks.SOLANA_DEVNET + ], + }, + isAddressCompatible: isSolanaAddress, + }, + [MultichainNetworks.SOLANA_TESTNET]: { + chainId: MultichainNetworks.SOLANA_TESTNET, + rpcUrl: '', // not used + ticker: 'SOL', + nickname: 'Solana (testnet)', + id: 'solana-testnet', + type: 'rpc', + rpcPrefs: { + imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[ + MultichainNetworks.SOLANA_TESTNET + ], + }, + isAddressCompatible: isSolanaAddress, + }, }; diff --git a/shared/lib/multichain.test.ts b/shared/lib/multichain.test.ts index 4c1bab12d03b..e719a1505130 100644 --- a/shared/lib/multichain.test.ts +++ b/shared/lib/multichain.test.ts @@ -3,6 +3,7 @@ import { getCaipNamespaceFromAddress, isBtcMainnetAddress, isBtcTestnetAddress, + isSolanaAddress, } from './multichain'; const BTC_MAINNET_ADDRESSES = [ @@ -61,6 +62,20 @@ describe('multichain', () => { ); }); + describe('isSolanaAddress', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(SOL_ADDRESSES)( + 'returns true if address is a valid Solana address: %s', + (address: string) => { + expect(isSolanaAddress(address)).toBe(true); + }, + ); + + it('should return false for invalid Solana addresses', () => { + expect(isSolanaAddress('invalid')).toBe(false); + }); + }); + describe('getChainTypeFromAddress', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each([...BTC_MAINNET_ADDRESSES, ...BTC_TESTNET_ADDRESSES])( @@ -87,7 +102,7 @@ describe('multichain', () => { 'returns ChainType.Ethereum for non-supported address: %s', (address: string) => { expect(getCaipNamespaceFromAddress(address)).toBe( - KnownCaipNamespace.Eip155, + KnownCaipNamespace.Solana, ); }, ); diff --git a/shared/lib/multichain.ts b/shared/lib/multichain.ts index 942a9ce6c964..02aebb72134f 100644 --- a/shared/lib/multichain.ts +++ b/shared/lib/multichain.ts @@ -1,5 +1,6 @@ import { CaipNamespace, KnownCaipNamespace } from '@metamask/utils'; import { validate, Network } from 'bitcoin-address-validation'; +import { PublicKey } from '@solana/web3.js'; /** * Returns whether an address is on the Bitcoin mainnet. @@ -28,6 +29,25 @@ export function isBtcTestnetAddress(address: string): boolean { return validate(address, Network.testnet); } +/** + * Returns whether an address is a valid Solana address, specifically an account's. + * Derived addresses (like Program's) will return false. + * See: https://stackoverflow.com/questions/71200948/how-can-i-validate-a-solana-wallet-address-with-web3js + * + * @param address - The address to check. + * @returns `true` if the address is a valid Solana address, `false` otherwise. + */ +export function isSolanaAddress(address: string): boolean { + try { + const solanaPublicKey = new PublicKey(address); + const isOnCurve = PublicKey.isOnCurve(solanaPublicKey); + + return isOnCurve; + } catch (error) { + return false; + } +} + /** * Returns the associated chain's type for the given address. * @@ -38,6 +58,11 @@ export function getCaipNamespaceFromAddress(address: string): CaipNamespace { if (isBtcMainnetAddress(address) || isBtcTestnetAddress(address)) { return KnownCaipNamespace.Bip122; } + + if (isSolanaAddress(address)) { + return KnownCaipNamespace.Solana; + } + // Defaults to "Ethereum" for all other cases for now. return KnownCaipNamespace.Eip155; } diff --git a/yarn.lock b/yarn.lock index 7890bcaa7b3f..a5d3fe120b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6509,6 +6509,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/utils@npm:10.0.1" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/c8e3d7578d05a1da4abb6c6712ec78ef6990801269f6529f4bb237b7d6e228d10a40738ccab81ad554f2fd51670267d086dc5be1a31c6d1f7040d4c0469d9d13 + languageName: node + linkType: hard + "@metamask/utils@npm:^8.1.0, @metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.3.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" @@ -6526,7 +6543,7 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1, @metamask/utils@npm:^9.3.0": +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" dependencies: @@ -8064,6 +8081,29 @@ __metadata: languageName: node linkType: hard +"@solana/web3.js@npm:^1.95.4": + version: 1.95.4 + resolution: "@solana/web3.js@npm:1.95.4" + dependencies: + "@babel/runtime": "npm:^7.25.0" + "@noble/curves": "npm:^1.4.2" + "@noble/hashes": "npm:^1.4.0" + "@solana/buffer-layout": "npm:^4.0.1" + agentkeepalive: "npm:^4.5.0" + bigint-buffer: "npm:^1.1.5" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + bs58: "npm:^4.0.1" + buffer: "npm:6.0.3" + fast-stable-stringify: "npm:^1.0.0" + jayson: "npm:^4.1.1" + node-fetch: "npm:^2.7.0" + rpc-websockets: "npm:^9.0.2" + superstruct: "npm:^2.0.2" + checksum: 10/353e04ac1110035ff108f16af4029c7a98f71cce841d45877c9bc4a354cdc58a051681603c92289b81e3dc5ef6b1567c6f866e4ba56a434db145e38a5a41d276 + languageName: node + linkType: hard + "@sovpro/delimited-stream@npm:^1.1.0": version: 1.1.0 resolution: "@sovpro/delimited-stream@npm:1.1.0" @@ -26472,7 +26512,7 @@ __metadata: "@metamask/test-dapp": "npm:8.7.0" "@metamask/transaction-controller": "npm:^38.1.0" "@metamask/user-operation-controller": "npm:^13.0.0" - "@metamask/utils": "npm:^9.3.0" + "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" "@noble/hashes": "npm:^1.3.3" "@octokit/core": "npm:^3.6.0" @@ -26489,6 +26529,7 @@ __metadata: "@sentry/cli": "npm:^2.19.4" "@sentry/types": "npm:^8.33.1" "@sentry/utils": "npm:^8.33.1" + "@solana/web3.js": "npm:^1.95.4" "@storybook/addon-a11y": "npm:^7.6.20" "@storybook/addon-actions": "npm:^7.6.20" "@storybook/addon-designs": "npm:^7.0.9"