UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

288 lines (287 loc) 11.8 kB
// Blockchain Service Manager // Handles both client-side API key requests and backend proxy requests // Service endpoints with defaults const DEFAULT_ENDPOINTS = { whatsonchain: { broadcast: "https://api.whatsonchain.com/v1/bsv/{network}/tx/raw", utxos: "https://api.whatsonchain.com/v1/bsv/{network}/address/{address}/unspent", balance: "https://api.whatsonchain.com/v1/bsv/{network}/address/{address}/balance", transactions: "https://api.whatsonchain.com/v1/bsv/{network}/address/{address}/history", }, taal: { broadcast: "https://api.taal.com/arc/v1/tx", utxos: "https://api.taal.com/api/v1/utxo/{address}", balance: "https://api.taal.com/api/v1/address/{address}/balance", transactions: "https://api.taal.com/api/v1/address/{address}/transactions", }, gorillapool: { broadcast: "https://arc.gorillapool.io/v1/tx", utxos: "https://jungle.gorillapool.io/v1/utxo/address/{address}", balance: "https://jungle.gorillapool.io/v1/address/{address}/balance", transactions: "https://jungle.gorillapool.io/v1/address/{address}/transactions", }, mattercloud: { broadcast: "https://api.mattercloud.io/api/v3/{network}/tx/send", utxos: "https://api.mattercloud.io/api/v3/{network}/address/{address}/utxo", balance: "https://api.mattercloud.io/api/v3/{network}/address/{address}/balance", transactions: "https://api.mattercloud.io/api/v3/{network}/address/{address}/tx", }, }; export class BlockchainService { config; constructor(config) { this.config = config; } // Get appropriate headers for a service getHeaders(service) { const headers = { "Content-Type": "application/json", }; if (this.config.mode === "proxy" && this.config.proxy?.headers) { return { ...headers, ...this.config.proxy.headers }; } if (this.config.mode === "client" && this.config.client?.apiKeys) { const apiKeys = this.config.client.apiKeys; // Service-specific API key headers switch (service) { case "taal": if (apiKeys.taal) { headers.Authorization = `Bearer ${apiKeys.taal}`; } break; case "mattercloud": if (apiKeys.mattercloud) { headers.api_key = apiKeys.mattercloud; } break; case "custom": if (apiKeys.custom) { Object.assign(headers, apiKeys.custom); } break; // WhatsOnChain and GorillaPool typically don't require API keys } } return headers; } // Get endpoint URL getEndpoint(operation, params) { if (this.config.mode === "proxy" && this.config.proxy) { // Use proxy endpoint const baseUrl = this.config.proxy.endpoint.replace(/\/$/, ""); return `${baseUrl}/${operation}`; } // Client mode - use direct service endpoints const service = this.config.client?.preferredService || "whatsonchain"; const endpoints = this.config.client?.endpoints || DEFAULT_ENDPOINTS[service]; const network = this.config.network || "main"; let endpoint = endpoints[operation] || DEFAULT_ENDPOINTS[service]?.[operation]; if (!endpoint) { throw new Error(`No endpoint configured for operation: ${operation}`); } // Replace placeholders endpoint = endpoint.replace("{network}", network); if (params) { for (const [key, value] of Object.entries(params)) { endpoint = endpoint.replace(`{${key}}`, value); } } return endpoint; } // Broadcast transaction async broadcastTransaction(tx) { const txHex = typeof tx === "string" ? tx : tx.toHex(); const endpoint = this.getEndpoint("broadcast"); const headers = this.getHeaders(this.config.client?.preferredService); let lastError = null; const retries = this.config.retries || 3; for (let attempt = 1; attempt <= retries; attempt++) { try { const response = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(this.config.mode === "proxy" ? { tx: txHex, operation: "broadcast" } : { txhex: txHex }), signal: AbortSignal.timeout(this.config.timeout || 30000), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const result = await response.json(); // Handle different response formats if (this.config.mode === "proxy") { // Proxy should return standardized format return result; } // Direct service responses vary if (typeof result === "string" && result.length === 64) { // WhatsOnChain returns just the txid return { success: true, txid: result }; } if (result.txid) { // Most services return an object with txid return { success: true, txid: result.txid }; } throw new Error("Invalid broadcast response format"); } catch (error) { lastError = error; console.warn(`Broadcast attempt ${attempt} failed:`, error); if (attempt < retries) { await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); } } } return { success: false, error: lastError?.message || "Broadcast failed after all retries", }; } // Get UTXOs for an address async getUTXOs(address) { const endpoint = this.getEndpoint("utxos", { address }); const headers = this.getHeaders(this.config.client?.preferredService); try { const response = await fetch(endpoint, { method: this.config.mode === "proxy" ? "POST" : "GET", headers, body: this.config.mode === "proxy" ? JSON.stringify({ address, operation: "utxos" }) : undefined, signal: AbortSignal.timeout(this.config.timeout || 30000), }); if (!response.ok) { throw new Error(`Failed to fetch UTXOs: ${response.statusText}`); } const data = await response.json(); // Normalize response format if (this.config.mode === "proxy") { return data.utxos || data; } // Different services have different response formats return this.normalizeUTXOs(data); } catch (error) { console.error("Failed to get UTXOs:", error); return []; } } // Get balance for an address async getBalance(address) { const endpoint = this.getEndpoint("balance", { address }); const headers = this.getHeaders(this.config.client?.preferredService); try { const response = await fetch(endpoint, { method: this.config.mode === "proxy" ? "POST" : "GET", headers, body: this.config.mode === "proxy" ? JSON.stringify({ address, operation: "balance" }) : undefined, signal: AbortSignal.timeout(this.config.timeout || 30000), }); if (!response.ok) { throw new Error(`Failed to fetch balance: ${response.statusText}`); } const data = await response.json(); // Normalize response format if (this.config.mode === "proxy") { return data; } return this.normalizeBalance(data); } catch (error) { console.error("Failed to get balance:", error); return { confirmed: 0, unconfirmed: 0 }; } } // Get transaction history for an address async getTransactions(address, limit = 50) { const endpoint = this.getEndpoint("transactions", { address }); const headers = this.getHeaders(this.config.client?.preferredService); try { const response = await fetch(endpoint, { method: this.config.mode === "proxy" ? "POST" : "GET", headers, body: this.config.mode === "proxy" ? JSON.stringify({ address, limit, operation: "transactions" }) : undefined, signal: AbortSignal.timeout(this.config.timeout || 30000), }); if (!response.ok) { throw new Error(`Failed to fetch transactions: ${response.statusText}`); } const data = await response.json(); // Return as-is for now, services have very different formats return Array.isArray(data) ? data : data.transactions || []; } catch (error) { console.error("Failed to get transactions:", error); return []; } } // Normalize UTXO formats from different services normalizeUTXOs(data) { if (!data) return []; // WhatsOnChain format if (Array.isArray(data)) { return data.map((utxo) => ({ txid: utxo.tx_hash || utxo.txid || "", vout: utxo.tx_pos ?? utxo.vout ?? utxo.outputIndex ?? 0, satoshis: utxo.value ?? utxo.satoshis ?? 0, script: utxo.script || "", confirmations: utxo.confirmations || 0, })); } // Other formats may need different handling return []; } // Normalize balance formats from different services normalizeBalance(data) { if (!data) return { confirmed: 0, unconfirmed: 0 }; // Simple number response if (typeof data === "number") { return { confirmed: data, unconfirmed: 0, }; } // Object response - check for different formats if (typeof data === "object") { // WhatsOnChain format if (typeof data.confirmed !== "undefined") { return { confirmed: data.confirmed, unconfirmed: data.unconfirmed || 0, }; } // Mattercloud format if (typeof data.balance !== "undefined") { return { confirmed: data.balance.confirmed_satoshis || 0, unconfirmed: data.balance.unconfirmed_satoshis || 0, }; } } return { confirmed: 0, unconfirmed: 0 }; } } // Factory function to create blockchain service from config export function createBlockchainService(config) { // Default to proxy mode with no endpoint (requires configuration) const defaultConfig = { mode: "proxy", proxy: { endpoint: "/api/blockchain", // Default relative endpoint }, network: "main", retries: 3, timeout: 30000, }; return new BlockchainService(config || defaultConfig); }