@web3cloud-io/vesting-widget
v1.0.3
Published
Embeddable token vesting widget SDK for Web3 applications
Downloads
584
Maintainers
Readme
@web3cloud-io/vesting-widget
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.
Installation
npm install @web3cloud-io/vesting-widgetPrerequisites
- 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.
Quick Integration Guide
Step 1: Install dependencies
# 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-walletsStep 2: Set up wallet providers
EVM (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>
);
}Step 3: Integrate the widget
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" />;
}Quick Start
Embed URL
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',
});What is hostRpc?
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.
Minimal Example (EVM)
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();Minimal Example (Solana)
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(),
});API Reference
createVestingWidget(options)
Creates and embeds the vesting widget into your application.
Options
| 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 |
Returns VestingWidget
| 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 |
createHostRpcFromProviders(config)
Helper to create hostRpc from wallet providers. This is the recommended way to set up the SDK.
Config Parameters
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
},
});Events
| 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 |
Context Configuration
Initial Page
Open widget on a specific page:
const widget = createVestingWidget({
// ...
context: {
initialPage: 'claim', // 'home', 'createVesting', 'projects', 'streams', etc.
},
});Page Parameters
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',
},
},
},
});Custom Styling
Customize widget appearance:
const widget = createVestingWidget({
// ...
context: {
styles: {
colors: {
primary: '#6366f1',
background: '#0a0a0a',
text: '#ffffff',
},
borderRadius: {
medium: '12px',
},
},
},
});Framework Examples
React (Complete Example with wagmi + wallet-adapter)
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)
Vue 3
<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>Vanilla JavaScript
<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>TypeScript
All types are exported:
import type {
VestingWidget,
EmbedContext,
VestingWidgetStyles,
HostRpcFunction,
RpcUnifiedRequest,
VestingCreatedPayload,
PageId,
PageParams,
SolanaHostConfig,
EvmHostConfig,
} from '@web3cloud-io/vesting-widget';Important Notes
Stable hostRpc Pattern (React)
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]);Wallet Change Notifications
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]);Solana signMessage (Optional but Recommended)
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,
}Next.js / SSR Note
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();
}, []);
}Permit Signing: Web3Cloud vs Custom Backend
When creating vesting streams for a project (projectId > 0), the widget needs a cryptographic signature (permit) from the project manager. You have two options:
Option 1: Web3Cloud Backend (Recommended) ✅
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!Option 2: Custom Backend (Enterprise) 🔐
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
Permit Types
| 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 |
Request Structure
// signPermit request via hostRpc
{
network: 'permit',
action: 'signPermit',
payload: {
type: 'evm-individual' | 'evm-batch' | 'solana-individual' | 'solana-batch',
projectId: '42',
data: { /* stream data */ }
}
}EVM Individual 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 hexEVM Batch Data
// 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...'Solana Individual Data
// 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...'
}Implementation Example
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
}
};Security Notes
- 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
Troubleshooting
Widget doesn't update when wallet changes
Problem: Widget shows old wallet address after connecting/disconnecting.
Solution: Make sure you're calling notifyWalletChanged() when wallet changes (see "Wallet Change Notifications" above).
Widget recreates on every render
Problem: Widget is destroyed and recreated constantly.
Solution: Use the stable reference pattern (see "Stable hostRpc Pattern" above).
TypeScript error: "Missing Transaction, VersionedTransaction"
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.
Solana wallet not connecting
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
});License
MIT © Web3Cloud
