Skip to content
Merged
4 changes: 2 additions & 2 deletions examples/hello/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@zetachain/toolkit": "16.1.3",
"@zetachain/wallet": "1.0.12",
"@zetachain/toolkit": "16.1.4",
"@zetachain/wallet": "1.0.13",
"clsx": "^2.1.1",
"ethers": "^6.13.2",
"react": "^19.1.0",
Expand Down
6 changes: 4 additions & 2 deletions examples/hello/frontend/src/ConfirmedContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function ConfirmedContent({
<div className="confirmed-content-link-chain">
{!connectedChainTxHash && <IconSpinner />}
<a
href={`${supportedChain.explorerUrl}${connectedChainTxHash}`}
href={supportedChain.explorerUrl(connectedChainTxHash)}
target="_blank"
rel="noreferrer noopener"
className={clsx('confirmed-content-link', {
Expand All @@ -95,7 +95,9 @@ export function ConfirmedContent({
<div className="confirmed-content-link-chain">
{!zetachainTxHash && <IconSpinner />}
<a
href={`${ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL}${zetachainTxHash}`}
href={ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL(
zetachainTxHash || ''
)}
target="_blank"
rel="noreferrer noopener"
className={clsx('confirmed-content-link', {
Expand Down
33 changes: 29 additions & 4 deletions examples/hello/frontend/src/ConnectedContent.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,7 +16,8 @@ import type { EIP6963ProviderDetail } from './types/wallet';
interface ConnectedContentProps {
selectedProvider: EIP6963ProviderDetail | null;
supportedChain: SupportedChain | undefined;
primaryWallet?: PrimaryWallet | null; // Dynamic wallet from context
primaryWallet?: PrimaryWallet | null;
account?: string | null;
}

const DynamicConnectedContent = ({
Expand All @@ -23,8 +26,29 @@ const DynamicConnectedContent = ({
primaryWallet,
}: ConnectedContentProps) => {
const { switchChain } = useDynamicSwitchChainHook();
const userWallets = useUserWallets();
const switchWallet = useSwitchWallet();

const primaryWalletChain = primaryWallet?.chain;
const walletIds: Record<string, string> = 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);
};

Expand Down Expand Up @@ -60,7 +84,7 @@ const DynamicConnectedContent = ({
const Eip6963ConnectedContent = ({
selectedProvider,
supportedChain,
primaryWallet,
account,
}: ConnectedContentProps) => {
const { switchChain } = useSwitchChain();

Expand Down Expand Up @@ -89,7 +113,7 @@ const Eip6963ConnectedContent = ({
<MessageFlowCard
selectedProvider={selectedProvider}
supportedChain={supportedChain}
primaryWallet={primaryWallet}
account={account}
/>
</div>
<Footer />
Expand All @@ -101,6 +125,7 @@ export function ConnectedContent({
selectedProvider,
supportedChain,
primaryWallet,
account,
}: ConnectedContentProps) {
return USE_DYNAMIC_WALLET ? (
<DynamicConnectedContent
Expand All @@ -112,7 +137,7 @@ export function ConnectedContent({
<Eip6963ConnectedContent
selectedProvider={selectedProvider}
supportedChain={supportedChain}
primaryWallet={primaryWallet}
account={account}
/>
);
}
16 changes: 14 additions & 2 deletions examples/hello/frontend/src/DynamicAppContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useUniversalSignInContext } from '@zetachain/wallet/react';
import { useMemo } from 'react';

import { ConnectedContent } from './ConnectedContent';
import { SUPPORTED_CHAINS } from './constants/chains';
Expand All @@ -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
Expand All @@ -27,4 +39,4 @@ export function DynamicAppContent() {
primaryWallet={primaryWallet}
/>
);
}
}
5 changes: 3 additions & 2 deletions examples/hello/frontend/src/Eip6963AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DisconnectedContent } from './DisconnectedContent';
import { useEip6963Wallet } from './hooks/useEip6963Wallet';

export function Eip6963AppContent() {
const { selectedProvider, decimalChainId } = useEip6963Wallet();
const { selectedProvider, decimalChainId, account } = useEip6963Wallet();

const supportedChain = SUPPORTED_CHAINS.find(
(chain) => chain.chainId === decimalChainId
Expand All @@ -20,6 +20,7 @@ export function Eip6963AppContent() {
<ConnectedContent
selectedProvider={selectedProvider}
supportedChain={supportedChain}
account={account}
/>
);
}
}
70 changes: 18 additions & 52 deletions examples/hello/frontend/src/MessageFlowCard.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import './MessageFlowCard.css';

import { evmCall } from '@zetachain/toolkit/chains/evm';
import { type PrimaryWallet } from '@zetachain/wallet';
import { ZeroAddress } from 'ethers';
import { useEffect, useRef, useState } from 'react';

import { Button } from './components/Button';
import { IconApprove, IconEnvelope, IconSendTitle } from './components/icons';
import { ConfirmedContent } from './ConfirmedContent';
import type { SupportedChain } from './constants/chains';
import { HELLO_UNIVERSAL_CONTRACT_ADDRESS } from './constants/contracts';
import { useHandleCall } from './hooks/useHandleCall';
import type { EIP6963ProviderDetail } from './types/wallet';
import { getSignerAndProvider } from './utils/ethersHelpers';
import { formatNumberWithLocale } from './utils/formatNumber';

interface MessageFlowCardProps {
selectedProvider: EIP6963ProviderDetail | null;
supportedChain: SupportedChain | undefined;
primaryWallet?: PrimaryWallet | null; // Dynamic wallet from context
account?: string | null; // EIP6963 account for non-dynamic route
}

export function MessageFlowCard({
selectedProvider,
supportedChain,
primaryWallet = null,
account = null,
}: MessageFlowCardProps) {

const MAX_STRING_LENGTH = 2000;
const [isUserSigningTx, setIsUserSigningTx] = useState(false);
const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false);
Expand All @@ -37,55 +36,22 @@ export function MessageFlowCard({
return new TextEncoder().encode(string).length;
};

const handleEvmCall = async () => {
try {
const signerAndProvider = await getSignerAndProvider({
selectedProvider,
primaryWallet,
});

if (!signerAndProvider) {
throw new Error('Failed to get signer');
}

const { signer } = signerAndProvider;

const evmCallParams = {
receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS,
types: ['string'],
values: [stringValue],
revertOptions: {
callOnRevert: false,
revertAddress: ZeroAddress,
revertMessage: '',
abortAddress: ZeroAddress,
onRevertGasLimit: 1000000,
},
};

const evmCallOptions = {
signer,
txOptions: {
gasLimit: 1000000,
},
};

setIsUserSigningTx(true);

const result = await evmCall(evmCallParams, evmCallOptions);

setIsTxReceiptLoading(true);

await result.wait();

setConnectedChainTxHash(result.hash);
} catch (error) {
console.error(error);
} finally {
const { handleCall } = useHandleCall({
primaryWallet,
selectedProvider,
supportedChain,
receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS,
message: stringValue,
account,
onSigningStart: () => setIsUserSigningTx(true),
onTransactionSubmitted: () => setIsTxReceiptLoading(true),
onTransactionConfirmed: (txHash: string) => setConnectedChainTxHash(txHash),
onError: (error: Error) => console.error('Transaction error:', error),
onComplete: () => {
setIsUserSigningTx(false);
setIsTxReceiptLoading(false);
}
};
},
});

// Auto-resize textarea based on content
useEffect(() => {
Expand Down Expand Up @@ -168,7 +134,7 @@ export function MessageFlowCard({
<div>
<Button
type="button"
onClick={handleEvmCall}
onClick={handleCall}
disabled={
!stringValue.length ||
!supportedChain ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -21,7 +22,9 @@ export const NetworkSelector = ({
// Convert chains to dropdown options
const options: DropdownOption<SupportedChain>[] = 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,
Expand Down
38 changes: 29 additions & 9 deletions examples/hello/frontend/src/constants/chains.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,79 @@
export interface SupportedChain {
explorerUrl: string;
explorerUrl: (txHash: string) => string;
name: string;
chainId: number;
chainType: 'EVM' | 'SOL';
icon: string;
colorHex: string;
}

export const SUPPORTED_CHAINS: SupportedChain[] = [
{
explorerUrl: 'https://sepolia.arbiscan.io/tx/',
explorerUrl: (txHash: string) => `https://sepolia.arbiscan.io/tx/${txHash}`,
name: 'Arbitrum Sepolia',
chainId: 421614,
chainType: 'EVM',
icon: '/logos/arbitrum-logo.svg',
colorHex: '#28446A',
},
{
explorerUrl: 'https://testnet.snowtrace.io/tx/',
explorerUrl: (txHash: string) =>
`https://testnet.snowtrace.io/tx/${txHash}`,
name: 'Avalanche Fuji',
chainId: 43113,
chainType: 'EVM',
icon: '/logos/avalanche-logo.svg',
colorHex: '#FF394A',
},
{
explorerUrl: 'https://sepolia.basescan.org/tx/',
explorerUrl: (txHash: string) =>
`https://sepolia.basescan.org/tx/${txHash}`,
name: 'Base Sepolia',
chainId: 84532,
chainType: 'EVM',
icon: '/logos/base-logo.svg',
colorHex: '#0052FF',
},
{
explorerUrl: 'https://testnet.bscscan.com/tx/',
explorerUrl: (txHash: string) => `https://testnet.bscscan.com/tx/${txHash}`,
name: 'BSC Testnet',
chainId: 97,
chainType: 'EVM',
icon: '/logos/bsc-logo.svg',
colorHex: '#E1A411',
},
{
explorerUrl: 'https://sepolia.etherscan.io/tx/',
explorerUrl: (txHash: string) =>
`https://sepolia.etherscan.io/tx/${txHash}`,
name: 'Ethereum Sepolia',
chainId: 11155111,
chainType: 'EVM',
icon: '/logos/ethereum-logo.svg',
colorHex: '#3457D5',
},
{
explorerUrl: 'https://amoy.polygonscan.com/tx/',
explorerUrl: (txHash: string) =>
`https://amoy.polygonscan.com/tx/${txHash}`,
name: 'Polygon Amoy',
chainId: 80002,
chainType: 'EVM',
icon: '/logos/polygon-logo.svg',
colorHex: '#692BD7',
},
{
explorerUrl: (txHash: string) =>
`https://solscan.io/tx/${txHash}?cluster=devnet`,
name: 'Solana Devnet',
chainId: 901,
chainType: 'SOL',
icon: '/logos/solana-logo.svg',
colorHex: '#9945FF',
},
];

export const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map(
(chain) => chain.chainId
);

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