stellarwallet/stellarBlockchainAPI.js

496 lines
16 KiB
JavaScript

(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);