Wrap the app with our new provider
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
VeChain Kit is an all-in-one SDK for building frontend applications on VeChain, supporting wallet integration, developer hooks, pre-built UI components, and more.





const { data, isLoading, error } = useCurrentBlock();$ npx create-vechain-dapp@latest
? Select template ›
❯ VeChain Kit Next.js Template (Chakra, React Query, SDK)@vechain/vechain-contract-types and @vechain/contract-getters, which you can use independently.npm create vechain-dappyarn create vechain-dappfonts: {
family: 'Inter, sans-serif', // Font family (e.g., "Inter, sans-serif", "'Roboto', sans-serif")
sizes: {
small: '12px', // Font size for small text
medium: '14px', // Font size for medium text
large: '16px', // Font size for large text
},
weights: {
normal: 400, // Normal font weight
medium: 500, // Medium font weight
bold: 700, // Bold font weight
},
}// Only customize font family
fonts: {
family: 'Inter, sans-serif',
}
// Only customize font sizes
fonts: {
sizes: {
medium: '15px',
large: '18px',
},
}function BlockInfo() {
const { data: block } = useCurrentBlock();
return <div>Current Block: {block?.number}</div>;
}const { data, isLoading, error } = useTxReceipt(txId, blockTimeout);function TransactionStatus({ txId }) {
const { data: receipt, isLoading } = useTxReceipt(txId);
if (isLoading) return <div>Loading...</div>;
return <div>Transaction Status: {receipt?.reverted ? 'Failed' : 'Success'}</div>;
}const events = await getEvents({
abi,
contractAddress,
eventName,
filterParams,
mapResponse,
nodeUrl,
});'use client';
import { VeChainKitProvider } from "@vechain/vechain-kit";
export function Providers({ children }) {
return (
<VeChainKitProvider>
{children}
</VeChainKitProvider>
);
}"use client";
import { WalletButton } from "@vechain/vechain-kit";
const Demo = () => {
return (
<div>
<WalletButton /> {/* Login Button */}
<p>{account?.address}</p> {/* Address of the connected account */}
</div>
)
}npm install --legacy-peer-deps @vechain/vechain-kit @chakra-ui/react@^2.8.2 \
@emotion/react@^11.14.0 \
@emotion/styled@^11.14.0 \
@tanstack/react-query@^5.64.2 \
@vechain/[email protected] \
framer-motion@^11.15.0Install peer dependenciesnpx create-vechain-dapp@latestconst queryClient = useQueryClient();
// Invalidate all blockchain queries
queryClient.invalidateQueries({ queryKey: ['VECHAIN_KIT'] });
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['VECHAIN_KIT', 'CURRENT_BLOCK'] });import { VeChainKitProvider } from '@vechain/vechain-kit';
export default function App({ Component, pageProps }: AppProps) {
return (
<VeChainKitProvider
privy={{
appId: process.env.NEXT_PUBLIC_PRIVY_APP_ID!,
clientId: process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID!,
loginMethods: ['google', 'twitter', 'sms', 'email'],
appearance: {
walletList: ['metamask', 'rainbow'],
accentColor: '#696FFD',
loginMessage: 'Select a social media profile',
logo: 'https://i.ibb.co/ZHGmq3y/image-21.png',
},
embeddedWallets: {
createOnLogin: 'all-users',
},
allowPasskeyLinking: true,
}}
...
//other props
>
{children}
</VeChainKitProvider>
);
}import { usePrivy } from "@vechain/vechain-kit";
const { user } = usePrivy();<VeChainKitProvider
theme={{
modal: {
useBottomSheetOnMobile: true
},
}}
>
{children}
</VeChainKitProvider>
import { useGetTokenUsdPrice } from '@vechain/vechain-kit';
function TokenPrice() {
const { data: vetPrice, isLoading, error } = useGetTokenUsdPrice('VET');
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error fetching price</div>;
return <div>VET Price: ${vetPrice}</div>;
}function TokenPrices() {
const { data: vetPrice } = useGetTokenUsdPrice('VET');
const { data: vthoPrice } = useGetTokenUsdPrice('VTHO');
const { data: b3trPrice } = useGetTokenUsdPrice('B3TR');
return (
<div>
<div>VET: ${vetPrice}</div>
<div>VTHO: ${vthoPrice}</div>
<div>B3TR: ${b3trPrice}</div>
</div>
);
}# Remove existing installations
rm -rf node_modules
rm package-lock.json # or yarn.lock
# Reinstall packages
npm install # or yarn install# Check what VeChain Kit expects
npm info @vechain/vechain-kit peerDependencies
# Install required peer dependencies
npm install @chakra-ui/react@^2.8.2 @tanstack/react-query@^5.64.2 @vechain/[email protected]npm uninstall @vechain/dapp-kit @vechain/dapp-kit-react @vechain/dapp-kit-ui// X Old DApp Kit
import { ConnectWallet } from '@vechain/dapp-kit-react';
// ✅ New VeChain Kit
import { WalletButton } from '@vechain/vechain-kit';rm -rf node_modules package-lock.json
npm install<VeChainKitProvider
feeDelegation={{
delegatorUrl: process.env.NEXT_PUBLIC_DELEGATOR_URL,
delegateAllTransactions: false
}}
// ... other config
>
<App />
</VeChainKitProvider><VeChainKitProvider
theme={{
modal: {
backgroundColor: isDarkMode ? '#1f1f1e' : '#ffffff',
},
textColor: isDarkMode ? 'rgb(223, 223, 221)' : '#2e2e2e',
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
blur: 'blur(3px)',
},
buttons: {
secondaryButton: {
bg: 'rgba(255, 255, 255, 0.1)',
color: '#ffffff',
border: 'none',
},
},
effects: {
glass: {
enabled: true,
intensity: 'low',
},
},
}}
// ... other props
>
{children}
</VeChainKitProvider>'use client';
import {
useWallet,
useSendTransaction,
useTransactionModal,
TransactionModal,
getConfig
} from '@vechain/vechain-kit';
import { IB3TR__factory } from '@vechain/vechain-kit/contracts';
import { humanAddress } from '@vechain/vechain-kit/utils';
import { useMemo, useCallback } from 'react';
export function TransactionExamples() {
const { account } = useWallet();
const b3trMainnetAddress = getConfig("main").b3trContractAddress;
const clauses = useMemo(() => {
const B3TRInterface = IB3TR__factory.createInterface();
const clausesArray: any[] = [];
clausesArray.push({
to: b3trMainnetAddress,
value: '0x0',
data: B3TRInterface.encodeFunctionData('transfer', [
"0x0, // receiver address
'0', // 0 B3TR (in wei)
]),
comment: `This is a dummy transaction to test the transaction modal. Confirm to transfer ${0} B3TR to ${humanAddress("Ox0")}`,
abi: B3TRInterface.getFunction('transfer'),
});
return clausesArray;
}, [connectedWallet?.address]);
const {
sendTransaction,
status,
txReceipt,
resetStatus,
isTransactionPending,
error,
} = useSendTransaction({
signerAccountAddress: account?.address ?? '',
});
const {
open: openTransactionModal,
close: closeTransactionModal,
isOpen: isTransactionModalOpen,
} = useTransactionModal();
// This is the function triggering the transaction and opening the modal
const handleTransaction = useCallback(async () => {
openTransactionModal();
await sendTransaction(clauses);
}, [sendTransaction, clauses, openTransactionModal]);
const handleTryAgain = useCallback(async () => {
resetStatus();
await sendTransaction(clauses);
}, [sendTransaction, clauses, resetStatus]);
return (
<>
<button
onClick={handleTransactionWithModal}
isLoading={isTransactionPending}
isDisabled={isTransactionPending}
>
Send B3TR
</button>
<TransactionModal
isOpen={isTransactionModalOpen}
onClose={closeTransactionModal}
status={status}
txReceipt={txReceipt}
txError={error}
onTryAgain={handleTryAgain}
uiConfig={{
title: 'Test Transaction',
description: `This is a dummy transaction to test the transaction modal. Confirm to transfer ${0} B3TR to ${
account?.address
}`,
showShareOnSocials: true,
showExplorerButton: true,
isClosable: true,
}}
/>
</>
);
}


