From c8e2f4789a552060af7b80d9ab91ac249e9a84c2 Mon Sep 17 00:00:00 2001 From: Hernan Clich Date: Wed, 1 Oct 2025 17:17:05 -0300 Subject: [PATCH 01/11] Support Solana wallet connections --- .../hello/frontend/src/ConnectedContent.tsx | 23 +++++++++++++++++++ .../hello/frontend/src/DynamicAppContent.tsx | 16 +++++++++++-- .../src/components/NetworkSelector.tsx | 6 ++++- .../hello/frontend/src/constants/chains.ts | 15 ++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/examples/hello/frontend/src/ConnectedContent.tsx b/examples/hello/frontend/src/ConnectedContent.tsx index a6772493..cea5eb05 100644 --- a/examples/hello/frontend/src/ConnectedContent.tsx +++ b/examples/hello/frontend/src/ConnectedContent.tsx @@ -1,6 +1,8 @@ import './ConnectedContent.css'; import { type PrimaryWallet } from '@zetachain/wallet'; +import { useSwitchWallet, useUserWallets } from '@zetachain/wallet/react'; +import { useMemo } from 'react'; import { NetworkSelector } from './components/NetworkSelector'; import type { SupportedChain } from './constants/chains'; @@ -23,8 +25,29 @@ const DynamicConnectedContent = ({ primaryWallet, }: ConnectedContentProps) => { const { switchChain } = useDynamicSwitchChainHook(); + const userWallets = useUserWallets(); + const switchWallet = useSwitchWallet(); + + const primaryWalletChain = primaryWallet?.chain; + const walletIds: Record = useMemo(() => { + const solanaWallet = userWallets.find( + (wallet) => wallet.chain === 'SOL' + )?.id; + const evmWallet = userWallets.find((wallet) => wallet.chain === 'EVM')?.id; + + return { + EVM: evmWallet || '', + SOL: solanaWallet || '', + }; + }, [userWallets]); const handleNetworkSelect = (chain: SupportedChain) => { + // We only switch wallet if the chain type is + // different from the primary wallet chain (i.e.: EVM -> SOL) + if (chain.chainType !== primaryWalletChain) { + switchWallet(walletIds[chain.chainType]); + } + switchChain(chain.chainId); }; diff --git a/examples/hello/frontend/src/DynamicAppContent.tsx b/examples/hello/frontend/src/DynamicAppContent.tsx index cda54c54..5981418a 100644 --- a/examples/hello/frontend/src/DynamicAppContent.tsx +++ b/examples/hello/frontend/src/DynamicAppContent.tsx @@ -1,4 +1,5 @@ import { useUniversalSignInContext } from '@zetachain/wallet/react'; +import { useMemo } from 'react'; import { ConnectedContent } from './ConnectedContent'; import { SUPPORTED_CHAINS } from './constants/chains'; @@ -8,7 +9,18 @@ export function DynamicAppContent() { const { primaryWallet, network } = useUniversalSignInContext(); const account = primaryWallet?.address || null; - const decimalChainId = network || null; + const decimalChainId = useMemo(() => { + if (typeof network === 'number') { + return network; + } + + // Solana Devnet id from `network` property + if (network === '103') { + return 901; + } + + return null; + }, [network]); const supportedChain = SUPPORTED_CHAINS.find( (chain) => chain.chainId === decimalChainId @@ -27,4 +39,4 @@ export function DynamicAppContent() { primaryWallet={primaryWallet} /> ); -} \ No newline at end of file +} diff --git a/examples/hello/frontend/src/components/NetworkSelector.tsx b/examples/hello/frontend/src/components/NetworkSelector.tsx index 9fedd2c2..e2b5f628 100644 --- a/examples/hello/frontend/src/components/NetworkSelector.tsx +++ b/examples/hello/frontend/src/components/NetworkSelector.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { SUPPORTED_CHAINS, type SupportedChain } from '../constants/chains'; +import { USE_DYNAMIC_WALLET } from '../constants/wallets'; import { Dropdown, type DropdownOption } from './Dropdown'; interface NetworkSelectorProps { @@ -21,10 +22,13 @@ export const NetworkSelector = ({ // Convert chains to dropdown options const options: DropdownOption[] = useMemo( () => - SUPPORTED_CHAINS.map((chain) => ({ + SUPPORTED_CHAINS.filter( + (chain) => USE_DYNAMIC_WALLET || chain.chainType === 'EVM' + ).map((chain) => ({ id: chain.chainId, label: chain.name, value: chain, + chainType: chain.chainType, icon: {chain.name}, colorHex: chain.colorHex, })), diff --git a/examples/hello/frontend/src/constants/chains.ts b/examples/hello/frontend/src/constants/chains.ts index fdd6d1d4..8848f8da 100644 --- a/examples/hello/frontend/src/constants/chains.ts +++ b/examples/hello/frontend/src/constants/chains.ts @@ -2,6 +2,7 @@ export interface SupportedChain { explorerUrl: string; name: string; chainId: number; + chainType: 'EVM' | 'SOL'; icon: string; colorHex: string; } @@ -11,6 +12,7 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://sepolia.arbiscan.io/tx/', name: 'Arbitrum Sepolia', chainId: 421614, + chainType: 'EVM', icon: '/logos/arbitrum-logo.svg', colorHex: '#28446A', }, @@ -18,6 +20,7 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://testnet.snowtrace.io/tx/', name: 'Avalanche Fuji', chainId: 43113, + chainType: 'EVM', icon: '/logos/avalanche-logo.svg', colorHex: '#FF394A', }, @@ -25,6 +28,7 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://sepolia.basescan.org/tx/', name: 'Base Sepolia', chainId: 84532, + chainType: 'EVM', icon: '/logos/base-logo.svg', colorHex: '#0052FF', }, @@ -32,6 +36,7 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://testnet.bscscan.com/tx/', name: 'BSC Testnet', chainId: 97, + chainType: 'EVM', icon: '/logos/bsc-logo.svg', colorHex: '#E1A411', }, @@ -39,6 +44,7 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://sepolia.etherscan.io/tx/', name: 'Ethereum Sepolia', chainId: 11155111, + chainType: 'EVM', icon: '/logos/ethereum-logo.svg', colorHex: '#3457D5', }, @@ -46,9 +52,18 @@ export const SUPPORTED_CHAINS: SupportedChain[] = [ explorerUrl: 'https://amoy.polygonscan.com/tx/', name: 'Polygon Amoy', chainId: 80002, + chainType: 'EVM', icon: '/logos/polygon-logo.svg', colorHex: '#692BD7', }, + { + explorerUrl: 'https://solscan.io/tx/', + name: 'Solana Devnet', + chainId: 901, + chainType: 'SOL', + icon: '/logos/solana-logo.svg', + colorHex: '#9945FF', + }, ]; export const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map( From c0cbd18490d8f16b982b2109b869ba89eb4f7cad Mon Sep 17 00:00:00 2001 From: Hernan Clich Date: Mon, 6 Oct 2025 12:04:56 -0300 Subject: [PATCH 02/11] Add support for solanaCall function --- .../hello/frontend/src/MessageFlowCard.tsx | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/examples/hello/frontend/src/MessageFlowCard.tsx b/examples/hello/frontend/src/MessageFlowCard.tsx index 9a7fbabc..a86d166b 100644 --- a/examples/hello/frontend/src/MessageFlowCard.tsx +++ b/examples/hello/frontend/src/MessageFlowCard.tsx @@ -1,7 +1,8 @@ import './MessageFlowCard.css'; import { evmCall } from '@zetachain/toolkit/chains/evm'; -import { type PrimaryWallet } from '@zetachain/wallet'; +import { solanaCall } from '@zetachain/toolkit/chains/solana'; +import { isSolanaWallet, type PrimaryWallet } from '@zetachain/wallet'; import { ZeroAddress } from 'ethers'; import { useEffect, useRef, useState } from 'react'; @@ -25,7 +26,6 @@ export function MessageFlowCard({ supportedChain, primaryWallet = null, }: MessageFlowCardProps) { - const MAX_STRING_LENGTH = 2000; const [isUserSigningTx, setIsUserSigningTx] = useState(false); const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false); @@ -37,48 +37,76 @@ export function MessageFlowCard({ return new TextEncoder().encode(string).length; }; - const handleEvmCall = async () => { + const handleCall = async () => { try { - const signerAndProvider = await getSignerAndProvider({ - selectedProvider, - primaryWallet, - }); - - if (!signerAndProvider) { - throw new Error('Failed to get signer'); + if (!primaryWallet) { + throw new Error('No primary wallet'); } - const { signer } = signerAndProvider; - - const evmCallParams = { + const callParams = { receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS, types: ['string'], values: [stringValue], revertOptions: { callOnRevert: false, - revertAddress: ZeroAddress, + revertAddress: primaryWallet.address, revertMessage: '', abortAddress: ZeroAddress, onRevertGasLimit: 1000000, }, }; - const evmCallOptions = { - signer, - txOptions: { - gasLimit: 1000000, - }, - }; + if (primaryWallet?.chain === 'EVM') { + const signerAndProvider = await getSignerAndProvider({ + selectedProvider, + primaryWallet, + }); + + if (!signerAndProvider) { + throw new Error('Failed to get signer'); + } + + const { signer } = signerAndProvider; - setIsUserSigningTx(true); + const evmCallOptions = { + signer, + txOptions: { + gasLimit: 1000000, + }, + }; - const result = await evmCall(evmCallParams, evmCallOptions); + setIsUserSigningTx(true); - setIsTxReceiptLoading(true); + const result = await evmCall(callParams, evmCallOptions); - await result.wait(); + setIsTxReceiptLoading(true); - setConnectedChainTxHash(result.hash); + await result.wait(); + + setConnectedChainTxHash(result.hash); + } else if ( + primaryWallet?.chain === 'SOL' && + isSolanaWallet(primaryWallet) + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const signer = await (primaryWallet as any).getSigner(); + + console.debug('SOL_SIGNER', { + signer, + publicKey: signer.publicKey, + }); + + const solanaCallOptions = { + signer, + chainId: '901', + }; + + const result = await solanaCall(callParams, solanaCallOptions); + + setIsTxReceiptLoading(true); + + setConnectedChainTxHash(result); + } } catch (error) { console.error(error); } finally { @@ -168,7 +196,7 @@ export function MessageFlowCard({