UNPKG

@tippingchain/sdk

Version:

TippingChain SDK - On-chain tipping with USDC payouts on ApeChain

1,527 lines (1,524 loc) 91.2 kB
import { getContract, readContract, createThirdwebClient, prepareContractCall } from 'thirdweb'; import { defineChain, arbitrum, base, avalanche, bsc, optimism, polygon, ethereum } from 'thirdweb/chains'; import { SUPPORTED_CHAINS, CONTRACT_CONSTANTS, getContractAddress, STREAMING_PLATFORM_TIPPING_ABI } from '@tippingchain/contracts-interface'; export { CONTRACT_CONSTANTS, MembershipTier, NETWORK_CONFIGS, RELAY_RECEIVER_ADDRESSES, SUPPORTED_CHAINS, SUPPORTED_TESTNETS, TIER_CREATOR_SHARES, getAllContractAddresses, getContractAddress, getRelayReceiverAddress, isContractDeployed } from '@tippingchain/contracts-interface'; // src/core/ApeChainTippingSDK.ts var ApeChainRelayService = class { constructor(isTestnet = true) { this.APECHAIN_ID = SUPPORTED_CHAINS.APECHAIN; this.BASE_SEPOLIA_ID = 84532; // Base Sepolia for testnet this.USDC_TOKEN_ADDRESS = CONTRACT_CONSTANTS.APECHAIN_USDC; this.BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; this.baseUrl = isTestnet ? "https://api.testnets.relay.link" : "https://api.relay.link"; this.isTestnet = isTestnet; } /** * Get a quote for relaying tokens to ApeChain * Makes actual API call to Relay.link for accurate pricing */ async getQuote(params) { try { const destinationChainId = this.isTestnet ? this.BASE_SEPOLIA_ID : this.APECHAIN_ID; const destinationToken = this.isTestnet ? this.BASE_SEPOLIA_USDC : this.USDC_TOKEN_ADDRESS; const normalizeTokenForAPI = (token) => { return token === "native" ? "0x0000000000000000000000000000000000000000" : token; }; const quoteRequest = { user: params.user || "0x0000000000000000000000000000000000000000", recipient: params.recipient, // Optional recipient address originChainId: params.fromChainId, destinationChainId, originCurrency: normalizeTokenForAPI(params.fromToken), destinationCurrency: normalizeTokenForAPI(destinationToken), amount: params.amount, tradeType: "EXACT_INPUT" }; const response = await this.makeRequest("POST", "/quote", quoteRequest); if (!response || typeof response !== "object") { throw new Error("Invalid API response format"); } const apiResponse = response; return { id: apiResponse.id || `quote-${Date.now()}`, fromChainId: params.fromChainId, toChainId: destinationChainId, fromToken: params.fromToken, toToken: destinationToken, amount: params.amount, estimatedOutput: apiResponse.destinationAmount || apiResponse.outputAmount || "0", fees: apiResponse.fees?.toString() || apiResponse.fee?.toString() || "0", estimatedTime: apiResponse.estimatedTime || apiResponse.duration || 300, route: apiResponse.route || apiResponse.steps || { source: "Relay.link API" } }; } catch (error) { console.warn("Relay.link API call failed, falling back to estimates:", error); const destinationChainId = this.isTestnet ? this.BASE_SEPOLIA_ID : this.APECHAIN_ID; const destinationToken = this.isTestnet ? this.BASE_SEPOLIA_USDC : this.USDC_TOKEN_ADDRESS; const amountBigInt = BigInt(params.amount); const estimatedOutput = (amountBigInt * BigInt(95) / BigInt(100)).toString(); return { id: `fallback-quote-${Date.now()}`, fromChainId: params.fromChainId, toChainId: destinationChainId, fromToken: params.fromToken, toToken: destinationToken, amount: params.amount, estimatedOutput, fees: (amountBigInt * BigInt(5) / BigInt(100)).toString(), estimatedTime: 300, route: { note: "Fallback estimate (API unavailable)" } }; } } /** * Estimate USDC output for a tip (deprecated - contracts handle relay automatically) * @deprecated Use getQuote directly instead */ async relayTipToApeChain(params) { try { const destinationChainId = this.isTestnet ? this.BASE_SEPOLIA_ID : this.APECHAIN_ID; const destinationToken = this.isTestnet ? this.BASE_SEPOLIA_USDC : this.USDC_TOKEN_ADDRESS; const quote = await this.getQuote({ fromChainId: params.fromChainId, fromToken: params.fromToken, toChainId: destinationChainId, toToken: destinationToken, amount: params.amount, user: params.userAddress, recipient: params.creatorAddress // Pass creator address as recipient }); return { success: true, relayId: quote.id, destinationChain: destinationChainId, estimatedUsdcAmount: quote.estimatedOutput }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error", destinationChain: this.isTestnet ? this.BASE_SEPOLIA_ID : this.APECHAIN_ID }; } } async makeRequest(method, endpoint, data) { try { const url = `${this.baseUrl}${endpoint}`; const options = { method, headers: { "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "TippingChain-SDK/2.0.0" } }; if (data && (method === "POST" || method === "PUT")) { options.body = JSON.stringify(data); } console.log(`Making ${method} request to ${url}`); console.log("Request payload:", data); const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); console.error(`API Error ${response.status}:`, errorText); throw new Error(`HTTP ${response.status}: ${response.statusText}. Response: ${errorText}`); } const responseData = await response.json(); console.log("API Response:", responseData); return responseData; } catch (error) { console.error("Request failed:", error); throw new Error(`Request failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } }; // src/services/TransactionStatusService.ts var DEFAULT_OPTIONS = { maxRetries: 100, // 100 * 3s = 5 minutes max retryInterval: 3e3, // 3 seconds timeout: 3e5, // 5 minutes confirmationsRequired: 1 }; var TransactionStatusService = class { constructor(client) { this.activeWatchers = /* @__PURE__ */ new Map(); this.client = client; } /** * Watch a transaction until it's confirmed or fails */ async watchTransaction(transactionHash, chain, options = {}) { const opts = { ...DEFAULT_OPTIONS, ...options }; const watcherKey = `${chain.id}-${transactionHash}`; if (this.activeWatchers.has(watcherKey)) { return this.activeWatchers.get(watcherKey).promise; } const abortController = new AbortController(); const promise = this._watchTransactionInternal( transactionHash, chain, opts, abortController.signal ); this.activeWatchers.set(watcherKey, { abort: abortController, promise }); promise.finally(() => { this.activeWatchers.delete(watcherKey); }); return promise; } /** * Watch a transaction with callback for real-time updates */ async watchTransactionWithCallback(transactionHash, chain, onUpdate, options = {}) { const opts = { ...DEFAULT_OPTIONS, ...options }; let retries = 0; const startTime = Date.now(); const poll = async () => { try { if (Date.now() - startTime > opts.timeout) { const update = { transactionHash, status: "failed", error: "Transaction monitoring timeout", timestamp: Date.now() }; onUpdate(update); return update; } const receipt = await this.getTransactionReceipt(transactionHash, chain); if (receipt) { const status = receipt.status === "success" ? "confirmed" : "failed"; const update = { transactionHash, status, receipt, timestamp: Date.now() }; onUpdate(update); return update; } if (retries < opts.maxRetries) { const update = { transactionHash, status: "pending", timestamp: Date.now() }; onUpdate(update); retries++; await new Promise((resolve) => setTimeout(resolve, opts.retryInterval)); return poll(); } else { const update = { transactionHash, status: "failed", error: "Transaction not found after maximum retries", timestamp: Date.now() }; onUpdate(update); return update; } } catch (error) { retries++; if (retries >= opts.maxRetries) { const update = { transactionHash, status: "failed", error: error instanceof Error ? error.message : "Unknown error", timestamp: Date.now() }; onUpdate(update); return update; } await new Promise((resolve) => setTimeout(resolve, opts.retryInterval)); return poll(); } }; return poll(); } /** * Get transaction receipt (simplified implementation) */ async getTransactionReceipt(transactionHash, chain) { try { const response = await fetch(`https://${chain.id}.rpc.thirdweb.com`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getTransactionReceipt", params: [transactionHash], id: 1 }) }); const data = await response.json(); const receipt = data.result; if (!receipt) { return null; } let confirmations = 1; return { transactionHash: receipt.transactionHash, blockNumber: parseInt(receipt.blockNumber, 16), blockHash: receipt.blockHash, gasUsed: parseInt(receipt.gasUsed, 16).toString(), effectiveGasPrice: receipt.effectiveGasPrice ? parseInt(receipt.effectiveGasPrice, 16).toString() : "0", status: receipt.status === "0x1" ? "success" : "failure", confirmations, timestamp: Date.now() // Use current timestamp as approximation }; } catch (error) { console.error("Error fetching transaction receipt:", error); return null; } } /** * Check if a transaction exists in the mempool or blockchain */ async getTransactionStatus(transactionHash, chain) { try { const receipt = await this.getTransactionReceipt(transactionHash, chain); if (receipt) { return receipt.status === "success" ? "confirmed" : "failed"; } try { const response = await fetch(`https://${chain.id}.rpc.thirdweb.com`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getTransactionByHash", params: [transactionHash], id: 1 }) }); const data = await response.json(); if (data.result) { return "pending"; } } catch (error) { console.warn("Error checking mempool:", error); } return "not_found"; } catch (error) { console.error("Error checking transaction status:", error); return "not_found"; } } /** * Cancel watching a specific transaction */ cancelWatch(transactionHash, chainId) { const watcherKey = `${chainId}-${transactionHash}`; const watcher = this.activeWatchers.get(watcherKey); if (watcher) { watcher.abort.abort(); this.activeWatchers.delete(watcherKey); } } /** * Cancel all active watchers */ cancelAllWatches() { for (const watcher of this.activeWatchers.values()) { watcher.abort.abort(); } this.activeWatchers.clear(); } /** * Get the number of active watchers */ getActiveWatchersCount() { return this.activeWatchers.size; } /** * Internal implementation for watching transactions */ async _watchTransactionInternal(transactionHash, chain, options, signal) { let retries = 0; const startTime = Date.now(); const poll = async () => { if (signal.aborted) { throw new Error("Transaction watching was cancelled"); } try { if (Date.now() - startTime > options.timeout) { return { transactionHash, status: "failed", error: "Transaction monitoring timeout", timestamp: Date.now() }; } const receipt = await this.getTransactionReceipt(transactionHash, chain); if (receipt) { if (receipt.confirmations >= options.confirmationsRequired) { return { transactionHash, status: receipt.status === "success" ? "confirmed" : "failed", receipt, timestamp: Date.now() }; } else { if (retries < options.maxRetries) { retries++; await new Promise((resolve) => setTimeout(resolve, options.retryInterval)); return poll(); } } } if (retries < options.maxRetries) { retries++; await new Promise((resolve) => setTimeout(resolve, options.retryInterval)); return poll(); } else { return { transactionHash, status: "failed", error: "Transaction not found after maximum retries", timestamp: Date.now() }; } } catch (error) { if (signal.aborted) { throw new Error("Transaction watching was cancelled"); } retries++; if (retries >= options.maxRetries) { return { transactionHash, status: "failed", error: error instanceof Error ? error.message : "Unknown error", timestamp: Date.now() }; } await new Promise((resolve) => setTimeout(resolve, options.retryInterval)); return poll(); } }; return poll(); } }; var DEFAULT_BALANCE_OPTIONS = { pollInterval: 1e4, // 10 seconds enableOptimisticUpdates: true, refreshAfterTransaction: true, maxRetries: 3 }; var BalanceWatcherService = class { // 5 seconds constructor(client) { this.activeWatchers = /* @__PURE__ */ new Map(); this.balanceCache = /* @__PURE__ */ new Map(); this.CACHE_DURATION = 5e3; this.client = client; } /** * Watch balance changes for an address */ watchBalance(address, chain, tokenAddress, onBalanceChange, options = {}) { const opts = { ...DEFAULT_BALANCE_OPTIONS, ...options }; const watcherKey = `${chain.id}-${address}-${tokenAddress || "native"}`; this.cancelBalanceWatch(watcherKey); const pollBalance = async () => { try { const currentBalance = await this.getBalance(address, chain, tokenAddress); const cached = this.balanceCache.get(watcherKey); const previousBalance = cached?.balance; if (!previousBalance || currentBalance !== previousBalance) { const update = { address, tokenAddress, balance: currentBalance, previousBalance, chainId: chain.id, timestamp: Date.now() }; onBalanceChange(update); } this.balanceCache.set(watcherKey, { balance: currentBalance, timestamp: Date.now() }); } catch (error) { console.error(`Error polling balance for ${watcherKey}:`, error); } }; pollBalance(); const interval = setInterval(pollBalance, opts.pollInterval); this.activeWatchers.set(watcherKey, { interval, lastBalance: "0", callback: onBalanceChange }); return watcherKey; } /** * Get current balance for an address */ async getBalance(address, chain, tokenAddress, useCache = true) { const cacheKey = `${chain.id}-${address}-${tokenAddress || "native"}`; if (useCache) { const cached = this.balanceCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { return cached.balance; } } try { let balance; if (tokenAddress) { const contract = getContract({ client: this.client, chain, address: tokenAddress }); const balanceResult = await readContract({ contract, method: "function balanceOf(address) view returns (uint256)", params: [address] }); balance = balanceResult.toString(); } else { const response = await fetch(`https://${chain.id}.rpc.thirdweb.com`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getBalance", params: [address, "latest"], id: 1 }) }); const data = await response.json(); if (data.result) { balance = parseInt(data.result, 16).toString(); } else { balance = "0"; } } this.balanceCache.set(cacheKey, { balance, timestamp: Date.now() }); return balance; } catch (error) { console.error(`Error fetching balance for ${address}:`, error); return "0"; } } /** * Refresh balance after a transaction */ async refreshBalanceAfterTransaction(transactionHash, address, chain, tokenAddress, maxWaitTime = 3e4) { const startTime = Date.now(); const watcherKey = `${chain.id}-${address}-${tokenAddress || "native"}`; const initialBalance = await this.getBalance(address, chain, tokenAddress, false); const poll = async () => { if (Date.now() - startTime > maxWaitTime) { throw new Error("Balance refresh timeout"); } const currentBalance = await this.getBalance(address, chain, tokenAddress, false); if (currentBalance !== initialBalance) { const update = { address, tokenAddress, balance: currentBalance, previousBalance: initialBalance, chainId: chain.id, timestamp: Date.now() }; const watcher = this.activeWatchers.get(watcherKey); if (watcher) { watcher.callback(update); } return update; } await new Promise((resolve) => setTimeout(resolve, 2e3)); return poll(); }; return poll(); } /** * Get balances across multiple chains */ async getMultiChainBalances(address, chains, tokenAddresses = {}) { const balancePromises = chains.map(async (chain) => { try { const nativeBalance = await this.getBalance(address, chain); const tokenAddrs = tokenAddresses[chain.id] || []; const tokenBalancePromises = tokenAddrs.map(async (tokenAddress) => ({ tokenAddress, balance: await this.getBalance(address, chain, tokenAddress) })); const tokenBalances = await Promise.all(tokenBalancePromises); const tokensMap = {}; tokenBalances.forEach(({ tokenAddress, balance }) => { tokensMap[tokenAddress] = balance; }); return { chainId: chain.id, native: nativeBalance, tokens: tokensMap }; } catch (error) { console.error(`Error fetching balances for chain ${chain.id}:`, error); return { chainId: chain.id, native: "0", tokens: {} }; } }); const results = await Promise.all(balancePromises); const balanceMap = {}; results.forEach((result) => { balanceMap[result.chainId] = { native: result.native, tokens: result.tokens }; }); return balanceMap; } /** * Force refresh all cached balances */ async refreshAllBalances() { const refreshPromises = Array.from(this.activeWatchers.entries()).map( async ([watcherKey, watcher]) => { try { const [chainId, address, tokenOrNative] = watcherKey.split("-"); const tokenAddress = tokenOrNative === "native" ? void 0 : tokenOrNative; this.balanceCache.delete(watcherKey); await this.getBalance(address, { id: parseInt(chainId) }, tokenAddress, false); } catch (error) { console.error(`Error refreshing balance for ${watcherKey}:`, error); } } ); await Promise.all(refreshPromises); } /** * Cancel a specific balance watch */ cancelBalanceWatch(watcherKey) { const watcher = this.activeWatchers.get(watcherKey); if (watcher) { clearInterval(watcher.interval); this.activeWatchers.delete(watcherKey); this.balanceCache.delete(watcherKey); } } /** * Cancel balance watch by parameters */ cancelBalanceWatchFor(address, chainId, tokenAddress) { const watcherKey = `${chainId}-${address}-${tokenAddress || "native"}`; this.cancelBalanceWatch(watcherKey); } /** * Cancel all balance watchers */ cancelAllBalanceWatches() { for (const [watcherKey, watcher] of this.activeWatchers) { clearInterval(watcher.interval); } this.activeWatchers.clear(); this.balanceCache.clear(); } /** * Get active watchers count */ getActiveWatchersCount() { return this.activeWatchers.size; } /** * Clear balance cache */ clearCache() { this.balanceCache.clear(); } /** * Get cached balance if available */ getCachedBalance(address, chainId, tokenAddress) { const cacheKey = `${chainId}-${address}-${tokenAddress || "native"}`; const cached = this.balanceCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { return cached.balance; } return null; } }; // src/services/RelayStatusService.ts var DEFAULT_RELAY_OPTIONS = { maxWaitTime: 6e5, // 10 minutes pollInterval: 5e3, // 5 seconds enableProgressUpdates: true }; var RelayStatusService = class { constructor(client) { this.activeRelayWatchers = /* @__PURE__ */ new Map(); // Relay.link API endpoints (if available) this.RELAY_API_BASE = "https://api.relay.link"; this.client = client; this.transactionStatusService = new TransactionStatusService(client); } /** * Track a relay transaction from source to destination */ async trackRelay(relayId, sourceChain, destinationChain, sourceTransactionHash, options = {}) { const opts = { ...DEFAULT_RELAY_OPTIONS, ...options }; if (this.activeRelayWatchers.has(relayId)) { return this.activeRelayWatchers.get(relayId).promise; } const abortController = new AbortController(); const promise = this._trackRelayInternal( relayId, sourceChain, destinationChain, sourceTransactionHash, opts, abortController.signal ); this.activeRelayWatchers.set(relayId, { abort: abortController, promise }); promise.finally(() => { this.activeRelayWatchers.delete(relayId); }); return promise; } /** * Track relay with callback for real-time updates */ async trackRelayWithCallback(relayId, sourceChain, destinationChain, sourceTransactionHash, onUpdate, options = {}) { const opts = { ...DEFAULT_RELAY_OPTIONS, ...options }; const startTime = Date.now(); let lastProgress = 0; const poll = async () => { try { if (Date.now() - startTime > opts.maxWaitTime) { const update = { relayId, status: "failed", progress: lastProgress, error: "Relay tracking timeout", timestamp: Date.now() }; onUpdate(update); return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "failed", progress: lastProgress, error: "Relay tracking timeout", sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } const relayStatus = await this.getRelayStatus( relayId, sourceChain, destinationChain, sourceTransactionHash ); if (relayStatus.progress !== lastProgress || opts.enableProgressUpdates) { const update = { relayId, status: relayStatus.status, progress: relayStatus.progress, error: relayStatus.error, timestamp: Date.now(), destinationTransactionHash: relayStatus.destinationTransactionHash }; onUpdate(update); lastProgress = relayStatus.progress; } if (relayStatus.status === "completed" || relayStatus.status === "failed") { return relayStatus; } await new Promise((resolve) => setTimeout(resolve, opts.pollInterval)); return poll(); } catch (error) { const update = { relayId, status: "failed", progress: lastProgress, error: error instanceof Error ? error.message : "Unknown error", timestamp: Date.now() }; onUpdate(update); throw error; } }; return poll(); } /** * Get current relay status */ async getRelayStatus(relayId, sourceChain, destinationChain, sourceTransactionHash) { try { const sourceStatus = await this.transactionStatusService.getTransactionStatus( sourceTransactionHash, sourceChain ); if (sourceStatus === "not_found" || sourceStatus === "failed") { return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "failed", progress: 0, error: sourceStatus === "not_found" ? "Source transaction not found" : "Source transaction failed", sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } if (sourceStatus === "pending") { return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "pending", progress: 25, sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } try { const apiStatus = await this.getRelayStatusFromAPI(relayId); if (apiStatus) { return apiStatus; } } catch (apiError) { console.warn("Relay API unavailable, using fallback method:", apiError); } const sourceReceipt = await this.transactionStatusService.getTransactionReceipt( sourceTransactionHash, sourceChain ); if (sourceReceipt) { const elapsedTime = Date.now() - (sourceReceipt.timestamp || Date.now()); const estimatedRelayTime = this.getEstimatedRelayTime(sourceChain.id, destinationChain.id); let progress = 50; let status = "relaying"; if (elapsedTime > estimatedRelayTime) { const destinationTxHash = await this.findDestinationTransaction( sourceTransactionHash, destinationChain, relayId ); if (destinationTxHash) { const destStatus = await this.transactionStatusService.getTransactionStatus( destinationTxHash, destinationChain ); if (destStatus === "confirmed") { progress = 100; status = "completed"; } else if (destStatus === "failed") { status = "failed"; progress = 75; } return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, destinationTransactionHash: destinationTxHash, status, progress, sourceAmount: "0", tokenSymbol: "USDC" }; } } else { progress = Math.min(95, 50 + elapsedTime / estimatedRelayTime * 45); } return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status, progress, sourceAmount: "0", tokenSymbol: "USDC", estimatedCompletionTime: (sourceReceipt.timestamp || Date.now()) + estimatedRelayTime }; } return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "initiated", progress: 10, sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } catch (error) { console.error("Error getting relay status:", error); return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "failed", progress: 0, error: error instanceof Error ? error.message : "Unknown error", sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } } /** * Cancel relay tracking */ cancelRelayTracking(relayId) { const watcher = this.activeRelayWatchers.get(relayId); if (watcher) { watcher.abort.abort(); this.activeRelayWatchers.delete(relayId); } } /** * Cancel all relay tracking */ cancelAllRelayTracking() { for (const watcher of this.activeRelayWatchers.values()) { watcher.abort.abort(); } this.activeRelayWatchers.clear(); } /** * Get estimated relay time between chains */ getEstimatedRelayTime(sourceChainId, destinationChainId) { let baseTime = 12e4; const slowChains = [1, 137, 10]; if (slowChains.includes(sourceChainId)) { baseTime += 6e4; } if (destinationChainId === 33139) { baseTime -= 3e4; } return Math.max(6e4, baseTime); } /** * Try to get relay status from Relay.link API */ async getRelayStatusFromAPI(relayId) { try { const response = await fetch(`${this.RELAY_API_BASE}/status/${relayId}`); if (!response.ok) { return null; } const data = await response.json(); return { relayId, sourceChain: data.sourceChain, destinationChain: data.destinationChain, sourceTransactionHash: data.sourceTx, destinationTransactionHash: data.destTx, status: this.mapApiStatusToRelayStatus(data.status), progress: data.progress || 0, sourceAmount: data.sourceAmount || "0", destinationAmount: data.destAmount, tokenSymbol: data.token || "USDC", estimatedCompletionTime: data.eta, actualCompletionTime: data.completedAt }; } catch (error) { console.warn("Failed to fetch from Relay API:", error); return null; } } /** * Map API status to our RelayStatus */ mapApiStatusToRelayStatus(apiStatus) { switch (apiStatus?.toLowerCase()) { case "pending": return "pending"; case "processing": case "bridging": return "relaying"; case "completed": case "success": return "completed"; case "failed": case "error": return "failed"; default: return "initiated"; } } /** * Try to find the destination transaction by looking for patterns */ async findDestinationTransaction(sourceTransactionHash, destinationChain, relayId) { try { return null; } catch (error) { console.error("Error finding destination transaction:", error); return null; } } /** * Generate a unique relay ID based on transaction hash and timestamp */ static generateRelayId(transactionHash, timestamp) { const ts = timestamp || Date.now(); return `relay_${transactionHash.slice(2, 10)}_${ts}`; } /** * Internal tracking implementation */ async _trackRelayInternal(relayId, sourceChain, destinationChain, sourceTransactionHash, options, signal) { const startTime = Date.now(); const poll = async () => { if (signal.aborted) { throw new Error("Relay tracking was cancelled"); } if (Date.now() - startTime > options.maxWaitTime) { return { relayId, sourceChain: sourceChain.id, destinationChain: destinationChain.id, sourceTransactionHash, status: "failed", progress: 0, error: "Relay tracking timeout", sourceAmount: "0", tokenSymbol: "UNKNOWN" }; } const relayStatus = await this.getRelayStatus( relayId, sourceChain, destinationChain, sourceTransactionHash ); if (relayStatus.status === "completed" || relayStatus.status === "failed") { return relayStatus; } await new Promise((resolve) => setTimeout(resolve, options.pollInterval)); return poll(); }; return poll(); } }; var TypedABI = STREAMING_PLATFORM_TIPPING_ABI; var ApeChainTippingSDK = class { constructor(config) { if (!config.clientId) { throw new Error("clientId is required"); } this.config = config; this.client = createThirdwebClient({ clientId: config.clientId }); this.relayService = new ApeChainRelayService(config.useTestnet || false); this.transactionStatus = new TransactionStatusService(this.client); this.balanceWatcher = new BalanceWatcherService(this.client); this.relayStatus = new RelayStatusService(this.client); } getContractAddress(chainId) { if (this.config.streamingPlatformAddresses && this.config.streamingPlatformAddresses[chainId]) { return this.config.streamingPlatformAddresses[chainId]; } return getContractAddress(chainId, this.config.useTestnet || false); } async sendTip(params) { try { const contractAddress = this.getContractAddress(params.sourceChainId); if (!contractAddress) { throw new Error(`Source chain ${params.sourceChainId} not supported or contract not deployed`); } const creator = await this.getCreator(params.creatorId, params.sourceChainId); if (!creator.active) { throw new Error(`Creator ${params.creatorId} is not active`); } const chain = this.getChainById(params.sourceChainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); let transaction; if (params.token === "native") { transaction = prepareContractCall({ contract, method: "function tipCreatorETH(uint256 creatorId)", params: [BigInt(params.creatorId)], value: BigInt(params.amount) }); } else { transaction = prepareContractCall({ contract, method: "function tipCreatorToken(uint256 creatorId, address token, uint256 amount)", params: [BigInt(params.creatorId), params.token, BigInt(params.amount)] }); } const result = await this.executeTransaction(transaction); const relayResult = await this.relayService.relayTipToApeChain({ fromChainId: params.sourceChainId, fromToken: params.token, amount: params.amount, creatorAddress: creator.wallet, // Use actual creator wallet from registry userAddress: params.userAddress, // User's wallet address for API targetToken: "USDC" // Target USDC on ApeChain }); return { success: true, sourceTransactionHash: result.transactionHash, relayId: relayResult.relayId, creatorId: params.creatorId, estimatedUsdcAmount: relayResult.estimatedUsdcAmount || "0" }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } // Creator management methods async addCreator(registration) { if (registration.chainId) { return this.addCreatorToChain( registration.creatorWallet, registration.tier, registration.thirdwebId, registration.chainId ); } const sourceChains = [ SUPPORTED_CHAINS.ETHEREUM, SUPPORTED_CHAINS.POLYGON, SUPPORTED_CHAINS.OPTIMISM, SUPPORTED_CHAINS.BSC, SUPPORTED_CHAINS.ABSTRACT, SUPPORTED_CHAINS.AVALANCHE, SUPPORTED_CHAINS.BASE, SUPPORTED_CHAINS.ARBITRUM, SUPPORTED_CHAINS.TAIKO ]; let creatorId = null; const errors = []; for (const chainId of sourceChains) { try { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { console.warn(`Chain ${chainId} not deployed, skipping`); continue; } const id = await this.addCreatorToChain( registration.creatorWallet, registration.tier, registration.thirdwebId, chainId ); if (creatorId === null) { creatorId = id; } else if (creatorId !== id) { console.warn(`Creator ID mismatch: expected ${creatorId}, got ${id} on chain ${chainId}`); } } catch (error) { errors.push(`Chain ${chainId}: ${error instanceof Error ? error.message : String(error)}`); } } if (creatorId === null) { throw new Error(`Failed to register creator on any chain. Errors: ${errors.join(", ")}`); } return creatorId; } async addCreatorToChain(creatorWallet, tier, thirdwebId, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported for creator registration or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const transaction = prepareContractCall({ contract, method: "function addCreator(address creatorWallet, uint8 tier, string thirdwebId)", params: [creatorWallet, tier, thirdwebId || ""] }); await this.executeTransaction(transaction); const creatorId = await this.readContract(contract, "getCreatorByWallet", [creatorWallet]); return Number(creatorId); } /** * Prepare a creator addition transaction for external execution * This method returns the prepared transaction without executing it, * allowing the calling application to handle wallet interaction */ async prepareAddCreatorTransaction(registration) { const chainId = registration.chainId || SUPPORTED_CHAINS.BASE; const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported for creator registration or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const transaction = prepareContractCall({ contract, method: "function addCreator(address creatorWallet, uint8 tier, string thirdwebId)", params: [ registration.creatorWallet, registration.tier, registration.thirdwebId || "" ] }); return { transaction, contractAddress, chainId }; } async getCreator(creatorId, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const creatorInfo = await this.readContract(contract, "function getCreatorInfo(uint256 creatorId) view returns (address wallet, bool active, uint256 totalTips, uint256 tipCount, uint8 tier, uint256 creatorShareBps)", [BigInt(creatorId)]); return { id: creatorId, wallet: creatorInfo[0], // wallet active: creatorInfo[1], // active totalTips: creatorInfo[2].toString(), // totalTips tipCount: Number(creatorInfo[3]), // tipCount tier: creatorInfo[4], // tier creatorShareBps: Number(creatorInfo[5]) // creatorShareBps }; } async getCreatorByWallet(walletAddress, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const creatorId = await this.readContract(contract, "getCreatorByWallet", [walletAddress]); if (Number(creatorId) === 0) { return null; } return this.getCreator(Number(creatorId), chainId); } /** * Get creator by thirdweb account ID * @param thirdwebId Thirdweb account ID * @param chainId Chain ID * @returns Creator information or null if not found */ async getCreatorByThirdwebId(thirdwebId, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const creatorId = await this.readContract(contract, "getCreatorByThirdwebId", [thirdwebId]); if (Number(creatorId) === 0) { return null; } return this.getCreator(Number(creatorId), chainId); } async updateCreatorWallet(creatorId, newWallet, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const transaction = prepareContractCall({ contract, method: "function updateCreatorWallet(uint256 creatorId, address newWallet)", params: [BigInt(creatorId), newWallet] }); const result = await this.executeTransaction(transaction); return result.success; } async updateCreatorTier(creatorId, newTier, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const transaction = prepareContractCall({ contract, method: "function updateCreatorTier(uint256 creatorId, uint8 newTier)", params: [BigInt(creatorId), newTier] }); const result = await this.executeTransaction(transaction); return result.success; } async calculateTipSplits(creatorId, tipAmount, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const result = await this.readContract( contract, "function calculateTipSplits(uint256 creatorId, uint256 tipAmount) view returns (uint256 platformFee, uint256 creatorAmount, uint256 businessAmount)", [BigInt(creatorId), BigInt(tipAmount)] ); return { platformFee: result[0].toString(), creatorAmount: result[1].toString(), businessAmount: result[2].toString() }; } async getCreatorUsdcBalanceOnApeChain(creatorAddress) { const apeChainAddress = this.getContractAddress(SUPPORTED_CHAINS.APECHAIN); if (!apeChainAddress) { throw new Error("ApeChain contract not deployed or configured"); } const chain = this.getChainById(SUPPORTED_CHAINS.APECHAIN); const contract = getContract({ client: this.client, chain, address: apeChainAddress, abi: TypedABI }); const balances = await this.readContract(contract, "getBalances", [creatorAddress]); return balances[1].toString(); } async getPlatformStats(chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const stats = await this.readContract(contract, "getPlatformStats", []); return { totalTips: stats[0].toString(), totalCount: Number(stats[1]), totalRelayed: stats[2].toString(), activeCreators: Number(stats[3]), autoRelayEnabled: stats[4] }; } async getTopCreators(limit = 10, chainId) { const contractAddress = this.getContractAddress(chainId); if (!contractAddress) { throw new Error(`Chain ${chainId} not supported or contract not deployed`); } const chain = this.getChainById(chainId); const contract = getContract({ client: this.client, chain, address: contractAddress, abi: TypedABI }); const maxCreators = Math.max(limit * 2, 100); const result = await this.readContract( contract, "getAllActiveCreators", [BigInt(maxCreators)] ); const [creatorIds, wallets] = result; const allCreators = []; for (let i = 0; i < creatorIds.length; i++) { allCreators.push({ id: Number(creatorIds[i]), wallet: wallets[i], active: true, // getAllActiveCreators only returns active creators totalTips: "0", // Will be fetched below tipCount: 0 // Will be fetched below }); } const creatorsToEnrich = allCreators.slice(0, Math.min(limit * 3, allCreators.length)); for (const creator of creatorsToEnrich) { try { const creatorInfo = await this.readContract( contract, "function getCreatorInfo(uint256 creatorId) view returns (address wallet, bool active, uint256 totalTips, uint256 tipCount, uint8 tier, uint256 creatorShareBps)", [BigInt(creator.id)] ); creator.totalTips = creatorInfo[2].toString(); creator.tipCount = Number(creatorInfo[3]); } catch (error) { console.warn(`Failed to get creator info for ID ${creator.id}:`, error); } } allCreators.sort((a, b) => { const aTips = BigInt(a.totalTips); const bTips = BigInt(b.totalTips); if (bTips > aTips) return 1; if (bTips < aTips) return -1; return 0; }); const topCreators = allCreators.slice(0, limit); return topCreators; } getChainById(chainId) { const chainMap = { // Mainnet chains 1: ethereum, 137: polygon, 10: optimism, 56: bsc, 43114: avalanche, 8453: base, 42161: arbitrum, 2741: defineChain({ id: 2741, name: "Abstract", rpc: "https://api.testnet.abs.xyz", nativeCurrency: { name: "Ethereum", symbol: "ETH", decimals: 18 } }), 33139: defineChain({ id: 33139, name: "ApeChain", rpc: "https://33139.rpc.thirdweb.com", nativeCurrency: { name: "APE", symbol: "APE", decimals: 18 } }), 167e3: defineChain({ id: 167e3, name: "Taiko", rpc: "https://rpc.mainnet.taiko.xyz", nativeCurrency: { name: "Ethereum", symbol: "ETH", decimals: 18 } }), // Testnets 421614: defineChain({ id: 421614, name: "Arbitrum Sepolia", rpc: "https://sepolia-rollup.arbitrum.io/rpc", nativeCurrency: { name: "Ethereum", symbol: "ETH", decimals: 18 } }), 80002: defineChain({ id: 80002, name: "Polygon Amoy", rpc: "https://rpc-amoy.polygon.technology", nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 } }), 84532: defineChain({ id: 84532, name: "Base Sepolia", rpc: "https://sepolia.base.org", nativeCurrency: { name: "Ethereum", symbol: "ETH", decimals: 18 } }) }; const chain = chainMap[chainId]; if (!chain) { throw new Error(`Unsupported chain ID: ${chainId}`); } return chain; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async executeTransaction(_transaction) { return { transactionHash: "0x" + Math.random().toString(16).substr(2, 64), blockNumber: Math.floor(Math.