@jim4565/dapp-wrapper-react
Version:
React components and hooks for @jim4565/dapp-wrapper with persistent wallet connection
1,418 lines (1,410 loc) • 79.8 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import React, { createContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react';
// Error Types
var WalletErrorCode;
(function (WalletErrorCode) {
WalletErrorCode[WalletErrorCode["USER_REJECTED"] = 4001] = "USER_REJECTED";
WalletErrorCode[WalletErrorCode["UNAUTHORIZED"] = 4100] = "UNAUTHORIZED";
WalletErrorCode[WalletErrorCode["UNSUPPORTED_METHOD"] = 4200] = "UNSUPPORTED_METHOD";
WalletErrorCode[WalletErrorCode["DISCONNECTED"] = 4900] = "DISCONNECTED";
WalletErrorCode[WalletErrorCode["CHAIN_DISCONNECTED"] = 4901] = "CHAIN_DISCONNECTED";
WalletErrorCode[WalletErrorCode["UNKNOWN_ERROR"] = -1] = "UNKNOWN_ERROR";
})(WalletErrorCode || (WalletErrorCode = {}));
class WalletError extends Error {
constructor(message, options) {
super(message);
this.name = 'WalletError';
this.code = (options === null || options === void 0 ? void 0 : options.code) || WalletErrorCode.UNKNOWN_ERROR;
this.reason = options === null || options === void 0 ? void 0 : options.reason;
this.method = options === null || options === void 0 ? void 0 : options.method;
if ((options === null || options === void 0 ? void 0 : options.cause) && 'cause' in Error.prototype) {
this.cause = options.cause;
}
}
}
// Storage Keys (consistent naming)
const STORAGE_KEYS = {
WALLET_ADDRESS: 'incentiv_wallet_address',
WALLET_CONNECTED: 'incentiv_wallet_connected',
EVER_CONNECTED: 'incentiv_wallet_ever_connected',
LAST_CONNECTED_AT: 'incentiv_wallet_last_connected',
CONTRACTS: 'incentiv_wallet_contracts'
};
/**
* WICHTIGE ÄNDERUNG in @jim4565/dapp-wrapper v1.x:
*
* Die zugrunde liegende Bibliothek unterscheidet jetzt automatisch zwischen:
*
* 1. VIEW/PURE Funktionen:
* - Lesen nur Blockchain-Daten
* - Benötigen KEINE Wallet-Verbindung
* - Werden direkt mit dem Provider ausgeführt
* - Beispiele: balanceOf, totalSupply, getName
*
* 2. NONPAYABLE/PAYABLE Funktionen:
* - Ändern den Blockchain-State
* - Benötigen eine Wallet-Verbindung für die Signatur
* - Werden mit dem Signer ausgeführt
* - Beispiele: transfer, approve, mint
*
* Diese Unterscheidung passiert automatisch basierend auf der ABI stateMutability.
*
* Für React-Entwickler bedeutet das:
* - useContractRead: Funktioniert ohne Wallet-Verbindung
* - useContractWrite: Erfordert weiterhin eine Wallet-Verbindung
* - useContract: Stellt Contract immer zur Verfügung, Wallet-Check erfolgt bei Methodenaufruf
*/
/**
* PersistenceService - MetaMask-style LocalStorage Management
*
* Handles secure storage and retrieval of wallet connection data
* with proper error handling and data validation.
*/
class PersistenceService {
/**
* Save wallet connection data securely
*/
static saveWalletData(address) {
try {
const timestamp = Date.now();
localStorage.setItem(STORAGE_KEYS.WALLET_ADDRESS, address);
localStorage.setItem(STORAGE_KEYS.WALLET_CONNECTED, 'true');
localStorage.setItem(STORAGE_KEYS.EVER_CONNECTED, 'true');
localStorage.setItem(STORAGE_KEYS.LAST_CONNECTED_AT, timestamp.toString());
}
catch (error) {
console.warn('⚠️ Failed to save wallet data:', error);
throw new WalletError('Failed to persist wallet connection', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error
});
}
}
/**
* Load stored wallet connection data
*/
static loadWalletData() {
try {
const address = localStorage.getItem(STORAGE_KEYS.WALLET_ADDRESS);
const wasConnected = localStorage.getItem(STORAGE_KEYS.WALLET_CONNECTED) === 'true';
const hasEverConnected = localStorage.getItem(STORAGE_KEYS.EVER_CONNECTED) === 'true';
const lastConnectedAt = parseInt(localStorage.getItem(STORAGE_KEYS.LAST_CONNECTED_AT) || '0', 10);
// Validate data integrity
if (address && !this.isValidAddress(address)) {
console.warn('⚠️ Invalid address in storage, clearing data');
this.clearWalletData();
return this.getEmptyWalletData();
}
// Check if data is too old (older than 30 days)
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
if (lastConnectedAt > 0 && lastConnectedAt < thirtyDaysAgo) {
this.clearWalletData();
return this.getEmptyWalletData();
}
return {
address,
wasConnected,
hasEverConnected,
lastConnectedAt
};
}
catch (error) {
console.warn('⚠️ Failed to load wallet data:', error);
return this.getEmptyWalletData();
}
}
/**
* Clear all wallet-related data
*/
static clearWalletData() {
try {
const keysToRemove = Object.values(STORAGE_KEYS);
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
}
catch (error) {
console.warn('⚠️ Failed to clear wallet data:', error);
}
}
/**
* Save contract configurations
*/
static saveContracts(contracts) {
try {
const contractData = {
version: this.VERSION,
timestamp: Date.now(),
contracts: contracts.map(contract => ({
name: contract.name,
address: contract.address,
// Don't store full ABI for security, just basic info
hasABI: !!contract.abi
}))
};
localStorage.setItem(STORAGE_KEYS.CONTRACTS, JSON.stringify(contractData));
}
catch (error) {
console.warn('⚠️ Failed to save contract data:', error);
}
}
/**
* Load contract configurations
*/
static loadContracts() {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CONTRACTS);
if (!stored)
return [];
const data = JSON.parse(stored);
// Validate version compatibility
if (data.version !== this.VERSION) {
localStorage.removeItem(STORAGE_KEYS.CONTRACTS);
return [];
}
return data.contracts || [];
}
catch (error) {
console.warn('⚠️ Failed to load contract data:', error);
return [];
}
}
/**
* Check if wallet should auto-reconnect
*
* Returns true only if:
* 1. User has ever connected before (prevents popup for first-time users)
* 2. Was connected in last session
* 3. Has valid stored address
* 4. Connection is not too old
*/
static shouldAutoReconnect() {
const { address, wasConnected, hasEverConnected, lastConnectedAt } = this.loadWalletData();
if (!hasEverConnected) {
return false;
}
if (!address || !wasConnected) {
return false;
}
// Check if connection is recent (within last 7 days)
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
if (lastConnectedAt > 0 && lastConnectedAt < sevenDaysAgo) {
return false;
}
return true;
}
/**
* Get storage usage statistics
*/
static getStorageStats() {
const items = [];
let totalSize = 0;
Object.values(STORAGE_KEYS).forEach(key => {
const value = localStorage.getItem(key);
if (value !== null) {
const size = new Blob([value]).size;
totalSize += size;
items.push({
key,
value: value.substring(0, 100), // Truncate for display
size
});
}
});
return {
totalItems: items.length,
totalSize,
items
};
}
/**
* Validate if address format is correct
*/
static isValidAddress(address) {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
/**
* Get empty wallet data structure
*/
static getEmptyWalletData() {
return {
address: null,
wasConnected: false,
hasEverConnected: false,
lastConnectedAt: 0
};
}
/**
* Check if localStorage is available
*/
static isStorageAvailable() {
try {
const testKey = `${this.STORAGE_PREFIX}test`;
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
}
catch (_a) {
return false;
}
}
/**
* Migrate data from old storage format (if needed)
*/
static migrateStorageIfNeeded() {
try {
// Check for old format data and migrate if needed
const oldAddress = localStorage.getItem('wallet_address');
const oldConnected = localStorage.getItem('wallet_connected');
if (oldAddress && !localStorage.getItem(STORAGE_KEYS.WALLET_ADDRESS)) {
localStorage.setItem(STORAGE_KEYS.WALLET_ADDRESS, oldAddress);
localStorage.setItem(STORAGE_KEYS.WALLET_CONNECTED, oldConnected || 'false');
localStorage.setItem(STORAGE_KEYS.EVER_CONNECTED, 'true');
localStorage.setItem(STORAGE_KEYS.LAST_CONNECTED_AT, Date.now().toString());
// Remove old keys
localStorage.removeItem('wallet_address');
localStorage.removeItem('wallet_connected');
}
}
catch (error) {
console.warn('⚠️ Storage migration failed:', error);
}
}
}
PersistenceService.VERSION = '1.0.0';
PersistenceService.STORAGE_PREFIX = 'incentiv_wallet_';
// WalletError is now imported from types
/**
* IncentivWalletService - Professional wrapper around @jim4565/dapp-wrapper
*
* Provides MetaMask-style connection patterns and 3-tier reconnection strategy
* for seamless integration with React components.
*/
class IncentivWalletService {
constructor() {
this.isInitialized = false;
this.eventListeners = new Map();
// Note: IncentivWrapper will be injected or imported
// For now, we prepare the interface
this.initializeEventHandlers();
}
/**
* Initialize with IncentivWrapper instance
*/
async initialize(wrapperInstance) {
try {
if (wrapperInstance) {
this.wrapper = wrapperInstance;
}
else {
// Dynamic import of @jim4565/dapp-wrapper
const { IncentivWrapper } = await import('@jim4565/dapp-wrapper');
this.wrapper = new IncentivWrapper();
}
this.isInitialized = true;
}
catch (error) {
console.error('❌ Failed to initialize IncentivWalletService:', error);
throw new WalletError('Failed to initialize wallet service', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error
});
}
}
/**
* 3-TIER RECONNECTION STRATEGY
* Following MetaMask and wagmi best practices
*/
/**
* TIER 1: Silent Connection Check
* Check if SDK already has an active session without triggering any popups
*/
async checkSilentConnection() {
var _a, _b;
if (!this.isInitialized) {
return null;
}
try {
// Check if wrapper already has an active connection
if (this.wrapper.isConnected && this.wrapper.isConnected()) {
const address = await this.extractAddressFromWrapper();
const normalizedAddress = this.normalizeAndValidateAddress(address);
if (normalizedAddress) {
return normalizedAddress;
}
}
// Alternative: Check if connection state is available
const connectionState = (_b = (_a = this.wrapper).getConnectionState) === null || _b === void 0 ? void 0 : _b.call(_a);
if ((connectionState === null || connectionState === void 0 ? void 0 : connectionState.isConnected) && connectionState.address) {
const normalizedStateAddress = this.normalizeAndValidateAddress(connectionState.address);
if (normalizedStateAddress) {
return normalizedStateAddress;
}
}
return null;
}
catch (error) {
return null;
}
}
/**
* TIER 2: Session Restore
* Try to restore previous session without user interaction
*/
async tryRestoreSession(expectedAddress) {
if (!this.isInitialized) {
return false;
}
try {
// Method 1: Try to restore connection directly
if (this.wrapper.restoreConnection) {
const restored = await this.wrapper.restoreConnection(expectedAddress);
if (restored) {
return true;
}
}
// Method 2: Try to create signer without popup
if (this.wrapper.getSigner) {
try {
const signer = await this.wrapper.getSigner();
const signerAddress = await signer.getAddress();
if ((signerAddress === null || signerAddress === void 0 ? void 0 : signerAddress.toLowerCase()) === expectedAddress.toLowerCase()) {
return true;
}
}
catch (signerError) {
// Silent failure - this is expected if no session exists
}
}
// Method 3: Check if connection is implicitly restored
if (this.wrapper.userAddress &&
this.wrapper.userAddress.toLowerCase() === expectedAddress.toLowerCase()) {
return true;
}
return false;
}
catch (error) {
return false;
}
}
/**
* TIER 3: Full Connect
* Complete connection flow with user interaction (popup)
*/
async connect() {
if (!this.isInitialized) {
throw new WalletError('Wallet service not initialized');
}
try {
// Use the wrapper's connect method
await this.wrapper.connect();
// Multiple attempts to get the address from wrapper with different property names
const rawAddress = await this.extractAddressFromWrapper();
// Normalize and validate address with detailed error reporting
const address = this.normalizeAndValidateAddress(rawAddress);
if (!address) {
console.error('Address validation failed:', {
received: rawAddress,
type: typeof rawAddress,
isArray: Array.isArray(rawAddress),
wrapperProperties: this.debugWrapperState()
});
throw new WalletError(`Invalid address received from wallet: ${JSON.stringify(rawAddress)}`);
}
// Save connection data for future reconnections
PersistenceService.saveWalletData(address);
// Emit connection event
this.emit('connect', { address });
return address;
}
catch (error) {
console.error('❌ Wallet connection failed:', error);
// Handle specific error cases
const errorWithCode = error;
if (errorWithCode.code === 4001) {
throw new WalletError('User rejected connection request', {
code: WalletErrorCode.USER_REJECTED,
method: 'connect'
});
}
if (errorWithCode.code === -32002) {
throw new WalletError('Connection request already pending', {
code: WalletErrorCode.UNKNOWN_ERROR,
method: 'connect'
});
}
throw new WalletError('Failed to connect wallet', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error,
method: 'connect'
});
}
}
/**
* Get wallet balance with multiple fallback strategies
*/
async getBalance(address) {
var _a;
if (!this.isInitialized) {
throw new WalletError('Wallet service not initialized');
}
try {
// Get target address using robust extraction
let targetAddress = address;
if (!targetAddress) {
targetAddress = await this.extractAddressFromWrapper();
const normalizedAddress = this.normalizeAndValidateAddress(targetAddress);
if (!normalizedAddress) {
throw new WalletError('No valid address available for balance query');
}
targetAddress = normalizedAddress;
}
// Strategy 1: Use wrapper's native balance method
if (this.wrapper.getBalance && typeof this.wrapper.getBalance === 'function') {
try {
const balance = await this.wrapper.getBalance(targetAddress);
return balance;
}
catch (error) {
}
}
// Strategy 2: Use wrapper's getWalletBalance method
if (this.wrapper.getWalletBalance && typeof this.wrapper.getWalletBalance === 'function') {
try {
const balance = await this.wrapper.getWalletBalance(targetAddress);
return balance;
}
catch (error) {
}
}
// Strategy 3: Use provider directly (most common fallback)
if (this.wrapper.provider) {
try {
const balanceWei = await this.wrapper.provider.getBalance(targetAddress);
// Convert Wei to Ether using ethers.js
let balanceEth;
if (typeof balanceWei === 'string' || typeof balanceWei === 'number') {
// Simple conversion for basic numbers
const weiNumber = BigInt(balanceWei.toString());
const ethNumber = Number(weiNumber) / 1e18;
balanceEth = ethNumber.toString();
}
else if (balanceWei.toString) {
// Handle BigNumber or other objects with toString
const weiString = balanceWei.toString();
const weiNumber = BigInt(weiString);
const ethNumber = Number(weiNumber) / 1e18;
balanceEth = ethNumber.toFixed(18).replace(/\.?0+$/, ''); // Remove trailing zeros
}
else {
// Fallback: return raw value as string
balanceEth = balanceWei.toString();
}
return balanceEth;
}
catch (error) {
}
}
// Strategy 4: Use signer provider
if ((_a = this.wrapper.signer) === null || _a === void 0 ? void 0 : _a.provider) {
try {
const balanceWei = await this.wrapper.signer.provider.getBalance(targetAddress);
const weiNumber = BigInt(balanceWei.toString());
const ethNumber = Number(weiNumber) / 1e18;
const balanceEth = ethNumber.toFixed(18).replace(/\.?0+$/, '');
return balanceEth;
}
catch (error) {
}
}
// Strategy 5: Check if ethers is available globally for direct RPC call
try {
const ethers = await import('ethers');
const provider = new ethers.providers.JsonRpcProvider('https://rpc.staging.incentiv.net');
const balanceWei = await provider.getBalance(targetAddress);
const balanceEth = ethers.utils.formatEther(balanceWei);
return balanceEth;
}
catch (error) {
}
throw new WalletError('No balance method available after trying all strategies');
}
catch (error) {
console.error('❌ Balance fetch failed:', error);
throw new WalletError('Failed to fetch balance', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error,
method: 'getBalance'
});
}
}
/**
* Get contract instance
*/
getContract(contractName) {
if (!this.isInitialized) {
throw new WalletError('Wallet service not initialized');
}
try {
// Use wrapper's getContract method
const contract = this.wrapper.getContract(contractName);
if (!contract) {
console.warn(`⚠️ Contract '${contractName}' not found`);
return null;
}
return contract;
}
catch (error) {
console.error(`❌ Failed to get contract '${contractName}':`, error);
throw new WalletError(`Failed to get contract: ${contractName}`, {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error,
method: 'getContract'
});
}
}
/**
* Register contracts with the wrapper
*/
registerContracts(contracts) {
if (!this.isInitialized) {
throw new WalletError('Wallet service not initialized');
}
try {
if (this.wrapper.registerContracts) {
this.wrapper.registerContracts(contracts);
}
else {
// Fallback to individual registration
contracts.forEach(contract => {
if (this.wrapper.registerContract) {
this.wrapper.registerContract(contract.name, contract.address, contract.abi);
}
});
}
// Save contract info to persistence
PersistenceService.saveContracts(contracts);
}
catch (error) {
console.error('❌ Contract registration failed:', error);
throw new WalletError('Failed to register contracts', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error,
method: 'registerContracts'
});
}
}
/**
* Send transaction through wrapper
*/
async sendTransaction(txRequest) {
if (!this.isInitialized) {
throw new WalletError('Wallet service not initialized');
}
try {
// @jim4565/dapp-wrapper gibt ein Objekt mit { hash, receipt } zurück
const result = await this.wrapper.sendTransaction(txRequest);
// Emit transaction event
this.emit('transaction', {
hash: result.hash,
status: 'confirmed', // Status ist 'confirmed' da wir das Receipt haben
receipt: result.receipt,
data: result
});
return result;
}
catch (error) {
console.error('❌ Transaction failed:', error);
throw new WalletError('Transaction failed', {
code: WalletErrorCode.UNKNOWN_ERROR,
cause: error,
method: 'sendTransaction'
});
}
}
/**
* Disconnect wallet
*/
disconnect() {
var _a;
try {
if ((_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.disconnect) {
this.wrapper.disconnect();
}
// Clear persistence
PersistenceService.clearWalletData();
// Emit disconnect event
this.emit('disconnect', {});
}
catch (error) {
console.warn('⚠️ Disconnect error:', error);
}
}
/**
* Check if wallet is connected
*/
isConnected() {
var _a, _b, _c;
if (!this.isInitialized)
return false;
return !!(((_b = (_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.isConnected) === null || _b === void 0 ? void 0 : _b.call(_a)) || ((_c = this.wrapper) === null || _c === void 0 ? void 0 : _c.userAddress));
}
/**
* Get current user address
*/
async getUserAddress() {
if (!this.isInitialized)
return null;
try {
const address = await this.extractAddressFromWrapper();
return this.normalizeAndValidateAddress(address);
}
catch (error) {
console.error('Error getting user address:', error);
return null;
}
}
/**
* Event handling (MetaMask-style)
*/
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
off(event, callback) {
const listeners = this.eventListeners.get(event);
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(event, data) {
const listeners = this.eventListeners.get(event) || [];
listeners.forEach(callback => {
try {
callback(data);
}
catch (error) {
console.error(`Error in event listener for '${event}':`, error);
}
});
}
/**
* Initialize event handlers for wrapper events
*/
initializeEventHandlers() {
// Will be populated after wrapper initialization
// This sets up listeners for wrapper events like accountsChanged, disconnect, etc.
}
/**
* Extract address from wrapper with multiple fallback strategies
*/
async extractAddressFromWrapper() {
if (!this.wrapper) {
return null;
}
try {
// Strategy 1: Standard userAddress property
if (this.wrapper.userAddress) {
return this.wrapper.userAddress;
}
// Strategy 2: Alternative address properties
const addressProperties = ['address', 'connectedAddress', 'walletAddress', 'account'];
for (const prop of addressProperties) {
if (this.wrapper[prop]) {
return this.wrapper[prop];
}
}
// Strategy 3: Call address getter methods
const addressMethods = ['getAddress', 'getUserAddress', 'getConnectedAddress', 'getAccount'];
for (const method of addressMethods) {
if (typeof this.wrapper[method] === 'function') {
try {
const result = await this.wrapper[method]();
if (result) {
return result;
}
}
catch (error) {
}
}
}
// Strategy 4: Check signer address
if (this.wrapper.signer) {
try {
const signerAddress = await this.wrapper.signer.getAddress();
if (signerAddress) {
return signerAddress;
}
}
catch (error) {
}
}
// Strategy 5: Check provider accounts
if (this.wrapper.provider) {
try {
const accounts = await this.wrapper.provider.listAccounts();
if (accounts && accounts.length > 0) {
return accounts[0];
}
}
catch (error) {
}
}
console.warn('⚠️ No address found in wrapper after all strategies');
return null;
}
catch (error) {
console.error('❌ Error extracting address from wrapper:', error);
return null;
}
}
/**
* Debug wrapper state for troubleshooting
*/
debugWrapperState() {
if (!this.wrapper) {
return { wrapper: 'null' };
}
const state = {
hasWrapper: !!this.wrapper,
wrapperType: typeof this.wrapper
};
// Check common properties
const propsToCheck = [
'userAddress', 'address', 'connectedAddress', 'walletAddress', 'account',
'isConnected', 'connected', 'signer', 'provider'
];
propsToCheck.forEach(prop => {
if (prop in this.wrapper) {
state[prop] = this.wrapper[prop];
}
});
// Check available methods
const methodsToCheck = [
'getAddress', 'getUserAddress', 'getConnectedAddress', 'getAccount',
'connect', 'disconnect', 'isConnected'
];
state.availableMethods = methodsToCheck.filter(method => typeof this.wrapper[method] === 'function');
return state;
}
/**
* Normalize and validate Ethereum address with flexible handling
* Handles various formats that MetaMask/wallets might return
*/
normalizeAndValidateAddress(address) {
try {
// Handle null/undefined
if (!address) {
return null;
}
// Handle array responses (MetaMask eth_requestAccounts returns ['0x...'])
let normalizedAddress;
if (Array.isArray(address)) {
if (address.length === 0) {
return null;
}
normalizedAddress = address[0];
}
else if (typeof address === 'string') {
normalizedAddress = address;
}
else {
console.warn('Unexpected address type:', typeof address, address);
return null;
}
// Trim whitespace and ensure string
normalizedAddress = normalizedAddress.trim();
// Basic format validation
if (!normalizedAddress || normalizedAddress.length !== 42) {
return null;
}
// Must start with 0x
if (!normalizedAddress.startsWith('0x')) {
return null;
}
// Case-insensitive hex validation (accepts checksum and non-checksum)
const hexPart = normalizedAddress.slice(2);
if (!/^[a-fA-F0-9]{40}$/.test(hexPart)) {
return null;
}
// Return in lowercase for consistent handling
return normalizedAddress.toLowerCase();
}
catch (error) {
console.error('Address normalization error:', error);
return null;
}
}
/**
* Legacy method for backward compatibility
*/
isValidAddress(address) {
return this.normalizeAndValidateAddress(address) !== null;
}
/**
* Cleanup resources
*/
destroy() {
this.eventListeners.clear();
this.isInitialized = false;
}
}
var IncentivWalletService$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
IncentivWalletService: IncentivWalletService
});
/**
* Initial Wallet State
*/
const initialWalletState = {
address: undefined,
isConnected: false,
isConnecting: false,
isReconnecting: false,
isDisconnecting: false,
error: null
};
/**
* Wallet State Reducer
*
* Follows Redux patterns for predictable state management
* with proper error handling and loading states.
*/
function walletReducer(state, action) {
var _a, _b;
switch (action.type) {
case 'SET_CONNECTING':
return {
...state,
isConnecting: true,
isReconnecting: false,
isDisconnecting: false,
error: null
};
case 'SET_RECONNECTING':
return {
...state,
isConnecting: false,
isReconnecting: true,
isDisconnecting: false,
error: null
};
case 'SET_CONNECTED':
return {
...state,
address: action.payload.address,
isConnected: true,
isConnecting: false,
isReconnecting: false,
isDisconnecting: false,
error: null
};
case 'SET_DISCONNECTING':
return {
...state,
isDisconnecting: true,
isConnecting: false,
isReconnecting: false,
error: null
};
case 'SET_DISCONNECTED':
return {
...initialWalletState, // Reset to initial state
// Keep some state for better UX
error: ((_a = action.payload) === null || _a === void 0 ? void 0 : _a.error) || null
};
case 'SET_ERROR':
return {
...state,
isConnecting: false,
isReconnecting: false,
isDisconnecting: false,
error: action.payload.error
};
case 'CLEAR_ERROR':
return {
...state,
error: null
};
case 'RESET_STATE':
return {
...initialWalletState,
error: ((_b = action.payload) === null || _b === void 0 ? void 0 : _b.keepError) ? state.error : null
};
default:
console.warn('Unknown wallet action type:', action.type);
return state;
}
}
/**
* Action Creators - Type-safe action creation
*/
const walletActions = {
setConnecting: () => ({
type: 'SET_CONNECTING'
}),
setReconnecting: () => ({
type: 'SET_RECONNECTING'
}),
setConnected: (address) => ({
type: 'SET_CONNECTED',
payload: { address }
}),
setDisconnecting: () => ({
type: 'SET_DISCONNECTING'
}),
setDisconnected: (error) => ({
type: 'SET_DISCONNECTED',
payload: error ? { error } : undefined
}),
setError: (error) => ({
type: 'SET_ERROR',
payload: { error }
}),
clearError: () => ({
type: 'CLEAR_ERROR'
}),
resetState: (keepError) => ({
type: 'RESET_STATE',
payload: keepError ? { keepError } : undefined
})
};
/**
* Selector functions for derived state
*/
const walletSelectors = {
/**
* Check if any loading state is active
*/
isLoading: (state) => {
return state.isConnecting || state.isReconnecting || state.isDisconnecting;
},
/**
* Check if wallet is ready for transactions
*/
isReady: (state) => {
return state.isConnected && !!state.address && !walletSelectors.isLoading(state);
},
/**
* Get formatted address for display
*/
getDisplayAddress: (state) => {
if (!state.address)
return '';
return `${state.address.slice(0, 6)}...${state.address.slice(-4)}`;
},
/**
* Check if error should be displayed to user
*/
shouldShowError: (state) => {
if (!state.error)
return false;
// Don't show user rejection errors
if ('code' in state.error && state.error.code === WalletErrorCode.USER_REJECTED) {
return false;
}
return true;
},
/**
* Get user-friendly error message
*/
getErrorMessage: (state) => {
if (!state.error)
return '';
// Handle specific error codes
if ('code' in state.error) {
switch (state.error.code) {
case WalletErrorCode.USER_REJECTED:
return 'Connection was cancelled by user';
case WalletErrorCode.UNAUTHORIZED:
return 'Wallet access was denied';
case WalletErrorCode.UNSUPPORTED_METHOD:
return 'Wallet does not support this operation';
case WalletErrorCode.DISCONNECTED:
return 'Wallet is disconnected';
case WalletErrorCode.CHAIN_DISCONNECTED:
return 'Please check your network connection';
default:
return state.error.message || 'An unknown error occurred';
}
}
return state.error.message || 'An error occurred';
},
/**
* Get current connection status
*/
getConnectionStatus: (state) => {
if (state.isConnecting)
return 'connecting';
if (state.isReconnecting)
return 'reconnecting';
if (state.isDisconnecting)
return 'disconnecting';
if (state.isConnected)
return 'connected';
if (state.error)
return 'error';
return 'disconnected';
}
};
/**
* React Context
*/
const IncentivWalletContext = createContext(undefined);
/**
* IncentivWalletProvider - Professional React Context Provider
*
* Implements 3-Tier Reconnection Strategy and provides wagmi-style API
* for seamless wallet connection management across the entire app.
*/
const IncentivWalletProvider = ({ children, config = {}, enableLogging = false }) => {
const [state, dispatch] = useReducer(walletReducer, initialWalletState);
const [balance, setBalance] = React.useState(undefined);
const [isBalanceLoading, setIsBalanceLoading] = React.useState(false);
// Service instances
const walletService = useRef(null);
const initPromise = useRef(null);
const isInitialized = useRef(false);
const balanceInterval = useRef(null);
// Configuration
const { contracts = [], autoConnect = true, reconnectOnMount = true, pollingInterval = 30000 // 30 seconds
} = config;
/**
* Initialize wallet service
*/
const initializeService = useCallback(async () => {
if (initPromise.current) {
return initPromise.current;
}
initPromise.current = (async () => {
try {
if (enableLogging) {
}
// Migrate storage if needed
PersistenceService.migrateStorageIfNeeded();
// Create service instance
walletService.current = new IncentivWalletService();
await walletService.current.initialize();
// Register contracts if provided
if (contracts.length > 0) {
walletService.current.registerContracts(contracts);
}
// Set up event listeners
setupEventListeners();
isInitialized.current = true;
if (enableLogging) {
}
}
catch (error) {
console.error('❌ Failed to initialize wallet service:', error);
dispatch(walletActions.setError(error));
}
})();
return initPromise.current;
}, [contracts, enableLogging]);
/**
* Set up event listeners for wallet events
*/
const setupEventListeners = useCallback(() => {
if (!walletService.current)
return;
// Listen for connection events
walletService.current.on('connect', (data) => {
dispatch(walletActions.setConnected(data.address));
refreshBalance();
});
// Listen for disconnection events
walletService.current.on('disconnect', () => {
dispatch(walletActions.setDisconnected());
setBalance(undefined);
stopBalancePolling();
});
// Listen for transaction events
walletService.current.on('transaction', (data) => {
// Refresh balance after transaction
setTimeout(refreshBalance, 2000);
});
}, [enableLogging]);
/**
* 3-TIER RECONNECTION STRATEGY
*/
const attemptReconnection = useCallback(async () => {
if (!walletService.current) {
await initializeService();
}
const service = walletService.current;
dispatch(walletActions.setReconnecting());
try {
const { address, wasConnected, hasEverConnected } = PersistenceService.loadWalletData();
// Only attempt reconnection if conditions are met
if (!address || !wasConnected || !hasEverConnected) {
if (enableLogging) {
}
dispatch(walletActions.setDisconnected());
return;
}
if (enableLogging) {
}
// TIER 1: Silent Connection Check
const silentAddress = await service.checkSilentConnection();
if (silentAddress && silentAddress.toLowerCase() === address.toLowerCase()) {
dispatch(walletActions.setConnected(silentAddress));
if (enableLogging) {
}
return;
}
// TIER 2: Session Restore
const sessionRestored = await service.tryRestoreSession(address);
if (sessionRestored) {
dispatch(walletActions.setConnected(address));
if (enableLogging) {
}
return;
}
// TIER 3: Check if we should attempt full connect
if (PersistenceService.shouldAutoReconnect()) {
try {
const reconnectedAddress = await service.connect();
if (reconnectedAddress.toLowerCase() === address.toLowerCase()) {
dispatch(walletActions.setConnected(reconnectedAddress));
if (enableLogging) {
}
return;
}
else {
console.warn('⚠️ Reconnected address does not match stored address');
PersistenceService.clearWalletData();
}
}
catch (connectError) {
if (enableLogging) {
}
}
}
// All tiers failed
dispatch(walletActions.setDisconnected());
if (enableLogging) {
}
}
catch (error) {
console.error('❌ Reconnection failed:', error);
dispatch(walletActions.setError(error));
}
}, [initializeService, enableLogging]);
/**
* Manual connect function
*/
const connect = useCallback(async () => {
if (!walletService.current) {
await initializeService();
}
dispatch(walletActions.setConnecting());
try {
const address = await walletService.current.connect();
dispatch(walletActions.setConnected(address));
if (enableLogging) {
}
}
catch (error) {
console.error('❌ Manual connection failed:', error);
dispatch(walletActions.setError(error));
}
}, [initializeService, enableLogging]);
/**
* Disconnect function
*/
const disconnect = useCallback(() => {
var _a;
dispatch(walletActions.setDisconnecting());
try {
(_a = walletService.current) === null || _a === void 0 ? void 0 : _a.disconnect();
dispatch(walletActions.setDisconnected());
if (enableLogging) {
}
}
catch (error) {
console.error('❌ Disconnection failed:', error);
dispatch(walletActions.setError(error));
}
}, [enableLogging]);
/**
* Reconnect function
*/
const reconnect = useCallback(async () => {
await attemptReconnection();
}, [attemptReconnection]);
/**
* Register contracts
*/
const registerContracts = useCallback((contractConfigs) => {
if (!walletService.current) {
console.warn('⚠️ Wallet service not initialized, cannot register contracts');
return;
}
try {
walletService.current.registerContracts(contractConfigs);
if (enableLogging) {
}
}
catch (error) {
console.error('❌ Contract registration failed:', error);
dispatch(walletActions.setError(error));
}
}, [enableLogging]);
/**
* Get contract instance
*/
const getContract = useCallback((name) => {
if (!walletService.current) {
console.warn('⚠️ Wallet service not initialized');
return null;
}
try {
return walletService.current.getContract(name);
}
catch (error) {
console.error(`❌ Failed to get contract '${name}':`, error);
return null;
}
}, []);
/**
* Refresh balance
*/
const refreshBalance = useCallback(async () => {
if (!state.isConnected || !state.address || !walletService.current) {
return;
}
setIsBalanceLoading(true);
try {
const newBalance = await walletService.current.getBalance(state.address);
setBalance(newBalance);
}
catch (error) {
console.error('❌ Balance refresh failed:', error);
}
finally {
setIsBalanceLoading(false);
}
}, [state.isConnected, state.address]);
/**
* Start balance polling
*/
const startBalancePolling = useCallback(() => {
if (balanceInterval.current)
return;
balanceInterval.current = setInterval(() => {
refreshBalance();
}, pollingInterval);
}, [refreshBalance, pollingInterval]);
/**
* Stop balance polling
*/
const stopBalancePolling = useCallback(() => {
if (balanceInterval.current) {
clearInterval(balanceInterval.current);
balanceInterval.current = null;
}
}, []);
/**
* Initialize on mount
*/
useEffect(() => {
const init = async () => {
await initializeService();
if (autoConnect && reconnectOnMount) {
await attemptReconnection();
}
};
init();
// Cleanup on unmount
return () => {
var _a;
stopBalancePolling();
(_a = walletService.current) === null || _a === void 0 ? void 0 : _a.destroy();
};
}, [initializeService, attemptReconnection, autoConnect, reconnectOnMount, stopBalancePolling]);
/**
* Handle balance polling based on connection status
*/
useEffect(() => {
if (state.isConnected && state.address) {
refreshBalance();
startBalancePolling();
}
else {
setBalance(undefined);
stopBalancePolling();
}
}, [state.isConnected, state.address, refreshBalance, startBalancePolling, stopBalancePolling]);
/**
* Context value
*/
const contextValue = {
// State
...state,
// Derived state
isReady: walletSelectors.isReady(state),
connector: 'incentiv',
// Actions
connect,
disconnect,
reconnect,
// Contract management
registerContracts,
getContract,
// Balance
refreshBalance,
balance,
isBalanceLoading
};
return (jsx(IncentivWalletContext.Provider, { value: contextValue, children: children }));
};
/**
* Hook to use the wallet context
*/
const useIncentivWalletContext = () => {
const context = React.useContext(IncentivWalletContext);
if (context === undefined) {
throw new Error('useIncentivWalletContext must be used within an IncentivWalletProvider');
}
return context;
};
/**
* useIncentivWallet - Main wallet hook (wagmi-inspired)
*
* Primary hook for accessing wallet connection state and actions.
* Provides a clean, type-safe API similar to wagmi's useAccount.
*/
function useIncentivWallet() {
const context = useIncentivWalletContext();
return {
// Connection state
address: context.address,
isConnected: context.isConnected,
isConnecting: context.isConnecting,
isReconnecting: context.isReconnecting,
isDisconnecting: context.isDisconnecting,
error: context.error,
// Balance state
balance: context.balance,
isBalanceLoading: context.isBalanceLoading,
refreshBalance: context.refreshBalance,
// Derived state
isReady: context.isReady,
connector: context.connector,
// Actions
connect: context.connect,
disconnect: context.disconnect,
reconnect: context.reconnect
};
}
/**
* useContract - Contract interaction hook
*
* Provides access to registered smart contracts with full typing support.
* Returns the contract instance with a