(function(GLOBAL) { 'use strict'; // Stellar Horizon API endpoints const HORIZON_URL = 'https://horizon.stellar.org'; // Mainnet const stellarAPI = {}; let StellarSdk = null; let server = null; // Initialize Stellar SDK when available stellarAPI.init = function() { if (typeof window !== 'undefined') { const sdkCandidate = window.StellarSdk || window['stellar-sdk'] || window.StellarBase; if (sdkCandidate) { let ServerClass = null; if (sdkCandidate.Server) { ServerClass = sdkCandidate.Server; StellarSdk = sdkCandidate; } else if (sdkCandidate.Horizon && sdkCandidate.Horizon.Server) { ServerClass = sdkCandidate.Horizon.Server; StellarSdk = sdkCandidate; // Store the full SDK object } if (ServerClass) { try { server = new ServerClass(HORIZON_URL); return true; } catch (error) { console.error('❌ Error creating Server instance:', error); return false; } } else { console.error('❌ Server class not found in StellarSdk'); return false; } } else { console.error('❌ StellarSdk not found on window'); return false; } } console.warn('⚠️ Window object not available'); return false; }; stellarAPI.forceInit = function() { return stellarAPI.init(); }; // Get account balance and info stellarAPI.getBalance = async function(address) { try { const response = await fetch(`${HORIZON_URL}/accounts/${address}`); if (!response.ok) { if (response.status === 404) { throw new Error('Account not found. The account may not be funded yet.'); } throw new Error(`Failed to fetch balance: ${response.status}`); } const data = await response.json(); // Find native XLM balance const nativeBalance = data.balances.find(b => b.asset_type === 'native'); return { address: data.account_id, balance: nativeBalance ? parseFloat(nativeBalance.balance) : 0, balanceXlm: nativeBalance ? parseFloat(nativeBalance.balance) : 0, sequence: data.sequence, subentryCount: data.subentry_count, numSponsoring: data.num_sponsoring || 0, numSponsored: data.num_sponsored || 0, balances: data.balances, // All balances including assets signers: data.signers, flags: data.flags, thresholds: data.thresholds, lastModifiedLedger: data.last_modified_ledger, // Minimum balance calculation: (2 + subentry_count) * 0.5 XLM minBalance: (2 + data.subentry_count) * 0.5 }; } catch (error) { throw error; } }; // Get transaction history with pagination stellarAPI.getTransactions = async function(address, options = {}) { const limit = options.limit || 10; const cursor = options.cursor || options.next || null; const order = options.order || 'desc'; // desc = newest first let url = `${HORIZON_URL}/accounts/${address}/transactions?limit=${limit}&order=${order}`; if (cursor) { url += `&cursor=${cursor}`; } try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch transactions: ${response.status}`); } const data = await response.json(); // Format transactions const transactions = await Promise.all((data._embedded.records || []).map(async tx => { // Get operations for this transaction to determine type and details const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`; let operations = []; try { const opsResponse = await fetch(opsUrl); if (opsResponse.ok) { const opsData = await opsResponse.json(); operations = opsData._embedded.records || []; } } catch (error) { console.warn('Failed to fetch operations for transaction:', tx.hash, error); } // Find payment operations const paymentOp = operations.find(op => op.type === 'payment' || op.type === 'create_account' ); let type = 'other'; let amount = 0; let amountXlm = 0; let receiver = null; let sender = tx.source_account; if (paymentOp) { if (paymentOp.type === 'payment') { type = paymentOp.from === address ? 'sent' : 'received'; amount = parseFloat(paymentOp.amount || 0); amountXlm = amount; receiver = paymentOp.to; sender = paymentOp.from; } else if (paymentOp.type === 'create_account') { type = paymentOp.funder === address ? 'sent' : 'received'; amount = parseFloat(paymentOp.starting_balance || 0); amountXlm = amount; receiver = paymentOp.account; sender = paymentOp.funder; } } // Parse timestamp const timestamp = new Date(tx.created_at).getTime() / 1000; return { id: tx.id, hash: tx.hash, ledger: tx.ledger, createdAt: tx.created_at, sourceAccount: tx.source_account, fee: parseInt(tx.fee_charged || tx.max_fee), feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000, operationCount: tx.operation_count, successful: tx.successful, // Payment details type: type, sender: sender, receiver: receiver, amount: amount, amountXlm: amountXlm, memo: tx.memo || null, memoType: tx.memo_type || null, // Compatibility fields roundTime: timestamp, confirmedRound: tx.ledger }; })); return { transactions, nextToken: data._embedded.records.length > 0 ? data._embedded.records[data._embedded.records.length - 1].paging_token : null, hasMore: data._embedded.records.length === limit, cursor: data._embedded.records.length > 0 ? data._embedded.records[data._embedded.records.length - 1].paging_token : null }; } catch (error) { throw error; } }; // Get transaction parameters (needed for sending) stellarAPI.getTransactionParams = async function(sourceAddress) { try { // Get latest ledger info for fee stats const response = await fetch(`${HORIZON_URL}/fee_stats`); const feeStats = await response.json(); // Base fee in stroops (0.00001 XLM = 100 stroops) const baseFee = feeStats.last_ledger_base_fee || '100'; return { fee: parseInt(baseFee), baseFee: baseFee, networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015', genesisId: 'stellar-mainnet', genesisHash: 'stellar-mainnet' }; } catch (error) { // Fallback to default fee return { fee: 100, baseFee: '100', networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015', genesisId: 'stellar-mainnet', genesisHash: 'stellar-mainnet' }; } }; // Build and sign transaction using Stellar SDK stellarAPI.buildAndSignTransaction = async function(params) { const { sourceAddress, destinationAddress, amount, secretKey, memo } = params; if (!StellarSdk || !server) { throw new Error('Stellar SDK not initialized. Please refresh the page.'); } try { // Load source account const sourceAccount = await server.loadAccount(sourceAddress); // Check if destination account exists let destinationExists = true; try { await server.loadAccount(destinationAddress); } catch (error) { if (error.response && error.response.status === 404) { destinationExists = false; } else { throw error; } } // Get fee stats const feeStats = await server.feeStats(); const fee = feeStats.max_fee.mode || (StellarSdk.BASE_FEE || '100'); // Build transaction let transaction = new StellarSdk.TransactionBuilder(sourceAccount, { fee: fee, networkPassphrase: StellarSdk.Networks.PUBLIC }); // Add operation based on whether destination exists if (destinationExists) { // Payment operation transaction = transaction.addOperation( StellarSdk.Operation.payment({ destination: destinationAddress, asset: StellarSdk.Asset.native(), amount: amount.toString() }) ); } else { // Create account operation (requires minimum 1 XLM) if (parseFloat(amount) < 1) { throw new Error('Creating a new account requires a minimum of 1 XLM'); } transaction = transaction.addOperation( StellarSdk.Operation.createAccount({ destination: destinationAddress, startingBalance: amount.toString() }) ); } // Add memo if provided if (memo) { transaction = transaction.addMemo(StellarSdk.Memo.text(memo)); } // Set timeout and build transaction = transaction.setTimeout(30).build(); // Sign transaction const keypair = StellarSdk.Keypair.fromSecret(secretKey); transaction.sign(keypair); return { transaction: transaction, xdr: transaction.toEnvelope().toXDR('base64'), hash: transaction.hash().toString('hex'), destinationExists: destinationExists, fee: parseInt(fee) }; } catch (error) { console.error('Error building transaction:', error); throw error; } }; // Submit signed transaction stellarAPI.submitTransaction = async function(transactionXDR) { if (!StellarSdk || !server) { throw new Error('Stellar SDK not initialized. Please refresh the page.'); } try { // Parse the XDR back to a transaction const transaction = new StellarSdk.Transaction(transactionXDR, StellarSdk.Networks.PUBLIC); // Submit to network const result = await server.submitTransaction(transaction); return { hash: result.hash, ledger: result.ledger, successful: result.successful, txId: result.hash }; } catch (error) { console.error('Error submitting transaction:', error); // Parse Stellar error if (error.response && error.response.data) { const errorData = error.response.data; let errorMsg = errorData.title || 'Transaction failed'; if (errorData.extras && errorData.extras.result_codes) { const codes = errorData.extras.result_codes; errorMsg += ': ' + (codes.transaction || codes.operations?.join(', ') || 'Unknown error'); } throw new Error(errorMsg); } throw error; } }; // Get single transaction by hash stellarAPI.getTransaction = async function(txHash) { try { const response = await fetch(`${HORIZON_URL}/transactions/${txHash}`); if (!response.ok) { throw new Error(`Transaction not found: ${response.status}`); } const tx = await response.json(); // Get operations for this transaction const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`; let operations = []; try { const opsResponse = await fetch(opsUrl); if (opsResponse.ok) { const opsData = await opsResponse.json(); operations = opsData._embedded.records || []; } } catch (error) { console.warn('Failed to fetch operations for transaction:', tx.hash, error); } // Find payment operations const paymentOp = operations.find(op => op.type === 'payment' || op.type === 'create_account' ); let type = 'other'; let amount = 0; let amountXlm = 0; let receiver = null; let sender = tx.source_account; if (paymentOp) { if (paymentOp.type === 'payment') { type = 'payment'; amount = parseFloat(paymentOp.amount || 0); amountXlm = amount; receiver = paymentOp.to; sender = paymentOp.from; } else if (paymentOp.type === 'create_account') { type = 'create_account'; amount = parseFloat(paymentOp.starting_balance || 0); amountXlm = amount; receiver = paymentOp.account; sender = paymentOp.funder; } } // Parse timestamp const timestamp = new Date(tx.created_at).getTime() / 1000; return { id: tx.id, hash: tx.hash, ledger: tx.ledger, createdAt: tx.created_at, sourceAccount: tx.source_account, fee: parseInt(tx.fee_charged || tx.max_fee), feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000, operationCount: tx.operation_count, successful: tx.successful, // Payment details type: type, sender: sender, receiver: receiver, amount: amount, amountXlm: amountXlm, memo: tx.memo || null, memoType: tx.memo_type || null, operations: operations, // Compatibility fields roundTime: timestamp, confirmedRound: tx.ledger }; } catch (error) { throw error; } }; // Format XLM amount for display stellarAPI.formatXLM = function(amount) { return parseFloat(amount).toFixed(7); }; // Parse XLM to stroops (1 XLM = 10,000,000 stroops) stellarAPI.parseXLM = function(xlm) { return Math.floor(parseFloat(xlm) * 10000000); }; // Validate Stellar address stellarAPI.isValidAddress = function(address) { // Stellar addresses start with 'G' and are 56 characters long if (!address || typeof address !== 'string') return false; if (address.length !== 56) return false; if (!address.startsWith('G')) return false; // Check if it's valid Base32 const BASE32_REGEX = /^[A-Z2-7]+$/; return BASE32_REGEX.test(address); }; // Validate Stellar secret key stellarAPI.isValidSecret = function(secret) { // Stellar secret keys start with 'S' and are 56 characters long if (!secret || typeof secret !== 'string') return false; if (secret.length !== 56) return false; if (!secret.startsWith('S')) return false; // Check if it's valid Base32 const BASE32_REGEX = /^[A-Z2-7]+$/; return BASE32_REGEX.test(secret); }; // Check initialization status stellarAPI.isInitialized = function() { return StellarSdk !== null && server !== null; }; GLOBAL.stellarAPI = stellarAPI; GLOBAL.xlmAPI = stellarAPI; // Alias for compatibility // Auto-initialize when SDK is available with retry logic if (typeof window !== 'undefined') { let initAttempts = 0; const maxAttempts = 5; function tryInit() { initAttempts++; const success = stellarAPI.init(); if (success) { } else if (initAttempts < maxAttempts) { const delay = initAttempts * 200; setTimeout(tryInit, delay); } else { console.error('❌ Failed to initialize Stellar SDK after', maxAttempts, 'attempts'); } } window.addEventListener('load', function() { setTimeout(tryInit, 100); }); } })(typeof window !== 'undefined' ? window : global);