import { VeChainKitProvider } from '@vechain/vechain-kit';
export default function App({ Component, pageProps }: AppProps) {
return (
<VeChainKitProvider
legalDocuments={{
allowAnalytics: true, // Enables optional consent for VeChainKit tracking
cookiePolicy: [
{
displayName: 'MyApp Policy', // (Optional) Custom display label
url: 'https://www.myapp.com/cookie-policy',
version: 1, // Increment to re-prompt users
required: false, // Optional: User sees a checkbox to opt in
},
],
privacyPolicy: [
{
url: 'https://www.myapp.com/privacy-policy',
version: 1, // Increment to re-prompt users
required: false, // Optional: can be skipped or rejected
},
],
termsAndConditions: [
{
displayName: 'MyApp T&C',
url: 'https://www.myapp.com/terms-and-conditions',
version: 1, // Increment to re-prompt users
required: true, // Required: must be accepted to proceed
},
],
}}
// ... other props
>
{children}
</VeChainKitProvider>
);
}
// OAuth Types
type OAuthProvider = 'google' | 'twitter' | 'apple' | 'discord';
interface OAuthOptions {
provider: OAuthProvider;
}
interface UseLoginWithOAuthReturn {
initOAuth: (options: OAuthOptions) => Promise<void>;
}
interface UseLoginWithPasskeyReturn {
loginWithPasskey: () => Promise<void>;
}
interface UseLoginWithVeChainReturn {
login: () => Promise<void>;
}// Example usage of Login hooks
import {
useLoginWithPasskey,
useLoginWithOAuth,
useLoginWithVeChain
} from '@vechain/vechain-kit';
const ExampleComponent = () => {
// Passkey authentication
const {
loginWithPasskey,
} = useLoginWithPasskey();
// OAuth authentication
const {
initOAuth,
} = useLoginWithOAuth();
// VeChain wallet authentication
const {
login: loginWithVeChain,
} = useLoginWithVeChain();
const handlePasskeyLogin = async () => {
try {
await loginWithPasskey();
console.log("Passkey login successful");
} catch (error) {
console.error("Passkey login failed:", error);
}
};
const handleOAuthLogin = async (provider: OAuthProvider) => {
try {
await initOAuth({ provider });
console.log(`${provider} OAuth login initiated`);
} catch (error) {
console.error("OAuth login failed:", error);
}
};
const handleVeChainLogin = async () => {
try {
await loginWithVeChain();
console.log("VeChain login successful");
} catch (error) {
console.error("VeChain login failed:", error);
}
};
return (
<div>
{/* Passkey Login */}
<button onClick={handlePasskeyLogin}>
Login with Passkey
</button>
{/* OAuth Login Options */}
<button onClick={() => handleOAuthLogin('google')}>
Login with Google
</button>
<button onClick={() => handleOAuthLogin('twitter')}>
Login with Twitter
</button>
<button onClick={() => handleOAuthLogin('apple')}>
Login with Apple
</button>
<button onClick={() => handleOAuthLogin('discord')}>
Login with Discord
</button>
{/* VeChain Wallet Login */}
<button onClick={handleVeChainLogin}>
Login with VeChain Wallet
</button>
</div>
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- Privy configuration for OAuth and Passkey
- Valid VeChain network configuration
- For VeChain login:
- Valid VECHAIN_PRIVY_APP_ID
- Proper error handling for popup blockers
- Mobile browser compatibility handling
*/overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)', // Overlay background color
blur: 'blur(10px)', // Overlay blur effect
}import type { VechainKitThemeConfig } from '@vechain/vechain-kit';
const theme: VechainKitThemeConfig = {
modal: {
backgroundColor: isDarkMode ? '#1f1f1e' : '#ffffff',
border: "1px solid #00000",
// backdropFilter?: string; // Backdrop filter for modal dialog (e.g., "blur(10px)")
// borderRadius?: string; // Modal dialog border radius (e.g., "24px", "1rem") - deprecated, use rounded instead
// rounded?: string | number; // Border radius (Chakra UI rounded prop: "sm", "md", "lg", "xl", "2xl", "3xl", "full", or number)
},
textColor: isDarkMode ? 'rgb(223, 223, 221)' : '#2e2e2e',
overlay: {
backgroundColor: isDarkMode
? 'rgba(0, 0, 0, 0.6)'
: 'rgba(0, 0, 0, 0.4)',
blur: 'blur(3px)',
},
buttons: {
primaryButton: {
bg: isDarkMode ? '#3182CE' : '#2B6CB0',
color: 'white',
border: 'none',
},
secondaryButton: {
bg: isDarkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.1)',
color: isDarkMode ? 'rgb(223, 223, 221)' : '#2e2e2e',
border: 'none',
// backdropFilter?: string; // Optional backdrop filter (e.g., "blur(10px)")
// rounded?: string | number; // Border radius (Chakra UI rounded prop: "sm", "md", "lg", "xl", "2xl", "3xl", "full", or number)
},
loginButton: {
bg: 'transparent',
color: isDarkMode ? 'white' : '#1a1a1a',
border: isDarkMode
? '1px solid rgba(255, 255, 255, 0.1)'
: '1px solid #ebebeb',
},
},
fonts: {
family: 'Inter, sans-serif',
sizes: {
small: '12px',
medium: '14px',
large: '16px',
},
weights: {
normal: 400,
medium: 500,
bold: 700,
},
},
effects: {
glass: {
enabled: true,
intensity: 'low',
},
},
};
<VeChainKitProvider theme={theme} {...otherProps}>
{children}
</VeChainKitProvider>;// Example usage of Utility hooks
import {
useGetChainId,
useGetNodeUrl,
useGetCustomTokenBalances,
useGetCustomTokenInfo,
useLegalDocuments
} from '@vechain/vechain-kit';
const ExampleComponent = () => {
const address = "0x..."; // User's wallet address
const tokenAddress = "0x..."; // Custom token address
// Get network information
const { data: chainId } = useGetChainId();
const nodeUrl = useGetNodeUrl();
// Get custom token information
const { data: tokenInfo } = useGetCustomTokenInfo(tokenAddress);
// Get token balances
const {
data: tokenBalances,
isLoading,
error
} = useGetCustomTokenBalances(address);
// Get legal documents data
const {
documentsNotAgreed,
documents,
agreements,
hasAgreedToRequiredDocuments,
} = useLegalDocuments();
console.log(
'Chain ID:', chainId,
'Node URL:', nodeUrl,
'Token Info:', tokenInfo,
'Token Balances:', tokenBalances,
'Has Agreed to Required Documents:', hasAgreedToRequiredDocuments,
'User Document Agreements:', agreements,
'Documents Not Agreed:', documentsNotAgreed,
'All Legal Documents:', documents
);
return (
// Your component JSX here
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- A valid thor connection
- Appropriate network configuration
- Valid input parameters where applicable
The token-related hooks work with ERC20 tokens and return:
- Original values (raw from contract)
- Scaled values (adjusted for decimals)
- Formatted values (human-readable)
*/'use client';
import { VStack, Text, Button, Box, HStack, Grid } from '@chakra-ui/react';
import {
WalletButton,
useAccountModal,
ProfileCard,
useWallet,
} from '@vechain/vechain-kit';
import { MdBrush } from 'react-icons/md';
import { CollapsibleCard } from '../../ui/CollapsibleCard';
export function UIControls() {
const { open } = useAccountModal();
const { account } = useWallet();
return (
<>
<VStack spacing={6} align="stretch" w={'full'}>
<Text textAlign="center">
VeChain Kit provides multiple ways to customize the UI
components. Here are some examples of different button
styles and variants.
</Text>
<HStack w={'full'} justifyContent={'space-between'}>
{/* Mobile Variants */}
<HStack w={'full'} justifyContent={'center'}>
<VStack
w={'fit-content'}
spacing={6}
p={6}
borderRadius="md"
bg="whiteAlpha.50"
>
<Text fontWeight="bold">
Account Button Variants
</Text>
<Text
fontSize="sm"
textAlign="center"
color="gray.400"
>
Note: Some variants might look different based
on connection state and available data. Eg:
"iconDomainAndAssets" will show the assets only
if the user has assets. And same for domain
name.
</Text>
<Grid
templateColumns={{
base: '1fr',
md: 'repeat(2, 1fr)',
}}
gap={8}
w="full"
justifyContent="space-between"
>
{/* First Column Items */}
<VStack alignItems="flex-start" spacing={8}>
<VStack alignItems="flex-start" spacing={2}>
<Box w={'fit-content'}>
<WalletButton
mobileVariant="icon"
desktopVariant="icon"
/>
</Box>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
variant: "icon"
</Text>
</VStack>
<VStack alignItems="flex-start" spacing={2}>
<Box w={'fit-content'}>
<WalletButton
mobileVariant="iconAndDomain"
desktopVariant="iconAndDomain"
/>
</Box>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
variant: "iconAndDomain"
</Text>
</VStack>
<VStack alignItems="flex-start" spacing={2}>
<Box w={'fit-content'}>
<WalletButton
mobileVariant="iconDomainAndAddress"
desktopVariant="iconDomainAndAddress"
/>
</Box>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
variant: "iconDomainAndAddress"
</Text>
</VStack>
</VStack>
{/* Second Column Items */}
<VStack alignItems={'flex-start'} spacing={8}>
<VStack alignItems="flex-start" spacing={2}>
<Box w={'fit-content'}>
<WalletButton
mobileVariant="iconDomainAndAssets"
desktopVariant="iconDomainAndAssets"
/>
</Box>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
variant: "iconDomainAndAssets"
</Text>
</VStack>
<VStack alignItems="flex-start" spacing={2}>
<Box w={'fit-content'}>
<WalletButton
mobileVariant="iconDomainAndAssets"
desktopVariant="iconDomainAndAssets"
buttonStyle={{
border: '2px solid #000000',
boxShadow:
'-2px 2px 3px 1px #00000038',
background: '#f08098',
color: 'white',
_hover: {
background: '#db607a',
border: '1px solid #000000',
boxShadow:
'-3px 2px 3px 1px #00000038',
},
transition: 'all 0.2s ease',
}}
/>
</Box>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
variant: "iconDomainAndAssets"
(styled)
</Text>
</VStack>
<VStack alignItems="flex-start" spacing={2}>
<Button onClick={open}>
<Text>This is a custom button</Text>
</Button>
<Text
fontSize="sm"
fontWeight="medium"
color="blue.300"
bg="whiteAlpha.100"
px={3}
py={1}
borderRadius="full"
>
no variant, custom button
</Text>
</VStack>
</VStack>
</Grid>
</VStack>
</HStack>
</HStack>
</VStack>
</>
);
}
/**
* Domain Management Hooks
* The hooks provide tools for interacting with VET domains and their functionality
*/
// Domain Resolution Hooks
- `useVechainDomain(addressOrDomain: string)`:
Returns { address?: string; domain?: string; isValidAddressOrDomain: boolean }
// Resolves VET domains to addresses and vice versa
- `useIsDomainProtected(domain: string)`:
Returns boolean
// Checks if a domain is protected from claiming
- `useGetDomainsOfAddress(address: string, parentDomain?: string)`:
Returns { domains: Array<{ name: string }> }
// Gets all domains owned by an address
// Domain Record Management Hooks
- `useGetTextRecords(domain: string)`:
Returns TextRecords object
// Gets all text records for a domain
- `useGetAvatar(domain: string)`:
Returns string | null
// Gets the avatar URL for a domain
- `useGetResolverAddress(domain: string)`:
Returns string
// Gets the resolver contract address for a domain
- `useUpdateTextRecord({
onSuccess?: () => void,
onError?: (error: Error) => void,
signerAccountAddress?: string,
resolverAddress: string
})`:
Returns { sendTransaction, isTransactionPending, error }
// Updates text records for a domain
// Subdomain Management Hooks
- `useClaimVeWorldSubdomain({
subdomain: string,
domain: string,
onSuccess?: () => void,
onError?: (error: Error) => void,
alreadyOwned?: boolean
})`:
Returns { sendTransaction, isTransactionPending, error }
// Claims a VeWorld subdomain
// Example Usage
const ExampleComponent = () => {
const address = "0x...";
const domain = "example.vet";
// Resolve domain or address
const { data: domainInfo } = useVechainDomain(address);
// Check domain protection
const { data: isProtected } = useIsDomainProtected(domain);
// Get owned domains
const { data: ownedDomains } = useGetDomainsOfAddress(address);
// Get domain records
const { data: textRecords } = useGetTextRecords(domain);
const { data: avatar } = useGetAvatar(domain);
const { data: avatarOfAddress } = useGetAvatarOfAddress(address);
// Update domain records
const { sendTransaction: updateRecord } = useUpdateTextRecord({
onSuccess: () => console.log("Record updated"),
resolverAddress: "0x..."
});
// Claim subdomain
const { sendTransaction: claimSubdomain } = useClaimVeWorldSubdomain({
subdomain: "mysub",
domain: "veworld.vet",
onSuccess: () => console.log("Subdomain claimed")
});
console.log(
'Domain Info:', domainInfo,
'Is Protected:', isProtected,
'Owned Domains:', ownedDomains,
'Text Records:', textRecords,
'Avatar:', avatar
);
return (
// Your component JSX
);
};
/*
Requirements:
- All hooks require a valid thor connection
- Appropriate network configuration must be set
- Valid input parameters must be provided
- Most hooks are part of the @tanstack/react-query ecosystem
Additional Notes:
- Domain resolution works for both .vet domains and addresses
- Text records can include various fields like email, description, url, etc.
- Subdomain claiming is specific to veworld.vet domains
- Protection status affects whether a domain can be claimed or transferred
*/// Error: Type 'string' is not assignable to type '`0x${string}`'
const address: `0x${string}` = userAddress;
// ✅ Solution
const contractAddress = getConfig(networkType).contractAddress as `0x${string}`;
const method = 'convertedB3trOf' as const;
// ✅ Validate addresses
import { humanAddress } from '@vechain/vechain-kit/utils';
const validatedAddress = humanAddress(address);import { hashFn } from 'wagmi/query';
// Error: Do not know how to serialize a BigInt
// ✅ Solution: set wagmi's hashfn as default queryKeyHashFn
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: hashFn,
retry: 0,
retryOnMount: false,
staleTime: 30000,
// other options
},
},
})
// ✅ Good: Consistent query key structure
const queryKey = getCallClauseQueryKeyWithArgs({
abi: VOT3__factory.abi,
address: contractAddress,
method: 'balanceOf' as const,
args: [userAddress],
});
// ✅ Proper invalidation
queryClient.invalidateQueries({ queryKey });// Old pattern
const functionAbi = vot3Abi.find((e) => e.name === "convertedB3trOf");
const res = await thor.account(contractAddress).method(functionAbi).call(address);
// ✅ New pattern
import { VOT3__factory } from '@vechain/vechain-kit/contracts';
return useCallClause({
abi: VOT3__factory.abi,
address: contractAddress,
method: 'convertedB3trOf' as const,
args: [address ?? ''],
queryOptions: { enabled: !!address },
});// Old manual transaction building
const buildConvertTx = (thor: Connex.Thor, amount: string) => {
const functionAbi = abi.find((e) => e.name === "convertToVOT3");
const clause = thor.account(contractAddress).method(functionAbi).asClause(amount);
return { ...clause, comment: `Convert ${amount}`, abi: functionAbi };
};
// ✅ New clause building
import { VOT3__factory } from '@vechain/vechain-kit/contracts';
const buildConvertTx = (thor: ThorClient, amount: string): EnhancedClause => {
const { clause } = thor.contracts
.load(contractAddress, VOT3__factory.abi)
.clause.convertToVOT3(ethers.parseEther(amount));
return {
...clause,
comment: `Convert ${humanNumber(amount)} B3TR to VOT3`,
};
};// ✅ Batch multiple calls
const useTokenData = (tokenAddress: string) => {
return useQuery({
queryKey: ['TOKEN_DATA', tokenAddress],
queryFn: async () => {
const results = await executeMultipleClausesCall({
thor,
calls: [
{ abi: ERC20__factory.abi, functionName: 'symbol', address: tokenAddress, args: [] },
{ abi: ERC20__factory.abi, functionName: 'decimals', address: tokenAddress, args: [] },
{ abi: ERC20__factory.abi, functionName: 'totalSupply', address: tokenAddress, args: [] },
],
});
return {
symbol: results[0][0],
decimals: results[1][0],
totalSupply: results[2][0],
};
},
enabled: !!tokenAddress,
staleTime: 60000, // Cache for 1 minute
});
};// ✅ retry configuration
const { data, error } = useCallClause({
queryOptions: {
retry: (failureCount, error) => {
if (error.message.includes('reverted')) return false;
if (error.message.includes('invalid')) return false;
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
});// ✅ Mock VeChain Kit hooks
jest.mock('@vechain/vechain-kit', () => ({
useWallet: () => ({
account: { address: '0x123...' },
isConnected: true,
}),
useCallClause: () => ({
data: [BigInt('1000000000000000000')],
isLoading: false,
error: null,
}),
}));// Example usage of IPFS hooks
import {
useIpfsImage,
useIpfsMetadata,
useSingleImageUpload,
useUploadImages
} from '@vechain/vechain-kit';
const ExampleComponent = () => {
// Fetch an NFT image from IPFS
const {
data: imageData,
isLoading: isImageLoading,
error: imageError
} = useIpfsImage("ipfs://...");
// Fetch metadata from IPFS
const {
data: metadata,
isLoading: isMetadataLoading,
error: metadataError
} = useIpfsMetadata<{
name: string;
description: string
}>("ipfs://...", true); // parse as JSON
// Handle single image upload
const {
uploadedImage,
onUpload: handleSingleUpload,
onRemove: handleSingleRemove,
isUploading: isSingleUploading
} = useSingleImageUpload({
compressImage: true,
defaultImage: undefined,
maxSizeMB: 0.4,
maxWidthOrHeight: 1920
});
// Handle multiple image uploads
const {
uploadedImages,
onUpload: handleMultipleUpload,
onRemove: handleMultipleRemove,
isUploading: isMultipleUploading
} = useUploadImages({
compressImages: true,
defaultImages: [],
maxSizeMB: 0.4,
maxWidthOrHeight: 1920
});
// Example upload handler
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
await handleSingleUpload(file);
}
};
// Example multiple files upload handler
const handleMultipleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
await handleMultipleUpload(files);
};
console.log(
'IPFS Image:', imageData,
'Loading Image:', isImageLoading,
'Metadata:', metadata,
'Loading Metadata:', isMetadataLoading,
'Uploaded Single Image:', uploadedImage,
'Single Upload in Progress:', isSingleUploading,
'Uploaded Multiple Images:', uploadedImages,
'Multiple Upload in Progress:', isMultipleUploading
);
return (
// Your component JSX here
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- Valid IPFS gateway configuration
- Network type configuration
- For image hooks:
- Supported formats: JPEG, PNG, GIF, BMP, TIFF, WebP, SVG
- Maximum file size: 10MB
- For upload hooks:
- Default compression to 0.4MB
- Max width/height: 1920px
- Web Worker support for compression
*/// NFT Types
interface NFTMetadata {
name: string;
description: string;
image: string;
attributes: {
trait_type: string;
value: string | number;
}[];
}
interface NFTImageHookParams {
address: string;
contractAddress: string;
}
interface NFTMetadataUriParams {
tokenId: string;
contractAddress: string;
}
interface NFTImageHookResult {
imageData: string | null;
imageMetadata: NFTMetadata | null;
tokenID: string | null;
isLoading: boolean;
error: Error | null;
}
interface NFTMetadataUriResult {
data: string | null;
isLoading: boolean;
error: Error | null;
}// Example usage of NFT hooks
import { useNFTImage, useNFTMetadataUri } from '@vechain/vechain-kit';
const ExampleComponent = () => {
const walletAddress = "0x...";
const tokenId = "1";
const contractAddress = "0x...";
// Get complete NFT data including image
const {
imageData,
imageMetadata,
tokenID,
isLoading: isLoadingNFT,
error: nftError
} = useNFTImage({
address: walletAddress,
contractAddress: contractAddress
});
// Get just the metadata URI
const {
data: metadataUri,
isLoading: isLoadingUri,
error: uriError
} = useNFTMetadataUri({
tokenId,
contractAddress
});
// Handle loading states
if (isLoadingNFT || isLoadingUri) {
return <div>Loading NFT data...</div>;
}
// Handle errors
if (nftError || uriError) {
return <div>Error: {nftError?.message || uriError?.message}</div>;
}
console.log(
'NFT Image:', imageData,
'NFT Metadata:', imageMetadata,
'Token ID:', tokenID,
'Metadata URI:', metadataUri
);
// Example of using the NFT data
return (
<div>
{imageData && (
<img
src={imageData}
alt={imageMetadata?.name || 'NFT'}
/>
)}
{imageMetadata && (
<div>
<h2>{imageMetadata.name}</h2>
<p>{imageMetadata.description}</p>
{imageMetadata.attributes?.map((attr, index) => (
<div key={index}>
{attr.trait_type}: {attr.value}
</div>
))}
</div>
)}
</div>
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- Valid thor connection
- Network type configuration
- Valid IPFS gateway for image fetching
- The hooks handle the complete flow:
1. Get token ID from address
2. Get metadata URI from token ID
3. Fetch metadata from IPFS
4. Fetch image from IPFS
Types:
interface NFTMetadata {
name: string;
description: string;
image: string;
attributes: {
trait_type: string;
value: string;
}[];
}
*/TransactionModal or TransactionToast components to show your users the progress and outcome of the transaction, or build your own UX and UI.// ✅ Good: Pre-fetch data
const { data } = useQuery(['someData'], fetchSomeData);
const sendTx = () => sendTransaction(data);
// ⛔ Bad: Fetching data during the transaction
const sendTx = async () => {
const data = await fetchSomeData();
return sendTransaction(data);
};'use client';
import {
useWallet,
useSendTransaction,
useTransactionModal,
TransactionModal,
getConfig
} from '@vechain/vechain-kit';
import { IB3TR__factory } from '@vechain/vechain-kit/contracts';
import { humanAddress } from '@vechain/vechain-kit/utils';
import { useMemo, useCallback } from 'react';
export function TransactionExamples() {
const { account } = useWallet();
const b3trMainnetAddress = getConfig("main").b3trContractAddress;
const clauses = useMemo(() => {
const B3TRInterface = IB3TR__factory.createInterface();
const clausesArray: any[] = [];
clausesArray.push({
to: b3trMainnetAddress,
value: '0x0',
data: B3TRInterface.encodeFunctionData('transfer', [
"0x0, // receiver address
'0', // 0 B3TR (in wei)
]),
comment: `This is a dummy transaction to test the transaction modal. Confirm to transfer ${0} B3TR to ${humanAddress("Ox0")}`,
abi: B3TRInterface.getFunction('transfer'),
});
return clausesArray;
}, [connectedWallet?.address]);
const {
sendTransaction,
status,
txReceipt,
resetStatus,
isTransactionPending,
error,
} = useSendTransaction({
signerAccountAddress: account?.address ?? '',
});
const {
open: openTransactionModal,
close: closeTransactionModal,
isOpen: isTransactionModalOpen,
} = useTransactionModal();
// This is the function triggering the transaction and opening the modal
const handleTransaction = useCallback(async () => {
openTransactionModal();
await sendTransaction(clauses);
}, [sendTransaction, clauses, openTransactionModal]);
const handleTryAgain = useCallback(async () => {
resetStatus();
await sendTransaction(clauses);
}, [sendTransaction, clauses, resetStatus]);
return (
<>
<button
onClick={handleTransactionWithModal}
isLoading={isTransactionPending}
isDisabled={isTransactionPending}
>
Send B3TR
</button>
<TransactionModal
isOpen={isTransactionModalOpen}
onClose={closeTransactionModal}
status={status}
txReceipt={txReceipt}
txError={error}
onTryAgain={handleTryAgain}
uiConfig={{
title: 'Test Transaction',
description: `This is a dummy transaction to test the transaction modal. Confirm to transfer ${0} B3TR to ${
account?.address
}`,
showShareOnSocials: true,
showExplorerButton: true,
isClosable: true,
}}
/>
</>
);
}
useSendTransaction({
..., //other config
suggestedMaxGas: 40000000, // Sets the gas limit directly
});useSendTransaction({
..., //other config
gasPadding: 0.1, // Adds 10% buffer to estimated gas
});/// Smart contract to allow delegate all requests
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FeeDelegation {
/**
* @dev Check if a transaction can be sponsored for a user
*/
function canSponsorTransactionFor(
address _origin,
address _to,
bytes calldata _data
) public view returns (bool) {
return true;
}
}import { Address, HDKey, Transaction, Secp256k1, Hex } from '@vechain/sdk-core';
// the default signer is a solo node seeded account
const DEFAULT_SIGNER = 'denial kitchen pet squirrel other broom bar gas better priority spoil cross'
export async function onRequestPost({ request, env }): Promise<Response> {
const body = await request.json()
console.log('Incoming request', body);
const signerWallet = HDKey.fromMnemonic((env.SIGNER_MNEMONIC ?? DEFAULT_SIGNER).split(' '), HDKey.VET_DERIVATION_PATH).deriveChild(0);
if (!signerWallet.publicKey || !signerWallet.privateKey) { throw new Error('Could not load signing wallet') }
const signerAddress = Address.ofPublicKey(signerWallet.publicKey)
const transactionToSign = Transaction.decode(
Buffer.from(body.raw.slice(2), 'hex'),
false
);
const transactionHash = transactionToSign.getSignatureHash(Address.of(body.origin))
const signature = Secp256k1.sign(transactionHash.bytes, signerWallet.privateKey)
return new Response(JSON.stringify({
signature: Hex.of(signature).toString(),
address: signerAddress.toString()
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
'access-control-allow-origin': '*'
}
})
}useSendTransaction() hook from @vechain/vechain-kit to send your transactions to the network.
Read how to use the hook here.// dapp-kit
const { account } = useWallet()
console.log(account) // 0x000000dsadsa
// vechain-kit
const { account } = useWallet()
console.log(account.address, account.domain, account.image)// Modal Types
interface ModalHookReturn {
open: () => void;
close: () => void;
isOpen: boolean;
}
type LoginMethod = 'ecosystem' | 'vechain' | 'dappkit' | 'passkey' | 'email' | 'google' | 'more';
interface LoginModalContentConfig {
showGoogleLogin: boolean;
showEmailLogin: boolean;
showPasskey: boolean;
showVeChainLogin: boolean;
showDappKit: boolean;
showEcosystem: boolean;
showMoreLogin: boolean;
isOfficialVeChainApp: boolean;
}
interface AccountModalContent {
type: string;
props?: Record<string, any>;
}// Example usage of Modal hooks
import {
useWalletModal,
useAccountModal,
useSendTokenModal,
useTransactionModal,
useLoginModalContent
} from '@modals';
const ExampleComponent = () => {
// Wallet modal management
const {
open: openWallet,
close: closeWallet,
isOpen: isWalletOpen
} = useWalletModal();
// Account modal management
const {
open: openAccount,
close: closeAccount,
isOpen: isAccountOpen
} = useAccountModal();
// Send token modal
const {
open: openSendToken,
close: closeSendToken,
isOpen: isSendTokenOpen
} = useSendTokenModal();
// Transaction modal
const {
open: openTransaction,
close: closeTransaction,
isOpen: isTransactionOpen
} = useTransactionModal();
// Login modal content configuration
const loginConfig = useLoginModalContent();
// Example handlers
const handleWalletClick = () => {
openWallet();
};
const handleSendToken = () => {
openSendToken();
};
const handleAccountSettings = () => {
openAccount();
};
return (
<div>
<button
onClick={handleWalletClick}
disabled={isWalletOpen}
>
Open Wallet
</button>
<button
onClick={handleSendToken}
disabled={isSendTokenOpen}
>
Send Tokens
</button>
<button
onClick={handleAccountSettings}
disabled={isAccountOpen}
>
Account Settings
</button>
{/* Login configuration display */}
{loginConfig.showGoogleLogin && (
<button>Login with Google</button>
)}
{loginConfig.showVeChainLogin && (
<button>Login with VeChain</button>
)}
{loginConfig.showPasskey && (
<button>Login with Passkey</button>
)}
{/* Transaction modal state */}
{isTransactionOpen && (
<div>Transaction in Progress...</div>
)}
</div>
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- ModalProvider context
- Valid wallet connection for some features
- Proper configuration for login methods
- Each modal manages its own state and content
*/

buttons: {
secondaryButton: {
bg: 'rgba(255, 255, 255, 0.1)', // Background color
color: '#ffffff', // Text color
border: '1px solid rgba(255, 255, 255, 0.2)', // Border (full CSS string)
},
}buttons: {
primaryButton: {
bg: '#3182CE', // Background color
color: '#ffffff', // Text color
border: 'none', // Border (full CSS string)
},
}buttons: {
tertiaryButton: {
bg: 'transparent', // Background color
color: '#ffffff', // Text color
border: 'none', // Border (full CSS string)
},
}effects: {
glass: {
enabled: true, // Enable glass effects
intensity: 'low' | 'medium' | 'high', // Glass intensity
},
backdropFilter: {
modal: 'blur(15px)', // Optional: override modal blur
// overlay blur is set via overlay.blur
},
}

buttons: {
loginButton: {
bg: 'transparent', // Background color
color: '#ffffff', // Text color
border: '1px solid rgba(255, 255, 255, 0.1)', // Border (full CSS string)
},
}buttons: {
secondaryButton: {
bg: 'rgba(255, 255, 255, 0.1)',
color: '#ffffff',
border: 'none',
},
primaryButton: {
bg: '#3182CE',
color: '#ffffff',
border: 'none',
},
loginButton: {
bg: 'transparent',
color: '#ffffff',
border: '1px solid rgba(255, 255, 255, 0.1)',
},
}
import { useDAppKitWalletModal } from '@vechain/vechain-kit';
export const LoginComponent = () => {
const { open: openWalletModal } = useDAppKitWalletModal();
return (
<Button onClick={openWalletModal}>
Open only "Connect Wallet"
</Button>
)}// Custom language detector that checks localStorage first, then prop, then browser
const customLanguageDetector = {
name: 'customDetector',
lookup: (options?: { languages?: string[] } | undefined) => {
// Check localStorage first (for persistence across page refreshes)
if (typeof window !== 'undefined') {
const storedLanguage = localStorage.getItem('i18nextLng');
if (storedLanguage && supportedLanguages.includes(storedLanguage)) {
return storedLanguage;
}
}
const propLanguage = options?.languages?.[0];
if (propLanguage && supportedLanguages.includes(propLanguage)) {
return propLanguage;
}
// Get browser language
const browserLang = navigator.language.split('-')[0];
if (browserLang && supportedLanguages.includes(browserLang)) {
return browserLang;
}
return 'en'; // fallback
},
cacheUserLanguage: (lng: string) => {
localStorage.setItem('i18nextLng', lng);
},
};import { VeChainKitProvider } from '@vechain/vechain-kit';
function App() {
const handleLanguageChange = (language: string) => {
console.log('Language changed to:', language);
// Update your app's language state
};
const handleCurrencyChange = (currency: 'usd' | 'eur' | 'gbp') => {
console.log('Currency changed to:', currency);
// Update your app's currency state
};
return (
<VeChainKitProvider
language="en"
defaultCurrency="usd"
onLanguageChange={handleLanguageChange}
onCurrencyChange={handleCurrencyChange}
// ... other props
>
{/* Your app */}
</VeChainKitProvider>
);
}import { useCurrentLanguage, useCurrentCurrency } from '@vechain/vechain-kit';
function MyComponent() {
const { currentLanguage, setLanguage } = useCurrentLanguage();
const { currentCurrency, setCurrency } = useCurrentCurrency();
return (
<div>
<p>Current language: {currentLanguage}</p>
<p>Current currency: {currentCurrency}</p>
<button onClick={() => setLanguage('fr')}>Change to French</button>
<button onClick={() => setCurrency('eur')}>Change to EUR</button>
</div>
);
}import { useVeChainKitConfig } from '@vechain/vechain-kit';
function MyComponent() {
const config = useVeChainKitConfig();
// Current runtime values
const currentLanguage = config.currentLanguage; // 'fr' (current value)
const currentCurrency = config.currentCurrency; // 'eur' (current value)
// Functions to change values from host app
config.setLanguage('de');
config.setCurrency('gbp');
}import { useCurrentLanguage, useCurrentCurrency } from '@vechain/vechain-kit';
import { useTranslation } from 'react-i18next';
function LanguageSelector() {
const { currentLanguage, setLanguage } = useCurrentLanguage();
const { i18n } = useTranslation();
const handleLanguageChange = (newLang: string) => {
// Update VeChainKit
setLanguage(newLang);
// Your i18n instance will also be updated automatically
};
return (
<select
value={currentLanguage}
onChange={(e) => handleLanguageChange(e.target.value)}
>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
</select>
);
}import { useState, useEffect } from 'react';
import { VeChainKitProvider } from '@vechain/vechain-kit';
function App() {
const [appLanguage, setAppLanguage] = useState('en');
const [appCurrency, setAppCurrency] = useState('usd');
// Sync VeChainKit language changes to your app
const handleLanguageChange = (language: string) => {
setAppLanguage(language);
// Update your app's i18n, routing, etc.
console.log('Language changed in VeChainKit:', language);
};
// Sync VeChainKit currency changes to your app
const handleCurrencyChange = (currency: 'usd' | 'eur' | 'gbp') => {
setAppCurrency(currency);
// Update your app's currency display, API calls, etc.
console.log('Currency changed in VeChainKit:', currency);
};
return (
<VeChainKitProvider
language={appLanguage}
defaultCurrency={appCurrency}
onLanguageChange={handleLanguageChange}
onCurrencyChange={handleCurrencyChange}
// ... other props
>
{/* Your app */}
</VeChainKitProvider>
);
}// ✅ Good: Pre-fetch data
const { data } = useQuery(['someData'], fetchSomeData);
const sendTx = () => sendTransaction(data);
// ❌ Bad: Fetching data during the transaction
const sendTx = async () => {
const data = await fetchSomeData(); // This delay causes popup blocking
return sendTransaction(data);
};// Load data when component mounts or when form changes
const { data: gasPrice } = useQuery(['gasPrice'], fetchGasPrice, {
staleTime: 30000 // Cache for 30 seconds
});
// Transaction handler is instant
const handleTransaction = () => {
sendTransaction({
gasPrice,
// ... other pre-loaded data
});
};const MyComponent = () => {
const { data, isLoading } = useQuery(['requiredData'], fetchData);
return (
<button
onClick={() => sendTransaction(data)}
disabled={isLoading}
>
{isLoading ? 'Preparing...' : 'Send Transaction'}
</button>
);
};
// ❌ Avoid
onClick={async () => {
const result = await someAsyncOperation();
sendTransaction(result);
}}
// ✅ Better
onClick={() => {
sendTransaction(preLoadedData);
}}Core API changes when migrating from VeChain Kit 1.x to 2.0 with practical examples
const executeBatchWithAuthorization = (txClauses: Connex.VM.Clause[]) => {
const toArray: string[] = [];
const valueArray: string[] = [];
const dataArray: string[] = [];
txClauses.forEach((clause) => {
toArray.push(clause.to ?? '');
valueArray.push(String(clause.value));
if (typeof clause.data === 'object' && 'abi' in clause.data) {
dataArray.push(encodeFunctionData(clause.data));
} else {
dataArray.push(clause.data || '0x');
}
});
const dataToSign = {
domain: {
name: 'Wallet',
version: '1',
chainId,
verifyingContract, // the smart account of the user
},
types: {
ExecuteBatchWithAuthorization: [
{ name: 'to', type: 'address[]' },
{ name: 'value', type: 'uint256[]' },
{ name: 'data', type: 'bytes[]' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
},
primaryType: 'ExecuteBatchWithAuthorization',
message: {
to: toArray,
value: valueArray,
data: dataArray,
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 60, // e.g. 1 minute from now
nonce: ethers.hexlify(ethers.randomBytes(32)), // nonce needs to be random
},
};
// Sign the typed data with Privy
const signature = (
await signTypedDataPrivy(typedData, {
uiOptions: {
title,
description,
buttonText,
},
})
).signature;
// Rebuild the clauses to call the executeBatchWithAuthorization
const clauses = [];
clauses.push(
Clause.callFunction(
Address.of(smartAccount.address),
ABIContract.ofAbi(SimpleAccountABI).getFunction(
'executeBatchWithAuthorization',
),
[
typedData.message.to,
typedData.message.value?.map((val) => BigInt(val)) ?? 0,
typedData.message.data,
BigInt(typedData.message.validAfter),
BigInt(typedData.message.validBefore),
typedData.message.nonce, // If your contract expects bytes32
signature as `0x${string}`,
],
),
);
//Now you can broadcast the transaction to the blockchain with one of your delegator wallets
// ...
};yarn add @chakra-ui/[email protected]import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { VeChainKitProvider } from "@vechain/vechain-kit";
import {
ChakraProvider,
ColorModeScript,
useColorMode,
} from "@chakra-ui/react";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { persister, queryClient } from "./utils/queryClient.ts";
export const AppWrapper = () => {
const { colorMode } = useColorMode();
return (
<VeChainKitProvider
// ... your config
>
<App />
</VeChainKitProvider>
);
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<ChakraProvider theme={stargateStakingTheme}>
<ColorModeScript initialColorMode="dark" />
<AppWrapper />
</ChakraProvider>
</PersistQueryClientProvider>
</React.StrictMode>
);<ChakraProvider theme={stargateStakingTheme}>
<ColorModeScript initialColorMode="dark" />
<AppWrapper />
</ChakraProvider>"use-client"
import { useConvertB3tr } from "@/hooks"
import { useWallet, useUpgradeRequired, useUpgradeSmartAccountModal } from "@vechain/vechain-kit"
// Example component allowing for B3TR to VOT3 conversion
export const ConvertModal = ({ isOpen, onClose }: Props) => {
const { account, connectedWallet, connection } = useWallet()
const isSmartAccountUpgradeRequired = useUpgradeRequired(
account?.address ?? "",
connectedWallet?.address ?? "",
3
)
const { open: openUpgradeModal } = useUpgradeSmartAccountModal()
// A custom convert b3tr to vot3 hook
const convertB3trMutation = useConvertB3tr({
amount,
})
const handleConvertB3tr = useCallback(() => {
if (connection.isConnectedWithPrivy && isSmartAccountUpgradeRequired) {
//Open Upgrade Modal
openUpgradeModal()
return
}
convertB3trMutation.resetStatus()
convertB3trMutation.sendTransaction(undefined)
}, [isSmartAccountUpgradeRequired, convertB3trMutation, openUpgradeModal, connection])
return (
<button onClick={handleConvertB3tr}> Convert my B3TR to VOT3 </button>
)
}const { open: openUpgradeSmartAccountModal } = useUpgradeSmartAccountModal({
accentColor: '#000000',
modalSize: 'xl',
});npm install @vechain/vechain-kit@^2.0.0
npm uninstall @thor-devkitrm -rf node_modules package-lock.json
npm install

// Before (1.x)
import { useConnex, useWallet, useTransaction } from '@vechain/vechain-kit';
// After (2.x)
import { useThor, useWallet, useBuildTransaction, useCallClause } from '@vechain/vechain-kit';import { useConnex } from '@vechain/vechain-kit';
const Component = () => {
const connex = useConnex();
// Access thor
const thor = connex.thor;
// Access vendor
const vendor = connex.vendor;
};import { useThor } from '@vechain/vechain-kit';
const Component = () => {
const thor = useThor();
// Thor is now directly available
// Vendor functionality is integrated into transaction methods
};const getBalance = async () => {
const functionAbi = contractAbi.find((e) => e.name === "balanceOf");
const res = await thor.account(contractAddress)
.method(functionAbi)
.call(address);
return ethers.formatEther(res.decoded[0]);
};import { useCallClause } from '@vechain/vechain-kit';
const useBalance = (address: string) => {
const { data, isLoading, error } = useCallClause({
abi: TokenContract__factory.abi,
address: contractAddress as `0x${string}`,
method: 'balanceOf' as const,
args: [address],
queryOptions: {
enabled: !!address,
select: (data) => ethers.formatEther(data[0]),
},
});
return { balance: data, isLoading, error };
};const fetchMultipleBalances = async (addresses: string[]) => {
const results = [];
for (const addr of addresses) {
const res = await thor.account(contractAddress)
.method(balanceOfAbi)
.call(addr);
results.push(res.decoded[0]);
}
return results;
};import { executeMultipleClausesCall } from '@vechain/vechain-kit';
const fetchMultipleBalances = async (addresses: string[]) => {
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map(addr => ({
abi: TokenContract__factory.abi,
functionName: 'balanceOf',
address: contractAddress,
args: [addr]
}))
});
return results.map(r => r.result[0]);
};const approve = async (spender: string, amount: string) => {
const functionAbi = contractAbi.find((e) => e.name === "approve");
const clause = thor.account(contractAddress)
.method(functionAbi)
.asClause(spender, amount);
const tx = connex.vendor.sign('tx', [clause]);
const result = await tx.request();
return result.txid;
};import { useBuildTransaction } from '@vechain/vechain-kit';
const useApprove = () => {
const {
sendTransaction,
status,
txReceipt,
isTransactionPending,
error,
resetStatus
} = useBuildTransaction({
clauseBuilder: (params: { spender: string; amount: string }) => {
const { clause } = thor.contracts
.load(contractAddress, TokenContract__factory.abi)
.clause.approve(params.spender, params.amount);
return [{
...clause,
comment: 'Approve tokens'
}];
}
});
return {
approve: sendTransaction,
status,
txReceipt,
isTransactionPending,
error,
resetStatus
};
};const complexTransaction = async () => {
const clauses = [
thor.account(token1).method(approveAbi).asClause(spender, amount1),
thor.account(token2).method(approveAbi).asClause(spender, amount2),
thor.account(dex).method(swapAbi).asClause(token1, token2, amount1)
];
const tx = connex.vendor.sign('tx', clauses);
const result = await tx.request();
return result;
};const useComplexTransaction = () => {
const { sendTransaction, status, txReceipt } = useBuildTransaction({
clauseBuilder: (params) => {
const clauses = [];
// Approve token1
const token1Contract = thor.contracts.load(token1, ERC20__factory.abi);
clauses.push({
...token1Contract.clause.approve(params.spender, params.amount1).clause,
comment: 'Approve token 1'
});
// Approve token2
const token2Contract = thor.contracts.load(token2, ERC20__factory.abi);
clauses.push({
...token2Contract.clause.approve(params.spender, params.amount2).clause,
comment: 'Approve token 2'
});
// Perform swap
const dexContract = thor.contracts.load(dex, DexABI);
clauses.push({
...dexContract.clause.swap(token1, token2, params.amount1).clause,
comment: 'Execute swap'
});
return clauses;
}
});
return { sendTransaction, status, txReceipt };
};const txWithOptions = async () => {
const clause = thor.account(contractAddress)
.method(methodAbi)
.asClause(...args);
const tx = connex.vendor.sign('tx', [clause])
.signer(signerAddress)
.gas(100000)
.link('https://example.com/callback')
.comment('Test transaction');
return await tx.request();
};const useTransactionWithOptions = () => {
const { sendTransaction } = useBuildTransaction({
clauseBuilder: (params) => {
const { clause } = thor.contracts
.load(contractAddress, ContractABI)
.clause.methodName(...params.args);
return [{
...clause,
comment: 'My transaction'
}];
},
suggestedMaxGas: 100000,
gasPadding: 0.25 // 25% gas padding
});
return { sendTransaction };
};const { events } = useEvents({
contractAddress,
eventName: 'Transfer',
filters: { from: address }
});// New events API - check documentation for updated usage
import { useEvents } from '@vechain/vechain-kit';
// The API has changed - refer to blockchain hooks documentationimport {
getCallClauseQueryKeyWithArgs,
getCallClauseQueryKey
} from '@vechain/vechain-kit';
// With arguments
const queryKeyWithArgs = getCallClauseQueryKeyWithArgs({
abi: ContractABI,
address: contractAddress,
method: 'balanceOf',
args: [userAddress]
});
// Without arguments (for methods with no parameters)
const queryKey = getCallClauseQueryKey({
abi: ContractABI,
address: contractAddress,
method: 'totalSupply'
});
// Use with React Query for cache invalidation
queryClient.invalidateQueries({ queryKey: queryKeyWithArgs });// Refresh specific contract data
const refreshBalance = () => {
const key = getCallClauseQueryKeyWithArgs({
abi: TokenABI,
address: tokenAddress,
method: 'balanceOf',
args: [account]
});
queryClient.invalidateQueries({ queryKey: key });
};/* In your global CSS file (e.g., globals.css) */
/* 1. Define layers with explicit priority */
@layer vechain-kit, host-app;
/* 2. Wrap your framework in host-app layer */
@layer host-app {
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Your custom styles here */
}@layer vechain-kit, host-app;
@layer host-app {
@tailwind base;
@tailwind components;
@tailwind utilities;
}@layer vechain-kit, host-app;
/* Import Tailwind v4 */
@import "tailwindcss";
/* Your custom styles */
@layer host-app {
/* Custom overrides */
}@layer vechain-kit, host-app;
@layer host-app {
@import 'bootstrap/dist/css/bootstrap.css';
}@layer vechain-kit, host-app;
@layer host-app {
body {
/* Your body styles */
}
.your-components {
/* Component styles */
}
}@layer host-app {
/* Fix image borders */
img {
border-style: solid !important;
}
/* Ensure body styles persist */
html body {
background: var(--your-bg);
font-family: var(--your-font);
}
}// Transaction Types
type TransactionStatus = 'ready' | 'pending' | 'waitingConfirmation' | 'success' | 'error';
type TransactionStatusErrorType = {
type: 'UserRejectedError' | 'RevertReasonError';
reason: string;
};
type EnhancedClause = {
to: string;
value?: string;
data?: string;
comment?: string;
abi?: any;
};
interface TransactionHookReturnValue {
sendTransaction: (clauses?: TransactionClause[]) => Promise<void>;
isTransactionPending: boolean;
isWaitingForWalletConfirmation: boolean;
txReceipt: TransactionReceipt | null;
status: TransactionStatus;
resetStatus: () => void;
error?: TransactionStatusErrorType;
}
interface TransferVETParams {
fromAddress: string;
receiverAddress: string;
amount: string;
onSuccess?: () => void;
onError?: (error: Error) => void;
}
interface TransferERC20Params {
fromAddress: string;
receiverAddress: string;
amount: string;
tokenAddress: string;
tokenName: string;
onSuccess?: () => void;
onError?: (error: Error) => void;
}
interface SendTransactionParams {
signerAccountAddress: string;
clauses: EnhancedClause[];
onTxConfirmed?: () => void;
onTxFailedOrCancelled?: () => void;
}// Example usage of Transaction hooks
import {
useSendTransaction,
useTransferERC20,
useTransferVET
} from '@vechain/vechain-kit';
const ExampleComponent = () => {
// Example of sending VET
const {
sendTransaction: sendVET,
isTransactionPending: isVETPending,
status: vetStatus,
error: vetError
} = useTransferVET({
fromAddress: "0x...",
receiverAddress: "0x...",
amount: "1.5",
onSuccess: () => console.log("VET transfer successful"),
onError: () => console.log("VET transfer failed")
});
// Example of sending ERC20 tokens
const {
sendTransaction: sendToken,
isTransactionPending: isTokenPending,
status: tokenStatus,
error: tokenError
} = useTransferERC20({
fromAddress: "0x...",
receiverAddress: "0x...",
amount: "100",
tokenAddress: "0x...",
tokenName: "TOKEN",
onSuccess: () => console.log("Token transfer successful"),
onError: () => console.log("Token transfer failed")
});
// Example of custom transaction
const {
sendTransaction: sendCustomTx,
isTransactionPending: isCustomPending,
status: customStatus,
error: customError
} = useSendTransaction({
signerAccountAddress: "0x...",
clauses: [
{
to: "0x...",
value: "0x0",
data: "0x...",
comment: "Custom transaction"
}
],
onTxConfirmed: () => console.log("Custom transaction successful"),
onTxFailedOrCancelled: () => console.log("Custom transaction failed")
});
const handleVETTransfer = async () => {
try {
await sendVET();
} catch (error) {
console.error("VET transfer error:", error);
}
};
const handleTokenTransfer = async () => {
try {
await sendToken();
} catch (error) {
console.error("Token transfer error:", error);
}
};
const handleCustomTransaction = async () => {
try {
await sendCustomTx();
} catch (error) {
console.error("Custom transaction error:", error);
}
};
return (
<div>
<button
onClick={handleVETTransfer}
disabled={isVETPending}
>
Send VET
</button>
<button
onClick={handleTokenTransfer}
disabled={isTokenPending}
>
Send Tokens
</button>
<button
onClick={handleCustomTransaction}
disabled={isCustomPending}
>
Send Custom Transaction
</button>
{/* Status displays */}
{vetStatus === 'pending' && <div>VET Transfer Pending...</div>}
{tokenStatus === 'pending' && <div>Token Transfer Pending...</div>}
{customStatus === 'pending' && <div>Custom Transaction Pending...</div>}
{/* Error displays */}
{vetError && <div>VET Error: {vetError.reason}</div>}
{tokenError && <div>Token Error: {tokenError.reason}</div>}
{customError && <div>Custom Error: {customError.reason}</div>}
</div>
);
};
export default ExampleComponent;
/*
Note: These hooks require:
- Valid thor connection
- Network configuration
- Valid wallet connection
- For ERC20 transfers: valid token contract address
- All amounts should be in their base units (e.g., wei for VET)
*/'use client';
import { VeChainKitProvider } from "@vechain/vechain-kit";
export function Providers({ children }) {
return (
<VeChainKitProvider>
{children}
</VeChainKitProvider>
);
}import { useWallet } from "@vechain/vechain-kit";
function MyComponent = () => {
const {
account,
connectedWallet,
smartAccount,
privyUser,
connection,
disconnect
} = useWallet();
return <></>
}
import dynamic from 'next/dynamic';
const VeChainKitProvider = dynamic(
async () => (await import('@vechain/vechain-kit')).VeChainKitProvider,
{ ssr: false }
);
export function Providers({ children }) {
return (
<VeChainKitProvider>
{children}
</VeChainKitProvider>
);
}'use client';
import { VeChainKitProvider } from "@vechain/vechain-kit";
export function VeChainKitProviderWrapper({ children }: { children: React.ReactNode }) {
return (
<VeChainKitProvider
// Network Configuration
network={{
type: "test", // "main" | "test" | "solo"
}}
// UI Configuration
darkMode={false}
language="en"
theme={{
backgroundColor: 'black'
}}
// Login Modal UI Customization
loginModalUI={{
logo: '/your-logo.png',
description: 'Welcome to our DApp',
}}
// Login Methods Configuration
loginMethods={[
{ method: "vechain", gridColumn: 4 },
{ method: "dappkit", gridColumn: 4 },
]}
// Sponsor transactions
feeDelegation={{
delegatorUrl: process.env.NEXT_PUBLIC_DELEGATOR_URL!,
}}
// Wallet Connection Configuration
dappKit={{
allowedWallets: ["veworld", "wallet-connect", "sync2"],
walletConnectOptions: {
projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID!,
metadata: {
name: "Your DApp Name",
description: "Your DApp description visible in wallets",
url: typeof window !== "undefined" ? window.location.origin : "",
icons: ["https://your-domain.com/logo.png"],
},
},
}}
>
{children}
</VeChainKitProvider>
);
}network: {
type: "main" | "test" | "solo" // Select mainnet or testnet or solo
}feeDelegation: {
delegatorUrl: string, // Fee delegation service URL
}loginMethods: [
// Always available methods
{ method: "vechain", gridColumn: 4 }, // VeChain social login
{ method: "dappkit", gridColumn: 4 }, // VeChain wallets
{ method: "ecosystem", gridColumn: 4 }, // Ecosystem apps (Mugshot, Cleanify, Greencart, etc.)
// Privy-dependent methods (require your own privy configuration)
{ method: "email", gridColumn: 2 }, // Email login
{ method: "passkey", gridColumn: 2 }, // Passkey authentication
{ method: "google", gridColumn: 4 }, // Google OAuth
{ method: "more", gridColumn: 2 }, // Additional Privy methods
]<VeChainKitProvider
privy={{
appId: "your-privy-app-id",
clientId: "your-privy-client-id",
// Additional Privy configuration
}}
loginMethods={[
// Now you can use Privy-dependent methods
{ method: "email", gridColumn: 2 },
{ method: "google", gridColumn: 4 },
{ method: "passkey", gridColumn: 2 },
]}
>
{children}
</VeChainKitProvider><VeChainKitProvider
loginMethods={[
{ method: "ecosystem", gridColumn: 4 }
]}
ecosystemApps={{
allowedApps: ["app-id-1", "app-id-2"], // Find app IDs in Privy dashboard
}}
>
{children}
</VeChainKitProvider>export type Wallet = {
address: string;
domain?: string;
image: string;
} | null;export type SmartAccount = Wallet & {
isDeployed: boolean;
isActive: boolean;
version: string | null;
};export type ConnectionSource = {
type: 'privy' | 'wallet' | 'privy-cross-app';
displayName: string;
};interface User {
/** The Privy-issued DID for the user. If you need to store additional information
* about a user, you can use this DID to reference them. */
id: string;
/** The datetime of when the user was created. */
createdAt: Date;
/** The user's email address, if they have linked one. It cannot be linked to another user. */
email?: Email;
/** The user's phone number, if they have linked one. It cannot be linked to another user. */
phone?: Phone;
/** The user's most recently linked wallet, if they have linked at least one wallet.
* It cannot be linked to another user.
* This wallet is the wallet that will be used for transactions and signing if it is connected.
**/
wallet?: Wallet;
/**
* The user's smart wallet, if they have set up through the Privy Smart Wallet SDK.
*/
smartWallet?: SmartWallet;
/** The user's Google account, if they have linked one. It cannot be linked to another user. */
google?: Google;
/** The user's Twitter account, if they have linked one. It cannot be linked to another user. */
twitter?: Twitter;
/** The user's Discord account, if they have linked one. It cannot be linked to another user. */
discord?: Discord;
/** The user's Github account, if they have linked one. It cannot be linked to another user. */
github?: Github;
/** The user's Spotify account, if they have linked one. It cannot be linked to another user. */
spotify?: Spotify;
/** The user's Instagram account, if they have linked one. It cannot be linked to another user. */
instagram?: Instagram;
/** The user's Tiktok account, if they have linked one. It cannot be linked to another user. */
tiktok?: Tiktok;
/** The user's LinkedIn account, if they have linked one. It cannot be linked to another user. */
linkedin?: LinkedIn;
/** The user's Apple account, if they have linked one. It cannot be linked to another user. */
apple?: Apple;
/** The user's Farcaster account, if they have linked one. It cannot be linked to another user. */
farcaster?: Farcaster;
/** The user's Telegram account, if they have linked one. It cannot be linked to another user. */
telegram?: Telegram;
/** The list of accounts associated with this user. Each account contains additional metadata
* that may be helpful for advanced use cases. */
linkedAccounts: Array<LinkedAccountWithMetadata>;
/** The list of MFA Methods associated with this user. */
mfaMethods: Array<MfaMethod>;
/**
* Whether or not the user has explicitly accepted the Terms and Conditions
* and/or Privacy Policy
*/
hasAcceptedTerms: boolean;
/** Whether or not the user is a guest */
isGuest: boolean;
/** Custom metadata field for a given user account */
customMetadata?: CustomMetadataType;
}// ✅ Good: Proper typing
const args: [string, bigint, boolean] = [address, amount, isEnabled];
const contractAddress = config.contractAddress as `0x${string}`;
const method = 'balanceOf' as const;
// ✅ Use contract factories
import { VOT3__factory } from '@vechain/vechain-kit/contracts';
const abi = VOT3__factory.abi;
// Avoid: Manual ABI definitions
const functionAbi = contractAbi.find((e) => e.name === "delegates");// ✅ Good: Conditional enablement
return useCallClause({
abi,
address: contractAddress,
method: 'getData',
args: [userAddress],
queryOptions: {
enabled: !!contractAddress && !!userAddress && isConnected,
},
});
// ✅ Good: Configure caching
queryOptions: {
staleTime: 30000, // 30 seconds for price data
refetchInterval: 60000, // Refetch every minute
}// ✅ Good: Transform in select
return useCallClause({
abi: VOT3__factory.abi,
address: contractAddress,
method: 'convertedB3trOf' as const,
args: [address ?? ''],
queryOptions: {
enabled: !!address,
select: (data) => ({
balance: ethers.formatEther(data[0]),
formatted: humanNumber(ethers.formatEther(data[0])),
}),
},
});
// Avoid: Transform in component
const transformedData = useMemo(() => ({
balance: data?.[0]?.toString(),
}), [data]); // Causes re-renders// ✅ Good: Comprehensive error handling
if (error) {
if (error.message.includes('reverted')) {
return <div>Contract call failed. Check parameters.</div>;
}
if (error.message.includes('network')) {
return <div>Network error. <button onClick={refetch}>Retry</button></div>;
}
return <div>Error: {error.message}</div>;
}
// ✅ Good: Retry logic
queryOptions: {
retry: (failureCount, error) => {
if (error.message.includes('reverted')) return false;
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
}// ✅ Good: Use getCallClauseQueryKey (without args)
export const getCurrentAllocationsRoundIdQueryKey = (
address: string,
networkType: NETWORK_TYPE
) =>
getCallClauseQueryKey({
abi: XAllocationVoting__factory.abi,
address: getConfig(networkType).contractAddress as `0x${string}`,
method: 'currentRoundId' as const,
});
// ✅ Good: Use getCallClauseQueryKeyWithArgs
export const getTokenBalanceQueryKey = (
address: string,
networkType: NETWORK_TYPE
) =>
getCallClauseQueryKeyWithArgs({
abi: VOT3__factory.abi,
address: getConfig(networkType).contractAddress as `0x${string}`,
method: 'balanceOf' as const,
args: [address],
});
// ✅ Good: Query invalidation after transactions
const mutation = useBuildTransaction({
clauseBuilder: buildClauses,
onTxConfirmed: () => {
queryClient.invalidateQueries({ queryKey: getTokenBalanceQueryKey(userAddress, networkType) });
},
});// ✅ Good: Memoize expensive calculations
const queryKey = useMemo(() =>
getTokenBalanceQueryKey(userAddress, tokenAddress),
[userAddress, tokenAddress]
);
// ✅ Good: Batch multiple calls
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map((address) => ({
abi: ERC20__factory.abi,
functionName: 'balanceOf',
address: address as `0x${string}`,
args: [userAddress],
})),
});// ✅ Good: Input validation
if (!isAddress(recipient)) {
throw new Error('Invalid recipient address');
}
const amountBN = BigInt(amount);
if (amountBN <= 0n) {
throw new Error('Amount must be positive');
}
// ✅ Good: Safe BigInt handling
const formatTokenAmount = (amount: bigint, decimals: number): string => {
try {
return ethers.formatUnits(amount, decimals);
} catch (error) {
return '0';
}
};import { VOT3__factory } from '@vechain/vechain-kit/contracts';
import { useCallClause, getCallClauseQueryKeyWithArgs } from '@vechain/vechain-kit';
const abi = VOT3__factory.abi;
const method = 'convertedB3trOf' as const;
export const useTokenBalance = (address?: string) => {
const { network } = useVeChainKitConfig();
const contractAddress = getConfig(network.type).contractAddress as `0x${string}`;
return useCallClause({
abi,
address: contractAddress,
method,
args: [address ?? ''],
queryOptions: {
enabled: !!address,
select: (data) => ({
balance: ethers.formatEther(data[0]),
formatted: humanNumber(ethers.formatEther(data[0])),
}),
},
});
};import { useQuery } from '@tanstack/react-query';
import { executeMultipleClausesCall } from '@vechain/vechain-kit';
export const useMultipleTokenData = (addresses: string[]) => {
const thor = useThor();
return useQuery({
queryKey: ['MULTIPLE_TOKENS', addresses],
queryFn: async () => {
const results = await executeMultipleClausesCall({
thor,
calls: addresses.map((address) => ({
abi: ERC20__factory.abi,
functionName: 'balanceOf',
address: address as `0x${string}`,
args: [userAddress],
})),
});
return addresses.map((address, index) => ({
address,
balance: ethers.formatEther(results[index][0]),
}));
},
enabled: !!addresses.length,
});
};import { useBuildTransaction, useWallet } from '@vechain/vechain-kit';
export const useTokenTransfer = () => {
const { account } = useWallet();
const thor = useThor();
return useBuildTransaction({
clauseBuilder: (recipient: string, amount: string) => {
if (!account?.address) return [];
const { clause } = thor.contracts
.load(tokenAddress, ERC20__factory.abi)
.clause.transfer(recipient, ethers.parseEther(amount));
return [{
...clause,
comment: `Transfer ${amount} tokens to ${recipient}`,
}];
},
onTxConfirmed: () => {
queryClient.invalidateQueries({ queryKey: ['TOKEN_BALANCE'] });
},
});
};const useApproveAndSwap = () => {
const { account } = useWallet();
const thor = useThor();
return useBuildTransaction({
clauseBuilder: (tokenAddress: string, amount: string) => {
if (!account?.address) return [];
return [
// Approve
{
...thor.contracts
.load(tokenAddress, ERC20__factory.abi)
.clause.approve(swapAddress, ethers.parseEther(amount)).clause,
comment: 'Approve token spending',
},
// Swap
{
...thor.contracts
.load(swapAddress, SwapContract__factory.abi)
.clause.swap(tokenAddress, ethers.parseEther(amount)).clause,
comment: 'Execute swap',
},
];
},
});
};const useContractCall = (address: string) => {
return useCallClause({
abi: ContractABI,
address: address as `0x${string}`,
method: 'getData',
args: [],
queryOptions: {
enabled: !!address,
retry: (failureCount, error) => {
if (error.message.includes('reverted')) return false;
return failureCount < 3;
},
},
});
};useB3trToUpgrade
// Custom Galaxy Member balance hook
const useGMBalance = (address: string) => {
return useCallClause({
abi: GalaxyMemberABI,
address: GM_CONTRACT_ADDRESS,
method: 'balanceOf',
args: [address],
queryOptions: {
enabled: !!address
}
});
};<VeChainKitProvider
feeDelegation={{
delegatorUrl: "https://sponsor-testnet.vechain.energy/by/YOUR_PROJECT_ID",
delegateAllTransactions: false,
}}
>
{children}
</VeChainKitProvider><VeChainKitProvider
feeDelegation={{
delegatorUrl: "YOUR_FEE_DELEGATION_URL",
delegateAllTransactions: true, // or false for social login only
}}
>
{children}
</VeChainKitProvider>import {
Address,
HDKey,
Transaction,
Secp256k1,
Hex
} from '@vechain/sdk-core';
// Default signer for development (use env variable in production)
const DEFAULT_SIGNER = 'denial kitchen pet squirrel other broom bar gas better priority spoil cross';
export async function onRequestPost({ request, env }): Promise<Response> {
try {
const body = await request.json();
console.log('Incoming fee delegation request:', body);
// Load signer wallet from mnemonic
const mnemonic = (env.SIGNER_MNEMONIC ?? DEFAULT_SIGNER).split(' ');
const signerWallet = HDKey.fromMnemonic(
mnemonic,
HDKey.VET_DERIVATION_PATH
).deriveChild(0);
if (!signerWallet.publicKey || !signerWallet.privateKey) {
throw new Error('Could not load signing wallet');
}
// Get signer address
const signerAddress = Address.ofPublicKey(signerWallet.publicKey);
// Decode and sign the transaction
const transactionToSign = Transaction.decode(
Buffer.from(body.raw.slice(2), 'hex'),
false
);
const transactionHash = transactionToSign.getSignatureHash(
Address.of(body.origin)
);
const signature = Secp256k1.sign(
transactionHash.bytes,
signerWallet.privateKey
);
return new Response(JSON.stringify({
signature: Hex.of(signature).toString(),
address: signerAddress.toString()
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
console.error('Fee delegation error:', error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}# .env
SIGNER_MNEMONIC="your twelve word mnemonic phrase here"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FeeDelegation {
address public owner;
mapping(address => bool) public whitelist;
uint256 public dailyLimit = 1000; // VTHO limit
mapping(address => uint256) public dailyUsage;
mapping(address => uint256) public lastUsageDate;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
function addToWhitelist(address user) external onlyOwner {
whitelist[user] = true;
}
function removeFromWhitelist(address user) external onlyOwner {
whitelist[user] = false;
}
/**
* @dev Check if a transaction can be sponsored
*/
function canSponsorTransactionFor(
address _origin,
address _to,
bytes calldata _data
) public view returns (bool) {
// Always sponsor for whitelisted addresses
if (whitelist[_origin]) {
return true;
}
// Check daily limit for others
uint256 today = block.timestamp / 86400;
if (lastUsageDate[_origin] < today) {
return true; // New day, can sponsor
}
return dailyUsage[_origin] < dailyLimit;
}
function updateUsage(address user, uint256 amount) external {
uint256 today = block.timestamp / 86400;
if (lastUsageDate[user] < today) {
dailyUsage[user] = amount;
lastUsageDate[user] = today;
} else {
dailyUsage[user] += amount;
}
}
}// Example monitoring middleware
async function monitorDelegation(request, response, next) {
const vthoBalance = await getVTHOBalance(DELEGATION_ADDRESS);
if (vthoBalance < MINIMUM_THRESHOLD) {
await sendAlert('Low VTHO balance for fee delegation');
}
// Log delegation request
await logDelegationRequest({
origin: request.body.origin,
timestamp: Date.now(),
transactionHash: request.body.hash
});
next();
}// Ensure social login users can transact without VTHO
const { sendTransaction } = useWallet();
try {
const tx = await sendTransaction({
to: '0x...',
value: '0',
data: '0x...'
});
console.log('Transaction sponsored:', tx);
} catch (error) {
console.error('Delegation failed:', error);
}{
"signature": "0x...",
"address": "0x..."
}'use client';
import { ReactElement, useCallback } from 'react';
import {
useSignMessage,
} from '@vechain/vechain-kit';
export function SigningExample(): ReactElement {
const {
signMessage,
isSigningPending: isMessageSignPending,
signature: messageSignature,
} = useSignMessage();
const handleSignMessage = useCallback(async () => {
try {
const signature = await signMessage('Hello VeChain!');
toast({
title: 'Message signed!',
description: `Signature: ${signature.slice(0, 20)}...`,
status: 'success',
duration: 1000,
isClosable: true,
});
} catch (error) {
toast({
title: 'Signing failed',
description:
error instanceof Error ? error.message : String(error),
status: 'error',
duration: 1000,
isClosable: true,
});
}
}, [signMessage, toast]);
return (
<>
<button
onClick={handleSignMessage}
isLoading={isMessageSignPending}
>
Sign Typed Data
</button>
{typedDataSignature && (
<h4>
{typedDataSignature}
</h4>
)}
</>
);
}'use client';
import { ReactElement, useCallback } from 'react';
import {
useWallet,
useSignTypedData,
} from '@vechain/vechain-kit';
// Example EIP-712 typed data
const exampleTypedData = {
domain: {
name: 'VeChain Example',
version: '1',
chainId: 1,
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
},
message: {
name: 'Alice',
wallet: '0x0000000000000000000000000000000000000000',
},
primaryType: 'Person',
};
export function SigningTypedDataExample(): ReactElement {
const {
signTypedData,
isSigningPending: isTypedDataSignPending,
signature: typedDataSignature,
} = useSignTypedData();
const { account } = useWallet()
const handleSignTypedData = useCallback(async () => {
try {
const signature = await signTypedData(exampleTypedData, {
signer: account?.address
});
alert({
title: 'Typed data signed!',
description: `Signature: ${signature.slice(0, 20)}...`,
status: 'success',
duration: 1000,
isClosable: true,
});
} catch (error) {
alert({
title: 'Signing failed',
description:
error instanceof Error ? error.message : String(error),
status: 'error',
duration: 1000,
isClosable: true,
});
}
}, [signTypedData, toast, account]);
return (
<>
<button
onClick={handleSignTypedData}
isLoading={isTypedDataSignPending}
>
Sign Typed Data
</button>
{typedDataSignature && (
<h4>
{typedDataSignature}
</h4>
)}
</>
);
}import { useSignatureVerification } from "../hooks/useSignatureVerification";
import { Modal, ModalOverlay, ModalContent, VStack, Text, Spinner, Button } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { ReactNode, useEffect } from "react";
import { useWallet } from "@vechain/vechain-kit";
import { clearSignature, usePostUser } from "@/hooks";
type Props = {
children: ReactNode;
};
export const SignatureVerificationWrapper = ({ children }: Props) => {
const { hasVerified, isVerifying, signature, verifySignature, value } = useSignatureVerification();
const { account } = useWallet();
const { t } = useTranslation();
const { mutate: postUser } = usePostUser();
useEffect(() => {
if (account?.address && !signature) {
verifySignature();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account?.address, signature]);
useEffect(() => {
// If user signed the signature we call our backend endpoint
if (signature && value.user !== "" && value.timestamp !== "" && account?.address) {
// When you want to execute the mutation:
postUser({
address: account.address,
value: value,
signature: signature,
});
}
}, [signature, value, account?.address, postUser]);
// if user disconnects we clear the signature
useEffect(() => {
if (!account) {
clearSignature();
}
}, [account]);
// Only show the modal if we have an account connected AND haven't verified yet AND are not in the process of verifying
const showModal = !!account?.address && (!hasVerified || !signature);
return (
<>
{children}
<Modal isOpen={showModal} onClose={() => {}} isCentered closeOnOverlayClick={false}>
<ModalOverlay />
<ModalContent p={6}>
<VStack spacing={4}>
{isVerifying ? (
<>
<Spinner size="xl" color="primary.500" />
<Text textAlign="center" fontSize="lg">
{t("Please sign the message to verify your wallet ownership")}
</Text>
</>
) : (
<>
<Text textAlign="center" fontSize="lg">
{t("Signature verification is mandatory to proceed. Please try again.")}
</Text>
<Button onClick={verifySignature} colorScheme="secondary">
{t("Try Again")}
</Button>
</>
)}
</VStack>
</ModalContent>
</Modal>
</>
);
};import React from "react";
import ReactDOM from "react-dom/client";
import { VeChainKitProviderWrapper } from "./components/VeChainKitProviderWrapper.tsx";
import { ChakraProvider, ColorModeScript } from "@chakra-ui/react";
import { lightTheme } from "./theme";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { AppProvider } from "./components/AppProvider.tsx";
import { SignatureVerificationWrapper } from "./components/SignatureVerificationWrapper.tsx";
(async () => {
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ChakraProvider theme={lightTheme}>
<ColorModeScript initialColorMode="light" />
<VeChainKitProviderWrapper>
<SignatureVerificationWrapper>
<AppProvider>
<RouterProvider router={router} />
</AppProvider>
</SignatureVerificationWrapper>
</VeChainKitProviderWrapper>
</ChakraProvider>
</React.StrictMode>,
);
})();
import { useWallet, useSignTypedData } from "@vechain/vechain-kit";
import { useState, useCallback } from "react";
import { ethers } from "ethers";
// EIP-712 typed data structure
const domain = {
name: "MyAppName",
version: "1",
};
const types = {
Authentication: [
{ name: "user", type: "address" },
{ name: "timestamp", type: "string" },
],
};
export type SignatureVerificationValue = {
user: string;
timestamp: string;
};
export const useSignatureVerification = () => {
const { account } = useWallet();
const { signTypedData } = useSignTypedData();
const [isVerifying, setIsVerifying] = useState(false);
const [value, setValue] = useState<SignatureVerificationValue>({
user: "",
timestamp: "",
});
const signature = localStorage.getItem(`login_signature`);
const verifySignature = useCallback(async () => {
if (!account?.address || isVerifying) return;
setValue({
user: account.address,
timestamp: new Date().toISOString(),
});
const existingSignature = localStorage.getItem(`login_signature`);
if (existingSignature) return;
setIsVerifying(true);
try {
const signature = await signTypedData({
domain,
types,
message: value,
primaryType: "Authentication",
}, {
signer: account?.address
});
const isValid = ethers.verifyTypedData(domain, types, value, signature);
if (!isValid) {
throw new Error("Signature verification failed");
}
localStorage.setItem(`login_signature`, signature);
} catch (error) {
console.error("Signature verification failed:", error);
} finally {
setIsVerifying(false);
}
}, [account?.address, isVerifying, signTypedData, value]);
const hasVerified = account?.address ? !!localStorage.getItem(`login_signature`) : false;
return { hasVerified, isVerifying, signature, verifySignature, value };
};import {ethers} from "ethers";
static async verifySignature(signature: string, value: { user: string , timestamp: string }): Promise<string> {
const domain = {
name: "My App Name",
version: "1",
};
const types = {
Authentication: [
{name: "user", type: "address"},
{name: "timestamp", type: "string"},
],
};
return ethers.verifyTypedData(domain, types, value, signature);
}
const signerAddress = await verifySignature(
signature,
value
);
if (signerAddress.toLowerCase() != address.toLowerCase()) {
return {
statusCode: 403,
body: JSON.stringify({
error: "Invalid signature",
}),
};
}
import { useConnex } from '@vechain/vechain-kit';
export default function Home(): ReactElement {
const connex = useConnex();
const handleSignWithConnex = () => {
connex.vendor.sign(
'cert',
{
purpose: 'identification',
payload: {
type: 'text',
content: 'Please sign this message to connect your wallet.',
},
}
).request();
}
return <button onClick={handleSignWithConnex}> Sign </button>
}'use client';
import { WalletButton } from '@vechain/vechain-kit';
export function Page() {
return (
<WalletButton />
);
}'use client';
import { useConnectModal, useAccountModal, useWallet } from '@vechain/vechain-kit';
export function CustomAuthButton() {
const { connection } = useWallet();
const {
open: openConnectModal,
close: closeConnectModal,
isOpen: isConnectModalOpen
} = useConnectModal();
const {
open: openAccountModal,
close: closeAccountModal,
isOpen: isAccountModalOpen
} = useAccountModal();
if (!connection.isConnected) {
return (
<button onClick={openConnectModal}>
Connect Wallet
</button>
);
}
return (
<button onClick={openAccountModal}>
View Account
</button>
);
}import { useLoginWithOAuth } from '@vechain/vechain-kit';
const SocialLoginComponent = () => {
const { initOAuth } = useLoginWithOAuth();
const handleOAuthLogin = async (provider: OAuthProvider) => {
try {
await initOAuth({ provider });
console.log(`${provider} OAuth login initiated`);
} catch (error) {
console.error("OAuth login failed:", error);
}
};
return (
<div className="social-login-container">
<h3>Sign in with:</h3>
<div className="social-buttons">
<button onClick={() => handleOAuthLogin('google')}>
<GoogleIcon /> Google
</button>
<button onClick={() => handleOAuthLogin('twitter')}>
<TwitterIcon /> Twitter
</button>
<button onClick={() => handleOAuthLogin('apple')}>
<AppleIcon /> Apple
</button>
<button onClick={() => handleOAuthLogin('discord')}>
<DiscordIcon /> Discord
</button>
</div>
</div>
);
};
export default SocialLoginComponent;import { useDAppKitWalletModal } from '@vechain/vechain-kit';
export const WalletOnlyLogin = () => {
const { open: openWalletModal } = useDAppKitWalletModal();
return (
<button onClick={openWalletModal}>
Connect Wallet
</button>
);
};import { useWallet } from '@vechain/vechain-kit';
function Component() {
const { connection, address, source } = useWallet();
if (connection.isConnected) {
console.log('Connected wallet:', address);
console.log('Connection source:', source);
}
}const handleConnect = async () => {
try {
await openConnectModal();
} catch (error) {
console.error('Connection failed:', error);
// Show user-friendly error message
}
};