Skip to content

Commit 70c51a7

Browse files
authored
feat: Add Solana support to Hello frontend example (#296)
1 parent 267f861 commit 70c51a7

File tree

10 files changed

+465
-85
lines changed

10 files changed

+465
-85
lines changed

examples/hello/frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@zetachain/toolkit": "16.1.3",
14-
"@zetachain/wallet": "1.0.12",
13+
"@zetachain/toolkit": "16.1.4",
14+
"@zetachain/wallet": "1.0.13",
1515
"clsx": "^2.1.1",
1616
"ethers": "^6.13.2",
1717
"react": "^19.1.0",

examples/hello/frontend/src/ConfirmedContent.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function ConfirmedContent({
7979
<div className="confirmed-content-link-chain">
8080
{!connectedChainTxHash && <IconSpinner />}
8181
<a
82-
href={`${supportedChain.explorerUrl}${connectedChainTxHash}`}
82+
href={supportedChain.explorerUrl(connectedChainTxHash)}
8383
target="_blank"
8484
rel="noreferrer noopener"
8585
className={clsx('confirmed-content-link', {
@@ -95,7 +95,9 @@ export function ConfirmedContent({
9595
<div className="confirmed-content-link-chain">
9696
{!zetachainTxHash && <IconSpinner />}
9797
<a
98-
href={`${ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL}${zetachainTxHash}`}
98+
href={ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL(
99+
zetachainTxHash || ''
100+
)}
99101
target="_blank"
100102
rel="noreferrer noopener"
101103
className={clsx('confirmed-content-link', {

examples/hello/frontend/src/ConnectedContent.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import './ConnectedContent.css';
22

33
import { type PrimaryWallet } from '@zetachain/wallet';
4+
import { useSwitchWallet, useUserWallets } from '@zetachain/wallet/react';
5+
import { useMemo } from 'react';
46

57
import { NetworkSelector } from './components/NetworkSelector';
68
import type { SupportedChain } from './constants/chains';
@@ -14,7 +16,8 @@ import type { EIP6963ProviderDetail } from './types/wallet';
1416
interface ConnectedContentProps {
1517
selectedProvider: EIP6963ProviderDetail | null;
1618
supportedChain: SupportedChain | undefined;
17-
primaryWallet?: PrimaryWallet | null; // Dynamic wallet from context
19+
primaryWallet?: PrimaryWallet | null;
20+
account?: string | null;
1821
}
1922

2023
const DynamicConnectedContent = ({
@@ -23,8 +26,29 @@ const DynamicConnectedContent = ({
2326
primaryWallet,
2427
}: ConnectedContentProps) => {
2528
const { switchChain } = useDynamicSwitchChainHook();
29+
const userWallets = useUserWallets();
30+
const switchWallet = useSwitchWallet();
31+
32+
const primaryWalletChain = primaryWallet?.chain;
33+
const walletIds: Record<string, string> = useMemo(() => {
34+
const solanaWallet = userWallets.find(
35+
(wallet) => wallet.chain === 'SOL'
36+
)?.id;
37+
const evmWallet = userWallets.find((wallet) => wallet.chain === 'EVM')?.id;
38+
39+
return {
40+
EVM: evmWallet || '',
41+
SOL: solanaWallet || '',
42+
};
43+
}, [userWallets]);
2644

2745
const handleNetworkSelect = (chain: SupportedChain) => {
46+
// We only switch wallet if the chain type is
47+
// different from the primary wallet chain (i.e.: EVM -> SOL)
48+
if (chain.chainType !== primaryWalletChain) {
49+
switchWallet(walletIds[chain.chainType]);
50+
}
51+
2852
switchChain(chain.chainId);
2953
};
3054

@@ -60,7 +84,7 @@ const DynamicConnectedContent = ({
6084
const Eip6963ConnectedContent = ({
6185
selectedProvider,
6286
supportedChain,
63-
primaryWallet,
87+
account,
6488
}: ConnectedContentProps) => {
6589
const { switchChain } = useSwitchChain();
6690

@@ -89,7 +113,7 @@ const Eip6963ConnectedContent = ({
89113
<MessageFlowCard
90114
selectedProvider={selectedProvider}
91115
supportedChain={supportedChain}
92-
primaryWallet={primaryWallet}
116+
account={account}
93117
/>
94118
</div>
95119
<Footer />
@@ -101,6 +125,7 @@ export function ConnectedContent({
101125
selectedProvider,
102126
supportedChain,
103127
primaryWallet,
128+
account,
104129
}: ConnectedContentProps) {
105130
return USE_DYNAMIC_WALLET ? (
106131
<DynamicConnectedContent
@@ -112,7 +137,7 @@ export function ConnectedContent({
112137
<Eip6963ConnectedContent
113138
selectedProvider={selectedProvider}
114139
supportedChain={supportedChain}
115-
primaryWallet={primaryWallet}
140+
account={account}
116141
/>
117142
);
118143
}

examples/hello/frontend/src/DynamicAppContent.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useUniversalSignInContext } from '@zetachain/wallet/react';
2+
import { useMemo } from 'react';
23

34
import { ConnectedContent } from './ConnectedContent';
45
import { SUPPORTED_CHAINS } from './constants/chains';
@@ -8,7 +9,18 @@ export function DynamicAppContent() {
89
const { primaryWallet, network } = useUniversalSignInContext();
910

1011
const account = primaryWallet?.address || null;
11-
const decimalChainId = network || null;
12+
const decimalChainId = useMemo(() => {
13+
if (typeof network === 'number') {
14+
return network;
15+
}
16+
17+
// Solana Devnet id from `network` property
18+
if (network === '103') {
19+
return 901;
20+
}
21+
22+
return null;
23+
}, [network]);
1224

1325
const supportedChain = SUPPORTED_CHAINS.find(
1426
(chain) => chain.chainId === decimalChainId
@@ -27,4 +39,4 @@ export function DynamicAppContent() {
2739
primaryWallet={primaryWallet}
2840
/>
2941
);
30-
}
42+
}

examples/hello/frontend/src/Eip6963AppContent.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DisconnectedContent } from './DisconnectedContent';
44
import { useEip6963Wallet } from './hooks/useEip6963Wallet';
55

66
export function Eip6963AppContent() {
7-
const { selectedProvider, decimalChainId } = useEip6963Wallet();
7+
const { selectedProvider, decimalChainId, account } = useEip6963Wallet();
88

99
const supportedChain = SUPPORTED_CHAINS.find(
1010
(chain) => chain.chainId === decimalChainId
@@ -20,6 +20,7 @@ export function Eip6963AppContent() {
2020
<ConnectedContent
2121
selectedProvider={selectedProvider}
2222
supportedChain={supportedChain}
23+
account={account}
2324
/>
2425
);
25-
}
26+
}

examples/hello/frontend/src/MessageFlowCard.tsx

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
import './MessageFlowCard.css';
22

3-
import { evmCall } from '@zetachain/toolkit/chains/evm';
43
import { type PrimaryWallet } from '@zetachain/wallet';
5-
import { ZeroAddress } from 'ethers';
64
import { useEffect, useRef, useState } from 'react';
75

86
import { Button } from './components/Button';
97
import { IconApprove, IconEnvelope, IconSendTitle } from './components/icons';
108
import { ConfirmedContent } from './ConfirmedContent';
119
import type { SupportedChain } from './constants/chains';
1210
import { HELLO_UNIVERSAL_CONTRACT_ADDRESS } from './constants/contracts';
11+
import { useHandleCall } from './hooks/useHandleCall';
1312
import type { EIP6963ProviderDetail } from './types/wallet';
14-
import { getSignerAndProvider } from './utils/ethersHelpers';
1513
import { formatNumberWithLocale } from './utils/formatNumber';
1614

1715
interface MessageFlowCardProps {
1816
selectedProvider: EIP6963ProviderDetail | null;
1917
supportedChain: SupportedChain | undefined;
2018
primaryWallet?: PrimaryWallet | null; // Dynamic wallet from context
19+
account?: string | null; // EIP6963 account for non-dynamic route
2120
}
2221

2322
export function MessageFlowCard({
2423
selectedProvider,
2524
supportedChain,
2625
primaryWallet = null,
26+
account = null,
2727
}: MessageFlowCardProps) {
28-
2928
const MAX_STRING_LENGTH = 2000;
3029
const [isUserSigningTx, setIsUserSigningTx] = useState(false);
3130
const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false);
@@ -37,55 +36,22 @@ export function MessageFlowCard({
3736
return new TextEncoder().encode(string).length;
3837
};
3938

40-
const handleEvmCall = async () => {
41-
try {
42-
const signerAndProvider = await getSignerAndProvider({
43-
selectedProvider,
44-
primaryWallet,
45-
});
46-
47-
if (!signerAndProvider) {
48-
throw new Error('Failed to get signer');
49-
}
50-
51-
const { signer } = signerAndProvider;
52-
53-
const evmCallParams = {
54-
receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS,
55-
types: ['string'],
56-
values: [stringValue],
57-
revertOptions: {
58-
callOnRevert: false,
59-
revertAddress: ZeroAddress,
60-
revertMessage: '',
61-
abortAddress: ZeroAddress,
62-
onRevertGasLimit: 1000000,
63-
},
64-
};
65-
66-
const evmCallOptions = {
67-
signer,
68-
txOptions: {
69-
gasLimit: 1000000,
70-
},
71-
};
72-
73-
setIsUserSigningTx(true);
74-
75-
const result = await evmCall(evmCallParams, evmCallOptions);
76-
77-
setIsTxReceiptLoading(true);
78-
79-
await result.wait();
80-
81-
setConnectedChainTxHash(result.hash);
82-
} catch (error) {
83-
console.error(error);
84-
} finally {
39+
const { handleCall } = useHandleCall({
40+
primaryWallet,
41+
selectedProvider,
42+
supportedChain,
43+
receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS,
44+
message: stringValue,
45+
account,
46+
onSigningStart: () => setIsUserSigningTx(true),
47+
onTransactionSubmitted: () => setIsTxReceiptLoading(true),
48+
onTransactionConfirmed: (txHash: string) => setConnectedChainTxHash(txHash),
49+
onError: (error: Error) => console.error('Transaction error:', error),
50+
onComplete: () => {
8551
setIsUserSigningTx(false);
8652
setIsTxReceiptLoading(false);
87-
}
88-
};
53+
},
54+
});
8955

9056
// Auto-resize textarea based on content
9157
useEffect(() => {
@@ -168,7 +134,7 @@ export function MessageFlowCard({
168134
<div>
169135
<Button
170136
type="button"
171-
onClick={handleEvmCall}
137+
onClick={handleCall}
172138
disabled={
173139
!stringValue.length ||
174140
!supportedChain ||

examples/hello/frontend/src/components/NetworkSelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMemo } from 'react';
22

33
import { SUPPORTED_CHAINS, type SupportedChain } from '../constants/chains';
4+
import { USE_DYNAMIC_WALLET } from '../constants/wallets';
45
import { Dropdown, type DropdownOption } from './Dropdown';
56

67
interface NetworkSelectorProps {
@@ -21,7 +22,9 @@ export const NetworkSelector = ({
2122
// Convert chains to dropdown options
2223
const options: DropdownOption<SupportedChain>[] = useMemo(
2324
() =>
24-
SUPPORTED_CHAINS.map((chain) => ({
25+
SUPPORTED_CHAINS.filter(
26+
(chain) => USE_DYNAMIC_WALLET || chain.chainType === 'EVM'
27+
).map((chain) => ({
2528
id: chain.chainId,
2629
label: chain.name,
2730
value: chain,
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,79 @@
11
export interface SupportedChain {
2-
explorerUrl: string;
2+
explorerUrl: (txHash: string) => string;
33
name: string;
44
chainId: number;
5+
chainType: 'EVM' | 'SOL';
56
icon: string;
67
colorHex: string;
78
}
89

910
export const SUPPORTED_CHAINS: SupportedChain[] = [
1011
{
11-
explorerUrl: 'https://sepolia.arbiscan.io/tx/',
12+
explorerUrl: (txHash: string) => `https://sepolia.arbiscan.io/tx/${txHash}`,
1213
name: 'Arbitrum Sepolia',
1314
chainId: 421614,
15+
chainType: 'EVM',
1416
icon: '/logos/arbitrum-logo.svg',
1517
colorHex: '#28446A',
1618
},
1719
{
18-
explorerUrl: 'https://testnet.snowtrace.io/tx/',
20+
explorerUrl: (txHash: string) =>
21+
`https://testnet.snowtrace.io/tx/${txHash}`,
1922
name: 'Avalanche Fuji',
2023
chainId: 43113,
24+
chainType: 'EVM',
2125
icon: '/logos/avalanche-logo.svg',
2226
colorHex: '#FF394A',
2327
},
2428
{
25-
explorerUrl: 'https://sepolia.basescan.org/tx/',
29+
explorerUrl: (txHash: string) =>
30+
`https://sepolia.basescan.org/tx/${txHash}`,
2631
name: 'Base Sepolia',
2732
chainId: 84532,
33+
chainType: 'EVM',
2834
icon: '/logos/base-logo.svg',
2935
colorHex: '#0052FF',
3036
},
3137
{
32-
explorerUrl: 'https://testnet.bscscan.com/tx/',
38+
explorerUrl: (txHash: string) => `https://testnet.bscscan.com/tx/${txHash}`,
3339
name: 'BSC Testnet',
3440
chainId: 97,
41+
chainType: 'EVM',
3542
icon: '/logos/bsc-logo.svg',
3643
colorHex: '#E1A411',
3744
},
3845
{
39-
explorerUrl: 'https://sepolia.etherscan.io/tx/',
46+
explorerUrl: (txHash: string) =>
47+
`https://sepolia.etherscan.io/tx/${txHash}`,
4048
name: 'Ethereum Sepolia',
4149
chainId: 11155111,
50+
chainType: 'EVM',
4251
icon: '/logos/ethereum-logo.svg',
4352
colorHex: '#3457D5',
4453
},
4554
{
46-
explorerUrl: 'https://amoy.polygonscan.com/tx/',
55+
explorerUrl: (txHash: string) =>
56+
`https://amoy.polygonscan.com/tx/${txHash}`,
4757
name: 'Polygon Amoy',
4858
chainId: 80002,
59+
chainType: 'EVM',
4960
icon: '/logos/polygon-logo.svg',
5061
colorHex: '#692BD7',
5162
},
63+
{
64+
explorerUrl: (txHash: string) =>
65+
`https://solscan.io/tx/${txHash}?cluster=devnet`,
66+
name: 'Solana Devnet',
67+
chainId: 901,
68+
chainType: 'SOL',
69+
icon: '/logos/solana-logo.svg',
70+
colorHex: '#9945FF',
71+
},
5272
];
5373

5474
export const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map(
5575
(chain) => chain.chainId
5676
);
5777

58-
export const ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL =
59-
'https://zetachain-testnet.blockscout.com/tx/';
78+
export const ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL = (txHash: string) =>
79+
`https://zetachain-testnet.blockscout.com/tx/${txHash}`;

0 commit comments

Comments
 (0)