Embeddable token vesting widget SDK for Web3 applications. Easily integrate token vesting functionality into any web application with support for EVM (Ethereum, Polygon, etc.) and Solana ecosystems.
npm install @web3cloud-io/vesting-widget- For EVM: A wallet provider with EIP-1193 interface (wagmi
WalletClient,window.ethereum, ethers.jsBrowserProvider, etc.) - For Solana:
@solana/web3.jspackage and a wallet adapter (e.g.,@solana/wallet-adapter-react)
Note: The SDK automatically imports
@solana/web3.jsTransaction classes when needed. You don't need to pass them manually.
# For EVM
npm install wagmi viem @rainbow-me/rainbowkit
# For Solana
npm install @solana/web3.js @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-walletsEVM (wagmi):
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
// Configure your chains and providers
function App() {
return (
<WagmiProvider config={wagmiConfig}>
<RainbowKitProvider>
<YourApp />
</RainbowKitProvider>
</WagmiProvider>
);
}Solana (wallet-adapter):
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
const wallets = [new PhantomWalletAdapter(), new SolflareWalletAdapter()];
function App() {
return (
<ConnectionProvider endpoint="https://api.mainnet-beta.solana.com">
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<YourApp />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}See "React (Complete Example)" section below for full production-ready code.
Minimal setup:
import { createVestingWidget, createHostRpcFromProviders } from '@web3cloud-io/vesting-widget';
import { useWalletClient } from 'wagmi';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
function MyComponent() {
const { data: walletClient } = useWalletClient();
const { connection, sendTransaction, wallet } = useWallet();
const hostRpc = useMemo(() => createHostRpcFromProviders({
evm: { walletClient },
solana: {
connection,
sendTransaction,
getPublicKey: () => wallet?.adapter?.publicKey ?? null,
},
}), [walletClient, connection, sendTransaction, wallet]);
useEffect(() => {
const widget = createVestingWidget({
container: document.getElementById('vesting-root'),
hostRpc,
ecosystem: 'evm',
});
return () => widget.destroy();
}, [hostRpc]);
return <div id="vesting-root" />;
}Production embed URL: https://embed.web3cloud.io
The widget loads from this URL by default. You can override it for development or custom deployments:
const widget = createVestingWidget({
container: document.getElementById('vesting-root'),
embedUrl: 'https://embed.web3cloud.io', // Production (default)
// embedUrl: 'http://localhost:5173', // Development
hostRpc,
ecosystem: 'evm',
});hostRpc is a function that handles wallet operations (signing transactions, getting accounts, etc.) for the widget. The SDK provides createHostRpcFromProviders() helper that creates this function from your wallet providers.
import { createVestingWidget, createHostRpcFromProviders } from '@web3cloud-io/vesting-widget';
// 1. Get your wallet client (example with window.ethereum)
const walletClient = window.ethereum; // or wagmi useWalletClient(), etc.
// 2. Create hostRpc function
const hostRpc = createHostRpcFromProviders({
evm: { walletClient },
});
// 3. Create widget
const widget = createVestingWidget({
container: document.getElementById('vesting-root'),
embedUrl: 'https://embed.web3cloud.io', // Production embed URL
hostRpc,
ecosystem: 'evm',
});
// 4. Listen to events
widget.on('CLOSE', () => console.log('Widget closed'));
widget.on('VESTING_CREATED', (payload) => {
console.log('Vesting created:', payload);
});
// 5. Cleanup when done
widget.destroy();import { createVestingWidget, createHostRpcFromProviders } from '@web3cloud-io/vesting-widget';
import { Connection } from '@solana/web3.js';
// 1. Get your Solana wallet and connection
// Example with @solana/wallet-adapter-react:
const { connection } = useConnection();
const { sendTransaction, wallet } = useWallet();
// 2. Create hostRpc function
const hostRpc = createHostRpcFromProviders({
solana: {
connection,
sendTransaction,
getPublicKey: () => wallet?.adapter?.publicKey ?? null,
},
});
// 3. Create widget
const widget = createVestingWidget({
container: document.getElementById('vesting-root'),
embedUrl: 'https://embed.web3cloud.io', // Production embed URL
hostRpc,
ecosystem: 'solana',
});
// 4. Notify widget when wallet changes
widget.notifyWalletChanged({
ecosystem: 'solana',
address: wallet?.adapter?.publicKey?.toBase58(),
});Creates and embeds the vesting widget into your application.
| Option | Type | Required | Description |
|---|---|---|---|
container |
HTMLElement |
Yes | DOM element to embed the widget into |
hostRpc |
HostRpcFunction |
Yes | Function to handle wallet RPC requests |
ecosystem |
'evm' | 'solana' |
Yes | Blockchain ecosystem |
embedUrl |
string |
No | URL of the embed application (default: https://embed.web3cloud.io) |
context |
EmbedContext |
No | Configuration context for the widget |
height |
number |
No | Height of the iframe in pixels (default: 420) |
className |
string |
No | CSS class for the iframe |
onClose |
() => void |
No | Callback when widget closes |
| Method | Description |
|---|---|
on(event, handler) |
Subscribe to widget events |
off(event, handler) |
Unsubscribe from widget events |
notifyWalletChanged(data) |
Notify widget of wallet changes |
destroy() |
Remove widget and cleanup resources |
Helper to create hostRpc from wallet providers. This is the recommended way to set up the SDK.
EVM (evm):
walletClient(required): Any object withrequest()method that follows EIP-1193 standard- Examples:
window.ethereum, wagmiWalletClient, ethers.jsBrowserProviderwrapped in{ request: (args) => provider.send(args.method, args.params) }
- Examples:
Solana (solana):
connection(required):Connectioninstance from@solana/web3.js- Example:
new Connection('https://api.mainnet-beta.solana.com')or fromuseConnection()hook
- Example:
sendTransaction(required): Function that sends a transaction to the network- Signature:
(tx: Transaction | VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<string> - Example: From
useWallet().sendTransactionorwallet.sendTransaction
- Signature:
getPublicKey(required): Function that returns the current wallet's public key ornullif not connected- Signature:
() => PublicKey | null | undefined - Example:
() => wallet?.adapter?.publicKey ?? null
- Signature:
signMessage(optional): Function for signing arbitrary messages- Signature:
(message: Uint8Array) => Promise<Uint8Array> - Example: From
useWallet().signMessage
- Signature:
const hostRpc = createHostRpcFromProviders({
evm: {
walletClient, // wagmi WalletClient, window.ethereum, or any EIP-1193 provider
},
solana: {
connection, // @solana/web3.js Connection
sendTransaction, // Function to send transactions
getPublicKey, // Function returning PublicKey or null
signMessage, // Optional: for signing messages
},
});| Event | Payload | Description |
|---|---|---|
CLOSE |
- | Widget requested to close |
VESTING_CREATED |
VestingCreatedPayload |
Vesting stream(s) created successfully |
SOLANA_CONNECT_REQUESTED |
- | User needs to connect Solana wallet |
EVM_CONNECT_REQUESTED |
- | User needs to connect EVM wallet |
NAVIGATE_BACK |
- | User wants to navigate back in host app |
Open widget on a specific page:
const widget = createVestingWidget({
// ...
context: {
initialPage: 'claim', // 'home', 'createVesting', 'projects', 'streams', etc.
},
});For pages with route params (camelCase for both initialPage and pageParams keys):
const widget = createVestingWidget({
// ...
context: {
initialPage: 'streamDetail',
pageParams: {
streamDetail: {
streamId: '123',
ecosystem: 'evm',
network: 'sepolia',
viewMode: 'view', // 'full' | 'view'
},
},
},
});
// Batch detail example
const widget = createVestingWidget({
// ...
context: {
initialPage: 'batchDetail',
pageParams: {
batchDetail: {
ecosystem: 'evm',
chainId: 11155111,
batchId: '5',
},
},
},
});Customize widget appearance:
const widget = createVestingWidget({
// ...
context: {
styles: {
colors: {
primary: '#6366f1',
background: '#0a0a0a',
text: '#ffffff',
},
borderRadius: {
medium: '12px',
},
},
},
});Full production-ready example with both EVM and Solana support:
import { useEffect, useMemo, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { useWalletModal } from '@solana/wallet-adapter-react-ui';
import { useWalletClient, useAccount, useChainId } from 'wagmi';
import {
createVestingWidget,
createHostRpcFromProviders,
type HostRpcFunction,
type VestingWidget
} from '@web3cloud-io/vesting-widget';
type Ecosystem = 'evm' | 'solana';
export default function VestingPlatform({ ecosystem = 'evm' }: { ecosystem?: Ecosystem }) {
const navigate = useNavigate();
const widgetRef = useRef<VestingWidget | null>(null);
// ========== EVM Setup ==========
const { data: walletClient } = useWalletClient();
const { address: evmAddress } = useAccount();
const chainId = useChainId();
// ========== Solana Setup ==========
const { sendTransaction, wallet, signMessage, publicKey: solanaPublicKey } = useWallet();
const { connection } = useConnection();
const { setVisible } = useWalletModal();
// ========== Create hostRpc ==========
// Recreated when providers change
const hostRpc = useMemo(() => createHostRpcFromProviders({
evm: { walletClient },
solana: {
connection,
sendTransaction,
getPublicKey: () => wallet?.adapter?.publicKey ?? null,
signMessage: signMessage ? async (message: Uint8Array) => {
const signature = await signMessage(message);
return signature;
} : undefined,
},
}), [connection, sendTransaction, wallet, walletClient, signMessage]);
// ========== Stable reference pattern ==========
// Prevents widget recreation when hostRpc changes
const hostRpcRef = useRef(hostRpc);
hostRpcRef.current = hostRpc;
const stableHostRpc: HostRpcFunction = useCallback(async (request) => {
return hostRpcRef.current(request);
}, []);
// ========== Create widget ==========
useEffect(() => {
const container = document.getElementById('vesting-root');
if (!container) return;
const widget = createVestingWidget({
container,
embedUrl: 'https://embed.web3cloud.io', // Production embed URL
ecosystem,
hostRpc: stableHostRpc,
context: {
showHeader: true,
lockNavigation: false,
},
onClose: () => navigate('/'),
});
// ========== Event handlers ==========
const handleClose = () => navigate('/');
const handleSolanaConnectRequested = () => {
console.log('Widget requested Solana wallet connection');
setVisible(true); // Open wallet modal
};
const handleNavigateBack = () => {
console.log('Widget requested navigation back');
navigate('/');
};
widget.on('CLOSE', handleClose);
widget.on('SOLANA_CONNECT_REQUESTED', handleSolanaConnectRequested);
widget.on('NAVIGATE_BACK', handleNavigateBack);
widget.on('VESTING_CREATED', (payload) => {
console.log('Vesting created:', payload);
// Track analytics, show notification, etc.
});
widgetRef.current = widget;
return () => {
widget.off('CLOSE', handleClose);
widget.off('SOLANA_CONNECT_REQUESTED', handleSolanaConnectRequested);
widget.off('NAVIGATE_BACK', handleNavigateBack);
widget.destroy();
widgetRef.current = null;
};
}, [ecosystem, stableHostRpc, setVisible, navigate]);
// ========== Notify widget about wallet changes ==========
// EVM wallet changes
useEffect(() => {
if (widgetRef.current && ecosystem === 'evm') {
widgetRef.current.notifyWalletChanged({
ecosystem: 'evm',
address: evmAddress,
chainId,
});
}
}, [evmAddress, chainId, ecosystem]);
// Solana wallet changes
useEffect(() => {
if (widgetRef.current && ecosystem === 'solana') {
widgetRef.current.notifyWalletChanged({
ecosystem: 'solana',
address: solanaPublicKey?.toBase58(),
});
}
}, [solanaPublicKey, ecosystem]);
return <div id="vesting-root" style={{ width: '100%', minHeight: '600px' }} />;
}Key points:
- β
Stable hostRpc pattern: Uses
useRef+useCallbackto prevent widget recreation - β Both ecosystems: Supports EVM and Solana with proper wallet change notifications
- β
Event handling: Handles
CLOSE,SOLANA_CONNECT_REQUESTED,NAVIGATE_BACK,VESTING_CREATED - β Proper cleanup: Removes event listeners and destroys widget on unmount
- β
Optional signMessage: Includes
signMessagefor Solana (optional but recommended)
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { createVestingWidget, createHostRpcFromProviders } from '@web3cloud-io/vesting-widget';
const container = ref<HTMLDivElement | null>(null);
let widget: any = null;
// Your wallet providers (from your Vue wallet setup)
const walletClient = computed(() => /* your EVM wallet client */);
const connection = computed(() => /* your Solana connection */);
const hostRpc = computed(() => createHostRpcFromProviders({
evm: { walletClient: walletClient.value },
solana: {
connection: connection.value,
sendTransaction: /* your sendTransaction */,
getPublicKey: () => /* your getPublicKey */,
},
}));
onMounted(() => {
if (!container.value) return;
widget = createVestingWidget({
container: container.value,
embedUrl: 'https://embed.web3cloud.io', // Production embed URL
hostRpc: hostRpc.value,
ecosystem: 'evm',
});
});
onUnmounted(() => {
widget?.destroy();
});
</script>
<template>
<div ref="container" style="width: 100%; height: 600px;" />
</template><div id="vesting-root"></div>
<script type="module">
import { createVestingWidget, createHostRpcFromProviders } from '@web3cloud-io/vesting-widget';
import { Connection } from '@solana/web3.js';
const hostRpc = createHostRpcFromProviders({
evm: { walletClient: window.ethereum },
solana: {
connection: new Connection('https://api.mainnet-beta.solana.com'),
sendTransaction: async (tx, conn) => window.solana.signAndSendTransaction(tx),
getPublicKey: () => window.solana?.publicKey ?? null,
},
});
const widget = createVestingWidget({
container: document.getElementById('vesting-root'),
embedUrl: 'https://embed.web3cloud.io', // Production embed URL
hostRpc,
ecosystem: 'evm',
onClose: () => widget.destroy(),
});
widget.on('VESTING_CREATED', (payload) => {
console.log('Vesting created:', payload);
});
</script>All types are exported:
import type {
VestingWidget,
EmbedContext,
VestingWidgetStyles,
HostRpcFunction,
RpcUnifiedRequest,
VestingCreatedPayload,
PageId,
PageParams,
SolanaHostConfig,
EvmHostConfig,
} from '@web3cloud-io/vesting-widget';Always use the stable reference pattern to prevent widget recreation:
// β
Correct: Stable reference
const hostRpcRef = useRef(hostRpc);
hostRpcRef.current = hostRpc;
const stableHostRpc = useCallback(async (request) => {
return hostRpcRef.current(request);
}, []);
// β Wrong: Direct usage causes widget recreation
useEffect(() => {
const widget = createVestingWidget({
hostRpc, // β Widget recreates on every hostRpc change!
});
}, [hostRpc]);Always notify the widget when wallet changes:
// EVM
useEffect(() => {
if (widgetRef.current && ecosystem === 'evm') {
widgetRef.current.notifyWalletChanged({
ecosystem: 'evm',
address: evmAddress,
chainId,
});
}
}, [evmAddress, chainId, ecosystem]);
// Solana
useEffect(() => {
if (widgetRef.current && ecosystem === 'solana') {
widgetRef.current.notifyWalletChanged({
ecosystem: 'solana',
address: solanaPublicKey?.toBase58(),
});
}
}, [solanaPublicKey, ecosystem]);Include signMessage for full functionality:
solana: {
connection,
sendTransaction,
getPublicKey: () => wallet?.adapter?.publicKey ?? null,
signMessage: signMessage ? async (message: Uint8Array) => {
const signature = await signMessage(message);
return signature;
} : undefined,
}The widget only works on the client side (uses window, document). For Next.js:
'use client';
import { useEffect } from 'react';
import { createVestingWidget } from '@web3cloud-io/vesting-widget';
export default function Page() {
useEffect(() => {
// Widget only works in browser
const widget = createVestingWidget({ ... });
return () => widget.destroy();
}, []);
}When creating vesting streams for a project (projectId > 0), the widget needs a cryptographic signature (permit) from the project manager. You have two options:
Web3Cloud acts as project manager. Permits are signed automatically.
| Feature | What you get |
|---|---|
| Permit signing | Automatic β widget handles it |
| Whitelist | Use @web3cloud-io/vesting-api SDK |
| Manager key | Stored by Web3Cloud |
| signPermit in hostRpc | Not needed |
# Install Backend API SDK for whitelist management
npm install @web3cloud-io/vesting-apiimport { ExternalApi, Configuration } from "@web3cloud-io/vesting-api";
const api = new ExternalApi(new Configuration({
apiKey: "your-api-key", // Get from project page
}));
// Manage who can create vestings
await api.addWhitelistedAddresses({
addressToTokenAddresses: [
{ address: "0xUser...", tokenAddress: "0xToken..." },
]
});
// Widget handles permit signing automatically!You are the project manager. Your backend signs permits.
| Feature | What you do |
|---|---|
| Permit signing | You implement |
| Authorization | Your custom logic |
| Manager key | Your secure servers |
| signPermit in hostRpc | Required |
When to use:
- Custom authorization rules (KYC, token balance checks, etc.)
- Full audit control
- Air-gapped security for manager key
- No dependency on Web3Cloud
| Type | Ecosystem | Description |
|---|---|---|
evm-individual |
EVM | EIP-712 signature for single stream |
evm-batch |
EVM | EIP-712 signature for batch of streams |
solana-individual |
Solana | Ed25519 signature for single stream |
solana-batch |
Solana | Ed25519 signature for batch |
// signPermit request via hostRpc
{
network: 'permit',
action: 'signPermit',
payload: {
type: 'evm-individual' | 'evm-batch' | 'solana-individual' | 'solana-batch',
projectId: '42',
data: { /* stream data */ }
}
}// data for type: 'evm-individual'
{
creator: '0x742d35Cc...', // Stream creator
token: '0x3Cef0E71...', // ERC-20 token
beneficiary: '0xRecipient...', // Beneficiary
amount: '1000000000000000000', // Wei (string)
startTime: 1704067200, // Unix timestamp
endTime: 1735689600, // Unix timestamp
curveName: 'Linear', // Curve type
curveData: '0x', // Curve params (hex)
beneficiaryName: 'Alice', // Name
cancellable: true,
transferable: false,
streamOwner: '0x000...000', // Zero = creator
nonce: '0', // Anti-replay (string)
deadline: '1704153600', // Permit deadline (string)
chainId: '11155111', // Chain ID (string)
verifyingContract: '0xVesting...', // Contract address
}
// Response: EIP-712 signature
return '0x...signature...' // 65 bytes hex// data for type: 'evm-batch'
{
creator: '0x742d35Cc...',
token: '0x3Cef0E71...',
beneficiaries: ['0xAddr1...', '0xAddr2...'], // Array
amounts: ['1000...', '2000...'], // Array
beneficiaryNames: ['Alice', 'Bob'], // Array
startTimes: ['1704067200', '1704067200'], // Array (strings)
endTimes: ['1735689600', '1735689600'], // Array (strings)
curveName: 'Linear',
curveData: '0x',
curveDataArray: ['0x', '0x'], // Per-beneficiary
cancellable: [true, true], // Array
transferable: [false, false], // Array
streamOwners: ['0x...', '0x...'], // Array
nonce: '1',
deadline: '1704153600',
chainId: '11155111',
verifyingContract: '0xVesting...',
}
// Response: EIP-712 signature
return '0x...signature...'// data for type: 'solana-individual'
{
creator: '7xKXtg2CW87d97...', // Creator pubkey
tokenMint: '6eSFJze3fdHgiV...', // SPL Token mint
beneficiary: 'Bc5qRutWijDNq...', // Beneficiary pubkey
amount: '1000000000', // Smallest unit
startTime: 1704067200,
endTime: 1735689600,
curveKind: { linear: {} }, // Anchor enum
curveData: { none: {} }, // Anchor enum
beneficiaryName: 'Alice',
cancellable: true,
transferable: false,
nonce: '0',
deadline: 1704153600,
programId: 'VESTxyz...', // Vesting program
managerPublicKey: 'Manager...', // Manager pubkey
}
// Response: Solana signature object
return {
signatureHex: '0x...',
signatureBase64: 'base64...',
digestHex: '0x...',
ed25519Instruction: { ... },
managerPublicKey: 'Pubkey...'
}const hostRpc: HostRpcFunction = async (request) => {
// Handle regular requests...
if (request.network === 'evm') { /* ... */ }
if (request.network === 'solana') { /* ... */ }
// Handle signPermit (Enterprise only)
if (request.network === 'permit' && request.action === 'signPermit') {
const { type, projectId, data } = request.payload;
// Call your backend
const response = await fetch('https://your-backend.com/api/sign-permit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, projectId, data }),
});
return response.json(); // Return signature
}
};- Never expose manager private key in frontend code
- Validate all data before signing
- Use nonce to prevent replay attacks
- Set short deadlines (1 hour recommended)
- Authenticate requests on your backend
Problem: Widget shows old wallet address after connecting/disconnecting.
Solution: Make sure you're calling notifyWalletChanged() when wallet changes (see "Wallet Change Notifications" above).
Problem: Widget is destroyed and recreated constantly.
Solution: Use the stable reference pattern (see "Stable hostRpc Pattern" above).
Problem: TypeScript complains about missing Transaction classes.
Solution: This is a caching issue. Rebuild the SDK:
cd sdk-vesting && npm run buildThen restart your TypeScript server in your IDE.
Problem: Widget shows "Solana wallet not connected" error.
Solution:
- Make sure
getPublicKey()returns a validPublicKeywhen wallet is connected - Listen to
SOLANA_CONNECT_REQUESTEDevent and open wallet modal:
widget.on('SOLANA_CONNECT_REQUESTED', () => {
setVisible(true); // Open wallet modal
});MIT Β© Web3Cloud