bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
288 lines (287 loc) • 11.8 kB
JavaScript
// 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);
}